Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 10 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -1816,7 +1816,7 @@ class Node extends rclnodejs.ShadowNode {
return result;
});

Type.destoryRawROS(message);
Type.destroyRawROS(message);
}

_addActionClient(actionClient) {
Expand Down
60 changes: 60 additions & 0 deletions lib/serialization.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion rosidl_gen/templates/message.dot
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ class {{=objectWrapper}} {
{{~}}
}

static destoryRawROS(msg) {
static destroyRawROS(msg) {
{{=objectWrapper}}.freeStruct(msg.refObject);
}

Expand Down
2 changes: 2 additions & 0 deletions src/addon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand Down
119 changes: 119 additions & 0 deletions src/rcl_serialization_bindings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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 <rmw/rmw.h>
#include <rmw/serialized_message.h>
#include <rosidl_runtime_c/message_type_support_struct.h>

#include <string>

#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();
Copy link

Copilot AI Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing exceptions from a destructor can lead to termination or unexpected behavior during object cleanup. Consider logging the error or handling the failure without throwing in the destructor.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing a JavaScript exception from a C++ destructor can lead to std::terminate. It's safer to log the error or handle cleanup without throwing in the destructor.

Suggested change
.ThrowAsJavaScriptException();
.Value(); // Log the error without throwing an exception.

Copilot uses AI. Check for mistakes.
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<Napi::String>().Utf8Value();
std::string message_sub_folder = info[1].As<Napi::String>().Utf8Value();
std::string message_name = info[2].As<Napi::String>().Utf8Value();
void* ros_msg = info[3].As<Napi::Buffer<char>>().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());

// Serialize
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<char> buffer = Napi::Buffer<char>::Copy(
env, reinterpret_cast<const char*>(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<Napi::String>().Utf8Value();
std::string message_sub_folder = info[1].As<Napi::String>().Utf8Value();
std::string message_name = info[2].As<Napi::String>().Utf8Value();
const rosidl_message_type_support_t* ts =
GetMessageTypeSupport(package_name, message_sub_folder, message_name);
Napi::Buffer<char> serialized = info[3].As<Napi::Buffer<char>>();
void* msg_taken = info[4].As<Napi::Buffer<char>>().Data();

// Create a serialized message object
rcl_serialized_message_t serialized_msg =
rmw_get_zero_initialized_serialized_message();
// Just copy pointer to avoid extra allocation and copy
serialized_msg.buffer_capacity = serialized.Length();
serialized_msg.buffer_length = serialized.Length();
serialized_msg.buffer = reinterpret_cast<uint8_t*>(serialized.Data());

// Deserialize
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
26 changes: 26 additions & 0 deletions src/rcl_serialization_bindings.h
Original file line number Diff line number Diff line change
@@ -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 <napi.h>

namespace rclnodejs {

Napi::Object InitSerializationBindings(Napi::Env env, Napi::Object exports);

}

#endif // SRC_RCL_SERIALIZATION_BINDINGS_H_
47 changes: 47 additions & 0 deletions test/test-serialization.js
Original file line number Diff line number Diff line change
@@ -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()
);
});
});
});
20 changes: 20 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/// <reference path="./base.d.ts" />

declare module 'rclnodejs' {
type Class = new (...args: any[]) => any;

/**
* Create a node.
*
Expand Down Expand Up @@ -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;
}
Loading