はじめに
この記事は、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++クラスを利用する