Skip to content

ntoskrnl7/node-binding

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Node Binding

This is a helper to bind c++ to nodejs using node-addon-api easily.

Contents

Overview

Please don't repeat argument checking or type conversion.

#include "napi.h"

double CAdd(double arg0, double arg1) { return arg0 + arg1; }

Napi::Value Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  if (info.Length() < 2) {
    Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
    return env.Null();
  }
  if (!info[0].IsNumber() || !info[1].IsNumber()) {
    Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
    return env.Null();
  }
  double arg0 = info[0].As<Napi::Number>().DoubleValue();
  double arg1 = info[1].As<Napi::Number>().DoubleValue();
  Napi::Number num = Napi::Number::New(env, CAdd(arg0, arg1);
  return num;
}

Instead do like below! Just this one line does everything above.

#include "node_binding/typed_call.h"

double CAdd(double arg0, double arg1) { return arg0 + arg1; }

Napi::Value Add(const Napi::CallbackInfo& info) {
  return node_binding::TypedCall(info, &CAdd);
}

How to use

bazel

To use node_binding, add the followings to your WORKSPACE file.

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "node_binding",
    sha256 = "<sha256>",
    strip_prefix = "node-binding-<commit>",
    urls = [
        "https://github.com/chokobole/node-binding/archive/<commit>.tar.gz",
    ],
)

load("@node_binding//bazel:node_binding_deps.bzl", "node_binding_deps")

node_binding_deps()

load("@node_binding//third_party/node_addon_api:install_node_addon_api.bzl", "install_node_addon_api")

install_node_addon_api(name = "node_addon_api")

Then, in your BUILD files, import and use the rules.

load("@node_binding//bazel:node_binding.bzl", "node_binding")
load("@node_binding//bazel:node_binding_cc.bzl", "node_binding_copts")

node_binding(
    name = "name",
    srcs = [
      ...
    ],
    copts = node_binding_copts(),
    deps = [
        "@node_binding",
    ],
)

Then this rule generates name.node.

node-gyp

To use node-binding, install package via npm install.

npm install node-binding

Follow examples! But in include_dirs, fill like below.

{
  "targets": [
    {
      ...
      "include_dirs": [
        "<!@(node -p \"require('node-binding').include\")",
      ],
    }
  ]
}

Usages

InstanceMethod with default arguments

// examples/calculator.h
class Calculator {
 public:
  void Increment(int a = 1) { result_ += a; }

 private:
  int result_;
};
// examples/calculator_js.h
class CalculatorJs : public Napi::ObjectWrap<CalculatorJs> {
 public:
  void Increment(const Napi::CallbackInfo& info);
};
// examples/calculator_js.cc
void CalculatorJs::Increment(const Napi::CallbackInfo& info) {
  if (info.Length() == 0) {
    TypedCall(info, &Calculator::Increment, calculator_.get(), 1);
  } else {
    TypedCall(info, &Calculator::Increment, calculator_.get());
  }
}
// examples/calculator.js
const c = new calculator.Calculator();
c.increment();
c.increment(1);

Constructor

To bind constructor, you have to include #include "node_binding/constructor.h".

Constructor<Class>::CallNew<Args> calls new Class(Args), whereas, Constructor<Class>::Calls<Args> calls Class(Args).

// examples/calculator.h
class Calculator {
 public:
  Calculator() : result_(0) {}
  explicit Calculator(int result) : result_(result) {}

 private:
  int result_;
};
// examples/calculator_js.h
class CalculatorJs : public Napi::ObjectWrap<CalculatorJs> {
 public:
  CalculatorJs(const Napi::CallbackInfo& info);
};
// examples/calculator_js.cc
#include "node_binding/constructor.h"

CalculatorJs::CalculatorJs(const Napi::CallbackInfo& info)
    : Napi::ObjectWrap<CalculatorJs>(info) {
  Napi::Env env = info.Env();
  if (info.Length() == 0) {
    calculator_ = std::unique_ptr<Calculator>(
        TypedConstruct(info, &Constructor<Calculator>::CallNew<>));
  } else if (info.Length() == 1) {
    calculator_ = std::unique_ptr<Calculator>(
        TypedConstruct(info, &Constructor<Calculator>::CallNew<int>));
  } else {
    THROW_JS_WRONG_NUMBER_OF_ARGUMENTS(env);
  }

  if (env.IsExceptionPending()) calculator_.reset();
}
// examples/calculator.js
new Calculator();
new Calculator(0);
new Calculator(0, 0);  // Throws exception!

InstanceAccessor

// examples/point.h
struct Point {
  int x;
  int y;

  Point(int x = 0, int y = 0) : x(x), y(y) {}
};
// examples/point_js.h
class PointJs : public Napi::ObjectWrap<PointJs> {
 public:
  void SetX(const Napi::CallbackInfo& info, const Napi::Value& v);
  void SetY(const Napi::CallbackInfo& info, const Napi::Value& v);

  Napi::Value GetX(const Napi::CallbackInfo& info);
  Napi::Value GetY(const Napi::CallbackInfo& info);

 private:
  Point point_;
};
// examples/point_js.cc
void PointJs::SetX(const Napi::CallbackInfo& info, const Napi::Value& v) {
  point_.x = ToNativeValue<int>(v);
}

void PointJs::SetY(const Napi::CallbackInfo& info, const Napi::Value& v) {
  point_.y = ToNativeValue<int>(v);
}

Napi::Value PointJs::GetX(const Napi::CallbackInfo& info) {
  return ToJSValue(info.Env(), point_.x);
}

Napi::Value PointJs::GetY(const Napi::CallbackInfo& info) {
  return ToJSValue(info.Env(), point_.y);
}
// examples/point.js
const p = new Point();
p.x = 1;
p.y = p.x * 2;

STL containers

To bind std::vector<T>, you have to include #include "node_binding/stl.h".

// test/4_stl/addon.cc
#include "node_binding/stl.h"

int Sum(const std::vector<int>& vec) {
  int ret = 0;
  for (int v: vec) {
    ret += v;
  }
  return ret;
}

std::vector<int> LinSpace(int from, int to, int step) {
  std::vector<int> ret;
  for (int i = from ;i < to; i += step) {
    ret.push_back(i);
  }
  return ret;
}
// test/test.js
console.log(sum([1, 2, 3]));  // 6
console.log(linSpace(1, 5, 1));  // [1, 2, 3, 4]

Conversion

C++ JS REFERENCE
bool Boolean
uint8_t Number
int8_t Number
uint16_t Number
int16_t Number
uint32_t Number
int32_t Number
uint64_t Number or BigInt BigInt if NAPI_EXPERIMENTAL is on
uint64_t Number or BigInt BigInt if NAPI_EXPERIMENTAL is on
float Number
double Number
std::string String
std::vector Array
std::function Function
function pointer Function
std::unordered_map<std::string, std::any> Object C++17 or later
node_binding::function Function
node_binding::thread_safe_function ThreadSafeFunction
node_binding::async_function Promise node_binding::ToPromise, node_binding::async_function::from
node_binding::cancellable_async_function { catch, then, finally, cancel: Function } : Promise node_binding::ToCancellablePromise, node_binding::cancellable_async_function::from
node_binding::object Object C++17 or later

Custom Conversion

To bind your own class, you have to include #include "node_binding/type_convertor.h" and write your own specialized template class TypeConvertor<> for your own class.

// examples/point_js.h
#include "node_binding/type_convertor.h"

class PointJs : public Napi::ObjectWrap<PointJs> {
 public:
  static Napi::Object New(Napi::Env env, const Point& p);
};

namespace node_binding {

  template <>
  class TypeConvertor<Point> {
   public:
    static Point ToNativeValue(const Napi::Value& value) {
      Napi::Object obj = value.As<Napi::Object>();

      return {
          TypeConvertor<int>::ToNativeValue(obj["x"]),
          TypeConvertor<int>::ToNativeValue(obj["y"]),
      };
    }

    static bool IsConvertible(const Napi::Value& value) {
      if (!value.IsObject()) return false;

      Napi::Object obj = value.As<Napi::Object>();

      return TypeConvertor<int>::IsConvertible(obj["x"]) &&
             TypeConvertor<int>::IsConvertible(obj["y"]);
    }

    static Napi::Value ToJSValue(const Napi::Env& env,
                                 const Point& value) {
      return PointJs::New(env, value);
    }
  };

}  // namespace node_binding
// examples/point_js.cc
Napi::Object PointJs::New(Napi::Env env, const Point& p) {
  Napi::EscapableHandleScope scope(env);

  Napi::Object object = constructor_.New({
      Napi::Number::New(env, p.x),
      Napi::Number::New(env, p.y),
  });

  return scope.Escape(napi_value(object)).ToObject();
}
// examples/rect.h
#include "examples/point.h"

struct Rect {
  Point top_left;
  Point bottom_right;
};
// examples/rect_js.h
class RectJs : public Napi::ObjectWrap<RectJs> {
 public:
  void SetTopLeft(const Napi::CallbackInfo& info, const Napi::Value& v);
  void SetBottomRight(const Napi::CallbackInfo& info, const Napi::Value& v);

  Napi::Value GetTopLeft(const Napi::CallbackInfo& info);
  Napi::Value GetBottomRight(const Napi::CallbackInfo& info);
};
// examples/rect_js.cc
#include "examples/point_js.h"

void RectJs::SetTopLeft(const Napi::CallbackInfo& info, const Napi::Value& v) {
  if (IsConvertible<Point>(v)) {
    rect_.top_left = ToNativeValue<Point>(v);
  }
}

void RectJs::SetBottomRight(const Napi::CallbackInfo& info,
                            const Napi::Value& v) {
  if (IsConvertible<Point>(v)) {
    rect_.bottom_right = ToNativeValue<Point>(v);
  }
}

Napi::Value RectJs::GetTopLeft(const Napi::CallbackInfo& info) {
  return ToJSValue(info.Env(), rect_.top_left);
}

Napi::Value RectJs::GetBottomRight(const Napi::CallbackInfo& info) {
  return ToJSValue(info.Env(), rect_.bottom_right);
}
// examples/rect.js
const topLeft = new Point(1, 5);
const bottomRight = new Point(5, 1);
const rect = new Rect(topLeft, bottomRight);

About

A header only library to bind c++ to nodejs using node-addon-api easily.

Topics

Resources

License

Stars

Watchers

Forks

Languages

  • C++ 76.5%
  • JavaScript 17.7%
  • Starlark 2.4%
  • Python 2.3%
  • Other 1.1%