はじめに
この記事は、javascriptからC++を呼び出す処理が必要になった時の備忘録です。
node-addon-apiというラッピングライブラリを活用します。
この記事の内容を「マネすれば動く」ように意識して書いています。
Electronなどのデスクトップアプリに応用できます。
事前準備
- 下記のパッケージは事前にインストールしておいてください
- npm
- node.js
応用記事はこちら!
1. Node.js からC++関数への引数、返り値まとめ
2. Node.jsからC++クラス、dllを使う
目次
##1. プロジェクトの新規作成
- 空のフォルダを新規作成します。今回は例として、
napi_sample
というフォルダ名にしました。 - 作成したフォルダに移動し、下記のコマンドを実行し、必要モジュールをインストールします。
$ npm init -y
$ npm install node bindings node-addon-api
- コマンド実行後、下記のようなフォルダ構成となります。
カレントディレクトリ
├── node_modules
├── package-lock.json
└── package.json
- さらに、下記のようなpackage.jsonが自動的に作成されます。
{
"name": "doit_myself",
"version": "1.0.0",
"description": "",
"main": "index.js", //<-- 開始時にこの.jsファイルが読み込まれる
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"bindings": "^1.5.0",
"node": "^15.8.0",
"node-addon-api": "^3.1.0" // <-- バージョンに注意。3.x.x を使用すること
}
}
index.jsを作成する
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
└── index.js <-- 新規作成
- 起動時に読み込まれるindex.jsを追加しましょう。
- 下記のようなサンプルとします。
console.log("Hello! node.js");
ターミナルで動作確認する
- ここまでの環境構築がうまくいっているか確認します。
- index.jsがあるディレクトリで下記のコマンドを入力してください。
$ node .
>> Hello! node.js
##2. Cppファイルを追加する
- ここからは、jsで利用するためのCppラッパークラスを作成していきます。
- wrapper.h, wrapper.cc, addon.ccの順に説明します。
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc <-- 新規作成
├── wrapper.cc <-- 新規作成
└── wrapper.h <-- 新規作成
※ 拡張子.cc はC++ファイルのことです。本質は .cppと変わりません。
ラッパークラスの作成
- ネイティブC++をラッピングするクラスを作成します。
- このクラスの目的は、jsから渡された引数をC++で解釈できる形にし、C++の返り値をjsが利用できる形式に変換して渡すことです。
- 下記のwrapper.h, wrapper.ccをテンプレとしてお使いください。
#ifndef WRAPPER
#define WRAPPER
#include <napi.h> // 必要なヘッダ
class Wrapper : public Napi::ObjectWrap<Wrapper> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
static Napi::Object NewInstance(Napi::Env env, const Napi::CallbackInfo& info);
Wrapper(const Napi::CallbackInfo& info);
~Wrapper();
Napi::Value getNum(const Napi::CallbackInfo& info);
private:
double m_value;
};
#endif
#include "wrapper.h"
#include <napi.h>
using namespace Napi;
// ---------------------------------------------------------- //
// ---------------------のり付け部分--------------------------- //
// ---------------------------------------------------------- //
// new() の定義
Napi::Object Wrapper::NewInstance(Napi::Env env, const Napi::CallbackInfo &info)
{
Napi::EscapableHandleScope scope(env);
// jsからコンストラクタに渡されるArgsは infoに配列として入っている
const std::initializer_list<napi_value> initArgList = {info[0]};
// ここでWrapper:::Wrapper()が呼ばれる
Napi::Object obj = env.GetInstanceData<Napi::FunctionReference>()->New(initArgList);
// gcにメモリ解放されないようにスコープを除外する
return scope.Escape(napi_value(obj)).ToObject();
}
// メンバ関数のバインド
Napi::Object Wrapper::Init(Napi::Env env, Napi::Object exports)
{
Napi::Function func = DefineClass(
env, "Wrapper", {
// ここにメンバ関数を登録する
InstanceMethod("getNum", &Wrapper::getNum),
// InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名"),
});
Napi::FunctionReference *constructor = new Napi::FunctionReference();
*constructor = Napi::Persistent(func);
env.SetInstanceData(constructor);
exports.Set("Wrapper", func);
return exports;
}
// ---------------------------------------------------------- //
// --------------- Wrapperクラスの定義はこれより下 --------------- //
// ---------------------------------------------------------- //
// コンストラクタ
Wrapper::Wrapper(const Napi::CallbackInfo &info)
: Napi::ObjectWrap<Wrapper>(info)
{
m_value = 0.0;
};
Wrapper::~Wrapper(){};
// メンバ関数
Napi::Value Wrapper::getNum(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
return Napi::Number::New(env, this->m_value);
}
- 補足
- wrapper.cpp内の関数
Napi::Function func = DefineClass()
において、自作のC++メンバ関数を登録する必要があります。サンプルコードInstanceMethod("getNum", &Wrapper::getNum),
のように、InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名")
として登録しなければなりません。
- wrapper.cpp内の関数
jsとの結合用cppファイルを作る
- 次に、Wrapperクラスをjsモジュールとしてエクスポートするための処理をaddon.ccに記述します。
- こちらも詳細説明は省略します。テンプレとしてお使いください。
#include <napi.h>
#include "wrapper.h"
#include <iostream>
// jsオブジェクトが初期化された時 new()の呼び出し
Napi::Object CreateObject(const Napi::CallbackInfo& info) {
return Wrapper::NewInstance(info.Env(), info);
}
// js内でexport()が呼び出されたとき
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
// 関数定義
Napi::Object new_exports = Napi::Function::New(env, CreateObject);
return Wrapper::Init(env, new_exports);
}
// jsへバインドするためのマクロ
// jsで、 export('bindings')('addon')と記述したとき、上記のInitAll()が呼び出される
NODE_API_MODULE(addon, InitAll)
##3. Cppファイルをビルドする
- 作成してたcppをビルドするための設定ファイルを作ります。
- binding.gyp というファイルです。
- VisualStudioのprojectのプロパティ設定、CMakeLists.txtと似たような設定をします。
binding.gypの追加
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp <-- 新規作成
{
"targets": [
{
# ↓addon.cc内の NODE_API_MODULE(addon, InitAll) と同名にする
"target_name": "addon",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
# ↓必要な.ccファイルを全て記述する
"sources": [ "addon.cc", "wrapper.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
}
]
}
- 上記ファイルをコピペいただければ問題ないです。
- 注意点として sources セクションには、使用する .cc (or .cpp) 拡張子のファイルを全て登録してください。
ビルド実行
- 下記コマンドを実行し、ビルドしてください。
$ npm install .
>> gyp info ok と表示されればビルド完了
エラーが出る場合
- package.jsonの node_addon_apiのバージョンを確認して下さい。(私は、 3.0.1でした)
- 以下のコマンドでリビルドができます
npx node-gyp configure // 設定を反映させるとき
npx node-gyp rebuild // プロジェクトをリビルドするとき
npx node-gyp build // C++ソースコードの変更を反映させるとき
##4. JavaScriptからビルドしたCppクラスを使う
- お待たせしました。最後に index.jsから wrapper.ccのクラスを使ってみましょう。
index.jsの書き換え
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js <-- 書き換え
├── addon.cc
├── wrapper.cc
├── wrapper.h
└── binding.gyp
- index.jsを以下のように書き換えてください。
// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれる
var Wrapper = require('bindings')('addon');
// addon.cc内の CreateObject() が呼ばれる
var obj = new Wrapper()
// wrapper.cc内で登録した getNum()が呼ばれる
console.log(obj.getNum());
index.jsの実行
ターミナルで次のように実行します。
$ node .
>> 0 と表示されれば成功
>> Wrapper.m_valueの値が表示されています。
お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。
参考リンク
- ドキュメント
-
node-addon-examples
- 本格的に学びたい方は、このexampleを順にやっていくと良いでしょう
Others
- コンストラクタに複数の引数を渡す
- メンバ関数の引数、返り値について
- 別のC++クラスを利用する