はじめに
最近、必要があって 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_sampleNode.js v12.13.0 で確認。
node-addon-api のバージョンは、1.7.1 で確認しています。それ以下のバージョンだと、恐らくコンパイルが通りません。
このサンプルプログラムは、Windows(7 / 10)でのみ動作確認しています。
まずは、呼び出し側プログラム (JavaScript) について
ソースファイル
'use strict'// 利用するAPIconst{EventEmitter}=require('events');const{inherits}=require('util');// アドオン初期化const{AsyncEmitter}=require('bindings')('async_emitter');// EventEmitter クラスを継承させるinherits(AsyncEmitter,EventEmitter);// 引き数は、イベント発生インターバル(秒)constemitter=newAsyncEmitter(1);// 'data' イベントリスナーemitter.on('data',(data,len)=>{console.log('event data =',data,' len =',len);});letiLength=8;letiCount=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::ObjectInit(Napi::Envenv,Napi::Objectexports){AsyncEmitter::Init(env,exports);returnexports;}NODE_API_MODULE(NODE_GYP_MODULE_NAME,Init)- 「アドオン初期化」時に、呼ばれるプログラムの実体
- "exports" は、Node.js モジュールの "module.exports" に相当
#include <napi.h>
#include <vector>
classAsyncEmitter:publicNapi::ObjectWrap<AsyncEmitter>{public:staticNapi::ObjectInit(Napi::Envenv,Napi::Objectexports);AsyncEmitter(constNapi::CallbackInfo&info);~AsyncEmitter();protected:classLocalWorker:publicNapi::AsyncWorker{public:staticLocalWorker*worker;staticNapi::ObjectReferencerefThis;staticNapi::FunctionReferencerefEmit;LocalWorker(Napi::Envenv,intinterval):Napi::AsyncWorker(env),interval(interval){}~LocalWorker(){}voidInitPerm(intlength,intcount){this->length=length;this->count=count;}protected:voidExecute();voidOnOK();private:intinterval;intlength;intcount;std::vector<uint8_t>data;};private:staticNapi::FunctionReferenceconstructor;Napi::ValueAsyncInit(constNapi::CallbackInfo&info);Napi::ValueAsyncQueue(constNapi::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よりも、Testsの方が、参考になった。
#include "async-emitter.h"
Napi::FunctionReferenceAsyncEmitter::constructor;Napi::ObjectAsyncEmitter::Init(Napi::Envenv,Napi::Objectexports){Napi::HandleScopescope(env);Napi::Functionfunc=DefineClass(env,"AsyncEmitter",{InstanceMethod("AsyncInit",&AsyncEmitter::AsyncInit),InstanceMethod("AsyncQueue",&AsyncEmitter::AsyncQueue)});constructor=Napi::Persistent(func);constructor.SuppressDestruct();exports.Set("AsyncEmitter",func);returnexports;}AsyncEmitter::AsyncEmitter(constNapi::CallbackInfo&info):Napi::ObjectWrap<AsyncEmitter>(info){Napi::Envenv=info.Env();Napi::Object_this=info.This().As<Napi::Object>();Napi::Function_emit=_this.Get("emit").As<Napi::Function>();intinterval=info[0].As<Napi::Number>().Int32Value();LocalWorker*worker=newLocalWorker(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::ValueAsyncEmitter::AsyncInit(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();intlength=info[0].As<Napi::Number>().Int32Value();intcount=info[1].As<Napi::Number>().Int32Value();LocalWorker::worker->InitPerm(length,count);returninfo.Env().Undefined();}Napi::ValueAsyncEmitter::AsyncQueue(constNapi::CallbackInfo&info){LocalWorker::worker->Queue();returninfo.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::ObjectReferenceAsyncEmitter::LocalWorker::refThis;Napi::FunctionReferenceAsyncEmitter::LocalWorker::refEmit;voidAsyncEmitter::LocalWorker::Execute(){std::this_thread::sleep_for(std::chrono::seconds(interval));std::srand(time(NULL));data.clear();for(inti=0;i<length;i++){uint8_trdata=rand()%0x100;data.push_back(rdata);}}voidAsyncEmitter::LocalWorker::OnOK(){Napi::Functionemit=refEmit.Value();Napi::Object_this=refThis.Value();Napi::Envenv=Env();Napi::HandleScopescope(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つのオブジェクト」の同時生成、
constemitter1=newAsyncEmitter(1);constemitter2=newAsyncEmitter(2);のようなことはできません。(やるとプログラムが落ちます。多分、メモリリークしてる。)
おそらく Reference が、static になっているからだと思いますが、static にしないと、コンパイルが通りません。(正直、この辺の理屈はまだよく理解していない・・・)
- 本当は、EventEmitter / inherits も、アドオン内に取り込みたかった(だぶん、出来るはず)のですが、そこまで至っていません。
- その他、Native アドオンについて、まだよく理解していないところがあるので、何か間違ったことを言っているかもしれません。
終わりに
以上です。Node.js の Native アドオンについての情報は、この程度の情報すらネットを検索しても(英語でも)殆どないのが現状です。(あっても、古い情報が多い)
この情報が、どなたかのお役に立てれば幸いです。