まえおき
Dart Advent Calendar 2019が思いのほか過疎っているので、2日目もなんとなく書いてみます。
この秋にリリースされたDart 2.6でよく取り上げられているのが dart2native というネイティブ実行ファイルをシングルバイナリで出力する機能ではないでしょうか。
Dartの公式Mediumでも言及されています。 https://medium.com/dartlang/dart2native-a76c815e6baf
「コマンドラインツール」, 「シングルバイナリ」 といえばGoが有名ですが、今秋からDartもその仲間入りしました。
(Kotlin/Native...? はよく知らないので誰か教えて下さい )
dart2nativeは結構なんでもシングルバイナリにできる
Hello Worldできるプログラムがシングルバイナリになって嬉しい人は多分いないでしょう。
コマンドラインツールとして使うのであれば、
- コマンドライン引数を解析する
- 標準入力を受け取る
- JSONをパースする
- HTTPクライアント
- HTTPサーバー
あたりはそろっていないと話になりません。
dart2nativeはリフレクションを使うようなライブラリ等、一部を除き、たいてい動かせる(らしい)ので、
- コマンドライン引数の解析→ args
- 標準入力を受け取る → dart:io, dart:convert
- JSONをパースする → dart:io, dart:convert
- HTTPクライアント → http
- HTTPサーバー → dart:io
などなど、適当に標準ライブラリを使ってコマンドラインツールは作れそうです。
例: リクエストの中身をPrintし、そのままオウム返しするコマンドラインHTTPサーバー
Webhookの試験をするときとかに使いたいやつです。
./echo_http_server -b 0.0.0.0 -p 3000
Listening on 0.0.0.0:3000...
のように起動して
$ echo '{"hoge":"fuga"}' | http -v POST http://localhost:3000/
POST / HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 16
Content-Type: application/json
Host: localhost:3000
User-Agent: HTTPie/1.0.2
{
"hoge": "fuga"
}
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
transfer-encoding: chunked
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
{
"hoge": "fuga"
}
HTTPリクエストを受けてオウム返しする簡易的なサーバーを作りたいとします。
雑にかくと、
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
class MyArgs {
final String address;
final int port;
MyArgs._(this.address, this.port);
factory MyArgs.parse(List<String> args) {
ArgResults parsedArgs = (ArgParser()
..addOption("bind", abbr: "b", defaultsTo: "127.0.0.1")
..addOption("port", abbr: "p", defaultsTo: "3000"))
.parse(args);
return MyArgs._(parsedArgs["bind"], int.parse(parsedArgs["port"]));
}
}
main(List<String> args) async {
final myArgs = MyArgs.parse(args);
final server = await HttpServer.bind(myArgs.address, myArgs.port);
// print("OS: ${Platform.operatingSystem}");
// print("OSVersion: ${Platform.operatingSystemVersion}");
print("Listening on ${myArgs.address}:${myArgs.port}");
await for (HttpRequest request in server) {
// POST /webhook のようなURL・メソッド表示
print("${request.method} ${request.uri}");
// ヘッダーを順に表示
request.headers.forEach((String key, List<String> values) {
values.forEach((String value) {
print("$key: $value");
});
});
// リクエストボディを表示
String body = await utf8.decodeStream(request);
print("\n$body");
// HTTP 200 OKでリクエストボディの内容をそのまま返す
await (request.response
..statusCode = 200
..write(body))
.close();
}
}
のようにDartコードを書き、
$ dart2native main.dart -o out/echo_http_server
$ cd out/
$ ./echo_http_server -p 3000
のようにdart2nativeを実行すれば、期待通りのHTTPサーバーが出来上がっているはずです。
dart2nativeの便利なところ、いまいちなところ
class, try/catch がふつうに使える
Goだと結構頑張らないといけないところが、Dartだとサラッと(Javaのように)無理なく書けます。
数年後も使い続けるようなツールや、並列処理をフル活用したいツールであれば、Goで書くのがよいでしょう。また、Goのほうが快適に書ける人はべつに無理にDartを使う必要はないと思います。
ただ、使い捨てで1〜2週間くらいの間だけなんとなく動いてくれたらいいツールであれば、Dartでさらっと書くという選択肢もあるよ!ということです。
async/await がシンプルに使える
JavaScriptユーザにはおなじみのasync/await は当然Dartでも使えますし、dart2nativeでも問題なく動作します。
コマンドライン引数の解析結果に型情報がない
args パッケージはPythonのargparse くらいの解析能力はありますが、解析結果が型情報をもっていない(dynamic)のです。
ライブラリの実装的にそうせざるを得ないのはわかりつつも、ソースコードに if (parsedArgs["need-notification"]) {
のような型情報がわからないものは散らかしたくないものです。
先のHTTP echoサーバーの例では、
class MyArgs {
final String address;
final int port;
MyArgs._(this.address, this.port);
factory MyArgs.parse(List<String> args) {
ArgResults parsedArgs = (ArgParser()
..addOption("bind", abbr: "b", defaultsTo: "127.0.0.1")
..addOption("port", abbr: "p", defaultsTo: "3000"))
.parse(args);
return MyArgs._(parsedArgs["bind"], int.parse(parsedArgs["port"]));
}
}
このようにコマンドライン引数の解析結果を型情報のある変数に入れ込むだけのデータクラスを用意していました。
おそらく大抵のケースではこのようなデータクラスを用意するのが吉なのではないかなー?と思います。
(もっといい方法があれば教えて下さい)
クロスコンパイルができない
2019秋時点で、唯一にして最大の欠点を上げるとすればコレかなと思います。
- Macで dart2nativeして得られるのはMac用のバイナリだけ、
- Linuxで dart2nativeして得られるのはLinux用のバイナリだけ
です。(see: https://github.com/dart-lang/sdk/issues/28617 )
MacやWindowsでもDockerを使えば簡単にLinux向けのバイナリは作れます
では、全OS向けにバイナリビルドをしたい場合にどうすればいいか?というと、Dart 2.6のアナウンスでは「CIを利用すればいい」など書いてありました。
name: dart2native
on: [push]
jobs:
build:
runs-on: windows-latest
container:
image: google/dart:latest
steps:
- uses: actions/checkout@v1
- name: Install dependencies
run: pub get
- name: dart2native
run: dart2native main.dart -v -o out/echo_${{ runner.os }}
GitHub Actionsでこんな感じでやればいいのでしょうか? いいえダメです...
おそらくこの記事に書かれているように、自前でアクションを定義すればできるでしょう。 うーん...面倒
現時点では、Windows/Mac/Linux全対応やarm向けのバイナリを作りたいケースでは、DartではなくGoを利用するのが現実的な選択肢ではないでしょうか。
まとめ
dart2nativeの登場で、使い捨て/自分専用なコマンドラインツールはDartを使って気軽に書くことができるようになりました。Goに加えて新たな選択肢が登場し、心強いです。
aws-sdk-dartなど便利なライブラリはまだDartにはありませんが、今後もし各種SDKのDart版が登場したら、コマンドラインツールはさらに作りやすくなると思います。
自分用コマンドラインツールを持っている方は、ぜひ一度Dartで何かを作ってみてはどうでしょうか。