node-gypそしてnanへ。おまけでTypeScript

github.com

nodeのアドオンを調べてたら本もなければ情報も少なく、この土日ずっと家族には仕事が忙しくてヤバいという体裁を装い篭ってたので、せめて記録ぐらいは残そうとサンプルを書きました。サンプルは超シンプルになりましたがこれは理解してからシンプルに書き直した結果で、元は何倍ものゴミクズです。

承前

  • nodeだけではできないことをやる。
    • 動画や画像の出力や解析はゼロから書くのは無理ゲーだけど良いライブラリが既にある
    • OSにインストールされているフォントを取得して使う。更に出力サイズはきっちり計る
  • パフォーマンス
    • 極重な処理をハイパーにこなす

既存のコマンドをシェルで叩き標準入出力やファイルシステムでツールをチェーンするのは手軽に実現できます。これは手元環境だけでなく、AWS Lambdaとかのサーバレス環境も要はDockerなのでこの方法は有効です。魔法を見て驚いて中身見たらexecやspawnかよとズッコケることたまにある。

無い物はシェルコマンドをGoやSwiftででも作れば良いのでしょう。そのほうがUNIX的な思考だし再利用もしやすい。でも、なんかズッコケるんだよね。燃えない。TypeScriptで楽に書きたいのに、nodeで無理なところ探してアドオン書こうというのは、やってみたかったという初めから答えあってのことです。手間を考えたらあんまり正解じゃなかった気もするけど、ギリギリ日曜の夜にはできるようになったからOK。

nodeのアドオンを作る

nodeのアドオンはnode-gypを使って作るのが便利なようです。これはマルチプラットフォーム開発環境を提供してくれて、プラットフォーム毎にMakefile等を調整してくれる。しかもnpmと組み合わせて利用者の手元でOSとnodeのバージョンに応じてコンパイルすることができるので配布が柔軟になります。あらかじめよくあるOSとnodeの組み合わせについてバイナリを用意しておくためには、node-pre-gypというのもあります。これはAWSに提供側ビルドをホストしておくことがすぐできる。

nodeのアドオンというのは結局はそのJSインタープリタであるV8の拡張なのですが、こいつが後方互換軽視なアグレッシブな姿勢で作られているらしく、つられてnodeもアグレッシブにならざるを得ない様子なので、過去からの互換性を維持するための抽象層がマクロとC++テンプレートで提供されています。それがNan。nodeアドオンの作りかたでググったら全く違ういろんな書き方が出てきます。これは記事が書かれた時期によるV8のAPI変化であり、書き手それぞれのNanの適用範囲の差であり、中にはNanを使っていない例も多いためでした。よってスマートかつモダンにnode-gypでV8かつフルNanなプログラミングをモノにしましょうという算段です。

ざっくりとした手順

実際はサンプル見てもらうことにして、ざっくりと手順をまとめると以下の感じ。

  • nodeのプロジェクトを作る。フォルダにpackage.jsonがあることが最低。
  • nodo-gyp、nanを用意する。node-gypはグローバルに入れるのが大勢のように思われる。
  • binding.gypを書く。これはnode-gypの唯一設定ファイル。
    • Nanの利用はおきまりのフレーズをここに書く。
    • 驚いたのはnodeのシェル実行でrequireを用いていた。node -e "require('nan')"
  • C++ソースコードを書く。V8とNanを利用。
  • package.scriptsに、"{ install": "node-gyp rebuild }" を追加する。
    • 開発時のビルドはこれをnpmやyarnで実行する
    • 公開後は利用者がnpm installないしyarn addしたと同時にビルドが走る
    • ちなみにnode-gyp rebuildはnode-gyp clean && node-gyp configure && node-gyp build
  • マルチプラットフォームはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。
  • 既存ライブラリのリンクはbinding.gypにうまく書ける仕組みがある。サンプルでは利用してない。

Nan::AsyncWorker

サンプルでは、TypeScriptコードから文字列の引数とコールバックをアドオンに送る->アドオンは文字列の引数を保存して作業を行い(サンプルでは何も作業していないけど)、結果がOKだったら非同期にコールバックに返す -> コールバックのTypeScriptコードが実行される、という段取りになります。

ポイントはNan::AsyncWorkerがlibuvをラップしていて別プロセスを立ち上げていることです。ハイパーな処理はメインに迷惑かけずにここでやってくれという。サンプルのような軽い処理だったら本当は同プロセス同期実行でいいですね。その上でさらなるポイントは非同期に入る前に文字列を保存しているところです。V8の環境では常にガベージコレクションが走っていて、メモリの解放はAPIの中で強力にサポートされているので、別プロセス非同期実行だと解放された後にメモリアクセスしてしまいます。ここが私の週末の最後の壁で何度もクラッシュさせてました。今はstrcpyでコピーしているけど多分V8のAPIの中にうまい書き方が隠れていそう。 *1

Nan::New<String>("Hello ").ToLocalChecked()

頻出イディオムとして上記のようなコードを書くことになりますが、Nan::Newでv8::MaybeLocalというクラスを作り、ToLocalCheckedでガベージコレクションの対象となるように登録します。

TypeScript

ここまでの論点でTypeScriptは全く関係ないです。全編C++。利用のコツとしては、bindingsなるライブラリを使うことです。const binding = require('bindings')(<binding.gypのtarget_name>); bindingsはバイナリの位置を探します。

const binding = require('bindings')('helloLib');

export const greetingPromise = (name: string): Promise<string> => {
    return new Promise<string>((resolve) => {
        binding.nativeHello(name, (msg: string) => {
            resolve(msg);
        });
    });
};

こんな感じで、最近流行りのPromise化も可能。

import { greeting, greetingPromise } from '../hello';

test('greetingPromise', async () => {
    console.warn(await greetingPromise('Promise World!'));
});

こんな感じにasync-awaitで書けます。素敵。

気づき

  • V8はガベージコレクションをやるんだなということを思い出せばかなり光が射してくる
  • コールバックの有無と同一プロセス/別プロセスは違うこと
  • Nanは常に何かを楽にしてくれようとしてる。彼の親切心が何処にあるのかを探せば真実が見える

25年ぶりのC++、そして新たな問題

対象言語としてC++ 11が必須となっています。で、25年ぶりだから遠い記憶のそれは当然C++ 11じゃないんですよ。私の経験にあるのは学生の時に16bitのVC++でした。その後すぐにDelphiで更にはJava時代がきちゃう。初見はどこまでがマクロでどこから何のAPIで、言語仕様的なものはどこなのかわかりませんでした。結局Nanマクロが黒魔術であり、私が単にC++を忘れていただけでC++ 11的な何かというのは少なかったのですが。。。

ところでそもそも何でこんなことをしているかというと、まずはC/C++で書かれた魔法的ライブラリ達を取り込むためですが、次いではOSにアクセスするためです。しかしそのOSとは私の場合はOSXなのです。これが問題。OSXは基本的にObjective-Cであり、近年Swiftで代替されてつつあるわけです。C++からSwiftをイージーに呼べればアドオンのベースだけC++で書いてあとはSwiftで良かったのだけど、結構調べましたがC++からSwift(SwiftからC++じゃなく)をうまく使う方法がわからなかったので「おいおい、この年でObjective-Cかよ。俺はもう45才だぞ」と暗い気持ちになりました。Objective-CiPhoneの初期のころにいくらかやりましたが、なんかしっくり来なかったのでちゃんと習得しなかった。老い先短いので、同じく老い先短いObjective-Cは避けたい。その点でC++はいつまでも老いずに良いね。

更に調べると、CoreGraphicsやCoreTextといった一部のフレームワークC++でOKなよう。でもC++版のガイドやサンプルは当然皆無。OSXOSSじゃないからヘッダの先はブラックボックスなので、ヘッダを読んでObjective-C版のドキュメントやサンプルを参考にするしかない。ま、OSXの範囲でやりたいことは幸いにCoreGraphicsやCoreTextに集約されているようなのでボチボチやります。

*1:隠れてた。AsyncWorkerのSaveToPersistentとGetFromPersistentを使う