實現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
