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

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

内容:

- ソースは [node-addon-examples](https://github.com/nodejs/node-addon-examples) をベースにしてます
- 足し算をする add という関数を JavaScript 側に見せる `2_function_arguments` にちょっと手を入れた程度です

# Node.js アドオンとは

[C++ addons](https://nodejs.org/api/addons.html#addons_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](https://github.com/nodejs/nan)
  - 各ライブラリのバージョン間の差異を吸収するツール(主にマクロ)を提供
- N-API
  - アドオン開発向けの C言語 API で、ABI(Application Binary Interface)を保証する
  - Node.js 本体と同じリポジトリでメンテされてる
- [node-addon-api](https://github.com/nodejs/node-addon-api)
  - N-API を C++ でラップして使いやすくしたもの
  - Node.js プロジェクトでメンテされてる

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

[^1]: [Native abstractions for Node.js](https://nodejs.org/api/addons.html#addons_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)."

# 環境

今回試した環境は以下の通り。
- Ubuntu 18.04
  - Node.js v12.18.3
  - npm 6.14.6
  - Python 3.6.9


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

## 開発環境の準備

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

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

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

まず main.cc を記述。

```c++: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 を用意。

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

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

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

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

## 実行

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

```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)
}
```

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

```bash
$ node sample.js 
8
Wrong arguments
```

# node-addon-api を使う場合

## 開発環境の準備

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

```bash
sudo apt install build-essential
sudo npm install -g node-gyp
```
[^2]: [CMake.js](https://nodejs.org/api/n-api.html#n_api_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."

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

package.json を用意。

```bash
npm init

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

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

以下のようになる。

```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` などのライブラリのヘッダーを直接インクルードしてはいけない。

```c++
#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 への例外を無効にする設定等をしてる。詳細は[ここ](https://github.com/nodejs/node-addon-api/blob/master/doc/setup.md)を参照。

```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' ],
    }
  ]
}
```

ビルド手順は同じ。

```bash
node-gyp configure
node-gyp build
```

## 実行

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

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

# 参考サイト

- [C++ addons](https://nodejs.org/api/addons.html)
- [C/C++ addons with N-API](https://nodejs.org/api/n-api.html)
- [node-addon-examples](https://github.com/nodejs/node-addon-examples)

以上