本文主要介绍如何在 Node-API 中实现不同类型的接口,主要有下面几种类型:

  • 同步调用
  • 基于 Napi::AsyncWorker 的异步调用,通过回调函数返回
  • 异步调用,返回 Promise
  • 基于 Napi::ThreadSafeFunction 的异步调用,通过回调函数返回

一、同步调用

同步调用会阻塞 Node.js 的主进程,适合接口能快速返回的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
Napi::Number Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 2)
throw Napi::TypeError::New(env, "Wrong number of arguments");

if (!info[0].IsNumber() || !info[1].IsNumber())
throw Napi::TypeError::New(env, "Wrong arguments");

const int ret = info[0].ToNumber().Int32Value() + info[1].ToNumber().Int32Value();

return Napi::Number::New(env, ret);
}

二、异步回调

在 Node-API 中,不能在系统子线程中直接调用 JavaScript 函数,但可以借助 Napi::AsyncWorker 类来实现异步执行任务,在Napi::AsyncWorker 类的 Execute 虚函数中执行具体的任务,并在其 OnOKOnError 虚函数中执行 JavaScript 回调函数,通过回调的方式来返回结果到 JavaScript。

如果在 Execute 函数中需要返回错误,可以调用 SetError 函数来设置错误信息,调用 SetError 函数后,OnError 函数就会被自动执行。需要注意的是,SetError 函数不会终止 Execute 函数的执行流程,通常会在 SetError 调用之后紧接 return 语句。

当 Execute 函数执行完以后,如果期间没有调用 SetError 函数,则 OnOK 函数也会被自动执行。

我们需要在 OnOK 或 OnError 中调用回调函数来将结果返回到 JavaScript,通常保持与 Node.js 官方模块的接口一样(当不是强制的),回调函数有 2 个参数 callback(err, result),失败时 err 不为 Null,成功时 err 为 Null。

下面示例以异步方式计算正整数 N 的 10 次方为例,通过回调方式返回计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Power10AsyncWorker : public Napi::AsyncWorker {
public:
Power10AsyncWorker(const Napi::Function& callback, int32_t n) :
Napi::AsyncWorker(callback),
n_(n) {
}

void Execute() override {
if (n_ <= 0) {
// N小于0则返回错误
SetError("N must larger than 0");
return;
}

result_ = 1;
for (int i = 0; i < 10; i++) {
// 由于CPU计算10次方太快了,这里每次暂停10ms,模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(10));
result_ *= n_;
}
}

void OnOK() override {
Callback().Call({Env().Null(), Napi::Value::From(Env(), result_)});
}

void OnError(const Napi::Error& e) override {
Callback().Call({e.Value(), Env().Null()});
}

private:
int32_t n_ = 0;
int32_t result_ = 0; // 存储结果
};

定义接口GetPower10,支持2个参数:整数N和回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void GetPower10(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 2)
throw Napi::TypeError::New(env, "Wrong number of arguments");

if (!info[0].IsNumber() || !info[1].IsFunction())
throw Napi::TypeError::New(env, "Wrong arguments");

int32_t n = info[0].ToNumber().Int32Value();
Napi::Function callback = info[1].As<Napi::Function>();

(new Power10AsyncWorker(callback, n))->Queue();
}

三、返回 Promise 对象

下面示例以异步计算正整数 N 的 20 次方为例,介绍如何返回 Promise 对象。当 N 小于 0 时,Promise 设置为 reject 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Power20AsyncWorker : public Napi::AsyncWorker {
public:
Power20AsyncWorker(const Napi::Env& env, const Napi::Promise::Deferred& deferred, int32_t n) :
Napi::AsyncWorker(env),
deferred_(deferred),
n_(n) {
}

void Execute() override {
if (n_ <= 0) {
// 小于0时返回错误
SetError("N must larger than 0");
return;
}

result_ = 1;
for (int i = 0; i < 20; i++) {
// 由于CPU计算20次方太快了,每次暂停10ms,模拟耗时操作
std::this_thread::sleep_for(std::chrono::milliseconds(10));
result_ *= n_;
}
}

void OnOK() override {
deferred_.Resolve(Napi::Number::New(Env(), result_));
}

void OnError(const Napi::Error& e) override {
deferred_.Reject(e.Value());
}

private:
Napi::Promise::Deferred deferred_;
int32_t n_ = 0;
int32_t result_ = 0; // 存储结果
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Napi::Value GetPower20(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 1)
throw Napi::TypeError::New(env, "Wrong number of arguments");

if (!info[0].IsNumber())
throw Napi::TypeError::New(env, "Wrong arguments");

int32_t n = info[0].ToNumber().Int32Value();
Napi::Function callback = info[1].As<Napi::Function>();

Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

(new Power20AsyncWorker(env, deferred, n))->Queue();

return deferred.Promise();
}

四、ThreadSafeFunction

前面的示例都没有自己启动系统子线程,而是通过 Napi::AsyncWorker 实现的异步调用,在本示例中我们使用 C++ 11 的 std::thread 开启系统子线程,在子线程中执行任务并调用回调函数返回结果。

要实现在系统子线程中调用 JavaScript 回调函数,需要借助于 Napi::ThreadSafeFunction 类来实现。

下面示例演示了在系统子线程中计算正整数 N 的 30 次方,并通过回调方式返回计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
void GetPower30(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() != 2)
throw Napi::TypeError::New(env, "Wrong number of arguments");

if (!info[0].IsNumber() || !info[1].IsFunction())
throw Napi::TypeError::New(env, "Wrong arguments");

int32_t n = info[0].ToNumber().Int32Value();
Napi::Function callback = info[1].As<Napi::Function>();

std::thread* nativeThread = nullptr;

Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
env,
callback,
"Resource Name", // Name
0, // Unlimited queue
1, // Only one thread will use this initially
[nativeThread](Napi::Env) { // Finalizer used to clean threads up
if (nativeThread && nativeThread->joinable()) {
nativeThread->join();
}
});

nativeThread = new std::thread([n, tsfn]() {
struct CallParam {
std::string error;
int32_t result = 0;
};

CallParam* param = new CallParam();

if (n > 0) {
param->result = 1;
for (int i = 0; i < 30; i++) {
param->result *= n;
}
}
else {
param->error = "N must larger than 0";
}

auto callback = [](Napi::Env env, Napi::Function jsCallback, CallParam* param) {
if (jsCallback && param) {
try {
if (param->error.empty()) {
jsCallback.Call({env.Null(), Napi::Number::New(env, param->result)});
}
else {
Napi::Error err = Napi::Error::New(env, param->error);
jsCallback.Call({err.Value(), env.Null()});
}
} catch (std::exception& e) {
#if (defined _WIN32 || defined WIN32)
OutputDebugStringA(e.what());
#endif
}
}

if (param) {
delete param;
}
};

napi_status status = tsfn.BlockingCall(param, callback);
if (status != napi_ok) {
// TODO How to handle error
}

tsfn.Release();
});
}

完整的示例代码以上传自 Github:

https://github.com/winsoft666/node-addon-sample