Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new message bindings that uses rosidl_generator_c #774

Open
wants to merge 68 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
51a0f6c
use js typedarray constructor to create view of underlying buffer
Nov 16, 2020
f39edb2
fix leak in RclTakeRequest
Nov 16, 2020
4c7af5a
Merge remote-tracking branch 'origin/develop' into fix/node-14
Dec 1, 2020
a287e2e
wip
Nov 17, 2020
0c56585
library linking problem
Dec 2, 2020
751705e
using regex on the cmake files to find link libraries
Dec 4, 2020
24d9ef7
collect all packages from all paths before generating, this is needed…
Dec 4, 2020
93dc352
Merge remote-tracking branch 'origin/develop' into feat/direct-ros-se…
Dec 4, 2020
de55631
remove package-lock
Dec 7, 2020
355fb02
link to correct library
Dec 7, 2020
47a1627
remove test.js
Dec 7, 2020
8085c7f
export as class
Dec 7, 2020
7fd4799
remove usage of deprecated functions
Dec 7, 2020
8c5d833
it works!
Dec 7, 2020
4e017ae
support publishing
Dec 8, 2020
ccc9370
fix not using offset
Dec 8, 2020
01685fe
benchmark c struct conversion
Dec 8, 2020
d6e5a66
conditionally use rosidl provider at compile time
Dec 8, 2020
74d2dcb
use parallel build
Dec 8, 2020
c8fcbcc
fix strings
Dec 9, 2020
750f1c8
add js to c benchmark
Dec 9, 2020
a413620
update benchmark with no zerocopy mode
Dec 10, 2020
5a29f19
more strict type checking
Dec 10, 2020
7966b96
u16string conversion
Dec 10, 2020
de17d58
Revert "more strict type checking"
Dec 10, 2020
297fccc
write array
Dec 10, 2020
ed57bed
primitive fixed size arrays
Dec 10, 2020
6cd3df4
skip type assertion, nan conversion it is generally safe, it will jus…
Dec 10, 2020
37238de
non-primitive fixed size arrays
Dec 10, 2020
7352bd9
api compatible rosidl message
Dec 11, 2020
8fb92f6
pass single process test
Dec 11, 2020
f403c78
support default messages
Dec 11, 2020
1606197
passes fixed array tests
Dec 11, 2020
a247084
additional tests for default values
Dec 11, 2020
bd6688d
publishing sequences
Dec 14, 2020
cbed718
remove exception, silently fill with "zero" or "default" value if inc…
Dec 14, 2020
51e3c51
support sequences
Dec 15, 2020
dcd002b
Merge remote-tracking branch 'origin/develop' into feat/direct-ros-se…
Dec 15, 2020
dbdd0f0
fix tests
Dec 15, 2020
b3ec04d
support float typed array
Dec 15, 2020
5343b77
fix tests
Dec 16, 2020
9eb5b5f
fix tests
Dec 16, 2020
dcbc0d7
fix tests
Dec 16, 2020
6754715
fix tests
Dec 16, 2020
9e8cf57
fix tests
Dec 16, 2020
3dc6f4f
remove postinstalls script
Dec 16, 2020
4aac6a0
cleanup
Dec 16, 2020
674a32c
allow runtime message generation
Jan 7, 2021
e7eccfb
replace generatorOptions tests with process.env tests
Jan 7, 2021
1fcf569
various fixes, skipping unrelated tests
Jan 7, 2021
3a807b7
generate messages for actions
Jan 8, 2021
0469116
revert change to skip zero initialization
Jan 8, 2021
123a0fa
pass non-primitive-msg-type test
Jan 8, 2021
3bd83bd
skip type check tests
Jan 8, 2021
b581642
pass rosidl-message-generator tests
Jan 8, 2021
fb2ac5c
skip invalid tests
Jan 8, 2021
a1db103
allow using .npmrc to enable rosidl bindings
Jan 8, 2021
d5ecbba
add docs for the new bindings
Jan 8, 2021
0c71b29
remove testing console.log
Jan 8, 2021
b894d93
move node-gyp to deps to support runtime generation for new bindings
Jan 12, 2021
a44fc09
use npx to launch node-gyp
Jan 12, 2021
a8e5140
no longer uses regex to guess cmake libraries
Jan 20, 2021
9ed4009
Merge remote-tracking branch 'upstream/develop' into feat/direct-ros-…
Jan 26, 2021
f8c9c9a
travis build matrix
Jan 26, 2021
bc1cc52
typos
Jan 26, 2021
395c4fe
typos
Jan 26, 2021
f402cd1
uncomment testing commented out code
Jan 26, 2021
07de22a
fix lint errors
Jan 26, 2021
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
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ branches:
only:
- develop

env:
- RCLNODEJS_USE_ROSIDL=true
- RCLNODEJS_USE_ROSIDL=false

script:
- 'if [ "$DOCKER_USERNAME" != "" ]; then sudo docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD; fi'
- sudo docker pull ubuntu:focal
- sudo docker build -t rcldocker .
- sudo docker run -v $(pwd):/root/rclnodejs --rm rcldocker bash -i -c 'cd /root/rclnodejs && cppcheck --suppress=syntaxError --enable=all src/*.cpp src/*.hpp && ./scripts/build.sh && npm test'
- sudo docker run -e RCLNODEJS_USE_ROSIDL=${RCLNODEJS_USE_ROSIDL} -v $(pwd):/root/rclnodejs --rm rcldocker bash -i -c 'cd /root/rclnodejs && cppcheck --suppress=syntaxError --enable=all src/*.cpp src/*.hpp && ./scripts/build.sh && npm test'
144 changes: 123 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
| develop | [![Build Status](https://travis-ci.org/RobotWebTools/rclnodejs.svg?branch=develop)](https://travis-ci.org/RobotWebTools/rclnodejs) | [![macOS Build Status](https://circleci.com/gh/RobotWebTools/rclnodejs/tree/develop.svg?style=shield)](https://circleci.com/gh/RobotWebTools/rclnodejs) | [![Build status](https://ci.appveyor.com/api/projects/status/upbc7tavdag1aa5e/branch/develop?svg=true)](https://ci.appveyor.com/project/minggangw/rclnodejs/branch/develop) |
| master | [![Build Status](https://travis-ci.org/RobotWebTools/rclnodejs.svg?branch=master)](https://travis-ci.org/RobotWebTools/rclnodejs) | [![macOS Build Status](https://circleci.com/gh/RobotWebTools/rclnodejs/tree/master.svg?style=shield)](https://circleci.com/gh/RobotWebTools/rclnodejs) | [![Build status](https://ci.appveyor.com/api/projects/status/upbc7tavdag1aa5e/branch/master?svg=true)](https://ci.appveyor.com/project/minggangw/rclnodejs/branch/master) |

**rclnodejs** is a Node.js client library for the Robot Operating System
([ROS 2](https://index.ros.org/doc/ros2/)). It provides a JavaScript API
and tooling for ROS 2 programming. TypeScript declarations, i.e., (*.d.ts),
**rclnodejs** is a Node.js client library for the Robot Operating System
([ROS 2](https://index.ros.org/doc/ros2/)). It provides a JavaScript API
and tooling for ROS 2 programming. TypeScript declarations, i.e., (\*.d.ts),
are included to support use in TypeScript projects.

Here's an example for how to create a ROS 2 node that publishes a string message in a few lines of JavaScript.
Expand Down Expand Up @@ -66,6 +66,103 @@ npm i [email protected]

- **Note:** to install rclnodejs from GitHub: add `"rclnodejs":"RobotWebTools/rclnodejs#<branch>"` to your `package.json` depdendency section.

## Experimental Message Bindings

In order to use the new bindings, you must either:

set RCLNODEJS_USE_ROSIDL=1 environment variable.
or
add a `.npmrc` file in your project directory with `rclnodejs_use_rosidl=true`.

The new experimental message bindings uses ros interfaces to perform serialization. The main advantage of the new bindings is better performance, it is ~25% faster for large messages (1mb) and ~800% faster for small messages (1kb). It is also safer as memory is managed by v8, you will no longer get undefined behaviors when you try to reference a message outside the subscription callbacks. Also as a result of moving to v8 managed memory, it fixes some memory leaks observed in the current bindings.

The downside is that the new bindings is not API compatible, it does a number of things differently.

1. The new bindings initialize nested message as plain js objects instead of the wrappers classes. As a result, they don't contain wrapper methods, for example, this wouldn't work

```js
const msg = new UInt8MultiArray();
console.log(msg.hasMember('foo')); // ok, `msg` is a UInt8MultiArrayWrapper
console.log(msg.layout.hasMember('bar')); // error, `layout` is a plain js object, there is no `hasMember` method
```

2. There is no array wrappers.

```js
const UInt8MultiArray = rclnodejs.require('std_msgs').msg.UInt8MultiArray;
const Byte = rclnodejs.require('std_msgs').msg.Byte;
const byteArray = new Byte.ArrayType(10); // error, there is no `ArrayType`
```

3. Primitives are initialized to their zero value.

```js
const Header = rclnodejs.require('std_msgs').msg.Header;
let header = new Header();
console.log(typeof header.frame_id); // 'string', in the old bindings this would be 'undefined'
```

4. Shortform for `std_msg` wrappers are not supported.

```js
const String = rclnodejs.require('std_msgs').msg.String;
const publisher = node.createPublisher(String, 'topic');
publisher.publish({ data: 'hello' }); // ok
publisher.publish('hello'); // error, shortform not supported
```

5. Primitive arrays are always deserialized to typed arrays.

```js
const subscription = node.createSubscription(
'std_msgs/msg/UInt8MultiArray',
'topic',
(msg) => {
console.log(msg.data instanceof Uint8Array); // always true, even if typed array is disabled in rclnodejs initialization
}
);
```

6. No conversion is done until serialization time.

```js
const UInt8MultiArray = rclnodejs.require('std_msgs').msg.UInt8MultiArray;
const msg = new UInt8MultiArray();
msg.data = [1, 2, 3];
console.log(msg.data instanceof Uint8Array); // false, assigning `msg.data` does not automatically convert it to typed array.
```

7. Does not throw on wrong types, they are silently converted to their zero value instead.

```js
const String = rclnodejs.require('std_msgs').msg.String;
const publisher = node.createPublisher(String, 'topic');
publish.publish({ data: 123 }); // does not throw, data is silently converted to an empty string.
```

8. Message memory is managed by v8. There is no longer any need to manually destroy messages and
do other house keeping.

```js
// With the old bindings, this may result in use-after-free as messages may be deleted when they
// leave the callback, even if there are still references to it. You will have to deep copy the message
// and manually destroy it with its `destroy` method when you don't need it anymore.
//
// This is safe with the new bindings, `lastMessage` will either be `undefined` or the last message received.
// Old messages are automatically garbage collected by v8 as they are no longer reachable.
let lastMessage;
const subscription = node.createSubscription(
'std_msgs/msg/UInt8MultiArray',
'topic',
(msg) => {
lastMessage = msg;
}
);
setTimeout(() => {
console.log(lastMessage);
}, 1000);
```

## API Documentation

API documentation is generated by `jsdoc` and can be viewed in the `docs/` folder or [on-line](http://robotwebtools.org/rclnodejs/docs/index.html). To create a local copy of the documentation run `npm run docs`.
Expand All @@ -75,6 +172,7 @@ API documentation is generated by `jsdoc` and can be viewed in the `docs/` folde
`rclnodejs` API can be used in TypeScript projects. You can find the TypeScript declaration files (\*.d.ts) in the `types/` folder.

Your `tsconfig.json` file should include the following compiler options:

```jsonc
{
"compilerOptions": {
Expand All @@ -98,18 +196,19 @@ rclnodejs.init().then(() => {
});
```

The benefits of using TypeScript become evident when working with more complex use-cases. ROS messages are defined in the `types/interfaces.d.ts` module. This module is updated as part of the `generate-messages` process described in the next section.
The benefits of using TypeScript become evident when working with more complex use-cases. ROS messages are defined in the `types/interfaces.d.ts` module. This module is updated as part of the `generate-messages` process described in the next section.

## ROS2 Interface Message Generation (important)
ROS components communicate by sending and receiving messages described
by the interface definition language (IDL). ROS client libraries such as
rclnodejs are responsible for converting these IDL message descriptions
into source code of their target language. For this, rclnodejs provides
the `generate-messages` npm script that reads in the IDL
messages files of a ROS environment and generates corresponding JavaScript
message interface files. Additionally, the tool generates the TypeScript
`interface.d.ts` file containing declarations for every IDL message file
processed.

ROS components communicate by sending and receiving messages described
by the interface definition language (IDL). ROS client libraries such as
rclnodejs are responsible for converting these IDL message descriptions
into source code of their target language. For this, rclnodejs provides
the `generate-messages` npm script that reads in the IDL
messages files of a ROS environment and generates corresponding JavaScript
message interface files. Additionally, the tool generates the TypeScript
`interface.d.ts` file containing declarations for every IDL message file
processed.

Learn more about ROS interfaces and IDL [here](https://index.ros.org/doc/ros2/Concepts/About-ROS-Interfaces/).

Expand All @@ -122,29 +221,32 @@ stringMsgObject.data = 'hello world';
```

### Maintaining Generated JavaScript Message Files
Message files are generated as a post-install step of the rclnodejs
installation process. Thereafter, you will need to manually run the
message generation script when new ROS message packages are installed
for which your ROS2-nodejs project has a dependency.

Message files are generated as a post-install step of the rclnodejs
installation process. Thereafter, you will need to manually run the
message generation script when new ROS message packages are installed
for which your ROS2-nodejs project has a dependency.

### Running `generate-messages` Utility
To use `generate-messages` from your Nodejs package, create an npm

To use `generate-messages` from your Nodejs package, create an npm
script entry in your package.json file as shown:

```
"scripts": {
"generate-messages": "generate-messages"
// your other scripts here
}
````
```

To run the script use `npm` as follows:

```
npm run generate-messages
```
The newly generated JavaScript files can be found at
`<yourproject>/node_modules/rclnodejs/generated/`.

The newly generated JavaScript files can be found at
`<yourproject>/node_modules/rclnodejs/generated/`.

## Contributing

Expand Down
72 changes: 72 additions & 0 deletions benchmark/rclnodejs/message/c-struct-to-js-obj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict';

/**
* Benchmarks the performance in converting from native C struct to javascript objects.
* Requires the ros package "test_msgs" to be available.
*/
const app = require('commander');
const generatorOptions = require('../../../generated/generator-options');

app.option('-r, --runs <n>', 'Number of times to run').parse(process.argv);
const runs = app.runs || 1;

const BasicTypes = require('../../../generated/test_msgs/test_msgs__msg__BasicTypes');

if (generatorOptions.idlProvider === 'rosidl') {
const rosMessage = BasicTypes.toRosMessage({
bool_value: false,
byte_value: 0,
char_value: 0,
float32_value: 0,
float64_value: 0,
int8_value: 0,
uint8_value: 0,
int16_value: 0,
uint16_value: 0,
int32_value: 0,
uint32_value: 0,
int64_value: BigInt(0),
uint64_value: BigInt(0),
});
const startTime = process.hrtime();
for (let i = 0; i < runs; i++) {
BasicTypes.toJsObject(rosMessage);
}
const timeTaken = process.hrtime(startTime);
console.log(
`Benchmark took ${timeTaken[0]} seconds and ${Math.ceil(
timeTaken[1] / 1000000
)} milliseconds.`
);
} else {
const msg = new BasicTypes({
bool_value: false,
byte_value: 0,
char_value: 0,
float32_value: 0,
float64_value: 0,
int8_value: 0,
uint8_value: 0,
int16_value: 0,
uint16_value: 0,
int32_value: 0,
uint32_value: 0,
int64_value: 0,
uint64_value: 0,
});
msg.freeze();
const rawMessage = msg._refObject;
const deserializeFunc = process.env.RCLNODEJS_NO_ZEROCOPY
? (rawMessage) => msg.toPlainObject(msg.deserialize(rawMessage))
: (rawMessage) => msg.deserialize(rawMessage);
const startTime = process.hrtime();
for (let i = 0; i < runs; i++) {
deserializeFunc(rawMessage);
}
const timeTaken = process.hrtime(startTime);
console.log(
`Benchmark took ${timeTaken[0]} seconds and ${Math.ceil(
timeTaken[1] / 1000000
)} milliseconds.`
);
}
68 changes: 68 additions & 0 deletions benchmark/rclnodejs/message/js-obj-to-c-struct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';

/**
* Benchmarks the performance in converting from to javascript objects to native C structs.
* Requires the ros package "test_msgs" to be available.
*/
const app = require('commander');
const generatorOptions = require('../../../generated/generator-options');

app.option('-r, --runs <n>', 'Number of times to run').parse(process.argv);
const runs = app.runs || 1;

const BasicTypes = require('../../../generated/test_msgs/test_msgs__msg__BasicTypes');

if (generatorOptions.idlProvider === 'rosidl') {
const jsObj = {
bool_value: false,
byte_value: 0,
char_value: 0,
float32_value: 0,
float64_value: 0,
int8_value: 0,
uint8_value: 0,
int16_value: 0,
uint16_value: 0,
int32_value: 0,
uint32_value: 0,
int64_value: BigInt(0),
uint64_value: BigInt(0),
};
const startTime = process.hrtime();
for (let i = 0; i < runs; i++) {
BasicTypes.toRosMessage(jsObj);
}
const timeTaken = process.hrtime(startTime);
console.log(
`Benchmark took ${timeTaken[0]} seconds and ${Math.ceil(
timeTaken[1] / 1000000
)} milliseconds.`
);
} else {
const jsObj = {
bool_value: false,
byte_value: 0,
char_value: 0,
float32_value: 0,
float64_value: 0,
int8_value: 0,
uint8_value: 0,
int16_value: 0,
uint16_value: 0,
int32_value: 0,
uint32_value: 0,
int64_value: 0,
uint64_value: 0,
};
const startTime = process.hrtime();
for (let i = 0; i < runs; i++) {
const msg = new BasicTypes(jsObj);
msg.serialize();
}
const timeTaken = process.hrtime(startTime);
console.log(
`Benchmark took ${timeTaken[0]} seconds and ${Math.ceil(
timeTaken[1] / 1000000
)} milliseconds.`
);
}
Loading