Haxeのマクロで外部プロセスに処理を移譲する場合の知見

HaxeでVue.jsの開発をするために、.vueファイル(Single File Component)コンパイラをNode.jsで実装して、マクロから呼び出してコード生成するということをやっていた。このエントリーを書いている時点では、コンパイラ自体はまだ「とりあえずコードが生成できている」というレベルなのだが、マクロから外部プロセスを起動する部分については知見が溜まってきたのでメモを残す。

なお、この記事で使用しているHaxeはv3.4.3である。Windows 10でしか動作確認していないが、おそらく他のOSでも挙動は同じになると思われる。

プロセスの起動方法

次のコードが基本形となる。

gist.github.com

ポイントとしては、

  • sys.io.Processを使う。
  • プロセスの起動確認にstdoutを読み込んでいる。これはstdoutが読めるまでブロッキングされる。
  • 外部プロセス側で「起動が完了したことが分かる何らかの値」をstdoutに出力しなければならない。
  • プロセスの起動が失敗するなどの要因で、プロセス側からstdoutがクローズされ、終端を読み込むとEofがthrowされてくる。

この方法だと、外部プロセス側で処理が完了したら、successfulでもなんでもいいので、なんらかの起動完了を表す文字列をstdoutに出力しなければならない。これがないと、readLine()で処理がブロッキングされてしまう。

上記以外に起動確認をする方法としては、リターンコードの確認(process.exitCode())をする方法もある。これもリターンコードが返されるまでブロッキングされる。ただ、後述する外部プロセスを常駐化することを考えると、この方法は利用できない。

また、補足として、process.stdoutの読み込みも当然ながらブロッキングされるため、こちらを監視して正常起動を確認するという方式はとることができない。

その他注意点

  • プロセス起動は比較的オーバーヘッドが大きいので、マクロ内で何度もプロセス起動をすべきではない。
    • 厳密な計測ではないが、手元の環境(Xeon E3-1241 v3)で試した際は、プロセス起動では300msec強かかるのに対して、localhostに対してのHTTPであれば20msec未満でリクエスト処理が完了する。
  • stdoutに改行なしで大きなデータ出力される場合、応答がなくなる?
    • 最初は都度プロセスを起動して、処理結果をstdoutに出力してデータのやり取りをする素朴な実装をしていたのだが、process.stdout.readAll().toString() という実装ではマクロの応答がなくなってしまう問題が発生した。readLine()の場合はデータサイズが大きくても問題は発生していない。
    • 原因未調査だが、プロセスが停止されている(=stdoutが閉じられている)状況でもデータのサイズが大きいと応答がなくなってしまうため、Haxeの標準ライブラリ側に問題がある可能性もあり。
  • stdoutでテキストデータのやり取りをする場合、Windows環境では文字エンコーディングの問題が生じる。

上記のような問題に遭遇し、色々試したのだが、マクロ起動時にコンパイラをサービスプロセスとして起動して、HTTP RPCで都度の処理を移譲する返す形に落ち着いた。

マクロ起動時に処理を実行するには、Haxeコンパイラパラメータで--macro https://haxe.org/manual/macro-initialization.html を指定する。また、Haxelib化している場合は、extraParams https://haxe.org/manual/haxelib-extraParams.html と併用すれば、ライブラリを参照するだけで自動で --macro が付与されるようにできる。

また、sys.io.Processは明示的にclose()もしくはkill()しなくても、マクロ終了時にプロセスを停止してくれるのだが、Haxeコンパイラの入力補完機能を利用している場合は事情が異なってくる。

この場合は、Haxeコンパイラプロセスが常駐状態になるので、入力補完が走るたびにプロセス起動処理が呼び出され、明示的に外部プロセスをクローズしない限りは起動しっぱなしになる。そのため、マクロ側かプロセス側のどちらかで(できれば両側で)多重起動を防止するコードが必要となる。マクロ側で対応する場合は、単純にsingletonとして処理すれば十分である。

追記:Haxeコンパイラの入力補完サービスの場合、Haxeコンパイラプロセスが停止しても起動した外部プロセスは停止されないようだ。これは明示的にハンドリングする方法がないようなので、ある程度お行儀をよくするには、外部プロセス側で一定期間利用されていなければ自動終了させるような仕組みを入れるしかないと思われる。