はじめに
- node.jsからC++関数を利用する際に、引数・返り値の渡し方について困ったことはありませんか?
- この記事では、node-addon-apiを使う場合の、引数・返り値の受け渡し方をまとめています。
 
- この記事では、
環境構築がまだの方は?
- この記事の内容をトライする前に、以下の記事で環境構築を完了させてください!
目次
1. 関数の基本形
- node-addon-apiを利用する場合、ラップされるC++の関数は以下のように定義します。
- 
Napi::Value 関数名(const Napi::CallbackInfo& info);- 引数の個数、型に関係なく、引数は const Napi::CallbackInfo& infoと記述
- 返り値は、型に関係なくNapi::Valueと記述
 
- 引数の個数、型に関係なく、引数は 
 
- 
- 以下は、関数宣言と関数定義の例です。
- 引数、返り値ともにvoidです。
 
- 引数、返り値ともに
void.h
# include <napi.h>
Napi::Value func(const Napi::CallbackInfo& info);
void.cpp
# include <napi.h>
Napi::Value func(const Napi::CallbackInfo& info) {
  // Do nothing.
  return env.Null();
}
2. 値の受け渡し
- js ↔︎ C++でやり取りするにあたって必要な引数、返り値の処理についてまとめました。
引数の型対応表
| C++型 | napi型 | 
|---|---|
| int | .As<Napi::Number>().Int32Value() | 
| double | .As<Napi::Number>().DoubleValue() | 
| std::string | .As<Napi::String>().ToString() | 
返り値の型対応表
| C++型 | napi型 | 
|---|---|
| int, double | return Napi::Number::New(env, C++変数名) | 
| std::string | return Napi::String::New(env, C++変数名) | 
- jsは数値型が1種類しかないため、数値であればNapi::Number型で良い
関数例
- 例として、jsから2つの引数(a,b)をC++で受け取り、その和をjsに返却する関数add()を考えてみましょう。
example.cpp
# include <napi.h>
Napi::Value add(const Napi::CallbackInfo& info) {
  // お約束
  Napi::Env env = info.Env();
  // 引数は、配列infoから取り出す。
  double a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();
  // C++で行いたい処理を行う
  double ans = a + b;
  // 返り値は、Napi::○○ 型にキャストして返却する
  return Napi::Number::New(env, ans);
}
- jsファイルからは次のように見えます
example.js
// (前処理は省略)
let ans = add(1,2)
console.log(ans);
// >> expected: 3
3. 配列の受け渡し
引数に配列を渡す( js → C++ )
array.cpp
Napi::Value setArr(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  Napi::Array arr = info[0].As<Array>();
  
  // C++の配列
  std::vector<double> vec(arr.Length(), 0.0);
  // for文で要素を順に代入
  for(size_t i = 0; i < arr.Length(); i++){
    Napi::Value val = arr[i];
    vec[i] = val.As<Napi::Number>().DoubleValue();
  }
  return env.Null();
}
返り値に配列を渡す( C++ → js )
array.cpp
Napi::Value getArr(const CallbackInfo& info){
  Napi::Env env = info.Env();
    
  // C++の配列
  std::vector<double> vec = {1.0, 0.5, 0.25};
  // for文で要素を順に代入
  Napi::Array outArr = Napi::Array::New(env, vec.size());
  for (size_t i = 0; i < vec.size(); i++) {
      outArr[i] = Napi::Number::New(env, vec[i]);
  }
  return outArr;
}
- jsファイルからは次のように利用します。
array.js
// (前処理は省略)
setArr([1,2,3,4,5]);
var arr = getArr();
console.log(arr);
// >> expected: [1.0, 0.5, 0.25]
4. (応用)異なるプリミティブ型を配列に入れて返却する
- C++で複数の返り値を返したい場合に有効です。
- リターンコードと計算結果の組み合わせなどを返却できます。
 
array2.cpp
Napi::Value getReturns(const CallbackInfo& info){
  Napi::Env env = info.Env();
  // do Something C++
  // 返り値として、 1.0 と "aabbcc" を返却する例
  const int returnArgNums = 2;
  int zero = 0;
  int one = 1;
  Napi::Array retArr = Napi::Array::New(env, returnArgNums);
  retArr[zero] = Napi::Number::New(env, 1.0);
  retArr[one] = Napi::String::New(env, "aabbcc");
  return retArr;
}
array2.js
// (前処理は省略)
var arr = getReturns();
console.log("ret1 =", arr[0], "ret2 =", arr[1]);
// >> expected: ret1 = 1 ret2 = aabbcc
5. 試してみよう
- 
前回の記事
 で作成したプロジェクトのwrapper.h,wrapper.cpp,index.jsを下記のコードで上書きすると、本記事の内容を試すことができます!
 ├── node_modules
 ├── package-lock.json
 ├── package.json
 ├── index.js  <-- 上書き
 ├── addon.cc
 ├── wrapper.cc <-- 上書き
 ├── wrapper.h  <-- 上書き
 └── binding.gyp
サンプルコードはこちら
wrapper.hを開く
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);
    Napi::Value add(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value setArr(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value getArr(const Napi::CallbackInfo& info); // <-- 追加
    Napi::Value getReturns(const Napi::CallbackInfo& info); // <-- 追加
private:
    double m_value;
};
# endif
wrapper.ccを開く
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("add", &Wrapper::add), // <-- 追加
            InstanceMethod("setArr", &Wrapper::setArr), // <-- 追加
            InstanceMethod("getArr", &Wrapper::getArr), // <-- 追加
            InstanceMethod("getReturns", &Wrapper::getReturns), // <-- 追加
        });
    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);
}
// 引数・返り値の受け渡し
Napi::Value Wrapper::add(const Napi::CallbackInfo& info) {
  // お約束
  Napi::Env env = info.Env();
  // 引数は、配列infoから取り出す。
  double a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();
  // C++で行いたい処理を行う
  double ans = a + b;
  // 返り値は、Napi::○○ 型にキャストして返却する
  return Napi::Number::New(env, ans);
}
// 配列の受け取り
Napi::Value Wrapper::setArr(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  Napi::Array arr = info[0].As<Array>();
  // C++の配列
  std::vector<double> vec(arr.Length(), 0.0);
  // for文で要素を順に代入
  for(size_t i = 0; i < arr.Length(); i++){
    Napi::Value val = arr[i];
    vec[i] = val.As<Napi::Number>().DoubleValue();
  }
  return env.Null();
}
// 配列の返却
Napi::Value Wrapper::getArr(const CallbackInfo& info){
  Napi::Env env = info.Env();
  // C++の配列
  std::vector<double> vec = {1.0, 0.5, 0.25};
  // for文で要素を順に代入
  Napi::Array outArr = Napi::Array::New(env, vec.size());
  for (size_t i = 0; i < vec.size(); i++) {
      outArr[i] = Napi::Number::New(env, vec[i]);
  }
  return outArr;
}
// プリミティブ型が混在した配列の返却
Napi::Value Wrapper::getReturns(const CallbackInfo& info){
  Napi::Env env = info.Env();
  // do Something C++
  // 返り値として、 1.0 と "aabbcc" を返却する例
  const int returnArgNums = 2;
  int zero = 0;
  int one = 1;
  Napi::Array retArr = Napi::Array::New(env, returnArgNums);
  retArr[zero] = Napi::Number::New(env, 1.0);
  retArr[one] = Napi::String::New(env, "aabbcc");
  return retArr;
}
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());
// ---- 本記事で追加した関数 ---- //
let ans = obj.add(1,2)
console.log(ans);
// >> expected: 3
obj.setArr([1,2,3,4,5]);
var arr = obj.getArr();
console.log(arr);
// >> expected: [1.0, 0.5, 0.25]
var arr = obj.getReturns();
console.log("ret1 =", arr[0], "ret2 =", arr[1]);
// >> expected: ret1 = 1 ret2 = aabbcc
// ---- 本記事で追加した関数 ---- //
その他
- 返り値にユーザー定義型やオブジェクトを返す場合は このあたり が参考になるかもしれません。