diff --git a/index.js b/index.js index 5e65c6ac..47f98bbb 100644 --- a/index.js +++ b/index.js @@ -189,6 +189,10 @@ let rcl = { * @param {string} [namespace=''] - The namespace used in ROS. * @param {Context} [context=Context.defaultContext()] - The context to create the node in. * @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior. + * @param {Array} [args=[]] - The arguments to pass to the node. + * @param {boolean} [useGlobalArguments=true] - If true, the node will use the global arguments + * from the context, otherwise it will only use the arguments + * passed in the args parameter. * @return {Node} A new instance of the specified node. * @throws {Error} If the given context is not registered. * @deprecated since 0.18.0, Use new Node constructor. @@ -197,9 +201,18 @@ let rcl = { nodeName, namespace = '', context = Context.defaultContext(), - options = NodeOptions.defaultOptions + options = NodeOptions.defaultOptions, + args = [], + useGlobalArguments = true ) { - return new this.Node(nodeName, namespace, context, options); + return new this.Node( + nodeName, + namespace, + context, + options, + args, + useGlobalArguments + ); }, /** diff --git a/lib/node.js b/lib/node.js index 9b6a1d53..bafe1af8 100644 --- a/lib/node.js +++ b/lib/node.js @@ -66,7 +66,9 @@ class Node extends rclnodejs.ShadowNode { nodeName, namespace = '', context = Context.defaultContext(), - options = NodeOptions.defaultOptions + options = NodeOptions.defaultOptions, + args = [], + useGlobalArguments = true ) { super(); @@ -74,7 +76,7 @@ class Node extends rclnodejs.ShadowNode { throw new TypeError('Invalid argument.'); } - this._init(nodeName, namespace, options, context); + this._init(nodeName, namespace, options, context, args, useGlobalArguments); debug( 'Finish initializing node, name = %s and namespace = %s.', nodeName, @@ -82,8 +84,14 @@ class Node extends rclnodejs.ShadowNode { ); } - _init(name, namespace, options, context) { - this.handle = rclnodejs.createNode(name, namespace, context.handle); + _init(name, namespace, options, context, args, useGlobalArguments) { + this.handle = rclnodejs.createNode( + name, + namespace, + context.handle, + args, + useGlobalArguments + ); Object.defineProperty(this, 'handle', { configurable: false, writable: false, diff --git a/src/rcl_context_bindings.cpp b/src/rcl_context_bindings.cpp index 8c6c5952..40dac48b 100644 --- a/src/rcl_context_bindings.cpp +++ b/src/rcl_context_bindings.cpp @@ -17,7 +17,6 @@ #include #include -#include #include // NOLINTNEXTLINE #include @@ -53,18 +52,9 @@ Napi::Value Init(const Napi::CallbackInfo& info) { // Preprocess argc & argv Napi::Array jsArgv = info[1].As(); - int argc = jsArgv.Length(); - char** argv = nullptr; - - if (argc > 0) { - argv = reinterpret_cast(malloc(argc * sizeof(char*))); - for (int i = 0; i < argc; i++) { - std::string arg = jsArgv.Get(i).As().Utf8Value(); - int len = arg.length() + 1; - argv[i] = reinterpret_cast(malloc(len * sizeof(char))); - snprintf(argv[i], len, "%s", arg.c_str()); - } - } + size_t argc = jsArgv.Length(); + char** argv = AbstractArgsFromNapiArray(jsArgv); + // Set up the domain id. size_t domain_id = RCL_DEFAULT_DOMAIN_ID; if (info.Length() > 2 && info[2].IsBigInt()) { @@ -87,11 +77,7 @@ Napi::Value Init(const Napi::CallbackInfo& info) { RCL_RET_OK, rcl_logging_configure(&context->global_arguments, &allocator), rcl_get_error_string().str); - for (int i = 0; i < argc; i++) { - free(argv[i]); - } - free(argv); - + RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); }); return env.Undefined(); } diff --git a/src/rcl_node_bindings.cpp b/src/rcl_node_bindings.cpp index 8138f999..c8dff982 100644 --- a/src/rcl_node_bindings.cpp +++ b/src/rcl_node_bindings.cpp @@ -24,6 +24,8 @@ #include #include +#include +// NOLINTNEXTLINE #include #include "macros.h" @@ -180,10 +182,34 @@ Napi::Value CreateNode(const Napi::CallbackInfo& info) { rcl_context_t* context = reinterpret_cast(context_handle->ptr()); - rcl_node_t* node = reinterpret_cast(malloc(sizeof(rcl_node_t))); + Napi::Array jsArgv = info[3].As(); + size_t argc = jsArgv.Length(); + char** argv = AbstractArgsFromNapiArray(jsArgv); + RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); }); + + rcl_arguments_t arguments = rcl_get_zero_initialized_arguments(); + rcl_ret_t ret = + rcl_parse_arguments(argc, argv, rcl_get_default_allocator(), &arguments); + if ((ret != RCL_RET_OK) || HasUnparsedROSArgs(arguments)) { + Napi::Error::New(env, "failed to parse arguments") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + RCPPUTILS_SCOPE_EXIT({ + if (RCL_RET_OK != rcl_arguments_fini(&arguments)) { + Napi::Error::New(env, "failed to fini arguments") + .ThrowAsJavaScriptException(); + rcl_reset_error(); + } + }); + bool use_global_arguments = info[4].As().Value(); + rcl_node_t* node = reinterpret_cast(malloc(sizeof(rcl_node_t))); *node = rcl_get_zero_initialized_node(); + rcl_node_options_t options = rcl_node_get_default_options(); + options.use_global_arguments = use_global_arguments; + options.arguments = arguments; THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK, rcl_node_init(node, node_name.c_str(), diff --git a/src/rcl_utilities.cpp b/src/rcl_utilities.cpp index f571d8b5..977c86f4 100644 --- a/src/rcl_utilities.cpp +++ b/src/rcl_utilities.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -260,4 +261,34 @@ Napi::Array ConvertToJSTopicEndpointInfoList( return list; } +char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv) { + size_t argc = jsArgv.Length(); + char** argv = nullptr; + + if (argc > 0) { + argv = reinterpret_cast(malloc(argc * sizeof(char*))); + for (size_t i = 0; i < argc; i++) { + std::string arg = jsArgv.Get(i).As().Utf8Value(); + int len = arg.length() + 1; + argv[i] = reinterpret_cast(malloc(len * sizeof(char))); + snprintf(argv[i], len, "%s", arg.c_str()); + } + } + return argv; +} + +void FreeArgs(char** argv, size_t argc) { + if (argv) { + for (size_t i = 0; i < argc; i++) { + free(argv[i]); + } + free(argv); + } +} + +bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args) { + int unparsed_ros_args_count = rcl_arguments_get_count_unparsed_ros(&rcl_args); + return unparsed_ros_args_count != 0; +} + } // namespace rclnodejs diff --git a/src/rcl_utilities.h b/src/rcl_utilities.h index a29fed64..5dde11ae 100644 --- a/src/rcl_utilities.h +++ b/src/rcl_utilities.h @@ -53,6 +53,13 @@ Napi::Array ConvertToJSTopicEndpointInfoList( Napi::Value ConvertToQoS(Napi::Env env, const rmw_qos_profile_t* qos_profile); +// `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs. +char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv); +// `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs. +void FreeArgs(char** argv, size_t argc); + +bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args); + } // namespace rclnodejs #endif // SRC_RCL_UTILITIES_H_ diff --git a/test/test-node.js b/test/test-node.js index 6637eda6..6be0b2e4 100644 --- a/test/test-node.js +++ b/test/test-node.js @@ -572,3 +572,85 @@ describe('Test the node with no handles attached when initializing', function () assert.notStrictEqual(node.getRMWImplementationIdentifier().length, 0); }); }); + +describe('Node arguments', function () { + this.timeout(60 * 1000); + + it('Test node arguments', async function () { + await rclnodejs.init(); + var node = rclnodejs.createNode( + 'publisher', + '/topic_getter', + Context.defaultContext(), + NodeOptions.defaultOptions, + ['--ros-args', '-r', '__ns:=/foo/bar'] + ); + assert.deepStrictEqual(node.namespace(), '/foo/bar'); + node.destroy(); + rclnodejs.shutdown(); + }); + + it('Test node global arguments', async function () { + await rclnodejs.init(Context.defaultContext(), [ + 'process_name', + '--ros-args', + '-r', + '__node:=global_node_name', + ]); + const node1 = rclnodejs.createNode( + 'publisher', + '/topic_getter', + Context.defaultContext(), + NodeOptions.defaultOptions, + ['--ros-args', '-r', '__ns:=/foo/bar'] + ); + + const node2 = rclnodejs.createNode( + 'my_node', + '/topic_getter', + Context.defaultContext(), + NodeOptions.defaultOptions, + [], + false + ); + + assert.deepStrictEqual(node1.name(), 'global_node_name'); + assert.deepStrictEqual(node2.name(), 'my_node'); + node1.destroy(); + node2.destroy(); + rclnodejs.shutdown(); + }); + + it('Test node invalid arguments', async function () { + await rclnodejs.init(); + assert.throws( + () => { + rclnodejs.createNode( + 'invalid_node1', + '/topic1', + Context.defaultContext(), + NodeOptions.defaultOptions, + ['--ros-args', '-r', 'not-a-remap'] + ); + }, + Error, + /failed to parse arguments/ + ); + + assert.throws( + () => { + rclnodejs.createNode( + 'invalid_node2', + '/topic2', + Context.defaultContext(), + NodeOptions.defaultOptions, + ['--ros-args', '--my-custom-flag'] + ); + }, + Error, + /failed to parse arguments/ + ); + + rclnodejs.shutdown(); + }); +}); diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index 69ef573e..43ae2400 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -82,6 +82,15 @@ expectType>( ); expectType(node.getFullyQualifiedName()); expectType(node.getRMWImplementationIdentifier()); +const nodeWithArgs = rclnodejs.createNode( + NODE_NAME, + 'topic', + rclnodejs.Context.defaultContext(), + rclnodejs.NodeOptions.defaultOptions, + ['--ros-args', '-r', '__ns:=/foo/bar'], + false +); +expectType(nodeWithArgs); // ---- LifecycleNode ---- const lifecycleNode = rclnodejs.createLifecycleNode(LIFECYCLE_NODE_NAME); diff --git a/types/index.d.ts b/types/index.d.ts index 603c0da0..40425052 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -11,6 +11,9 @@ declare module 'rclnodejs' { * @param namespace - The namespace used in ROS, default is an empty string. * @param context - The context, default is Context.defaultContext(). * @param options - The node options, default is NodeOptions.defaultOptions. + * @param args - The arguments to be passed to the node, default is an empty array. + * @param useGlobalArguments - If true, the node will use global arguments, default is true. + * If false, the node will not use global arguments. * @returns The new Node instance. * @deprecated since 0.18.0, Use new Node constructor. */ @@ -18,7 +21,9 @@ declare module 'rclnodejs' { nodeName: string, namespace?: string, context?: Context, - options?: NodeOptions + options?: NodeOptions, + args?: string[], + useGlobalArguments?: boolean ): Node; /**