📚

随意のタイミングで JavaScript の関数を呼び出す Node.js のネイティブ拡張アドオン向け C++ クラスを作ってみた

2022/11/13に公開

はじめに

こんにちは、わたる です。

50 の手習いで Web アプリを作ってみようと調べた時に得たものの備忘録(以下「50 の手習い備忘録」)第2弾です。

前回の 拙記事 は JavaScript での WebAPI を使って PUSH する考え方でしたが、今回はタイトルにもあります通り「サーバー側で C++ のネイティブ拡張を使って立ち上がっているワーカスレッドから、随意のタイミングで JavaScript の関数をコールする C++ のクラスライブラリのご提供」になります。長いよ。

今回のネイティブ拡張も 以前の拙記事 と同様、N-API / node-addon-api を用いた実装を考えております。

C++ のネイティブ拡張アドオンから JavaScript の関数を呼ぶ

単純に JavaScript の関数を呼び出したい場合、JavaScript 側からコールバックを指定すれば(下記例)呼べますが、この場合は JavaScript ⇒ C++ へ関数コールした同一のコンテキストでしか動作できません。

呼び出し側.js
const addon = require('./build/Release/addon');
addon.func(arg, (result) => {
    console.log('call back.') ;
}) ;
アドオン側.cc
// 仕掛け等は省略
// JavaScript 側から「func」と呼ばれると「func」が呼ばれるような仕組みになっていると想定
func( Napi::CallbackInfo& info )
{
        :
    // result の結果に何かが入っている状況
    Napi::Function _callback_func = info[1].As<Napi::Function>() ;
    _callback_func.Call( info.Ent(), result ) ;
}

一方、常駐の C++ のプログラムから随意のタイミングで呼びだしたい場合、2つの課題があります。

課題1:CallbackInfo のインスタンスを保持できない

C++ の Node-API にて随意のタイミングで JavaScript の関数を呼ぶ場合、CallbackInfo クラスのインスタンスの保持が必要ですが、CallbackInfo はコピーコンストラクタが使えず、下記例のような実装ではコンパイルエラーとなります。

呼び出し側.js
const addon = require('./build/Release/addon');
addon.func(arg, (result) => {
    console.log('call back.') ;
}) ;
アドオン側.cc
// incude等や仕掛けは省略
class CC_Test
{
private:
    Napi::CallbackInfo myCallback ;
    Napi::Value result ;
public:
    CC_Test( Napi::CallbackInfo& ) ;
    call_func() ;
} ;
 :
CC_Test::CC_Test( Napi:: CallbackInfo& info ) : myCallback(info)  ★←これが設定できない
{
}
    :
CC_Test::call_func()
{
        :
    Napi::Function _callback_func = myCallback[1].As<Napi::Function>() ;
    _callback_func.Call( info.Ent(), result ) ;
}

課題2:ワーカスレッド側から JavaScript の関数をコールすると落ちる

課題1は CallbackInfo のオブジェクトと関数の Reference を取れば保持できますが、仮にその Reference を元に常駐のワーカースレッド内でコールすると SegF が出ます。

おそらくですが、JavaScript 側の関数が終わるとスレッドとして完了させる必要があるのにもかかわらず、更に実行させようとするからではないかと考えていますが、機会があったらデバッガを使って調べたいと思います。

課題解決に向けて…

で、色々ハマって調べていたら下記のページを発見。

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

この記事の筆者の方も記載していますが、このページで示していたサンプルはインスタンスを複数個確保できない、という課題がありました。

で、ようやく本題

その課題を解決した、C++ のネイティブ拡張側から JavaScript の関数を随意のタイミングでコールでき、かつ、複数のインスタンスを用意できる「関数エミッタクラス」を作り、そのサンプルを用意してみました。

今回実装したクラスは、このサンプルを元に static 外しを行って複数個仕込めるようにしました。

環境とソースファイル

環境

  • OS : Ubuntu 18.04 LTS
  • Node.js : v16.17.1
  • npm : 8.15.0

実行イメージ

ポンチ画レベルで恐縮ですが、今回のサンプルの処理イメージになります、

ソースファイル

ファイル構成は下記になります。

FuncEmitter
 ┣━ binding.gyp
 ┣━ include
 ┃ ┣━ CAddOn.h
 ┃ ┣━ CFuncEmitter.h
 ┃ ┗━ CWorkerThread.h
 ┣━ package.json
 ┣━ sample.js
 ┗━ source
   ┣━ CAddOn.cc
   ┣━ CFuncEmitter.cc
   ┗━ CWorkerThread.cc

関数エミッタクラスのサンプルです。

CFuncEmitter.cc
#include <string>
#include <vector>
#include <napi.h>
#include <iostream>
#include "CFuncEmitter.h"

CFuncEmitter::CFuncEmitter(const Napi::CallbackInfo& info, std::string _emit)
    : Napi::AsyncWorker(info.Env())
{
    Napi::Object _this = info.This().As<Napi::Object>() ;
    // イベント発生関数(emit)の実体を取得
    Napi::Function _func = _this.Get("emit").As<Napi::Function>() ;

    // this オブジェクトと関数オブジェクトの永続性を担保する
    m_this = Napi::Persistent(_this) ;
    m_this.SuppressDestruct() ;
    m_func = Napi::Persistent(_func) ;
    m_func.SuppressDestruct() ;

    // JavaScript の関数名をセット
    m_strEmitFunc = _emit ;
}

void CFuncEmitter::Execute()
{
    // ここで_func.Callすると落ちる
}

void CFuncEmitter::OnOK()
{
    Napi::Function _func = m_func.Value() ;
    Napi::Object _this = m_this.Value() ;
    Napi::Env _env = Env() ;

    Napi::HandleScope scope(_env) ;

    // 引数の設定
    // ※このサンプルは文字列の配列
    std::vector<napi_value> _arg = {
        Napi::String::New(_env, m_strEmitFunc),
    } ;
    for (uint32_t idx = 0 ; idx < m_vArg.size() ; idx++) {
        _arg.push_back(Napi::String::New(_env, m_vArg.at(idx))) ;
    }
    // 実行
    _func.Call(_this, _arg) ;
}

void CFuncEmitter::emitQueue(std::vector<std::string> _arg)
{
    // 呼び元から指定された引数をセット
    m_vArg = _arg ;
    // 実行するためのワーカスレッドを起動
    Queue() ;
}
CFuncEmitter.h
#ifndef __CFUNCEMITTER_H
#define __CFUNCEMITTER_H

#include <string>
#include <vector>
#include <napi.h>

class CFuncEmitter : public Napi::AsyncWorker
{
private:
    Napi::ObjectReference m_this ;
    Napi::FunctionReference m_func ;
    std::string m_strEmitFunc ;
    std::vector<std::string> m_vArg ;

protected:
    void Execute() ;
    void OnOK() ;

public:
    CFuncEmitter(const Napi::CallbackInfo&, std::string) ;
    virtual ~CFuncEmitter() {}

    Napi::Object getThis() { return m_this.Value() ; }
    Napi::Function getFunc() { return m_func.Value() ; }

    void emitQueue(std::vector<std::string>) ;
} ;
#endif

このエミッタクラスを呼び出す C++ と JavaScript のサンプルです。

CAddOn.cc
#include <iostream>
#include <napi.h>
#include "CAddOn.h"
#include "CWorkerThread.h"

Napi::FunctionReference CAddOn::s_constructor;

CAddOn::CAddOn(const Napi::CallbackInfo& info) : Napi::ObjectWrap<CAddOn>(info)
{
    std::cout << "CAddOn::constructor" << std::endl ;

    // サーバサイドのメインとして裏で動かすワーカスレッドの起動
    CWorkerThread* pWorkerThread = new CWorkerThread(info) ;
    pWorkerThread->startTask() ;
}

Napi::Object CAddOn::_init(const Napi::Env env, Napi::Object exports)
{
    // 今回のサンプルでは JavaScript 側からコールすることはないが、
    // 用意しておかないと実行時にランタイムエラーが発生するので
    // ダミーでもいいので作っておく(なぜだろう…?)
    Napi::HandleScope scope(env) ;
    Napi::Function func = DefineClass(env, "CAddOn", {
        InstanceMethod("test", &CAddOn::_test),
    }) ;

    std::cout << "_init" << std::endl ;

    s_constructor = Napi::Persistent(func) ;
    s_constructor.SuppressDestruct();
    exports.Set("CAddOn", func) ;

    return exports ;
}

Napi::Value CAddOn::_test(const Napi::CallbackInfo& info)
{
    // 使われないがダミーとして用意
    std::string _arg = info[0].As<Napi::String>().Utf8Value() ;
    std::cout << __FUNCTION__ << _arg << std::endl ;
    return info.Env().Undefined() ;
}

Napi::Object Init(Napi::Env env, Napi::Object exports)
{
    std::cout << "Init." << std::endl ;
    return CAddOn::_init(env, exports) ;
}
NODE_API_MODULE(addon, Init)
CAddOn.h
#ifndef __CADDON_H
#define __CADDON_H

#include <string>
#include <napi.h>

class CAddOn : public Napi::ObjectWrap<CAddOn>
{
private:
    Napi::Value _test( const Napi::CallbackInfo& info ) ;
    static Napi::FunctionReference s_constructor;

public:
    CAddOn(const Napi::CallbackInfo& info) ;
    virtual ~CAddOn() {}
    static Napi::Object _init( const Napi::Env env, Napi::Object exports ) ;
} ;

#endif

裏で動作させるワーカスレッドのサンプルになります。

CWorkerThread.cc
#include <iostream>
#include <string>
#include <napi.h>
#include "CWorkerThread.h"
#include "CFuncEmitter.h"

CWorkerThread::CWorkerThread(const Napi::CallbackInfo& info)
    : Napi::AsyncWorker(info.Env())
{
    std::cout << "call WorkerThread constructor" << std::endl ;
    m_pFuncEmitter = new CFuncEmitter(info, "EmitSample1") ;
    m_pFuncEmitter->SuppressDestruct() ;
    m_pFuncEmitter2 = new CFuncEmitter(info, "EmitSample2") ;
    m_pFuncEmitter2->SuppressDestruct() ;
}

CWorkerThread::~CWorkerThread()
{
    std::cout << "call WorkerThread destructor" << std::endl ;
}

void CWorkerThread::startTask()
{
    std::cout << "startTask" << std::endl ;

    // Create Worker Thread.
    Queue() ;
}

void CWorkerThread::Execute()
{
    std::string str_input ;

    std::cout << "mainLoop" << std::endl ;
    while ( true ) {
        // prompt
        std::cout << "% " ;

        // get string
        std::getline(std::cin, str_input) ;
        if (str_input == "call" ) {
            m_pFuncEmitter2->emitQueue({"call func2."}) ;
        } else {
            m_pFuncEmitter->emitQueue({str_input}) ;
        }
    }
}
CWorkerThread.h
#ifndef __CWORKER_THREAD_H
#define __CWORKER_THREAD_H

#include <vector>
#include <napi.h>

class CFuncEmitter ;
class CWorkerThread : public Napi::AsyncWorker
{
private:
    CFuncEmitter* m_pFuncEmitter ;
    CFuncEmitter* m_pFuncEmitter2 ;

protected:
    void Execute() ;

public:
    CWorkerThread(const Napi::CallbackInfo&) ;
    virtual ~CWorkerThread() ;
    void startTask() ;
} ;
#endif

JavaScript のサンプルです。

sample.js
'use strict'
const express = require('express') ;

const app = express() ;

const { CAddOn } = require('bindings')('addon') ;
// events を継承して on イベントを使えるようにする
const { EventEmitter } = require('events') ;
const { inherits } = require('util') ;
inherits(CAddOn, EventEmitter) ;

const addon = new CAddOn() ;
addon.on('EmitSample1', (msg) =>
{
    console.log(`EmitSample1`) ;
    console.log(`msg : ${msg}`) ;
}) ;

addon.on('EmitSample2', (msg) =>
{
    console.log(`EmitSample2`) ;
    console.log(`msg : ${msg}`) ;
}) ;

// listen
app.listen(55555, () =>
{
    console.log('start. port on 55555.') ;
}) ;

npm ビルド用の binding.gyp と package.json は下記です。

binding.gyp
{
    "targets": [
        {
            "target_name": "addon",
            "sources": [
                "source/CAddOn.cc", "source/CFuncEmitter.cc", "source/CWorkerThread.cc",
                "include/CAddOn.h", "include/CWorkerThread.h", "include/CFuncEmitter.h" ],
            "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
            "include_dirs": [ "<!@(node -p \"require( 'node-addon-api' ).include\")", "include" ],
        }
    ]
}
package.json
{
  "name": "func_emitter_sample",
  "version": "1.0.0",
  "description": "",
  "main": "sample.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "install": "node-gyp rebuild",
    "start": "node sample.js"
  },
  "author": "",
  "license": "MIT",
  "gypfile": true,
  "dependencies": {
    "bindings": "^1.5.0",
    "node-addon-api": "^5.0.0"
  }
}

これらを用意して $ npm install と打てばビルド出来ます。
実行は $ npm start で実行です。

適当な文字列を打てば下記が出ます。

EmitSample1
msg : 打ったメッセージ

「call」と打てば下記が出ます

EmitSample2
msg : call func2.

終了は Ctrl+C です。

終わりに

サンプルソースの github は下記になります。
https://github.com/wattak777/FuncEmitter

Discussion