9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Node.js C++ アドオンの開発 (作業メモ)

Posted at

この記事について

サンプルレベルの Node.js アドオンを C++ で書いてみた際の作業メモです。

内容:

  • ソースは node-addon-examples をベースにしてます
  • 足し算をする add という関数を JavaScript 側に見せる 2_function_arguments にちょっと手を入れた程度です

Node.js アドオンとは

C++ addons の説明が分かりやすいので引用。

Addons are dynamically-linked shared objects written in C++. The require() function can load addons as ordinary Node.js modules. Addons provide an interface between JavaScript and C/C++ libraries.

Node.js アドオンを作る方法

Node.js のアドオンを作るには以下の方法がある。

  1. 内部の V8/libuv/Node.js ライブラリを直接使う
  2. nan(Native Abstractions for Node.js) を使う
  3. N-API を使う
  4. node-addon-api を使う

それぞれの特徴をざっくりまとめると以下の通り。

  • v8/libuv/Node.js ライブラリを直接使う
    • 複雑、かつ、各ライブラリのバージョンアップと変更の影響をモロに受けるので大変 (特に V8 はリリースごとに大きく変わるらしい1)
    • 各ライブラリを直接触りたい場合以外は使うべきではない
  • nan
    • 各ライブラリのバージョン間の差異を吸収するツール(主にマクロ)を提供
  • N-API
    • アドオン開発向けの C言語 API で、ABI(Application Binary Interface)を保証する
    • Node.js 本体と同じリポジトリでメンテされてる
  • node-addon-api
    • N-API を C++ でラップして使いやすくしたもの
    • Node.js プロジェクトでメンテされてる

Node.js のドキュメントでは N-API と node-addon-api を推奨してる。
今回は、「v8/libuv/Node.jsライブラリを直接使う」「node-addon-api」 を使ってみる。

環境

今回試した環境は以下の通り。

  • Ubuntu 18.04
    • Node.js v12.18.3
    • npm 6.14.6
    • Python 3.6.9

v8/libuv/Node.js ライブラリを直接使う場合

開発環境の準備

ビルドに必要なツール類をインストールする。

sudo apt install build-essential
sudo npm install -g node-gyp

開発

以下、適当な作業用ディレクトリで開発を行う。

まず main.cc を記述。

main.cc
#include <node.h>
#include <cstring>

namespace demo {

void ThrowTypeError(v8::Isolate *isolate, const char* msg)
{
    size_t msgSize = std::strlen(msg);
    v8::Local<v8::String> v8Msg =
        v8::String::NewFromUtf8(isolate, msg, v8::NewStringType::kNormal, static_cast<int>(msgSize)).ToLocalChecked();

    // Throw an Error that is passed back to JavaScript
    isolate->ThrowException(v8::Exception::TypeError(v8Msg));
}

void AddMethod(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    v8::Isolate* isolate = args.GetIsolate();

    // Check the number of arguments passed.
    if ( args.Length() < 2 ) {
        ThrowTypeError(isolate, "Wrong number of arguments");
        return;
    }

    // Check the argument types
    if ( ! args[0]->IsNumber() || ! args[1]->IsNumber() ) {
        ThrowTypeError(isolate, "Wrong arguments");
        return;
    }

    // Perform the operation
    double arg0 = args[0].As<v8::Number>()->Value();
    double arg1 = args[1].As<v8::Number>()->Value();
    v8::Local<v8::Number> answer = v8::Number::New(isolate, arg0 + arg1);
    
    // Set the return value (using the passed in FunctionCallbackInfo<Value>&)
    args.GetReturnValue().Set(answer);
}

void Initialize(v8::Local<v8::Object> exports)
{
    NODE_SET_METHOD(exports, "add", AddMethod);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

} // namespace demo

ビルド

同じディレクトリにビルド用の binding.gyp を用意。

binding.gyp
{
  "targets": [
    {
      "target_name": "myaddon",
      "sources": [ "main.cc" ]
    }
  ]
}

上記ディレクトリからビルドを実行する。

# Makefile 等の作成。build ディレクトリ配下に出力される。
node-gyp configure

# ビルドの実行。build/Release/ に myaddon.node というファイルが作成される。
node-gyp build

実行

今回作ったアドオン myaddon を使う JavaScript を用意。成功する場合と失敗する場合の両方を試してる。

sample.js
const myaddon = require('./build/Release/myaddon')

// 成功する場合 → 8 が返る。
const ans1 = myaddon.add(5, 3)
console.log(ans1)

// 失敗する場合(引数に数値ではなく文字列を渡してる) → 例外が投げられる。
try {
    const ans2 = myaddon.add(5, "abc")
    console.log(ans2)
} catch (e) {
    console.log(e.message)
}

実行結果。意図した通りに動いてる。

$ node sample.js 
8
Wrong arguments

node-addon-api を使う場合

開発環境の準備

ビルドに必要なツール類をインストールする。node-gyp の代わりに CMake も使えるが2、今回はそのまま node-gyp を使った。

sudo apt install build-essential
sudo npm install -g node-gyp

開発

以下、適当な作業用ディレクトリで開発を行う。

package.json を用意。

npm init

# dependencies に node-addon-api を追加
npm install node-addon-api

# package.json に `"gypfile": true` を追加する
vi package.json

以下のようになる。

{
  "name": "myaddon",
  "version": "1.0.0",
  "description": "",
  "main": "sample.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "node-addon-api": "^3.0.0"
  },
  "gypfile": true
}

次に main.cc を用意。内容は同じく足し算の関数 add のエクスポートだが、node-addon-api を使うと V8の複雑な型が消えてだいぶすっきりする。

なお、node-addon-api を使うには napi.h をインクルードする。v8.h, uv.h, node.h などのライブラリのヘッダーを直接インクルードしてはいけない。

#include <napi.h>

namespace demo {

Napi::Value AddMethod(const Napi::CallbackInfo& info)
{
    Napi::Env env = info.Env();

    // Check the number of arguments passed.
    if ( info.Length() < 2 ) {
        Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
        return env.Null();
    }

    // Check the argument type
    if ( ! info[0].IsNumber() || ! info[1].IsNumber() ) {
        Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
        return env.Null();
    }

    // Perform the operation
    double p1 = info[0].As<Napi::Number>().DoubleValue();
    double p2 = info[1].As<Napi::Number>().DoubleValue();
    Napi::Number answer = Napi::Number::New(env, p1 + p2);

    return answer;
}

Napi::Object Initialize(Napi::Env env, Napi::Object exports)
{
    exports.Set(Napi::String::New(env, "add"), 
                Napi::Function::New(env, AddMethod));
    return exports;
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Initialize)

} // namespace demo

ビルド

ビルド用の binding.gyp を用意。少し複雑になるが、C++ から JavaScript への例外を無効にする設定等をしてる。詳細はここを参照。

{
  "targets": [
    {
      "target_name": "myaddon",
      "cflags!": [ "-fno-exceptions" ],
      "cflags_cc!": [ "-fno-exceptions" ],
      "sources": [ "main.cc" ],
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")"
      ],
      'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
    }
  ]
}

ビルド手順は同じ。

node-gyp configure
node-gyp build

実行

実行手順と結果もまったく同じのため、簡易的に記載する。

# sample.js は同じものを用意しておく。
$ node sample.js 
8
Wrong arguments

参考サイト

以上

  1. Native abstractions for Node.js には次の記述がある: " The V8 API can, and has, changed dramatically from one V8 release to the next (and one major Node.js release to the next)."

  2. CMake.js より引用 : "CMake.js is an alternative build system based on CMake. CMake.js is a good choice for projects that already use CMake or for developers affected by limitations in node-gyp."

9
4
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
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?