C++ で作った Node.js 用アドオンから任意のタイミングで JavaScript の関数を呼び出すクラスを作ってみた

こんにちは、wattak777 です。

# はじめに

筆者は C/C++ 畑の経験が長かったためか、Node.js でプログラムを組んでいると「C や C++ でやった方が早く作れるのになぁ」と C++ のアドオンで実装することがしばしばあります。

この時、C++ から JavaScript の関数を呼び出したい場合、JavaScript 側からコールバックを指定すれば（下記例）呼べますが、JavaScript から呼び出しトリガーが必要となります。

```JavaScript:呼び出し側.js
const addon = require('./build/Release/addon');
addon.func(arg, (result) => {
    console.log('call back.') ;
}) ;
```
```C++:アドオン側.cpp
// 仕掛け等は省略
// 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 の関数を呼び出す

基本的な考え方は下記です。

:::note information
Napi::AsyncWorker の終了イベント「OnOK」上で関数を呼び出す
:::

ことで実現が出来ます。
下記のページが非常に参考になりました。
[Node.js の C++ によるアドオンで、AsyncWorker からイベントを受け取る](https://qiita.com/dejirutek/items/d04a1e5fb880e364891b)

下記、エミッタクラスのサンプルです。
このページで紹介されているクラスはRefernceデータをstaticで定義されているため複数の箇所からのコールが出来ない仕組みになっていましたが、これだと複数のインスタンスを実装することが可能です。

```C++: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++:CFuncEmitter.cpp
#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 のサンプルです。
```C++:CAddOn.cpp
#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)
```
```C++:CAddOn.h
#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
```
```C++:CWorkerThread.cpp
#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}) ;
		}
	}
}
```
```C++:CWorkerThread.h
#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
```
```JavaScript:test.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.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 は下記です。
```binding.gyp
{
	"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\")" ],
		}
	]
}
```
```json:package.json
{
  "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 等でぜひ欲しい、という方がいらっしゃったら一報ください。

# 参考文献
[Node.js の C++ によるアドオンで、AsyncWorker からイベントを受け取る](https://qiita.com/dejirutek/items/d04a1e5fb880e364891b)



[^1]: 筆者は主に C/C++ を使うもので…。