13
11

More than 3 years have passed since last update.

【Step-By-Step】Node.jsからC++クラスを利用するための環境構築

Last updated at Posted at 2021-02-13

はじめに

この記事は、javascriptからC++を呼び出す処理が必要になった時の備忘録です。
node-addon-apiというラッピングライブラリを活用します。
この記事の内容を「マネすれば動く」ように意識して書いています。
Electronなどのデスクトップアプリに応用できます。

事前準備

  • 下記のパッケージは事前にインストールしておいてください
    • npm
    • node.js

応用記事はこちら!

1. Node.js からC++関数への引数、返り値まとめ

2. Node.jsからC++クラス、dllを使う

目次

1. プロジェクトの新規作成

  • 空のフォルダを新規作成します。今回は例として、napi_sampleというフォルダ名にしました。
  • 作成したフォルダに移動し、下記のコマンドを実行し、必要モジュールをインストールします。
terminal
$ npm init -y
$ npm install node bindings node-addon-api
  • コマンド実行後、下記のようなフォルダ構成となります。

 カレントディレクトリ
  ├── node_modules
  ├── package-lock.json
  └── package.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を追加しましょう。
    • 下記のようなサンプルとします。
index.js
console.log("Hello! node.js");

ターミナルで動作確認する

  • ここまでの環境構築がうまくいっているか確認します。
    • index.jsがあるディレクトリで下記のコマンドを入力してください。
terminal
$ 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をテンプレとしてお使いください。
wrapper.h
#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
wrapper.cc

#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++メンバ関数名")として登録しなければなりません。

jsとの結合用cppファイルを作る

  • 次に、Wrapperクラスをjsモジュールとしてエクスポートするための処理をaddon.ccに記述します。
    • こちらも詳細説明は省略します。テンプレとしてお使いください。
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 <-- 新規作成

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) 拡張子のファイルを全て登録してください。

ビルド実行

  • 下記コマンドを実行し、ビルドしてください。
terminal
$ npm install .

  >> gyp info ok と表示されればビルド完了

エラーが出る場合

  • package.jsonの node_addon_apiのバージョンを確認して下さい。(私は、 3.0.1でした)
  • 以下のコマンドでリビルドができます
bash
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を以下のように書き換えてください。
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の実行

ターミナルで次のように実行します。

terminal

$ node .
>> 0 と表示されれば成功
>> Wrapper.m_valueの値が表示されています。

お疲れ様でした。
node-addon-api は"お約束ごと"が多いので、
私はこの記事のようなテンプレを作り、使いまわしています。
参考になれば幸いです。

参考リンク

Others

  • コンストラクタに複数の引数を渡す
  • メンバ関数の引数、返り値について
  • 別のC++クラスを利用する
13
11
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
13
11