发现了个好玩的东西,来学一手(实际上就是跟文档随便过一下再拿中文随便记录一下):https://nodejs.org/dist/latest-v18.x/docs/api/addons.html

Addons are dynamically-linked shared objects written in C++. The require() function can load addons as ordinary Node.js modules. Addons provide an interface between JavaScript and C/C++ libraries.

共享模块,一眼看上去好像 wasm 的说 |・ω・`)

使用 C++ 编写源码,编译成 addon.node 这样的二进制文件供 node 调用。

似乎要先面熟一些东西:

  • V8:node 用来执行 js 的 C++ 库,萌新阶段就听说的牛逼玩意儿 QwQ

  • libuv:用来实现 node 的 event loop, worker, asynchronous 的 C++ 库

    • Addon authors should avoid blocking the event loop with I/O or other time-intensive tasks by offloading work via libuv to non-blocking system operations, worker threads, or a custom use of libuv threads.

  • Internal Node.js libraries:node 自身提供一些 addon 可以使用的 C++ API,最重要比如 node::ObjectWrap

  • 只有 libuv, OpenSSL, V8, and zlib symbols 被 node 重新暴露出来,其他似乎要手动链接 qwq

# 编写 Hello World

先跟官网来粘一份代码:

// hello.cc
#include <node.h>
namespace demo
{
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Object;
  using v8::String;
  using v8::Value;
  void Method(const FunctionCallbackInfo<Value> &args)
  {
    Isolate *isolate = args.GetIsolate();
    args.GetReturnValue().Set(String::NewFromUtf8(
                                  isolate, "world")
                                  .ToLocalChecked());
  }
  void Initialize(Local<Object> exports)
  {
    NODE_SET_METHOD(exports, "hello", Method);
  }
  NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo

所有 node addon 必须像这样暴露一个初始化函数:

void Initialize(Local<Object> exports);
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) // 后面没分号

The module_name must match the filename of the final binary (excluding the .node suffix).

上面例子中,the initialization function is Initialize and the addon module name is addon .(先比着抄就完事了)

# 构建

这里使用 node-gyp 来编译写好的 C++ 成一个二进制文件 addon.node

node-gyp 作为 npm 内置的一部分,被设计为正常情况下只有在 npm install 装一些 node addons 的时候才可以用内置的 node-gyp 来针对平台编译 addon,自己用的话需要手动安装 node-gyp,参考 github 仓库

小插曲:不知道为啥遇到一堆网络问题,npm, yarn, tencent 都试了一遍,还是 cnpm 最顶(顺带安利一个叫 nrm 的工具,顺带半个晚上进去了 QAQ)

在项目根目录新建一个叫 binding.gyp 的配置文件,供 node-gyp 来编译 addon

binding.gyp:

{
  "targets": [
    {
      "target_name": "addon",
      "sources": ["hello.cc"] // 这里注意 cc 文件的路径
    }
  ]
}

然后使用 node-gyp configure 会发现新增了一个 build 文件夹,这边 mac 平台里面有个 Makefile,然后再 node-gyp build 会发现新增了 build/Release/addon.node ,就编译完了

# 在 node 中使用 addon

直接在 node 中使用 require 即可引入刚刚编译的模块(像调用普通模块一样调用二进制文件,怎么这么像 wasm..)

hello.js:

const addon = require('./build/Release/addon');
console.log(addon.hello());
// Prints: 'world'

编译的二进制文件路径可能因为编译方式不同而不同,可以使用 node-bindings 来确定这个路径。

yarn add bindings
import bindings from 'bindings';

const addon = bindings('addon.node');

console.log(addon.hello());

# 传递参数

还是官网的例子:

addon.cc

#include <node.h>
namespace demo
{
  using v8::Exception;
  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Number;
  using v8::Object;
  using v8::String;
  using v8::Value;
  // This is the implementation of the "add" method
  // Input arguments are passed using the
  // const FunctionCallbackInfo<Value>& args struct
  void Add(const FunctionCallbackInfo<Value> &args)
  {
    Isolate *isolate = args.GetIsolate();
    // Check the number of arguments passed.
    if (args.Length() < 2)
    {
      // Throw an Error that is passed back to JavaScript
      isolate->ThrowException(Exception::TypeError(
          String::NewFromUtf8(isolate,
                              "Wrong number of arguments")
              .ToLocalChecked()));
      return;
    }
    // Check the argument types
    if (!args[0]->IsNumber() || !args[1]->IsNumber())
    {
      isolate->ThrowException(Exception::TypeError(
          String::NewFromUtf8(isolate,
                              "Wrong arguments")
              .ToLocalChecked()));
      return;
    }
    // Perform the operation
    double value =
        args[0].As<Number>()->Value() + args[1].As<Number>()->Value();
    Local<Number> num = Number::New(isolate, value);
    // Set the return value (using the passed in
    // FunctionCallbackInfo<Value>&)
    args.GetReturnValue().Set(num);
  }
  void Init(Local<Object> exports)
  {
    NODE_SET_METHOD(exports, "add", Add);
  }
  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
} // namespace demo

# 总结

这个看上去像 wasm 的东西大概优点就跟 wasm 差不多叭,能在 node 里跑 C++ 的话对性能提升不用说,还有些 js 实现不了的奇奇怪怪的操作(比如获取某个变量在内存中的表示形式 qwq)

顺带隔壁 deno 什么时候出个 Rust addons 呀

更新于