背景
一些业务功能需要nodejs和c++互相调用
方案选型
选择封装性和便捷性最好的 node-addon-api
关键技术
js调用c++
异步回调(同步方式太简单)
异步回调需要继承AsyncWorker,一般需要覆写Execute方法。
编译可参考下面 binding.gyp 模板。
SimpleAsyncWorker.h
#pragma once
#include <napi.h>
using namespace Napi;
class SimpleAsyncWorker : public AsyncWorker {
public:
SimpleAsyncWorker(Function& callback, int runTime);
virtual ~SimpleAsyncWorker(){};
void Execute();
void OnOK();
private:
int runTime;
};
SimpleAsyncWorker.cc
#include "SimpleAsyncWorker.h"
#include <chrono>
#include <thread>
SimpleAsyncWorker::SimpleAsyncWorker(Function& callback, int runTime)
: AsyncWorker(callback), runTime(runTime){};
void SimpleAsyncWorker::Execute() {
std::this_thread::sleep_for(std::chrono::seconds(runTime));
if (runTime == 4) SetError("Oops! Failed after 'working' 4 seconds.");
};
void SimpleAsyncWorker::OnOK() {
std::string msg = "SimpleAsyncWorker returning after 'working' " +
std::to_string(runTime) + " seconds.";
Callback().Call({Env().Null(), String::New(Env(), msg)});
};
模块导出 RunSimpleAsyncWorker.cc
#include "SimpleAsyncWorker.h"
Value runSimpleAsyncWorker(const CallbackInfo& info) {
int runTime = info[0].As<Number>();
Function callback = info[1].As<Function>();
SimpleAsyncWorker* asyncWorker = new SimpleAsyncWorker(callback, runTime);
asyncWorker->Queue();
std::string msg =
"SimpleAsyncWorker for " + std::to_string(runTime) + " seconds queued.";
return String::New(info.Env(), msg.c_str());
};
Object Init(Env env, Object exports) {
exports["runSimpleAsyncWorker"] = Function::New(
env, runSimpleAsyncWorker, std::string("runSimpleAsyncWorker"));
return exports;
}
NODE_API_MODULE(addon, Init)
测试 Test.js
const runWorker = require('../build/Release/napi-asyncworker-example-native');
let result = runWorker.runSimpleAsyncWorker(2, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
result = runWorker.runSimpleAsyncWorker(4, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
result = runWorker.runSimpleAsyncWorker(8, AsyncWorkerCompletion);
console.log("runSimpleAsyncWorker returned '"+result+"'.");
function AsyncWorkerCompletion (err, result) {
if (err) {
console.log("SimpleAsyncWorker returned an error: ", err);
} else {
console.log("SimpleAsyncWorker returned '"+result+"'.");
}
};
输出如下
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 2 seconds queued.'.
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 4 seconds queued.'.
runSimpleAsyncWorker returned 'SimpleAsyncWorker for 8 seconds queued.'.
SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 2 seconds.'.
SimpleAsyncWorker returned an error: [Error: Oops! Failed after 'working' 4 seconds.]
SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 8 seconds.'.
c++调用js
c++异步回调js方法
通过TypedThreadSafeFunction 构造全局tsfn对象,再在native线程调用
编译可参考后面CMakeLists.txt模板
c++ 代码 clock.cc
#include <chrono>
#include <napi.h>
#include <thread>
using namespace Napi;
using Context = Reference<Value>;
using DataType = int;
void CallJs(Napi::Env env, Function callback, Context *context, DataType *data);
using TSFN = TypedThreadSafeFunction<Context, DataType, CallJs>;
using FinalizerDataType = void;
std::thread nativeThread;
TSFN tsfn;
Value Start(const CallbackInfo &info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
throw TypeError::New(env, "Expected two arguments");
} else if (!info[0].IsFunction()) {
throw TypeError::New(env, "Expected first arg to be function");
} else if (!info[1].IsNumber()) {
throw TypeError::New(env, "Expected second arg to be number");
}
int count = info[1].As<Number>().Int32Value();
// Create a new context set to the the receiver (ie, `this`) of the function
// call
Context *context = new Reference<Value>(Persistent(info.This()));
// Create a ThreadSafeFunction
tsfn = TSFN::New(
env,
info[0].As<Function>(), // JavaScript function called asynchronously
"Resource Name", // Name
0, // Unlimited queue
1, // Only one thread will use this initially
context,
[](Napi::Env, FinalizerDataType *,
Context *ctx) { // Finalizer used to clean threads up
nativeThread.join();
delete ctx;
});
// Create a native thread
nativeThread = std::thread([count] {
for (int i = 0; i < count; i++) {
// Create new data
int *value = new int(clock());
// Perform a blocking call
napi_status status = tsfn.BlockingCall(value);
if (status != napi_ok) {
// Handle error
break;
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// Release the thread-safe function
tsfn.Release();
});
return Boolean::New(env, true);
}
// Transform native data into JS data, passing it to the provided
// `callback` -- the TSFN's JavaScript function.
void CallJs(Napi::Env env, Function callback, Context *context,
DataType *data) {
// Is the JavaScript environment still available to call into, eg. the TSFN is
// not aborted
if (env != nullptr) {
// On Node-API 5+, the `callback` parameter is optional; however, this example
// does ensure a callback is provided.
if (callback != nullptr) {
callback.Call(context->Value(), {Number::New(env, *data)});
}
}
if (data != nullptr) {
// We're finished with the data.
delete data;
}
}
Napi::Object Init(Napi::Env env, Object exports) {
exports.Set("start", Function::New(env, Start));
return exports;
}
NODE_API_MODULE(clock, Init)
测试 Test.js
const { start } = require('bindings')('clock');
start.call(new Date(), function (clock) {
const context = this;
console.log(context, clock);
}, 5);
编译
cmake-js 方式
需要 CMakeLists.txt
- 先全局安装 cmake-js
npm i -g cmake-js
已安装就不需要这一步了 cmake-js build
CMakeLists.txt模板如下
project (clock)
include_directories(${CMAKE_JS_INC} node_modules/node-addon-api/)
cmake_minimum_required(VERSION 3.18)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(${CMAKE_JS_INC})
file(GLOB SOURCE_FILES "*.cc")
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB})
# Include Node-API wrappers
execute_process(COMMAND node -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADDON_API_DIR
)
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR})
# define NAPI_VERSION
add_definitions(-DNAPI_VERSION=4)
node-gyp方式
需要 binding.gyp
- 先全局安装 node-gyp
npm i -g node-gyp
已安装就不需要这一步了 node-gyp build
binding.gyp 模板如下
{
'targets': [
{
'target_name': 'napi-asyncworker-example-native',
'sources': [ 'src/RunSimpleAsyncWorker.cc', 'src/SimpleAsyncWorker.cc' ],
'include_dirs': ["<!@(node -p \"require('node-addon-api').include\")"],
'dependencies': ["<!(node -p \"require('node-addon-api').gyp\")"],
'cflags!': [ '-fno-exceptions' ],
'cflags_cc!': [ '-fno-exceptions' ],
'xcode_settings': {
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
'CLANG_CXX_LIBRARY': 'libc++',
'MACOSX_DEPLOYMENT_TARGET': '10.7'
},
'msvs_settings': {
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
}
}
]
}
vs方式
可参考cmake-js 或者node-gyp 编译生成的中间解决方案