Vincent Chan 的巴士站 🚉

使用 N-API 和 CMake 为 Node.js 添加 C++ 拓展

以前为 Node.js 编写拓展的时候,使用的是 Node.js 的 C++ addon API,直接使用 v8 提供的 api 和 Node.js 打交道。使用 C++ Addon 的 API 有个缺点就是 ABI 层面不兼容,升级 Node.js 之后 extension 要重新编译。

现在 Node.js 提供了 N-API。这个 API 是 C 层面的支持(也有 C++ 绑定)。同时,使用 N-API 写出来的拓展是 ABI 兼容的。

而使用 N-API 编写 Node.js 扩展不一定用 node-gyp 进行编译,也可以使用 cmake, 这对一些使用 CMake 的 C++ 项目可以说非常友好了。

使用方法

按照官方教程

$ yarn add node-addon-api cmake-js bindings

然后在 package.json 里面加上一句:

"scripts": {
  "install": "cmake-js compile"
}

然后愉快的 yarn install 就可以使用了。

贴一下我的 CMakeList.txt 配置:

project (zparser)
cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)

include_directories(${CMAKE_JS_INC})
file(GLOB SOURCE_FILES "src/*.cc" "src/*.h")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

set(JSON_BuildTests OFF CACHE INTERNAL "")

# Include N-API wrappers
execute_process(COMMAND node -p "require('node-addon-api').include"
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        OUTPUT_VARIABLE NODE_ADDON_API_DIR
        )
string(REPLACE "\n" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
string(REPLACE "\"" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
target_include_directories(${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

可以说非常简单易用了。

Api 使用

C++ 官方文档 的介绍还是比较简陋的,如果不了解 C-API 用起来有些地方会比较困惑。 但是如果用过之前用过 v8 的 addon-api 的话,还是比较容易理解的。

大部分的 Object 和 Array 等操作都有,你甚至可以用 ObjectWrap 去定义自己 的 Class:

class Example : public Napi::ObjectWrap<Example> {
  public:
    static Napi::Object Init(Napi::Env env, Napi::Object exports);
    Example(const Napi::CallbackInfo &info);

  private:
    static Napi::FunctionReference constructor;
    double _value;
    Napi::Value GetValue(const Napi::CallbackInfo &info);
    Napi::Value SetValue(const Napi::CallbackInfo &info);
};

我目前没有发现的就是如何定义一个 Undefined 的 Value。所以用到 Undefined 的时候,我就不得不用 C-API 去定义一个 napi_value 来用 undefined。好在 C++ 的 API 可以和 C 混用:

napi_value* result = nullptr;
if (napi_get_undefined(env, result) == napi_ok) {
  // do something
}

在 js 里面的使用也很简单,不过要配合 bindings 库使用:

const example = require('bindings')('example');