はじめに
- node.jsからC++関数を利用するための記事、第三弾です。
- 今回は、C++のネイティブクラスをラッピングしたり、dllを利用するための方法をまとめます。
- この記事では、
node-addon-api
を利用しています。
環境構築がまだの方は?
- この記事の内容をトライする前に、以下の記事で環境構築を完了させてください!
目次
1. ネイティブC++クラスの追加
- node.jsで利用したいネイティブのCppファイルを追加します。
- 前回の記事で作成したプロジェクトを以下のように変更します。
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── binding.gyp <-- 設定変更
├── wrapper.cc
├── wrapper.h
└── native_cpp <-- フォルダ追加
├── classA.cpp <-- 新規作成
└── classA.h <-- 新規作成
今回は例として、classAというサンプルクラスを追加します。
classA.hを見る
#pragma once
#include <iostream>
#include <string>
// 所持金を登録・表示するクラスです。classClassA{public:ClassA();//コンストラクタ~ClassA();// デストラクタ// メンバ関数voidset_name(std::stringname);voidset_money(longmoney);std::stringshow_money();private:// メンバ変数std::stringm_name;longm_money;};
classA.cppを見る
//#include "pch.h"#include "classA.h"
#include <iostream>
#include <string>
#include <sstream>
ClassA::ClassA(){m_name="None";m_money=0;}ClassA::~ClassA(){}// 名前を登録するvoidClassA::set_name(std::stringname){m_name=name;}// 金額を代入するvoidClassA::set_money(longmoney){m_money=money;}// 金額を表示するstd::stringClassA::show_money(){std::stringstreamss;ss<<m_name<<" has "<<m_money<<" yen "<<std::endl;returnss.str();}
- 新しくCppファイルを追加したので、
binding.gyp
を変更します。- ワイルドカードを使う方法は、こちらのStackOverflowが参考になりました。
{"targets":[{# ↓addon.cc内の NODE_API_MODULE(addon, InitAll) と同名にする
"target_name":"addon","cflags!":["-fno-exceptions"],"cflags_cc!":["-fno-exceptions"],# ↓必要な.ccファイルを全て記述する
# ワイルドカードを使って、native_cpp内のファイルを全て読み込む
"sources":["addon.cc","wrapper.cc","<!@(node -p \"require('fs').readdirSync('./native_cpp').map(f=>'native_cpp/'+f).join(' ')\")"],"include_dirs":["<!@(node -p \"require('node-addon-api').include\")"],"defines":['NAPI_DISABLE_CPP_EXCEPTIONS'],}]}
- この時点で一旦確認を行いましょう。
$ npm install.>> gyp info ok と表示されればビルド完了
2. ネイティブC++クラスのラッピング
- 前章でビルドが通ったら、ネイティブクラス
ClassA
をラッピングします。- 前記事で作成した
Wrapper
クラスを書き換えます。
- 前記事で作成した
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── binding.gyp
├── wrapper.cc <-- 書き換え
├── wrapper.h <-- 書き換え
└── native_cpp
├── classA.cpp
└── classA.h
書き換えたコードは以下をご確認ください。
wrapper.hを見る
#ifndef WRAPPER
#define WRAPPER
#include <napi.h> // 必要なヘッダ
#include "./native_cpp/classA.h"
classWrapper:publicNapi::ObjectWrap<Wrapper>{public:staticNapi::ObjectInit(Napi::Envenv,Napi::Objectexports);staticNapi::ObjectNewInstance(Napi::Envenv,constNapi::CallbackInfo&info);Wrapper(constNapi::CallbackInfo&info);~Wrapper();// クラスAのラッピング関数Napi::ValuesetName(constNapi::CallbackInfo&info);Napi::ValuesetMoney(constNapi::CallbackInfo&info);Napi::ValueshowMoney(constNapi::CallbackInfo&info);private:ClassA*m_classA;};#endif
wrapper.cppを見る
#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("setName",&Wrapper::setName),InstanceMethod("setMoney",&Wrapper::setMoney),InstanceMethod("showMoney",&Wrapper::showMoney),});Napi::FunctionReference*constructor=newNapi::FunctionReference();*constructor=Napi::Persistent(func);env.SetInstanceData(constructor);exports.Set("Wrapper",func);returnexports;}// ---------------------------------------------------------- //// ---------- これより下で ClassAのラッピングを定義する ----------- //// ---------------------------------------------------------- //// コンストラクタWrapper::Wrapper(constNapi::CallbackInfo&info):Napi::ObjectWrap<Wrapper>(info){m_classA=newClassA();};Wrapper::~Wrapper(){deletem_classA;m_classA=nullptr;};// メンバ関数Napi::ValueWrapper::setName(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();std::stringname=info[0].As<Napi::String>().ToString();m_classA->set_name(name);returnenv.Null();}Napi::ValueWrapper::setMoney(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();intmoney=info[0].As<Napi::Number>().Int32Value();m_classA->set_money(money);returnenv.Null();}Napi::ValueWrapper::showMoney(constNapi::CallbackInfo&info){Napi::Envenv=info.Env();std::stringans=m_classA->show_money();returnNapi::String::New(env,ans);}
3. javaScriptからC++クラスを使ってみる
- では実際に、
index.js
からCppクラスを呼び出してみましょう。- index.jsを以下のように書き直します。
// addon.cc内の NODE_API_MODULE(addon, InitAll) が呼ばれるvarWrapper=require('bindings')('addon');// addon.cc内の CreateObject() が呼ばれるvarobj=newWrapper()// wrapper.cc内でラッピングしたClassAの関数が使えるobj.setName("Tanaka Taro");obj.setMoney(1000);console.log(obj.showMoney());
- ターミナルからコマンドを実行し、下記のように表示されれば成功です。
$ node .>> Tanaka Taro has 1000 yen
4. dllを利用する
- dllを利用するための手順は簡単です。
- .libファイルを準備する(Windowsの場合)
binding.gyp
にlibrariesセクションを追加し、そこに追加した.libファイルのパスを記入する- ターミナルで
npm install .
を実行し、ビルドする - ./build/Releaseフォルダに .dllファイルを追加する(Windowsの場合)
Example
第二章で作成したプロジェクトを以下のように変更します。
カレントディレクトリ
├── node_modules
├── package-lock.json
├── package.json
├── index.js
├── addon.cc
├── binding.gyp
├── wrapper.cc
├── wrapper.h
├── native_cpp
├──classA.cpp<-- 削除
├── classA.lib <-- 追加
└── classA.h
├── build
├── Release
├── classA.dll <-- 追加 (Windowsの場合)次に、binding.gypを編集し、include_directoriesセクションを追加します。
{"targets":[{"target_name":"addon","cflags!":["-fno-exceptions"],"cflags_cc!":["-fno-exceptions"],"sources":["addon.cc","wrapper.cc","<!@(node -p \"require('fs').readdirSync('./native_cpp').map(f=>'native_cpp/'+f).join(' ')\")"],"include_dirs":["<!@(node -p \"require('node-addon-api').include\")"],# 【追加】ここにライブラリファイルを登録する
"libraries":["<(module_root_dir)/native_cpp/libclassA.dylib"],"defines":['NAPI_DISABLE_CPP_EXCEPTIONS'],}]}
- 最後に、ビルド&実行して終了です。
$ npm install.$ node .