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.
- package/README.md +46 -37
- package/index.js +62 -23
- package/lib/action/client.js +67 -3
- package/lib/action/server.js +1 -3
- package/lib/distro.js +2 -1
- package/lib/lifecycle_publisher.js +2 -2
- package/lib/message_info.js +94 -0
- package/lib/node.js +90 -14
- package/lib/parameter.js +5 -9
- package/lib/parameter_event_handler.js +468 -0
- package/lib/parameter_watcher.js +12 -12
- package/lib/service.js +8 -4
- package/lib/subscription.js +38 -5
- package/lib/time_source.js +3 -20
- package/lib/timer.js +2 -1
- package/lib/wait_for_message.js +111 -0
- package/package.json +7 -4
- package/prebuilds/linux-arm64/humble-jammy-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/jazzy-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-arm64/kilted-noble-arm64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/humble-jammy-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/jazzy-noble-x64-rclnodejs.node +0 -0
- package/prebuilds/linux-x64/kilted-noble-x64-rclnodejs.node +0 -0
- package/rosidl_gen/generate_worker.js +3 -13
- package/rosidl_gen/idl_generator.js +210 -0
- package/rosidl_gen/index.js +3 -12
- package/rosidl_gen/packages.js +1 -3
- package/rosidl_gen/primitive_types.js +2 -2
- package/rosidl_parser/idl_parser.py +437 -0
- package/rosidl_parser/parser.py +2 -4
- package/rosidl_parser/rosidl_parser.js +27 -0
- package/scripts/run_asan_test.sh +118 -0
- package/src/executor.cpp +37 -2
- package/src/executor.h +11 -0
- package/src/macros.h +2 -2
- package/src/rcl_action_client_bindings.cpp +88 -12
- package/src/rcl_action_server_bindings.cpp +24 -13
- package/src/rcl_client_bindings.cpp +13 -5
- package/src/rcl_context_bindings.cpp +10 -11
- package/src/rcl_graph_bindings.cpp +2 -2
- package/src/rcl_guard_condition_bindings.cpp +12 -3
- package/src/rcl_lifecycle_bindings.cpp +34 -15
- package/src/rcl_node_bindings.cpp +11 -4
- package/src/rcl_publisher_bindings.cpp +12 -3
- package/src/rcl_service_bindings.cpp +12 -3
- package/src/rcl_subscription_bindings.cpp +92 -21
- package/src/rcl_timer_bindings.cpp +24 -9
- package/src/rcl_type_description_service_bindings.cpp +9 -1
- package/src/rcl_utilities.cpp +2 -2
- package/tools/jsdoc/Makefile +5 -0
- package/tools/jsdoc/README.md +96 -0
- package/tools/jsdoc/build-index.js +610 -0
- package/tools/jsdoc/publish.js +854 -0
- package/tools/jsdoc/regenerate-published-docs.js +605 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/tools/jsdoc/static/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/tools/jsdoc/static/fonts/OpenSans-Light-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/tools/jsdoc/static/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/tools/jsdoc/static/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/tools/jsdoc/static/scripts/linenumber.js +25 -0
- package/tools/jsdoc/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/tools/jsdoc/static/scripts/prettify/lang-css.js +36 -0
- package/tools/jsdoc/static/scripts/prettify/prettify.js +738 -0
- package/tools/jsdoc/static/styles/jsdoc-default.css +1012 -0
- package/tools/jsdoc/static/styles/prettify-jsdoc.css +111 -0
- package/tools/jsdoc/static/styles/prettify-tomorrow.css +132 -0
- package/tools/jsdoc/tmpl/augments.tmpl +10 -0
- package/tools/jsdoc/tmpl/container.tmpl +193 -0
- package/tools/jsdoc/tmpl/details.tmpl +143 -0
- package/tools/jsdoc/tmpl/example.tmpl +2 -0
- package/tools/jsdoc/tmpl/examples.tmpl +13 -0
- package/tools/jsdoc/tmpl/exceptions.tmpl +17 -0
- package/tools/jsdoc/tmpl/layout.tmpl +83 -0
- package/tools/jsdoc/tmpl/mainpage.tmpl +163 -0
- package/tools/jsdoc/tmpl/members.tmpl +43 -0
- package/tools/jsdoc/tmpl/method.tmpl +124 -0
- package/tools/jsdoc/tmpl/params.tmpl +133 -0
- package/tools/jsdoc/tmpl/properties.tmpl +110 -0
- package/tools/jsdoc/tmpl/returns.tmpl +12 -0
- package/tools/jsdoc/tmpl/source.tmpl +8 -0
- package/tools/jsdoc/tmpl/tutorial.tmpl +19 -0
- package/tools/jsdoc/tmpl/type.tmpl +7 -0
- package/types/action_client.d.ts +8 -0
- package/types/index.d.ts +34 -0
- package/types/message_info.d.ts +72 -0
- package/types/node.d.ts +21 -0
- package/types/parameter_event_handler.d.ts +139 -0
- package/types/subscription.d.ts +14 -2
- package/rosidl_convertor/README.md +0 -298
- package/rosidl_convertor/idl_convertor.js +0 -50
- package/rosidl_convertor/idl_convertor.py +0 -1250
- package/test_data_integrity.js +0 -108
- package/test_repro_exact.js +0 -57
- package/test_repro_hz.js +0 -86
- package/test_repro_pub.js +0 -36
- package/test_repro_stress.js +0 -83
- package/test_repro_sub.js +0 -64
- package/test_xproc_data.js +0 -64
- package/types/interfaces.d.ts +0 -8895
package/rosidl_gen/packages.js
CHANGED
|
@@ -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
|
-
|
|
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 ((
|
|
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 ((
|
|
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)
|
package/rosidl_parser/parser.py
CHANGED
|
@@ -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) <
|
|
52
|
-
print('
|
|
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 ==="
|