Nobind
`pybind11`-like API for Node.js using C++17 fold expressions
Install / Use
/learn @mmomtchev/NobindREADME
nobind17
Experimental next-gen binding framework for Node.js / Node-API inspired by pybind11
Inspired by pybind11 and embind, in turn inspired by the groundbreaking Boost.Python.
This framework is designed around C++17 fold expressions.
It has one defining characteristic that sets it apart from pybind11 and embind - every wrapper is statically generated at compile time and has no run-time state. All the state information is constexpr and it is encoded in the template parameters. The wrappers are instantiated by obtaining a pointer to the wrapper function.
This allows for both a (slightly) better performance and code simplicity.
The unit tests run on:
- g++ 9.4 on Linux (the default compiler on Ubuntu 20.04)
- clang 13 on macOS (the default compiler on macOS 11)
- MSVC 19.29 on Windows (Visual Studio 16.11 aka 2019)
However because of edge cases when it comes to C++17 support, the recommended compiler versions are:
- g++ 10.5 on Linux (the alternative choice on Ubuntu 20.04 and the default one on Ubuntu 22.04)
- clang 13 on macOS (the default compiler on macOS 11)
- MSVC 19.37 on Windows (Visual Studio 17.7 aka 2022)
It is meant as an easy to use entry-level light-weight binding framework for simple projects that target only Node.js.
Complex projects should continue to use SWIG which is cross-platform and cross-language.
Currently, the project should be considered of a recent release quality.
The first npm module to use it is @mmomtchev/ffmpeg, you can check it for advanced usage examples.
A future compatible layer should allow to target both embind and nobind17 with shared declarations.
Full pybind11 compatibility is also a very long term goal - allowing a module to support both Node.js and Python.
You can use nobind-example-project and hadron-nobind-example-project as a template for creating a new nobind17 based project using node-gyp or hadron as build system.
Comparison vs SWIG Node-API
| Feature | SWIG Node-API | nobind17 |
| --- | --- | --- |
| Design goal | Create bindings for (almost) any C or C++ calling semantic with (almost) native feel | Easy to use, easy to learn, be able to wrap most C++ calling semantics, including asynchronous methods, without understanding the Node.js memory management or thread model, be able to go a little further by tweaking the typemaps |
| Target use | Commercial-grade bindings for large C++ libraries | Very fast porting of C++ code with few methods/classes |
| Method of operation | Custom C++ header compiler, uses its own interface language, generates C++ code | Collection of C++ templates to be included in the project |
| Method of using | Must write metaprogramming code | Must enumerate the binded methods using C++ syntax |
| C++ requirements | C++11 | C++17 with some features such as wrapping of lambdas requiring C++20 |
| C++ types | Almost all, nested classes support is very limited | No functions pointers, no nested classes, enums are not automatic |
| C++ preprocessing integration | Yes, can expose macros to JS | No |
| C++ namespaces | Can be exposed to JS with some limitations and manual work | Supported in C++ but not exposed to JS |
| C++ iterators | manual | automatic |
| Buffers / ArrayBuffers / TypedArrays | Yes | Only Buffers for now |
| STL | Complete, supports both JS using C++ STLs without copying and C++ using JS types with copying | Limited, all passing of STL arguments is by copying |
| Async | Automatic | Automatic |
| Async locking | Yes, with automatic dead-lock prevention | Yes, but no deadlock prevention |
| Smart pointers | Yes | Not at the moment, but planned |
| TypeScript support | Automatic | Automatic |
| ES6 named exports for all C/C++ functions | Yes, automatic | No, must write it |
| WASM/Browser support | Yes | Not at the moment, but planned through embind compatibility |
| Cross-platform | Yes | Yes |
| Input language | Both C and C++ | Mostly C++, many usual C API semantics are not well supported |
| Target language | Most dynamic languages | An eventual abstraction layer between nobind17, embind and pybind11 is planned in theory |
| Exposing C++ inheritance to JavaScript | Yes, automatic with implicit downcasting support, diamond inheritance is not supported | Yes, but no automatic downcasting support and no diamond inheritance |
| Overloading | Yes | Only for constructors, overloaded methods must be renamed to be usable in JS |
| Optional arguments with default values | Yes, automatic | No, all arguments become mandatory |
| Complex argument transformations (for example C++ expects (char**, size_t*) as input argument, JS expects Buffer as returned type) | Yes | Only 1:1 and 1:0 transformations of input arguments |
| Custom type casters | Yes | Yes |
| Interfacing between multiple modules | Yes | No |
Usage
nobind17 is a set of C++17 templates that must be included directly in the user project.
It is published as an npm package that will also install node-addon-api.
Starting from Node.js 18, C++17 is the default build mode for both Node.js itself and for addons. Unless you set manually NAPI_VERSION in your project, nobind17@2.0 defaults to NAPI_VERSION=8 which requires Node.js 16 or later. Older versions use NAPI_VERSION=6 which allows backward compatibility of the generated binary addon with Node.js 14 and later - even when using Node.js 18 as the build platform.
nobind17 is designed to be very easy to use - there is no learning curve at all - while allowing to deal with the most common situations that arise when creating bindings for C++ libraries to be used from Node.js.
The following tutorial should be enough to get you started with your C++ project.
You can also check node-ffmpeg as an example for a large project using nobind17.
The environment
Create a a binding.gyp, then create a package.json for your project and install nobind17:
binding.gyp
{
'target_defaults': {
'includes': [
# These are the correct compiler options
# to enable C++ exceptions with node-gyp
'except.gypi'
]
},
'targets': [
{
'target_name': 'my-shiny-cpp-bindings',
'sources': [
# This is the file that contains your bindings
# (from the tutorial below)
'src/my-shiny-cpp-bindings.cc'
# List your C++ files here
# If you have a large library, check
# https://github.com/mmomtchev/node-ffmpeg
# for inspiration, it builds ffmpeg with conan
],
'include_dirs': [
'<!@(node -p "require(\'node-addon-api\').include")',
'<!@(node -p "require(\'nobind17\').include")'
]
}
]
}
npm init # ... answer questions
npm install nobind17
cp node_modules/node-addon-api/except.gypi .
You will be building your project with node-gyp configure build. node-gyp is usually installed globally.
C++17 is the default build mode starting from Node.js 18.x.
Module definition
Let's try to wrap a simple C++ class:
class Hello {
public:
std::string name;
Hello(const std::string &s) : name(s) {}
std::string Greet(const std::string &s) {
std::stringstream r;
r << "hello " << s << " " << name_;
return r.str();
}
};
Start by creating a module:
// nobind will automatically include napi.h and it will
// define NAPI_VERSION and NAPI_EXPERIMENTAL
// If you include it yourself without these, you will be
// missing the synchronous finalizers
#include <nobind.h>
// Define a new module
NOBIND_MODULE(my_cpp_bindings, m) {
// Expose a C++ class called Hello
m.def<Hello>("Hello")
// Include a constructor with a single const std::string & argument
.cons<const std::string &>();
}
Adding methods
nobind17 supports global methods and instance and static class methods. All of them are declared by using .def():
// Expose a global function global_fn
m.def<&global_fn>("global_fn");
m.def<MyClass>("Hello")
.cons<std::string &>()
// Expose a class method (whether it is static or instance)
.def(&Hello::Greet, "greet");
nobind17 will identify the type of the class method, static methods will be available through the class itself and instance methods will be available through the object instance.
A class can have multiple constructors, including a default one (use <> for its arguments). The number of arguments on the JavaScript side determine which one will be used. If there a multiple constructors expecting the same number of arguments, they will be tried in the order of their declaration - the first one which is able to convert its arguments will win.
Overloaded methods, other than constructors, must be explicitly resolved and each signature must have a different name in JavaScript.
Arguments will be automatically converted. The C++ type of the wrapped function selects the type converter. The basic types supported out of the box are:
| JavaScript type | C++ type |
| --- | --- |
| number | int, short, long, unsigned, unsigned short, unsigned long, long long, unsigned long long, double, float |
| string | std::string, char * |
| boolean | bool |
| object | `std::map<string,
