0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-29

こんにちは、wattak777 です。

はじめに

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

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

呼び出し側.js
const addon = require('./build/Release/addon');
addon.func(arg, (result) => {
    console.log('call back.') ;
}) ;
アドオン側.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 の関数を呼び出す

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

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

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

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

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
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 のサンプルです。

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)
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
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}) ;
		}
	}
}
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
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\")" ],
		}
	]
}
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 からイベントを受け取る

0
2
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
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?