こんにちは、wattak777 です。
はじめに
筆者は C/C++ 畑の経験が長かったためか、Node.js でプログラムを組んでいると「C や C++ でやった方が早く作れるのになぁ」と C++ のアドオンで実装することがしばしばあります。
この時、C++ から JavaScript の関数を呼び出したい場合、JavaScript 側からコールバックを指定すれば(下記例)呼べますが、JavaScript から呼び出しトリガーが必要となります。
const addon = require('./build/Release/addon');
addon.func(arg, (result) => {
console.log('call back.') ;
}) ;
// 仕掛け等は省略
// JavaScript 側から「func」と呼ばれると「func」が呼ばれるような仕組みになっていると想定
func( Napi::CallbackInfo& info )
{
:
// result の結果に何かが入っている状況
Napi::Function _callback_func = info[1].As<Napi::Function>() ;
_callback_func.Call( info.Ent(), result ) ;
}
JavaScript からのトリガーだけではなく、C++ で実装しているプログラム側が任意のタイミングで JavaScript 側の関数をコールする仕組みが必要になったので、アドオン⇒JavaScript の関数を呼び出すクラスを実装してみました。
なお、C++ は Node-API を使用しました。
本記事において、Node.js や npm、Node-API のセットアップについては割愛いたします。
あらゆるサンプルがございますので、それらをご参照ください。
なお、筆者の環境は下記となります。
OS:Ubuntu 18.04 LTS
node バージョン:v10.24.1
npm バージョン:6.14.12
C++のアドオンから JavaScript の関数を呼び出す
基本的な考え方は下記です。
Napi::AsyncWorker の終了イベント「OnOK」上で関数を呼び出す
ことで実現が出来ます。
下記のページが非常に参考になりました。
Node.js の C++ によるアドオンで、AsyncWorker からイベントを受け取る
下記、エミッタクラスのサンプルです。
このページで紹介されているクラスはRefernceデータをstaticで定義されているため複数の箇所からのコールが出来ない仕組みになっていましたが、これだと複数のインスタンスを実装することが可能です。
#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
#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>() ;
Napi::Function _func = _this.Get("emit").As<Napi::Function>() ;
m_this = Napi::Persistent(_this) ;
m_this.SuppressDestruct() ;
m_func = Napi::Persistent(_func) ;
m_func.SuppressDestruct() ;
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() ;
}
このエミッタクラスを呼び出す C++ と JavaScript のサンプルです。
#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)
{
CWorkerThread* pWorkerThread = new CWorkerThread(info) ;
pWorkerThread->startTask() ;
}
Napi::Object CAddOn::_init(const Napi::Env env, Napi::Object exports)
{
Napi::HandleScope scope(env) ;
Napi::Function func = DefineClass(env, "CAddOn", {
InstanceMethod("start", &CAddOn::_start),
}) ;
s_constructor = Napi::Persistent(func) ;
s_constructor.SuppressDestruct();
exports.Set("CAddOn", func) ;
return exports ;
}
// JavaScript からの同期関数サンプル
Napi::Value CAddOn::_start(const Napi::CallbackInfo& info)
{
std::string _arg = info[0].As<Napi::String>().Utf8Value() ;
std::cout << "_start" << _arg << std::endl ;
return info.Env().Undefined() ;
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
return CAddOn::_init(env, exports) ;
}
NODE_API_MODULE(addon, Init)
#ifndef __CINTER_FACE_H
#define __CINTER_FACE_H
#include <string>
#include <napi.h>
class CAddOn : public Napi::ObjectWrap<CAddOn>
{
private:
Napi::Value _start( 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
#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, "JSfunc") ;
m_pFuncEmitter->SuppressDestruct() ;
m_pFuncEmitter2 = new CFuncEmitter(info, "JSfunc2") ;
m_pFuncEmitter2->SuppressDestruct() ;
}
CWorkerThread::~CWorkerThread()
{
std::cout << "call WorkerThread destructor" << std::endl ;
}
void CWorkerThread::startTask()
{
Queue() ;
}
void CWorkerThread::Execute()
{
std::string str_input ;
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}) ;
}
}
}
#ifndef __CROOM_MANAGER_H
#define __CROOM_MANAGER_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
'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.start('Start.') ;
addon.on('JSfunc', (msg) =>
{
var retval = true ;
console.log(`msg : ${msg}`) ;
if ( msg === 'false' ) {
return false ;
}
return true ;
}) ;
addon.on('JSfunc2', (msg) =>
{
var retval = true ;
console.log(`msg2 : ${msg}`) ;
}) ;
// listen
app.listen(55555, () =>
{
console.log('Start. port on 55555.') ;
}) ;
npm ビルド用の binding.gyp と package.json は下記です。
{
"targets": [
{
"target_name": "addon",
"sources": [ "CAddOn.cpp", "CFuncEmitter.cpp", "CWorkerThread.cpp" ],
"defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ],
"include_dirs": [ "<!@(node -p \"require( 'node-addon-api' ).include\")" ],
}
]
}
{
"name": "call_sample",
"version": "1.0.0",
"description": "",
"main": "test.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"install": "node-gyp rebuild"
},
"author": "",
"license": "ISC",
"gypfile": true,
"dependencies": {
"bindings": "^1.5.0",
"node-addon-api": "^5.0.0"
}
}
これらを用意して $ npm install
と打てばビルド出来ます。
実行は $ node test.js
で実行です。
適当な文字列を打てば下記が出ます。
msg : 打ったメッセージ
「call」と打てば下記が出ます
msg2 : call func2.
終了は Ctrl+C です。
終わりに
サンプルソースを github 等でぜひ欲しい、という方がいらっしゃったら一報ください。