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

# はじめに

最近、必要があって 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'

// 利用するＡＰＩ
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();
```

### サンプルプログラムの動作
「指定した時間間隔（１秒）で、指定したバイト数（８バイト）の乱数を、指定した回数（５回）だけ返す」
というものです。

#### 実行例

```
>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 処理を記述|

+ ファイル全体は、前述の[リンク](https://github.com/dejirutek/async-emitter_sample)を参照のこと。

```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 については、<br>
https://github.com/nodejs/node-addon-api/blob/master/doc/object_wrap.md <br>
を、参照のこと。これは簡単に言うと「C++ のクラスを、Node.js のオブジェクト（クラス）に見せる」仕組み。これにより、「他のNode.js モジュール（今回だと、EventEmitter）から、継承」が出来るようになる。
+ AsyncWorker の使い方は、[Examples] (https://github.com/nodejs/node-addon-examples) よりも、[Tests](https://github.com/nodejs/node-addon-api/tree/master/test) の方が、参考になった。

```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 でも何でも使える。<br>
（この場所以外では、例えば Sleep すると「Node.js のイベントループ自体」が止まってしまうのでマズい。）
+ OnOK() は、Execute() が終了した時点で呼ばれる。Reference.Value() で、元の実体が取れる。
+ emit.Call() は、Node.js 上での "emitter.emit()" と同じ。

# 補足

+ このサンプルプログラムは、機能検証を目的としたもなので、例外処理は入っていませんのでご注意ください。
+ このアドオンは、「クラスのような形」をしていますが、例えば「インターバルの違う２つのオブジェクト」の同時生成、

```JavaScript
const emitter1 = new AsyncEmitter(1);
const emitter2 = new AsyncEmitter(2);
```

のようなことはできません。（やるとプログラムが落ちます。多分、メモリリークしてる。）
おそらく Reference が、static になっているからだと思いますが、static にしないと、コンパイルが通りません。（正直、この辺の理屈はまだよく理解していない・・・）

+ 本当は、EventEmitter / inherits も、アドオン内に取り込みたかった（だぶん、出来るはず）のですが、そこまで至っていません。
+ その他、Native アドオンについて、まだよく理解していないところがあるので、何か間違ったことを言っているかもしれません。

# 終わりに
以上です。Node.js の Native アドオンについての情報は、この程度の情報すらネットを検索しても（英語でも）殆どないのが現状です。（あっても、古い情報が多い）
この情報が、どなたかのお役に立てれば幸いです。