はじめに
この記事は、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"}}
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> // 必要なヘッダ
classWrapper:publicNapi::ObjectWrap<Wrapper>{public:staticNapi::ObjectInit(Napi::Envenv,Napi::Objectexports);staticNapi::ObjectNewInstance(Napi::Envenv,constNapi::CallbackInfo&info);Wrapper(constNapi::CallbackInfo&info);~Wrapper();Napi::ValuegetNum(constNapi::CallbackInfo&info);private:doublem_value;};#endif
#include "wrapper.h"
#include <napi.h>
usingnamespaceNapi;// ---------------------------------------------------------- //// ---------------------のり付け部分--------------------------- //// ---------------------------------------------------------- //// new() の定義Napi::ObjectWrapper::NewInstance(Napi::Envenv,constNapi::CallbackInfo&info){Napi::EscapableHandleScopescope(env);// jsからコンストラクタに渡されるArgsは infoに配列として入っているconststd::initializer_list<napi_value>initArgList={info[0]};// ここでWrapper:::Wrapper()が呼ばれるNapi::Objectobj=env.GetInstanceData<Napi::FunctionReference>()->New(initArgList);// gcにメモリ解放されないようにスコープを除外するreturnscope.Escape(napi_value(obj)).ToObject();}// メンバ関数のバインドNapi::ObjectWrapper::Init(Napi::Envenv,Napi::Objectexports){Napi::Functionfunc=DefineClass(env,"Wrapper",{// ここにメンバ関数を登録するInstanceMethod("getNum",&Wrapper::getNum),// InstanceMethod("jsから呼び出す際の関数名", "呼び出したいC++メンバ関数名"),});Napi::FunctionReference*constructor=newNapi::FunctionReference();*constructor=Napi::Persistent(func);env.SetInstanceData(constructor);exports.Set("Wrapper",func);returnexports;}// ---------------------------------------------------------- //// --------------- Wrapperクラスの定義はこれより下 --------------- //// ---------------------------------------------------------- //// コンストラクタWrapper::Wrapper(constNapi::CallbackInfo&info):Napi::ObjectWrap<Wrapper>(info){m_value=0.0;};Wrapper::~Wrapper(){};// メンバ関数Napi::ValueWrapper::getNum(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();returnNapi::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::ObjectCreateObject(constNapi::CallbackInfo&info){returnWrapper::NewInstance(info.Env(),info);}// js内でexport()が呼び出されたときNapi::ObjectInitAll(Napi::Envenv,Napi::Objectexports){// 関数定義Napi::Objectnew_exports=Napi::Function::New(env,CreateObject);returnWrapper::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 と表示されればビルド完了
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) が呼ばれるvarWrapper=require('bindings')('addon');// addon.cc内の CreateObject() が呼ばれるvarobj=newWrapper()// wrapper.cc内で登録した getNum()が呼ばれるconsole.log(obj.getNum());
index.jsの実行
ターミナルで次のように実行します。
$ node .>> 0 と表示されれば成功
>> Wrapper.m_valueの値が表示されています。
お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。
参考リンク
- ドキュメント
- node-addon-examples
- 本格的に学びたい方は、このexampleを順にやっていくと良いでしょう
Others
- コンストラクタに複数の引数を渡す
- メンバ関数の引数、返り値について
- 別のC++クラスを利用する