LoginSignup
2
4

More than 3 years have passed since last update.

Node.js の C++ によるアドオンで、AsyncWorker からイベントを受け取る

Posted at

はじめに

最近、必要があって 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) について

ソースファイル

addon.js
'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

解説

つぎに、アドオン本体プログラム (C++) について

ファイルリスト

ファイル名 適用
binding.gyp node-gyp 定義ファイル(説明は省略)
addon.cc 初期化の時に呼ばれる
async-emitter.h クラス定義、他
async-emitter.cc 主に Node.js とのインターフェースを記述
local-worker.cc 主に Native 処理を記述
  • ファイル全体は、前述のリンクを参照のこと。
addon.cc
#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" に相当
async-emitter.h
#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 よりも、Tests の方が、参考になった。
async-emitter.cc
#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 が入っている。
local-worker.cc
#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 アドオンについての情報は、この程度の情報すらネットを検索しても(英語でも)殆どないのが現状です。(あっても、古い情報が多い)
この情報が、どなたかのお役に立てれば幸いです。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4