はじめに
最近、必要があって Node.js の Native アドオンを作りました。
その中で、外部とのデータやり取りで AsyncWorker を使っていたのですが、
値を受け取るときに Callback を使うサンプルはたくさんあるのですが、
イベントで返すサンプルが見つからなくてハマったので、ここに作り方をメモしておきます。
準備
現在、Node.js の Native アドオンの作成には色々方法がありますが、
今回のサンプルプログラムは、node-addon-api (N-API の C++ラッパー) を使っています。
(というか、他のやり方は「おまじない」だらけで、何が必要なのかがよくわからなかった・・・)
アドオンは、node-gyp を使ってビルドするのですが、環境作成方法などについては、先人の方々の優良記事がたくさんありますので割愛します。
(node-gyp タグで検索すると、たくさん見つかります。)
今回のサンプルプログラム作成に当たり、
https://qiita.com/Satachito/items/fa681ba96dc8e52ca7c1
が、非常に参考になりました。
-
この記事のプログラム一式は、以下のところにあります。
https://github.com/dejirutek/async-emitter_sample -
Node.js v12.13.0 で確認。
-
node-addon-api のバージョンは、1.7.1 で確認しています。それ以下のバージョンだと、恐らくコンパイルが通りません。
-
このサンプルプログラムは、Windows(7 / 10)でのみ動作確認しています。
まずは、呼び出し側プログラム (JavaScript) について
ソースファイル
'use strict'
// 利用するAPI
const { EventEmitter } = require('events');
const { inherits } = require('util');
// アドオン初期化
const { AsyncEmitter } = require('bindings')('async_emitter');
// EventEmitter クラスを継承させる
inherits(AsyncEmitter, EventEmitter);
// 引き数は、イベント発生インターバル(秒)
const emitter = new AsyncEmitter(1);
// 'data' イベントリスナー
emitter.on('data', (data, len) => {
console.log('event data =', data, ' len =', len);
});
let iLength = 8;
let iCount = 5;
// Workerパラメータ初期化
emitter.AsyncInit(iLength, iCount);
// キューに投入
emitter.AsyncQueue();
サンプルプログラムの動作
「指定した時間間隔(1秒)で、指定したバイト数(8バイト)の乱数を、指定した回数(5回)だけ返す」
というものです。
実行例
>node addon.js
event data = <Buffer a4 6c 40 6f 4f 13 8b 08> len = 8
event data = <Buffer a7 68 08 6f 9a 6a b3 99> len = 8
event data = <Buffer aa 65 d0 6e e4 c1 dc 2a> len = 8
event data = <Buffer ad 61 98 6d 2f 18 04 ba> len = 8
event data = <Buffer b1 5d 60 6c 79 6e 2d 4b> len = 8
解説
- アドオンプログラム自体には、イベント発生機能はないので、Node API の EventEmitter Class から継承しています。
- この辺は、https://github.com/nodejs/abi-stable-node-addon-examples/tree/master/inherits_from_event_emitter/node-addon-api を参考にしています。
つぎに、アドオン本体プログラム (C++) について
ファイルリスト
ファイル名 | 適用 |
---|---|
binding.gyp | node-gyp 定義ファイル(説明は省略) |
addon.cc | 初期化の時に呼ばれる |
async-emitter.h | クラス定義、他 |
async-emitter.cc | 主に Node.js とのインターフェースを記述 |
local-worker.cc | 主に Native 処理を記述 |
- ファイル全体は、前述のリンクを参照のこと。
#include <napi.h>
#include "async-emitter.h"
Napi::Object Init(Napi::Env env, Napi::Object exports) {
AsyncEmitter::Init(env, exports);
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
- 「アドオン初期化」時に、呼ばれるプログラムの実体
- "exports" は、Node.js モジュールの "module.exports" に相当
#include <napi.h>
#include <vector>
class AsyncEmitter : public Napi::ObjectWrap<AsyncEmitter>
{
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
AsyncEmitter(const Napi::CallbackInfo& info);
~AsyncEmitter();
protected:
class LocalWorker : public Napi::AsyncWorker
{
public:
static LocalWorker *worker;
static Napi::ObjectReference refThis;
static Napi::FunctionReference refEmit;
LocalWorker(Napi::Env env, int interval)
: Napi::AsyncWorker(env), interval(interval) {}
~LocalWorker() {}
void InitPerm(int length, int count) {
this->length = length;
this->count = count;
}
protected:
void Execute();
void OnOK();
private:
int interval;
int length;
int count;
std::vector<uint8_t> data;
};
private:
static Napi::FunctionReference constructor;
Napi::Value AsyncInit(const Napi::CallbackInfo &info);
Napi::Value AsyncQueue(const Napi::CallbackInfo &info);
};
- ここでのポイントは「ObjectWrap のサブクラスとして AsyncWorker」を、置いたところ。
- ObjectWrap については、
https://github.com/nodejs/node-addon-api/blob/master/doc/object_wrap.md
を、参照のこと。これは簡単に言うと「C++ のクラスを、Node.js のオブジェクト(クラス)に見せる」仕組み。これにより、「他のNode.js モジュール(今回だと、EventEmitter)から、継承」が出来るようになる。 - AsyncWorker の使い方は、[Examples] (https://github.com/nodejs/node-addon-examples) よりも、Tests の方が、参考になった。
#include "async-emitter.h"
Napi::FunctionReference AsyncEmitter::constructor;
Napi::Object AsyncEmitter::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
Napi::Function func = DefineClass(env, "AsyncEmitter", {
InstanceMethod("AsyncInit", &AsyncEmitter::AsyncInit),
InstanceMethod("AsyncQueue", &AsyncEmitter::AsyncQueue)
});
constructor = Napi::Persistent(func);
constructor.SuppressDestruct();
exports.Set("AsyncEmitter", func);
return exports;
}
AsyncEmitter::AsyncEmitter(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<AsyncEmitter>(info)
{
Napi::Env env = info.Env();
Napi::Object _this = info.This().As<Napi::Object>();
Napi::Function _emit = _this.Get("emit").As<Napi::Function>();
int interval = info[0].As<Napi::Number>().Int32Value();
LocalWorker *worker = new LocalWorker(env, interval);
worker->SuppressDestruct();
worker->refThis = Napi::Persistent(_this);
worker->refThis.SuppressDestruct();
worker->refEmit = Napi::Persistent(_emit);
worker->refEmit.SuppressDestruct();
LocalWorker::worker = worker;
}
AsyncEmitter::~AsyncEmitter() {
LocalWorker::worker = nullptr;
}
Napi::Value AsyncEmitter::AsyncInit(const Napi::CallbackInfo& info)
{
Napi::Env env = info.Env();
int length = info[0].As<Napi::Number>().Int32Value();
int count = info[1].As<Napi::Number>().Int32Value();
LocalWorker::worker->InitPerm(length, count);
return info.Env().Undefined();
}
Napi::Value AsyncEmitter::AsyncQueue(const Napi::CallbackInfo &info)
{
LocalWorker::worker->Queue();
return info.Env().Undefined();
}
- ここでのポイントは「各オブジェクトの Reference を取って、それを SuppressDestruct() して、消えないようにした」ところ。こうしないと、AsyncWorker の Execute() 処理が終わって戻ってきた(OnOK()の時点)ときに、オブジェクトが消えてしまう。
- "refThis" と "refEmit" には、各々、"Node.js 上の this"(この場合、このアドオン自身を指す) と "emitter.emit()" (https://nodejs.org/api/events.html#events_emitter_emit_eventname_args を参照) の、Reference が入っている。
#include "async-emitter.h"
#include <thread>
#include <chrono>
#include <ctime> // time
#include <cstdlib> // srand, rand
AsyncEmitter::LocalWorker *AsyncEmitter::LocalWorker::worker = nullptr;
Napi::ObjectReference AsyncEmitter::LocalWorker::refThis;
Napi::FunctionReference AsyncEmitter::LocalWorker::refEmit;
void AsyncEmitter::LocalWorker::Execute()
{
std::this_thread::sleep_for(std::chrono::seconds(interval));
std::srand( time(NULL) );
data.clear();
for(int i = 0; i < length; i++) {
uint8_t rdata = rand() % 0x100;
data.push_back(rdata);
}
}
void AsyncEmitter::LocalWorker::OnOK()
{
Napi::Function emit = refEmit.Value();
Napi::Object _this = refThis.Value();
Napi::Env env = Env();
Napi::HandleScope scope(env);
if (data.size()) {
emit.Call(
_this,
{
Napi::String::New(env, "data"),
Napi::Buffer<uint8_t>::Copy(env, data.data(), data.size()),
Napi::Number::New(env, length)
}
);
}
if (--count) {
worker->Queue();
}
}
- Execute() は、Native の世界で「何でもやり放題」なので Sleep でも EventWait でも何でも使える。
(この場所以外では、例えば Sleep すると「Node.js のイベントループ自体」が止まってしまうのでマズい。) - OnOK() は、Execute() が終了した時点で呼ばれる。Reference.Value() で、元の実体が取れる。
- emit.Call() は、Node.js 上での "emitter.emit()" と同じ。
補足
- このサンプルプログラムは、機能検証を目的としたもなので、例外処理は入っていませんのでご注意ください。
- このアドオンは、「クラスのような形」をしていますが、例えば「インターバルの違う2つのオブジェクト」の同時生成、
const emitter1 = new AsyncEmitter(1);
const emitter2 = new AsyncEmitter(2);
のようなことはできません。(やるとプログラムが落ちます。多分、メモリリークしてる。)
おそらく Reference が、static になっているからだと思いますが、static にしないと、コンパイルが通りません。(正直、この辺の理屈はまだよく理解していない・・・)
- 本当は、EventEmitter / inherits も、アドオン内に取り込みたかった(だぶん、出来るはず)のですが、そこまで至っていません。
- その他、Native アドオンについて、まだよく理解していないところがあるので、何か間違ったことを言っているかもしれません。
終わりに
以上です。Node.js の Native アドオンについての情報は、この程度の情報すらネットを検索しても(英語でも)殆どないのが現状です。(あっても、古い情報が多い)
この情報が、どなたかのお役に立てれば幸いです。