Skip to content

Commit 3214793

Browse files
committed
Support creating node with ros args (#1166)
Adds support for passing ROS command-line arguments when creating nodes by extending both the native and JavaScript APIs and verifying behavior with new tests. - Introduced helper functions to marshal JS arrays into C-style `argv` and detect unparsed ROS args - Extended the C++ `CreateNode` binding and the JS `Node` and `createNode` wrappers to accept `args` and a `useGlobalArguments` flag - Added TypeScript and JavaScript tests covering normal remapping, global arguments, and invalid-argument error handling Fix: #1165
1 parent 4ba7dbb commit 3214793

File tree

9 files changed

+193
-26
lines changed

9 files changed

+193
-26
lines changed

index.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ let rcl = {
189189
* @param {string} [namespace=''] - The namespace used in ROS.
190190
* @param {Context} [context=Context.defaultContext()] - The context to create the node in.
191191
* @param {NodeOptions} [options=NodeOptions.defaultOptions] - The options to configure the new node behavior.
192+
* @param {Array} [args=[]] - The arguments to pass to the node.
193+
* @param {boolean} [useGlobalArguments=true] - If true, the node will use the global arguments
194+
* from the context, otherwise it will only use the arguments
195+
* passed in the args parameter.
192196
* @return {Node} A new instance of the specified node.
193197
* @throws {Error} If the given context is not registered.
194198
* @deprecated since 0.18.0, Use new Node constructor.
@@ -197,9 +201,18 @@ let rcl = {
197201
nodeName,
198202
namespace = '',
199203
context = Context.defaultContext(),
200-
options = NodeOptions.defaultOptions
204+
options = NodeOptions.defaultOptions,
205+
args = [],
206+
useGlobalArguments = true
201207
) {
202-
return new this.Node(nodeName, namespace, context, options);
208+
return new this.Node(
209+
nodeName,
210+
namespace,
211+
context,
212+
options,
213+
args,
214+
useGlobalArguments
215+
);
203216
},
204217

205218
/**

lib/node.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,24 +66,32 @@ class Node extends rclnodejs.ShadowNode {
6666
nodeName,
6767
namespace = '',
6868
context = Context.defaultContext(),
69-
options = NodeOptions.defaultOptions
69+
options = NodeOptions.defaultOptions,
70+
args = [],
71+
useGlobalArguments = true
7072
) {
7173
super();
7274

7375
if (typeof nodeName !== 'string' || typeof namespace !== 'string') {
7476
throw new TypeError('Invalid argument.');
7577
}
7678

77-
this._init(nodeName, namespace, options, context);
79+
this._init(nodeName, namespace, options, context, args, useGlobalArguments);
7880
debug(
7981
'Finish initializing node, name = %s and namespace = %s.',
8082
nodeName,
8183
namespace
8284
);
8385
}
8486

85-
_init(name, namespace, options, context) {
86-
this.handle = rclnodejs.createNode(name, namespace, context.handle);
87+
_init(name, namespace, options, context, args, useGlobalArguments) {
88+
this.handle = rclnodejs.createNode(
89+
name,
90+
namespace,
91+
context.handle,
92+
args,
93+
useGlobalArguments
94+
);
8795
Object.defineProperty(this, 'handle', {
8896
configurable: false,
8997
writable: false,

src/rcl_context_bindings.cpp

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include <rcl/logging.h>
1818
#include <rcl/rcl.h>
1919

20-
#include <cstdio>
2120
#include <rcpputils/scope_exit.hpp>
2221
// NOLINTNEXTLINE
2322
#include <string>
@@ -53,18 +52,9 @@ Napi::Value Init(const Napi::CallbackInfo& info) {
5352

5453
// Preprocess argc & argv
5554
Napi::Array jsArgv = info[1].As<Napi::Array>();
56-
int argc = jsArgv.Length();
57-
char** argv = nullptr;
58-
59-
if (argc > 0) {
60-
argv = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
61-
for (int i = 0; i < argc; i++) {
62-
std::string arg = jsArgv.Get(i).As<Napi::String>().Utf8Value();
63-
int len = arg.length() + 1;
64-
argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char)));
65-
snprintf(argv[i], len, "%s", arg.c_str());
66-
}
67-
}
55+
size_t argc = jsArgv.Length();
56+
char** argv = AbstractArgsFromNapiArray(jsArgv);
57+
6858
// Set up the domain id.
6959
size_t domain_id = RCL_DEFAULT_DOMAIN_ID;
7060
if (info.Length() > 2 && info[2].IsBigInt()) {
@@ -87,11 +77,7 @@ Napi::Value Init(const Napi::CallbackInfo& info) {
8777
RCL_RET_OK, rcl_logging_configure(&context->global_arguments, &allocator),
8878
rcl_get_error_string().str);
8979

90-
for (int i = 0; i < argc; i++) {
91-
free(argv[i]);
92-
}
93-
free(argv);
94-
80+
RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); });
9581
return env.Undefined();
9682
}
9783

src/rcl_node_bindings.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <rcl_yaml_param_parser/parser.h>
2525
#include <rcl_yaml_param_parser/types.h>
2626

27+
#include <rcpputils/scope_exit.hpp>
28+
// NOLINTNEXTLINE
2729
#include <string>
2830

2931
#include "macros.h"
@@ -180,10 +182,34 @@ Napi::Value CreateNode(const Napi::CallbackInfo& info) {
180182
rcl_context_t* context =
181183
reinterpret_cast<rcl_context_t*>(context_handle->ptr());
182184

183-
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(malloc(sizeof(rcl_node_t)));
185+
Napi::Array jsArgv = info[3].As<Napi::Array>();
186+
size_t argc = jsArgv.Length();
187+
char** argv = AbstractArgsFromNapiArray(jsArgv);
188+
RCPPUTILS_SCOPE_EXIT({ FreeArgs(argv, argc); });
189+
190+
rcl_arguments_t arguments = rcl_get_zero_initialized_arguments();
191+
rcl_ret_t ret =
192+
rcl_parse_arguments(argc, argv, rcl_get_default_allocator(), &arguments);
193+
if ((ret != RCL_RET_OK) || HasUnparsedROSArgs(arguments)) {
194+
Napi::Error::New(env, "failed to parse arguments")
195+
.ThrowAsJavaScriptException();
196+
return env.Undefined();
197+
}
184198

199+
RCPPUTILS_SCOPE_EXIT({
200+
if (RCL_RET_OK != rcl_arguments_fini(&arguments)) {
201+
Napi::Error::New(env, "failed to fini arguments")
202+
.ThrowAsJavaScriptException();
203+
rcl_reset_error();
204+
}
205+
});
206+
bool use_global_arguments = info[4].As<Napi::Boolean>().Value();
207+
rcl_node_t* node = reinterpret_cast<rcl_node_t*>(malloc(sizeof(rcl_node_t)));
185208
*node = rcl_get_zero_initialized_node();
209+
186210
rcl_node_options_t options = rcl_node_get_default_options();
211+
options.use_global_arguments = use_global_arguments;
212+
options.arguments = arguments;
187213

188214
THROW_ERROR_IF_NOT_EQUAL(RCL_RET_OK,
189215
rcl_node_init(node, node_name.c_str(),

src/rcl_utilities.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <rmw/topic_endpoint_info.h>
2020
#include <uv.h>
2121

22+
#include <cstdio>
2223
#include <memory>
2324
#include <string>
2425

@@ -260,4 +261,34 @@ Napi::Array ConvertToJSTopicEndpointInfoList(
260261
return list;
261262
}
262263

264+
char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv) {
265+
size_t argc = jsArgv.Length();
266+
char** argv = nullptr;
267+
268+
if (argc > 0) {
269+
argv = reinterpret_cast<char**>(malloc(argc * sizeof(char*)));
270+
for (size_t i = 0; i < argc; i++) {
271+
std::string arg = jsArgv.Get(i).As<Napi::String>().Utf8Value();
272+
int len = arg.length() + 1;
273+
argv[i] = reinterpret_cast<char*>(malloc(len * sizeof(char)));
274+
snprintf(argv[i], len, "%s", arg.c_str());
275+
}
276+
}
277+
return argv;
278+
}
279+
280+
void FreeArgs(char** argv, size_t argc) {
281+
if (argv) {
282+
for (size_t i = 0; i < argc; i++) {
283+
free(argv[i]);
284+
}
285+
free(argv);
286+
}
287+
}
288+
289+
bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args) {
290+
int unparsed_ros_args_count = rcl_arguments_get_count_unparsed_ros(&rcl_args);
291+
return unparsed_ros_args_count != 0;
292+
}
293+
263294
} // namespace rclnodejs

src/rcl_utilities.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ Napi::Array ConvertToJSTopicEndpointInfoList(
5353

5454
Napi::Value ConvertToQoS(Napi::Env env, const rmw_qos_profile_t* qos_profile);
5555

56+
// `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs.
57+
char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv);
58+
// `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs.
59+
void FreeArgs(char** argv, size_t argc);
60+
61+
bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args);
62+
5663
} // namespace rclnodejs
5764

5865
#endif // SRC_RCL_UTILITIES_H_

test/test-node.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,3 +572,85 @@ describe('Test the node with no handles attached when initializing', function ()
572572
assert.notStrictEqual(node.getRMWImplementationIdentifier().length, 0);
573573
});
574574
});
575+
576+
describe('Node arguments', function () {
577+
this.timeout(60 * 1000);
578+
579+
it('Test node arguments', async function () {
580+
await rclnodejs.init();
581+
var node = rclnodejs.createNode(
582+
'publisher',
583+
'/topic_getter',
584+
Context.defaultContext(),
585+
NodeOptions.defaultOptions,
586+
['--ros-args', '-r', '__ns:=/foo/bar']
587+
);
588+
assert.deepStrictEqual(node.namespace(), '/foo/bar');
589+
node.destroy();
590+
rclnodejs.shutdown();
591+
});
592+
593+
it('Test node global arguments', async function () {
594+
await rclnodejs.init(Context.defaultContext(), [
595+
'process_name',
596+
'--ros-args',
597+
'-r',
598+
'__node:=global_node_name',
599+
]);
600+
const node1 = rclnodejs.createNode(
601+
'publisher',
602+
'/topic_getter',
603+
Context.defaultContext(),
604+
NodeOptions.defaultOptions,
605+
['--ros-args', '-r', '__ns:=/foo/bar']
606+
);
607+
608+
const node2 = rclnodejs.createNode(
609+
'my_node',
610+
'/topic_getter',
611+
Context.defaultContext(),
612+
NodeOptions.defaultOptions,
613+
[],
614+
false
615+
);
616+
617+
assert.deepStrictEqual(node1.name(), 'global_node_name');
618+
assert.deepStrictEqual(node2.name(), 'my_node');
619+
node1.destroy();
620+
node2.destroy();
621+
rclnodejs.shutdown();
622+
});
623+
624+
it('Test node invalid arguments', async function () {
625+
await rclnodejs.init();
626+
assert.throws(
627+
() => {
628+
rclnodejs.createNode(
629+
'invalid_node1',
630+
'/topic1',
631+
Context.defaultContext(),
632+
NodeOptions.defaultOptions,
633+
['--ros-args', '-r', 'not-a-remap']
634+
);
635+
},
636+
Error,
637+
/failed to parse arguments/
638+
);
639+
640+
assert.throws(
641+
() => {
642+
rclnodejs.createNode(
643+
'invalid_node2',
644+
'/topic2',
645+
Context.defaultContext(),
646+
NodeOptions.defaultOptions,
647+
['--ros-args', '--my-custom-flag']
648+
);
649+
},
650+
Error,
651+
/failed to parse arguments/
652+
);
653+
654+
rclnodejs.shutdown();
655+
});
656+
});

test/types/index.test-d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@ expectType<rclnodejs.Options<string | rclnodejs.QoS>>(
8282
);
8383
expectType<string>(node.getFullyQualifiedName());
8484
expectType<string>(node.getRMWImplementationIdentifier());
85+
const nodeWithArgs = rclnodejs.createNode(
86+
NODE_NAME,
87+
'topic',
88+
rclnodejs.Context.defaultContext(),
89+
rclnodejs.NodeOptions.defaultOptions,
90+
['--ros-args', '-r', '__ns:=/foo/bar'],
91+
false
92+
);
93+
expectType<rclnodejs.Node>(nodeWithArgs);
8594

8695
// ---- LifecycleNode ----
8796
const lifecycleNode = rclnodejs.createLifecycleNode(LIFECYCLE_NODE_NAME);

types/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ declare module 'rclnodejs' {
1111
* @param namespace - The namespace used in ROS, default is an empty string.
1212
* @param context - The context, default is Context.defaultContext().
1313
* @param options - The node options, default is NodeOptions.defaultOptions.
14+
* @param args - The arguments to be passed to the node, default is an empty array.
15+
* @param useGlobalArguments - If true, the node will use global arguments, default is true.
16+
* If false, the node will not use global arguments.
1417
* @returns The new Node instance.
1518
* @deprecated since 0.18.0, Use new Node constructor.
1619
*/
1720
function createNode(
1821
nodeName: string,
1922
namespace?: string,
2023
context?: Context,
21-
options?: NodeOptions
24+
options?: NodeOptions,
25+
args?: string[],
26+
useGlobalArguments?: boolean
2227
): Node;
2328

2429
/**

0 commit comments

Comments
 (0)