rclnodejs 1.8.2 → 1.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +46 -37
  2. package/index.js +62 -23
  3. package/lib/action/client.js +67 -3
  4. package/lib/action/server.js +1 -3
  5. package/lib/distro.js +2 -1
  6. package/lib/lifecycle_publisher.js +2 -2
  7. package/lib/message_info.js +94 -0
  8. package/lib/node.js +90 -14
  9. package/lib/parameter.js +5 -9
  10. package/lib/parameter_event_handler.js +468 -0
  11. package/lib/parameter_watcher.js +12 -12
  12. package/lib/service.js +8 -4
  13. package/lib/subscription.js +38 -5
  14. package/lib/time_source.js +3 -20
  15. package/lib/timer.js +2 -1
  16. package/lib/wait_for_message.js +111 -0
  17. package/package.json +7 -4
  18. package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
  19. package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
  20. package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
  21. package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
  22. package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
  23. package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
  24. package/rosidl_gen/generate_worker.js +3 -13
  25. package/rosidl_gen/idl_generator.js +210 -0
  26. package/rosidl_gen/index.js +3 -12
  27. package/rosidl_gen/packages.js +1 -3
  28. package/rosidl_gen/primitive_types.js +2 -2
  29. package/rosidl_parser/idl_parser.py +437 -0
  30. package/rosidl_parser/parser.py +2 -4
  31. package/rosidl_parser/rosidl_parser.js +27 -0
  32. package/scripts/run_asan_test.sh +118 -0
  33. package/src/executor.cpp +37 -2
  34. package/src/executor.h +11 -0
  35. package/src/macros.h +2 -2
  36. package/src/rcl_action_client_bindings.cpp +88 -12
  37. package/src/rcl_action_server_bindings.cpp +24 -13
  38. package/src/rcl_client_bindings.cpp +13 -5
  39. package/src/rcl_context_bindings.cpp +10 -11
  40. package/src/rcl_graph_bindings.cpp +2 -2
  41. package/src/rcl_guard_condition_bindings.cpp +12 -3
  42. package/src/rcl_lifecycle_bindings.cpp +34 -15
  43. package/src/rcl_node_bindings.cpp +11 -4
  44. package/src/rcl_publisher_bindings.cpp +12 -3
  45. package/src/rcl_service_bindings.cpp +12 -3
  46. package/src/rcl_subscription_bindings.cpp +92 -21
  47. package/src/rcl_timer_bindings.cpp +24 -9
  48. package/src/rcl_type_description_service_bindings.cpp +9 -1
  49. package/src/rcl_utilities.cpp +2 -2
  50. package/tools/jsdoc/Makefile +5 -0
  51. package/tools/jsdoc/README.md +96 -0
  52. package/tools/jsdoc/build-index.js +610 -0
  53. package/tools/jsdoc/publish.js +854 -0
  54. package/tools/jsdoc/regenerate-published-docs.js +605 -0
  55. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
  56. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
  57. package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
  58. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
  59. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
  60. package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
  61. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
  62. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
  63. package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
  64. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
  65. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
  66. package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
  67. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
  68. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
  69. package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
  70. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
  71. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
  72. package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
  73. package/tools/jsdoc/static/scripts/linenumber.js +25 -0
  74. package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  75. package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
  76. package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
  77. package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
  78. package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
  79. package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
  80. package/tools/jsdoc/tmpl/augments.tmpl +10 -0
  81. package/tools/jsdoc/tmpl/container.tmpl +193 -0
  82. package/tools/jsdoc/tmpl/details.tmpl +143 -0
  83. package/tools/jsdoc/tmpl/example.tmpl +2 -0
  84. package/tools/jsdoc/tmpl/examples.tmpl +13 -0
  85. package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
  86. package/tools/jsdoc/tmpl/layout.tmpl +83 -0
  87. package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
  88. package/tools/jsdoc/tmpl/members.tmpl +43 -0
  89. package/tools/jsdoc/tmpl/method.tmpl +124 -0
  90. package/tools/jsdoc/tmpl/params.tmpl +133 -0
  91. package/tools/jsdoc/tmpl/properties.tmpl +110 -0
  92. package/tools/jsdoc/tmpl/returns.tmpl +12 -0
  93. package/tools/jsdoc/tmpl/source.tmpl +8 -0
  94. package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
  95. package/tools/jsdoc/tmpl/type.tmpl +7 -0
  96. package/types/action_client.d.ts +8 -0
  97. package/types/index.d.ts +34 -0
  98. package/types/message_info.d.ts +72 -0
  99. package/types/node.d.ts +21 -0
  100. package/types/parameter_event_handler.d.ts +139 -0
  101. package/types/subscription.d.ts +14 -2
  102. package/rosidl_convertor/README.md +0 -298
  103. package/rosidl_convertor/idl_convertor.js +0 -50
  104. package/rosidl_convertor/idl_convertor.py +0 -1250
  105. package/test_data_integrity.js +0 -108
  106. package/test_repro_exact.js +0 -57
  107. package/test_repro_hz.js +0 -86
  108. package/test_repro_pub.js +0 -36
  109. package/test_repro_stress.js +0 -83
  110. package/test_repro_sub.js +0 -64
  111. package/test_xproc_data.js +0 -64
  112. package/types/interfaces.d.ts +0 -8895
@@ -150,9 +150,7 @@ async function generateMsgForSrv(filePath, interfaceInfo, pkgMap) {
150
150
  const arr = data.split(/-{3,}/);
151
151
  if (arr.length == 2) {
152
152
  const packagePath = path.join(serviceMsgPath, interfaceInfo.pkgName);
153
- if (!fs.existsSync(packagePath)) {
154
- fs.mkdirSync(packagePath);
155
- }
153
+ fs.mkdirSync(packagePath, { recursive: true });
156
154
 
157
155
  await fsp.writeFile(path.join(packagePath, requestMsgName), arr[0]);
158
156
  await fsp.writeFile(path.join(packagePath, responseMsgName), arr[1]);
@@ -26,14 +26,14 @@ const StringRefStruct = StructType({
26
26
 
27
27
  function initString(str, own = false) {
28
28
  if (own) {
29
- if ((!str) instanceof Buffer) {
29
+ if (!(str instanceof Buffer)) {
30
30
  throw new TypeError(
31
31
  'Invalid argument: should provide a Node Buffer to bindingsStringInit()'
32
32
  );
33
33
  }
34
34
  rclnodejs.initString(str);
35
35
  } else {
36
- if ((!str) instanceof StringRefStruct) {
36
+ if (!(str instanceof StringRefStruct)) {
37
37
  throw new TypeError(
38
38
  'Invalid argument: should provide a type of StringRefStruct'
39
39
  );
@@ -0,0 +1,437 @@
1
+ # Copyright (c) 2025, The Robot Web Tools Contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ IDL file parser that uses rosidl_parser to directly parse .idl files and
17
+ produce the same JSON spec format as parser.py does for .msg files.
18
+
19
+ This eliminates the need for the intermediate .idl -> .msg conversion step.
20
+ """
21
+
22
+ import sys
23
+ import json
24
+
25
+ from rosidl_parser.parser import parse_idl_string
26
+ from rosidl_parser.definition import (
27
+ AbstractNestedType,
28
+ Action,
29
+ Array,
30
+ BasicType,
31
+ BoundedSequence,
32
+ BoundedString,
33
+ BoundedWString,
34
+ Message,
35
+ NamedType,
36
+ NamespacedType,
37
+ Service,
38
+ UnboundedSequence,
39
+ UnboundedString,
40
+ UnboundedWString,
41
+ )
42
+
43
+ # Mapping from IDL basic type names to ROS2 message type names
44
+ IDL_TYPE_TO_ROS2 = {
45
+ 'boolean': 'bool',
46
+ 'octet': 'byte',
47
+ 'char': 'char',
48
+ 'wchar': 'wchar',
49
+ 'int8': 'int8',
50
+ 'uint8': 'uint8',
51
+ 'int16': 'int16',
52
+ 'uint16': 'uint16',
53
+ 'int32': 'int32',
54
+ 'uint32': 'uint32',
55
+ 'int64': 'int64',
56
+ 'uint64': 'uint64',
57
+ 'float': 'float32',
58
+ 'double': 'float64',
59
+ 'long double': 'float64',
60
+ 'short': 'int16',
61
+ 'long': 'int32',
62
+ 'long long': 'int64',
63
+ 'unsigned short': 'uint16',
64
+ 'unsigned long': 'uint32',
65
+ 'unsigned long long': 'uint64',
66
+ }
67
+
68
+
69
+ def _map_basic_type(typename):
70
+ """Map an IDL basic type name to its ROS2 equivalent."""
71
+ return IDL_TYPE_TO_ROS2.get(typename, typename)
72
+
73
+
74
+ def _get_nestable_type_info(type_obj):
75
+ """
76
+ Extract type info from a nestable type (the inner type for arrays/sequences,
77
+ or the type itself for non-nested types).
78
+
79
+ Returns a dict with: pkgName, type, stringUpperBound, isPrimitiveType
80
+ """
81
+ if isinstance(type_obj, BasicType):
82
+ ros2_type = _map_basic_type(type_obj.typename)
83
+ return {
84
+ 'pkgName': None,
85
+ 'type': ros2_type,
86
+ 'stringUpperBound': None,
87
+ 'isPrimitiveType': True,
88
+ }
89
+ elif isinstance(type_obj, (UnboundedString, BoundedString)):
90
+ string_upper_bound = None
91
+ if isinstance(type_obj, BoundedString):
92
+ string_upper_bound = type_obj.maximum_size
93
+ return {
94
+ 'pkgName': None,
95
+ 'type': 'string',
96
+ 'stringUpperBound': string_upper_bound,
97
+ 'isPrimitiveType': True,
98
+ }
99
+ elif isinstance(type_obj, (UnboundedWString, BoundedWString)):
100
+ string_upper_bound = None
101
+ if isinstance(type_obj, BoundedWString):
102
+ string_upper_bound = type_obj.maximum_size
103
+ return {
104
+ 'pkgName': None,
105
+ 'type': 'wstring',
106
+ 'stringUpperBound': string_upper_bound,
107
+ 'isPrimitiveType': True,
108
+ }
109
+ elif isinstance(type_obj, NamespacedType):
110
+ # Convert Token objects to plain strings if needed
111
+ namespaces = [str(ns) for ns in type_obj.namespaces]
112
+ pkg_name = namespaces[0] if namespaces else None
113
+ return {
114
+ 'pkgName': pkg_name,
115
+ 'type': str(type_obj.name),
116
+ 'stringUpperBound': None,
117
+ 'isPrimitiveType': False,
118
+ }
119
+ elif isinstance(type_obj, NamedType):
120
+ # NamedType is typically resolved by typedefs, but handle it just in case
121
+ return {
122
+ 'pkgName': None,
123
+ 'type': str(type_obj.name),
124
+ 'stringUpperBound': None,
125
+ 'isPrimitiveType': False,
126
+ }
127
+ else:
128
+ raise ValueError(f'Unexpected nestable type: {type(type_obj).__name__}')
129
+
130
+
131
+ def _get_type_json(member_type):
132
+ """
133
+ Convert a rosidl_parser type object to the JSON format expected by
134
+ the rclnodejs templates.
135
+
136
+ Returns a dict matching the spec.fields[].type format:
137
+ {isArray, arraySize, isUpperBound, isDynamicArray, isFixedSizeArray,
138
+ pkgName, type, stringUpperBound, isPrimitiveType}
139
+ """
140
+ if isinstance(member_type, Array):
141
+ # Fixed-size array: e.g., float64[9]
142
+ inner_info = _get_nestable_type_info(member_type.value_type)
143
+ return {
144
+ 'isArray': True,
145
+ 'arraySize': member_type.size,
146
+ 'isUpperBound': False,
147
+ 'isDynamicArray': False,
148
+ 'isFixedSizeArray': True,
149
+ **inner_info,
150
+ }
151
+ elif isinstance(member_type, BoundedSequence):
152
+ # Bounded dynamic array: e.g., sequence<float64, 3>
153
+ inner_info = _get_nestable_type_info(member_type.value_type)
154
+ return {
155
+ 'isArray': True,
156
+ 'arraySize': member_type.maximum_size,
157
+ 'isUpperBound': True,
158
+ 'isDynamicArray': True,
159
+ 'isFixedSizeArray': False,
160
+ **inner_info,
161
+ }
162
+ elif isinstance(member_type, UnboundedSequence):
163
+ # Unbounded dynamic array: e.g., sequence<float64>
164
+ inner_info = _get_nestable_type_info(member_type.value_type)
165
+ return {
166
+ 'isArray': True,
167
+ 'arraySize': None,
168
+ 'isUpperBound': False,
169
+ 'isDynamicArray': True,
170
+ 'isFixedSizeArray': False,
171
+ **inner_info,
172
+ }
173
+ else:
174
+ # Non-array/sequence type
175
+ inner_info = _get_nestable_type_info(member_type)
176
+ return {
177
+ 'isArray': False,
178
+ 'arraySize': None,
179
+ 'isUpperBound': False,
180
+ 'isDynamicArray': False,
181
+ 'isFixedSizeArray': False,
182
+ **inner_info,
183
+ }
184
+
185
+
186
+ def _get_default_value(member):
187
+ """Extract default value from member annotations.
188
+
189
+ rosidl_parser returns array/sequence defaults as tuple-like strings,
190
+ e.g., "(0, 127, -128)" or "('', 'max value', 'min value')".
191
+ The .msg parser returns these as Python lists [0, 127, -128].
192
+ We need to convert the tuple-strings to proper lists.
193
+ """
194
+ try:
195
+ default_annotation = member.get_annotation_value('default')
196
+ if isinstance(default_annotation, dict) and 'value' in default_annotation:
197
+ value = default_annotation['value']
198
+ else:
199
+ value = default_annotation
200
+
201
+ # If the value is a string that looks like a tuple "(a, b, c)",
202
+ # parse it into a Python list
203
+ if isinstance(value, str) and value.startswith('(') and value.endswith(')'):
204
+ value = _parse_tuple_string(value, member)
205
+
206
+ return value
207
+ except ValueError:
208
+ return None
209
+
210
+
211
+ def _parse_tuple_string(s, member):
212
+ """
213
+ Parse a tuple-like string from rosidl_parser into a proper Python list.
214
+
215
+ Handles formats like:
216
+ "(0, 127, -128)" -> [0, 127, -128]
217
+ "(1.125, 0.0, -1.125)" -> [1.125, 0.0, -1.125]
218
+ "(False, True, False)" -> [False, True, False]
219
+ "('', 'max value', 'min value')" -> ['', 'max value', 'min value']
220
+ """
221
+ inner = s[1:-1] # Strip outer parentheses
222
+
223
+ # Determine the element type from the member's type
224
+ member_type = member.type
225
+ if isinstance(member_type, AbstractNestedType):
226
+ element_type = member_type.value_type
227
+ else:
228
+ element_type = member_type
229
+
230
+ # Parse string arrays specially (may contain commas in values)
231
+ if isinstance(element_type, (UnboundedString, BoundedString,
232
+ UnboundedWString, BoundedWString)):
233
+ return _parse_string_tuple(inner)
234
+
235
+ # For boolean arrays
236
+ if isinstance(element_type, BasicType) and element_type.typename == 'boolean':
237
+ parts = [p.strip() for p in inner.split(',')]
238
+ return [p == 'True' for p in parts]
239
+
240
+ # For numeric arrays
241
+ parts = [p.strip() for p in inner.split(',')]
242
+ result = []
243
+ for part in parts:
244
+ if not part:
245
+ continue
246
+ # Try int first, then float
247
+ try:
248
+ result.append(int(part))
249
+ except ValueError:
250
+ try:
251
+ result.append(float(part))
252
+ except ValueError:
253
+ result.append(part)
254
+ return result
255
+
256
+
257
+ def _parse_string_tuple(inner):
258
+ """Parse the inside of a tuple containing quoted strings.
259
+
260
+ e.g., "'', 'max value', 'min value'" -> ['', 'max value', 'min value']
261
+ """
262
+ result = []
263
+ i = 0
264
+ while i < len(inner):
265
+ if inner[i] == "'":
266
+ # Find matching closing quote
267
+ j = i + 1
268
+ while j < len(inner):
269
+ if inner[j] == "'":
270
+ result.append(inner[i + 1:j])
271
+ i = j + 1
272
+ break
273
+ j += 1
274
+ else:
275
+ break
276
+ elif inner[i] == '"':
277
+ j = i + 1
278
+ while j < len(inner):
279
+ if inner[j] == '"':
280
+ result.append(inner[i + 1:j])
281
+ i = j + 1
282
+ break
283
+ j += 1
284
+ else:
285
+ break
286
+ else:
287
+ i += 1
288
+ return result
289
+
290
+
291
+ def _get_constant_type_str(constant):
292
+ """Get the ROS2 type string for a constant."""
293
+ if isinstance(constant.type, BasicType):
294
+ return _map_basic_type(constant.type.typename)
295
+ elif isinstance(constant.type, (UnboundedString, BoundedString)):
296
+ return 'string'
297
+ elif isinstance(constant.type, (UnboundedWString, BoundedWString)):
298
+ return 'wstring'
299
+ return str(constant.type)
300
+
301
+
302
+ def get_json_from_message(msg, pkg_name=None):
303
+ """
304
+ Convert a rosidl_parser Message object to JSON spec matching
305
+ what parser.py produces for .msg files.
306
+ """
307
+ namespaced_type = msg.structure.namespaced_type
308
+ namespaces = [str(ns) for ns in namespaced_type.namespaces]
309
+ msg_name = str(namespaced_type.name)
310
+
311
+ if pkg_name is None:
312
+ pkg_name = namespaces[0] if namespaces else ''
313
+
314
+ # Build fields
315
+ fields = []
316
+ for member in msg.structure.members:
317
+ field = {
318
+ 'name': member.name,
319
+ 'type': _get_type_json(member.type),
320
+ 'default_value': _get_default_value(member),
321
+ }
322
+ fields.append(field)
323
+
324
+ # Build constants
325
+ constants = []
326
+ for constant in msg.constants:
327
+ constants.append({
328
+ 'type': _get_constant_type_str(constant),
329
+ 'name': constant.name,
330
+ 'value': constant.value,
331
+ })
332
+
333
+ return {
334
+ 'constants': constants,
335
+ 'fields': fields,
336
+ 'baseType': {
337
+ 'pkgName': pkg_name,
338
+ 'type': msg_name,
339
+ 'stringUpperBound': None,
340
+ 'isPrimitiveType': False,
341
+ },
342
+ 'msgName': msg_name,
343
+ }
344
+
345
+
346
+ def get_json_from_service(srv):
347
+ """
348
+ Convert a rosidl_parser Service object to JSON spec matching
349
+ what parser.py produces for .srv files.
350
+ """
351
+ namespaces = [str(ns) for ns in srv.namespaced_type.namespaces]
352
+ pkg_name = namespaces[0] if namespaces else ''
353
+ srv_name = str(srv.namespaced_type.name)
354
+
355
+ return {
356
+ 'pkgName': pkg_name,
357
+ 'srvName': srv_name,
358
+ 'request': get_json_from_message(srv.request_message, pkg_name),
359
+ 'response': get_json_from_message(srv.response_message, pkg_name),
360
+ }
361
+
362
+
363
+ def get_json_from_action(action):
364
+ """
365
+ Convert a rosidl_parser Action object to JSON spec matching
366
+ what parser.py produces for .action files.
367
+ """
368
+ namespaces = [str(ns) for ns in action.namespaced_type.namespaces]
369
+ pkg_name = namespaces[0] if namespaces else ''
370
+ action_name = str(action.namespaced_type.name)
371
+
372
+ return {
373
+ 'pkgName': pkg_name,
374
+ 'actionName': action_name,
375
+ 'goal': get_json_from_message(action.goal, pkg_name),
376
+ 'result': get_json_from_message(action.result, pkg_name),
377
+ 'feedback': get_json_from_message(action.feedback, pkg_name),
378
+ }
379
+
380
+
381
+ def parse_idl_file(idl_file_path):
382
+ """
383
+ Parse an .idl file and return a JSON object in the same format
384
+ as parser.py produces.
385
+
386
+ The return value depends on the interface type:
387
+ - Message: same format as parse_message_file
388
+ - Service: same format as parse_service_file
389
+ - Action: same format as parse_action_file
390
+
391
+ Returns a dict with 'type' indicating the interface kind and 'spec'
392
+ containing the JSON spec.
393
+ """
394
+ with open(idl_file_path, 'r') as f:
395
+ content_str = f.read()
396
+
397
+ content = parse_idl_string(content_str)
398
+
399
+ # Find the main element (Message, Service, or Action)
400
+ messages = content.get_elements_of_type(Message)
401
+ services = content.get_elements_of_type(Service)
402
+ actions = content.get_elements_of_type(Action)
403
+
404
+ if actions:
405
+ action = actions[0]
406
+ return {
407
+ 'type': 'action',
408
+ 'spec': get_json_from_action(action),
409
+ }
410
+ elif services:
411
+ service = services[0]
412
+ return {
413
+ 'type': 'service',
414
+ 'spec': get_json_from_service(service),
415
+ }
416
+ elif messages:
417
+ message = messages[0]
418
+ return {
419
+ 'type': 'message',
420
+ 'spec': get_json_from_message(message),
421
+ }
422
+ else:
423
+ raise ValueError(f'No Message, Service, or Action found in {idl_file_path}')
424
+
425
+
426
+ if __name__ == '__main__':
427
+ if len(sys.argv) < 2:
428
+ print('Usage: idl_parser.py <idl_file_path>', file=sys.stderr)
429
+ sys.exit(1)
430
+
431
+ try:
432
+ result = parse_idl_file(sys.argv[1])
433
+ print(json.dumps(result))
434
+ sys.exit(0)
435
+ except Exception as e:
436
+ print(str(e), file=sys.stderr)
437
+ sys.exit(1)
@@ -15,7 +15,6 @@
15
15
  import sys
16
16
  import json
17
17
 
18
- import rosidl_parser
19
18
  from rosidl_adapter import parser
20
19
 
21
20
  def get_json_object_from_base_type_object(base_type_obj):
@@ -41,15 +40,14 @@ def get_json_object_from_msg_spec_object(msg_spec_object):
41
40
  for constant in msg_spec_object.constants:
42
41
  constants.append({'type': constant.type, 'name': constant.name, 'value': constant.value})
43
42
 
44
- msg_name = {'msgName': msg_spec_object.msg_name}
45
43
  json_obj = {'constants': constants, 'fields': fields, 'baseType': get_json_object_from_base_type_object(msg_spec_object.base_type),
46
44
  'msgName': msg_spec_object.msg_name}
47
45
 
48
46
  return json_obj
49
47
 
50
48
  if __name__ == '__main__':
51
- if len(sys.argv) < 3:
52
- print('Wrong number of argments')
49
+ if len(sys.argv) < 4:
50
+ print('Usage: {} <command> <packageName> <filePath>'.format(sys.argv[0]), file=sys.stderr)
53
51
  sys.exit(1)
54
52
  try:
55
53
  parser_method = getattr(parser, sys.argv[1])
@@ -41,6 +41,33 @@ const rosidlParser = {
41
41
  return this._parseFile('parse_action_file', packageName, filePath);
42
42
  },
43
43
 
44
+ /**
45
+ * Parse an .idl file directly using rosidl_parser (no .msg conversion needed).
46
+ * Returns an object with { type: 'message'|'service'|'action', spec: ... }
47
+ * where spec matches the same format as parseMessageFile/parseServiceFile/parseActionFile.
48
+ */
49
+ parseIdlFile(filePath) {
50
+ return new Promise((resolve, reject) => {
51
+ const args = [path.join(__dirname, 'idl_parser.py'), filePath];
52
+ const [pythonExecutableFile, pythonExecutableArgs] = pythonExecutable;
53
+ execFile(
54
+ pythonExecutableFile,
55
+ pythonExecutableArgs.concat(args),
56
+ (err, stdout, stderr) => {
57
+ if (err) {
58
+ reject(
59
+ new Error(
60
+ `There was an error parsing IDL file "${filePath}": "${err}"; stderr was: ${stderr}`
61
+ )
62
+ );
63
+ } else {
64
+ resolve(this._parseJSONObject(stdout));
65
+ }
66
+ }
67
+ );
68
+ });
69
+ },
70
+
44
71
  _parseJSONObject(str) {
45
72
  // For nodejs >= `contextSupportedVersion`, we leverage context parameter to
46
73
  // convert unsafe integer to string, otherwise, json-bigint is used.
@@ -0,0 +1,118 @@
1
+ #!/bin/bash
2
+ # Run tests with AddressSanitizer (ASan/LSan) to detect memory leaks and errors
3
+ # in the native N-API addon.
4
+ #
5
+ # Requires: ROS2 sourced, g++ with libasan
6
+ #
7
+ # Usage:
8
+ # bash scripts/run_asan_test.sh # rebuild + run all tests
9
+ # bash scripts/run_asan_test.sh test/test-node.js # rebuild + run specific test(s)
10
+ # bash scripts/run_asan_test.sh --no-build test/test-node.js # skip rebuild
11
+ # bash scripts/run_asan_test.sh --exclude test/test-serialization.js # exclude specific test(s)
12
+
13
+ set -e
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
17
+ cd "$PROJECT_DIR"
18
+
19
+ # Parse flags
20
+ DO_BUILD=1
21
+ ARGS=()
22
+ EXCLUDES=()
23
+ NEXT_IS_EXCLUDE=0
24
+ for arg in "$@"; do
25
+ if [[ $NEXT_IS_EXCLUDE -eq 1 ]]; then
26
+ EXCLUDES+=("$arg")
27
+ NEXT_IS_EXCLUDE=0
28
+ elif [[ "$arg" == "--no-build" ]]; then
29
+ DO_BUILD=0
30
+ elif [[ "$arg" == "--exclude" ]]; then
31
+ NEXT_IS_EXCLUDE=1
32
+ else
33
+ ARGS+=("$arg")
34
+ fi
35
+ done
36
+
37
+ if [[ $NEXT_IS_EXCLUDE -eq 1 ]]; then
38
+ echo "Error: --exclude requires a test file argument"
39
+ echo "Usage: bash scripts/run_asan_test.sh --exclude test/test-foo.js"
40
+ exit 1
41
+ fi
42
+
43
+ # Step 1: Build with ASan
44
+ if [[ $DO_BUILD -eq 1 ]]; then
45
+ echo "=== Building with AddressSanitizer ==="
46
+ CXXFLAGS="-fsanitize=address" npx node-gyp -j 16 rebuild --debug
47
+ fi
48
+
49
+ # Step 2: Locate libasan
50
+ LIBASAN=$(g++ -print-file-name=libasan.so)
51
+ if [[ "$LIBASAN" == "libasan.so" ]]; then
52
+ # g++ returned just the name, try to find the full path
53
+ LIBASAN=$(find /usr/lib* -name "libasan.so*" -type f 2>/dev/null | head -1)
54
+ fi
55
+
56
+ if [[ -z "$LIBASAN" || ! -f "$LIBASAN" ]]; then
57
+ echo "Warning: Could not find libasan.so, proceeding without LD_PRELOAD"
58
+ echo "If you see 'ASan not the first DSO' errors, install libasan: sudo apt install libasan6"
59
+ LIBASAN=""
60
+ fi
61
+
62
+ # Step 3: Set up environment
63
+ export LSAN_OPTIONS="suppressions=$PROJECT_DIR/suppr.txt"
64
+ if [[ -n "$LIBASAN" ]]; then
65
+ export LD_PRELOAD="$LIBASAN"
66
+ fi
67
+
68
+ # Step 3.5: Temporarily hide prebuilds so the debug build is loaded
69
+ PREBUILDS_DIR="$PROJECT_DIR/prebuilds"
70
+ PREBUILDS_BAK="$PROJECT_DIR/.prebuilds_asan_bak"
71
+ MOVED_PREBUILDS=0
72
+ if [[ -d "$PREBUILDS_DIR" ]]; then
73
+ if [[ -d "$PREBUILDS_BAK" ]]; then
74
+ echo "Error: $PREBUILDS_BAK already exists (previous interrupted run?)."
75
+ echo "Please remove it manually and retry: rm -rf $PREBUILDS_BAK"
76
+ exit 1
77
+ fi
78
+ mv "$PREBUILDS_DIR" "$PREBUILDS_BAK"
79
+ MOVED_PREBUILDS=1
80
+ fi
81
+
82
+ # Restore prebuilds on exit (even on failure)
83
+ cleanup() {
84
+ if [[ $MOVED_PREBUILDS -eq 1 && -d "$PREBUILDS_BAK" ]]; then
85
+ mv "$PREBUILDS_BAK" "$PREBUILDS_DIR"
86
+ fi
87
+ }
88
+ trap cleanup EXIT
89
+
90
+ # Step 4: Build mocha exclude args from --exclude flags, blocklist.json, and asan_blocklist.json
91
+ MOCHA_EXCLUDES=()
92
+ for exc in "${EXCLUDES[@]}"; do
93
+ MOCHA_EXCLUDES+=(--ignore "$exc")
94
+ done
95
+
96
+ # Auto-exclude tests from blocklist.json (Linux entries) and asan_blocklist.json
97
+ for blocklist in "$PROJECT_DIR/test/blocklist.json" "$PROJECT_DIR/test/asan_blocklist.json"; do
98
+ if [[ -f "$blocklist" ]]; then
99
+ while IFS= read -r test_file; do
100
+ MOCHA_EXCLUDES+=(--ignore "test/$test_file")
101
+ done < <(node -e "
102
+ const bl = require('$blocklist');
103
+ const entries = bl.Linux || bl;
104
+ if (Array.isArray(entries)) entries.forEach(t => console.log(t));
105
+ " 2>/dev/null)
106
+ fi
107
+ done
108
+
109
+ # Step 5: Run tests
110
+ if [[ ${#ARGS[@]} -gt 0 ]]; then
111
+ echo "=== Running ASan test: ${ARGS[*]} ==="
112
+ node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js "${MOCHA_EXCLUDES[@]}" "${ARGS[@]}"
113
+ else
114
+ echo "=== Running all ASan tests ==="
115
+ node --expose-gc node_modules/.bin/mocha -r test/gc-on-exit.js "${MOCHA_EXCLUDES[@]}" 'test/test-*.js'
116
+ fi
117
+
118
+ echo "=== ASan test complete ==="