diff --git a/binding.gyp b/binding.gyp index 18c41cf7..0d9ff1ad 100644 --- a/binding.gyp +++ b/binding.gyp @@ -35,6 +35,7 @@ './src/rcl_names_bindings.cpp', './src/rcl_node_bindings.cpp', './src/rcl_publisher_bindings.cpp', + './src/rcl_serialization_bindings.cpp', './src/rcl_service_bindings.cpp', './src/rcl_subscription_bindings.cpp', './src/rcl_time_point_bindings.cpp', diff --git a/index.js b/index.js index 47f98bbb..22063127 100644 --- a/index.js +++ b/index.js @@ -53,6 +53,10 @@ const { getActionNamesAndTypes, } = require('./lib/action/graph.js'); const ServiceIntrospectionStates = require('./lib/service_introspection.js'); +const { + serializeMessage, + deserializeMessage, +} = require('./lib/serialization.js'); /** * Get the version of the generator that was used for the currently present interfaces. @@ -183,6 +187,12 @@ let rcl = { /** {@link getActionNamesAndTypes} function */ getActionNamesAndTypes: getActionNamesAndTypes, + /** {@link serializeMessage} function */ + serializeMessage: serializeMessage, + + /** {@link deserializeMessage} function */ + deserializeMessage: deserializeMessage, + /** * Create and initialize a node. * @param {string} nodeName - The name used to register in ROS. diff --git a/lib/node.js b/lib/node.js index bafe1af8..4369428c 100644 --- a/lib/node.js +++ b/lib/node.js @@ -1816,7 +1816,7 @@ class Node extends rclnodejs.ShadowNode { return result; }); - Type.destoryRawROS(message); + Type.destroyRawROS(message); } _addActionClient(actionClient) { diff --git a/lib/serialization.js b/lib/serialization.js new file mode 100644 index 00000000..b8462f84 --- /dev/null +++ b/lib/serialization.js @@ -0,0 +1,60 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const rclnodejs = require('bindings')('rclnodejs'); + +class Serialization { + /** + * Serialize a message to a buffer. + * @param {object} message - The message to serialize. + * @param {function} typeClass - The class of the message type to serialize. + * @return {Buffer} The serialized message as a Buffer. + */ + static serializeMessage(message, typeClass) { + if (!(message instanceof typeClass)) { + throw new TypeError('Message must be a valid ros2 message type'); + } + return rclnodejs.serialize( + typeClass.type().pkgName, + typeClass.type().subFolder, + typeClass.type().interfaceName, + message.serialize() + ); + } + + /** + * Deserialize a message from a buffer. + * @param {Buffer} buffer - The buffer containing the serialized message. + * @param {function} typeClass - The class of the message type to deserialize into. + * @return {object} The deserialized message object. + */ + static deserializeMessage(buffer, typeClass) { + if (!(buffer instanceof Buffer)) { + throw new TypeError('Buffer is required for deserialization'); + } + const rosMsg = new typeClass(); + rclnodejs.deserialize( + typeClass.type().pkgName, + typeClass.type().subFolder, + typeClass.type().interfaceName, + buffer, + rosMsg.toRawROS() + ); + return rosMsg; + } +} + +module.exports = Serialization; diff --git a/rosidl_gen/templates/message.dot b/rosidl_gen/templates/message.dot index 1f8939f9..eff59e08 100644 --- a/rosidl_gen/templates/message.dot +++ b/rosidl_gen/templates/message.dot @@ -506,7 +506,7 @@ class {{=objectWrapper}} { {{~}} } - static destoryRawROS(msg) { + static destroyRawROS(msg) { {{=objectWrapper}}.freeStruct(msg.refObject); } diff --git a/src/addon.cpp b/src/addon.cpp index 277678a1..b6b5c0cd 100644 --- a/src/addon.cpp +++ b/src/addon.cpp @@ -30,6 +30,7 @@ #include "rcl_names_bindings.h" #include "rcl_node_bindings.h" #include "rcl_publisher_bindings.h" +#include "rcl_serialization_bindings.h" #include "rcl_service_bindings.h" #include "rcl_subscription_bindings.h" #include "rcl_time_point_bindings.h" @@ -88,6 +89,7 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) { rclnodejs::InitEventHandleBindings(env, exports); #endif rclnodejs::InitLifecycleBindings(env, exports); + rclnodejs::InitSerializationBindings(env, exports); rclnodejs::ShadowNode::Init(env, exports); rclnodejs::RclHandle::Init(env, exports); diff --git a/src/rcl_serialization_bindings.cpp b/src/rcl_serialization_bindings.cpp new file mode 100644 index 00000000..cffe7bd9 --- /dev/null +++ b/src/rcl_serialization_bindings.cpp @@ -0,0 +1,116 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "rcl_serialization_bindings.h" + +#include +#include +#include + +#include + +#include "rcl_utilities.h" + +namespace { + +struct SerializedMessage { + explicit SerializedMessage(Napi::Env env, rcutils_allocator_t allocator) + : env(env) { + rcl_msg = rmw_get_zero_initialized_serialized_message(); + rcutils_ret_t rcutils_ret = + rmw_serialized_message_init(&rcl_msg, 0u, &allocator); + if (RCUTILS_RET_OK != rcutils_ret) { + Napi::Error::New(env, "failed to initialize serialized message") + .ThrowAsJavaScriptException(); + } + } + + ~SerializedMessage() { + rcutils_ret_t ret = rmw_serialized_message_fini(&rcl_msg); + if (RCUTILS_RET_OK != ret) { + Napi::Error::New(env, + "failed to fini rcl_serialized_msg_t in destructor.") + .ThrowAsJavaScriptException(); + rcutils_reset_error(); + } + } + + rcl_serialized_message_t rcl_msg; + Napi::Env env; +}; + +} // namespace + +namespace rclnodejs { + +Napi::Value Serialize(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + std::string package_name = info[0].As().Utf8Value(); + std::string message_sub_folder = info[1].As().Utf8Value(); + std::string message_name = info[2].As().Utf8Value(); + void* ros_msg = info[3].As>().Data(); + const rosidl_message_type_support_t* ts = + GetMessageTypeSupport(package_name, message_sub_folder, message_name); + + // Create a serialized message object. + SerializedMessage serialized_msg(env, rcutils_get_default_allocator()); + + rmw_ret_t rmw_ret = rmw_serialize(ros_msg, ts, &serialized_msg.rcl_msg); + if (RMW_RET_OK != rmw_ret) { + Napi::Error::New(env, "Failed to serialize ROS message") + .ThrowAsJavaScriptException(); + return env.Undefined(); + } + Napi::Buffer buffer = Napi::Buffer::Copy( + env, reinterpret_cast(serialized_msg.rcl_msg.buffer), + serialized_msg.rcl_msg.buffer_length); + return buffer; +} + +Napi::Value Deserialize(const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + + std::string package_name = info[0].As().Utf8Value(); + std::string message_sub_folder = info[1].As().Utf8Value(); + std::string message_name = info[2].As().Utf8Value(); + const rosidl_message_type_support_t* ts = + GetMessageTypeSupport(package_name, message_sub_folder, message_name); + Napi::Buffer serialized = info[3].As>(); + void* msg_taken = info[4].As>().Data(); + + // Create a serialized message object. + rcl_serialized_message_t serialized_msg = + rmw_get_zero_initialized_serialized_message(); + serialized_msg.buffer_capacity = serialized.Length(); + serialized_msg.buffer_length = serialized.Length(); + serialized_msg.buffer = reinterpret_cast(serialized.Data()); + + rmw_ret_t rmw_ret = rmw_deserialize(&serialized_msg, ts, msg_taken); + + if (RMW_RET_OK != rmw_ret) { + Napi::Error::New(env, "failed to deserialize ROS message") + .ThrowAsJavaScriptException(); + } + + return env.Undefined(); +} + +Napi::Object InitSerializationBindings(Napi::Env env, Napi::Object exports) { + exports.Set("serialize", Napi::Function::New(env, Serialize)); + exports.Set("deserialize", Napi::Function::New(env, Deserialize)); + return exports; +} + +} // namespace rclnodejs diff --git a/src/rcl_serialization_bindings.h b/src/rcl_serialization_bindings.h new file mode 100644 index 00000000..5c168f31 --- /dev/null +++ b/src/rcl_serialization_bindings.h @@ -0,0 +1,26 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SRC_RCL_SERIALIZATION_BINDINGS_H_ +#define SRC_RCL_SERIALIZATION_BINDINGS_H_ + +#include + +namespace rclnodejs { + +Napi::Object InitSerializationBindings(Napi::Env env, Napi::Object exports); + +} + +#endif // SRC_RCL_SERIALIZATION_BINDINGS_H_ diff --git a/test/test-serialization.js b/test/test-serialization.js new file mode 100644 index 00000000..d7b5ff17 --- /dev/null +++ b/test/test-serialization.js @@ -0,0 +1,47 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const assert = require('assert'); +const rclnodejs = require('../index.js'); +const { serializeMessage, deserializeMessage } = require('../index.js'); + +describe('rclnodejs publisher test suite', function () { + [ + { + type: 'std_msgs/msg/String', + value: 'Hello ROS 2.0 Serialization!', + }, + { + type: 'std_msgs/msg/MultiArrayDimension', + value: { label: 'label name 0', size: 256, stride: 4 }, + }, + { + type: 'geometry_msgs/msg/Point', + value: { x: 1.5, y: 2.75, z: 3.0 }, + }, + ].forEach((testCase) => { + it('Test serialize a message of type ' + testCase.type, function () { + const MyMessage = rclnodejs.require(testCase.type); + const rosMsg = new MyMessage(testCase.value); + const buffer = serializeMessage(rosMsg, MyMessage); + + assert(buffer instanceof Buffer); + const deserializedRosMsg = deserializeMessage(buffer, MyMessage); + assert.deepStrictEqual( + deserializedRosMsg.toPlainObject(), + rosMsg.toPlainObject() + ); + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 40425052..94b7b19a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,6 +1,8 @@ /// declare module 'rclnodejs' { + type Class = new (...args: any[]) => any; + /** * Create a node. * @@ -187,4 +189,22 @@ declare module 'rclnodejs' { * @returns An array of the names and types. */ function getActionNamesAndTypes(node: Node): NamesAndTypesQueryResult; + + /** + * Serialize a message to a Buffer. + * + * @param message - The message to be serialized. + * @param typeClass - The type class of the message. + * @returns A Buffer containing the serialized message. + */ + function serializeMessage(message: object, typeClass: Class): Buffer; + + /** + * Deserialize a message from a Buffer. + * + * @param buffer - The Buffer containing the serialized message. + * @param typeClass - The type class of the message. + * @returns An Object representing the deserialized message. + */ + function deserializeMessage(buffer: Buffer, typeClass: Class): object; }