NAPI 开发 C++ Addon

实现C++和NodeJS代码的联合编程,总结下来有下面几种途径:

1. Nodeffi

由于node-ffi只支持win32系统,在nodejs11以及以上版本也就不再支持,用的会越来越少

2. Emscripten

脱胎于asm.js,适用于C++代码比较固化的情况。

3. v8

官方的原生编写c++ addon的接口,代码不容易理解

4. Napi

本文介绍,对v8进行进一步的封装。

 

一. NAPI 介绍

Node-API(以前称为N-API)是用于构建本机插件的API。它独立于底层JavaScript运行时(例如V8),并作为Node.js本身的一部分进行维护。该API在所有版本的Node.js中都是稳定的应用程序二进制接口(ABI)。它旨在使附加组件与基础JavaScript引擎中的更改隔离,并使为一个主要版本编译的模块可以在Node.js的更高主要版本上运行,而无需重新编译。ABI稳定性指南》提供了更深入的说明。

使用标题为C ++ Addons的部分中概述的相同方法/工具来构建/打包Addons唯一的区别是本机代码使用的API集。代替使用V8或Node.js API的本机抽象,使用Node-API中可用的功能。

Node-API公开的API通常用于创建和操作JavaScript值。概念和操作通常映射到ECMA-262语言规范中指定的概念。API具有以下属性:

  • 所有Node-API调用均返回类型为的状态码napi_status此状态指示API调用是成功还是失败。
  • API的返回值通过out参数传递。
  • 所有JavaScript值都在名为的不透明类型之后抽象 napi_value
  • 如果是错误状态代码,则可以使用获取其他信息napi_get_last_error_info在错误处理部分错误处理中可以找到更多信息

Node-API是一种C API,可确保跨Node.js版本和不同编译器级别的ABI稳定性。C ++ API可能更易于使用。

为了支持使用C ++,项目维护了一个名为的C ++包装器模块node-addon-api该包装器提供了一个可内联的C ++ API。内置的二进制文件node-addon-api将取决于由Node.js导出的基于Node-API C的函数的符号。node-addon-api是编写调用Node-API的代码的更有效方法。

node-addon-api不是node.js的一部分,需要独立安装,后面会演示用法

 

二. Hello world 示例

官网上提供的最简单的example,js调用hello,返回world

 1 // hello.cc using Node-API
 2 #include <node_api.h>
 3 
 4 namespace demo {
 5 
 6 napi_value Method(napi_env env, napi_callback_info args) {
 7   napi_value greeting;
 8   napi_status status;
 9 
10   status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
11   if (status != napi_ok) return nullptr;
12   return greeting;
13 }
14 
15 napi_value init(napi_env env, napi_value exports) {
16   napi_status status;
17   napi_value fn;
18 
19   status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
20   if (status != napi_ok) return nullptr;
21 
22   status = napi_set_named_property(env, exports, "hello", fn);
23   if (status != napi_ok) return nullptr;
24   return exports;
25 }
26 
27 NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
28 
29 }  // namespace demo

 

三. NAPI 的编译

编译NAPI有两种方式:node-gyp 和 cmake-js

node-gyp:

安装:npm install -g node-gyp

在项目中要使用node-gyp, 在项目目录下添加binding.gyp文件:

{
  "targets": [
    {
      "target_name": "addon",
      "include_dirs": ["/path/to/include/dir"],
      "library_dirs": ["/path/to/library/dir"],
      "libraries": ["libXXX.so"],
      "sources": [ "addon.cc", "example.cc" ]
    }
  ]
}

执行命令 node-gyp configure build 之后可以看到项目build文件夹下有个XXX.node文件,就是我们调用时要的二进制文件。

cmake-js:

安装: npm install -g cmake-js 

安装完成后,可以编写CMakeLists.txt文件来编译:

cmake_minimum_required(VERSION 3.5)

project(cmake-js-test LANGUAGES CXX)

include_directories(${CMAKE_JS_INC})

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

file(GLOB SOURCE_FILES "./*cc" "./*.h")

add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})

set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

target_link_libraries(${PROJECT_NAME} ${PROJECT_LINK_LIBS} ${CMAKE_JS_LIB})

在目录下执行cmake-js compile 或者用Qt Creator也可以打开该项目,点击build

编译好了以后,同样在build目录下有XXX.node文件。

用Qt Creator编译的代码可以用Qt Creator附加js进程调试,VSCode应该也可以。

 

nodejs调用上例中的hello world,就2行代码:

1 const addon = require('./build/cmake-js-test');
2 console.log(addon.hello());

 

四. NAPI 包装类

Wrap一个类在napi中代码比较难以理解,本例中使用node-addon-api。

在js项目中安装依赖:

npm install --save-dev node-addon-api

npm install --save-dev bindings

在目录下写一个类:

MyObject.h

 1 #ifndef MYOBJECT_H
 2 #define MYOBJECT_H
 3 
 4 #include <napi.h>
 5 
 6 class MyObject : public Napi::ObjectWrap<MyObject> {
 7  public:
 8   static Napi::Object Init(Napi::Env env, Napi::Object exports);
 9   MyObject(const Napi::CallbackInfo& info);
10 
11  private:
12   Napi::Value GetValue(const Napi::CallbackInfo& info);
13   Napi::Value PlusOne(const Napi::CallbackInfo& info);
14   Napi::Value Multiply(const Napi::CallbackInfo& info);
15 
16   double value_;
17 };
18 
19 #endif

MyObject.cc

 1 #include "example.h"
 2 
 3 Napi::Object MyObject::Init(Napi::Env env, Napi::Object exports) {
 4   Napi::Function func =
 5       DefineClass(env,
 6                   "MyObject",
 7                   {InstanceMethod("plusOne", &MyObject::PlusOne),
 8                    InstanceMethod("value", &MyObject::GetValue),
 9                    InstanceMethod("multiply", &MyObject::Multiply)});
10 
11   Napi::FunctionReference* constructor = new Napi::FunctionReference();
12   *constructor = Napi::Persistent(func);
13   env.SetInstanceData(constructor);
14 
15   exports.Set("MyObject", func);
16   return exports;
17 }
18 
19 MyObject::MyObject(const Napi::CallbackInfo& info)
20     : Napi::ObjectWrap<MyObject>(info) {
21   Napi::Env env = info.Env();
22 
23   int length = info.Length();
24 
25   if (length <= 0 || !info[0].IsNumber()) {
26     Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
27     return;
28   }
29 
30   Napi::Number value = info[0].As<Napi::Number>();
31   this->value_ = value.DoubleValue();
32 }
33 
34 Napi::Value MyObject::GetValue(const Napi::CallbackInfo& info) {
35   double num = this->value_;
36 
37   return Napi::Number::New(info.Env(), num);
38 }
39 
40 Napi::Value MyObject::PlusOne(const Napi::CallbackInfo& info) {
41   this->value_ = this->value_ + 1;
42 
43   return MyObject::GetValue(info);
44 }
45 
46 Napi::Value MyObject::Multiply(const Napi::CallbackInfo& info) {
47   Napi::Number multiple;
48   if (info.Length() <= 0 || !info[0].IsNumber()) {
49     multiple = Napi::Number::New(info.Env(), 1);
50   } else {
51     multiple = info[0].As<Napi::Number>();
52   }
53 
54   Napi::Object obj = info.Env().GetInstanceData<Napi::FunctionReference>()->New(
55       {Napi::Number::New(info.Env(), this->value_ * multiple.DoubleValue())});
56 
57   return obj;
58 }

接口addon.cc

1 #include <napi.h>
2 #include <node/node_api.h>
3 #include "example.h"
4 
5 Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
6   return MyObject::Init(env, exports);
7 }
8 
9 NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll)

CMakeList:

cmake_minimum_required(VERSION 3.5)

project(cmake-js-test LANGUAGES CXX)

include_directories(${CMAKE_JS_INC})

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

file(GLOB SOURCE_FILES "./*cc" "./*.h")

include_directories(/usr/lib/node_modules/node-addon-api)
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES})

set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")

target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})

编译号后build文件夹看到addon.node

 

 nodejs测试:

 1 var addon = require('bindings')('addon');
 2 
 3 var obj = new addon.MyObject(10);
 4 console.log( obj.plusOne() ); // 11
 5 console.log( obj.plusOne() ); // 12
 6 console.log( obj.plusOne() ); // 13
 7 
 8 console.log( obj.multiply().value() ); // 13
 9 console.log( obj.multiply(10).value() ); // 130
10 
11 var newobj = obj.multiply(-1);
12 console.log( newobj.value() ); // -13
13 console.log( obj === newobj ); // false

这里有丰富的例子可以学习。

https://github.com/nodejs/node-addon-examples

 

posted @ 2021-05-13 17:32  Asp1rant  阅读(2184)  评论(0编辑  收藏  举报