ちょっと色々なフェーズが動いたと言いますか、最近それはそれで転換点でイソイソしてるので、小休止みたいな所でdocker-tags-zigというツールを作ってみました。 一応コードの方はオープンにしといたので、気になる方がいらっしゃったら参考になるかも知れません。成果物も落とせます。

んなことやってる場合かい、ってこともあるんですが、気晴らしに誰にも指図されないコードをガーッと好きなように一気に書いてしまう、というのは一種のよくある手段です。現実逃避とも言う。

今回は「bashでみんなやってるようなことだからバイナリサイズを小さく。でもbashに依存しないマルチOS対応で」が目標で、その為にzigを使ってみました。が、思った以上に難しかった。何が難しく感じたのって所を書いてみたいと思います。

依存性の解決

恐らくドギツイ点はここです。個人開発以外で使い物になるのかまだ「難しい」ってのを如実に示してる。低レイヤの自己完結的な操作ツールを作る以外の用法が使い方浮かびにくそうでした。Rust覚えたほうが普通に良いかも知れない。

ツールは安定版のzig 0.13.0で開発しています。しかし、現段階ではサードパーティのライブラリ作者がまともにリリースをしておらず、言語としての破壊的変更も頻発しています。

その為使うライブラリ1つ1つのGitHubのコミットを漁って「安定的に現在のランタイムで動作するコミット」を探し、それをモジュールとして組み込むと言う異様にトリッキーなことをしました。こんなサードパーティの使い方は他の言語で中々やったことがありません。他の人が作ったNode製のOSSを1回指摘したくらいのイマドキ稀な事象です。

例えば以下のような形です。

.dependencies = .{
    .chroma = .{
        .url = "https://github.com/adia-dev/chroma-zig/archive/c8cd1144aa800f87db3c4bee3d1f0fa0e8c96feb.zip",
        .hash = "12209a8a991121bba3b21f31d275588690dc7c0d7fa9c361fd892e782dd88e0fb2ba",
    },

これは保守性の観点から言ったらかなりの困難を伴います。今回のものは「作って終わり」で良いんですが、継続的にツールを進化させようと考えたら困難が相当伴います。どこかのコミットで「動かなくなる」リスクが高めなので、こんな仕掛けをわざわざやって「とりあえずは今後環境再現するときも動くように」と布石を打っている点で工夫しています。

zigには「コミットを引っ張ってモジュールに組み込める」機能があるにはあるってことが言えるので、それだけでも良いかも知れません。

アルゴリズムについて

多分comptimeとか使ってもっと頭いい方法があるかもしれない。 単純に言うと、Linqで言うfoo.OrderBy(x => x.Foo)みたいなことがやりたかったんだけど、zig自体あまりよく理解っていなくて、ゴリ押しのアルゴリズムを組みました。

ちょっと恥ずかしいのが以下のようなもの。単純に言うと「取得したAPIのレスポンスリストからキーにしたい文字列リストと対応する辞書を作成し、文字列リストをソートした後、その順番で新規のリストに射影している」みたいな処理です。要は、本当は構造体の特定のメンバでソートしたい(今回は名前が1.14より1.15が上に上がってきたほうが見やすそうに感じたので、タグ名の降順ソート)。 これでもzulという凄腕が作ったライブラリがあるので大分ショートカットしてる方かも知れません。

// 辞書でストア
var tag_maps = std.StringHashMap(model.ImageTagInfo).init(allocator);
defer tag_maps.deinit();
for (schemas) |s| {
    try tag_maps.put(s.tag, s);
}

// キーでのソートリスト作成
var tag_sorted = std.ArrayList([]const u8).init(allocator);
defer tag_sorted.deinit();
for (schemas) |s| {
    try tag_sorted.append(s.tag);
}
zul.sort.strings(tag_sorted.items, .desc);

// 結果リスト作成
var results = std.ArrayList(model.ImageTagInfo).init(allocator);
for (tag_sorted.items) |tag| {
    const s = tag_maps.get(tag).?;
    try results.append(s);

この問題は少し修正しました

うーん、私の昔を思い出すような実装手順。今でもケースに応じてやりますが。

これ実はリストある分Cの方が難しそうに見えて、Cの方が楽です。Cだと配列操作にマクロを活用できます。コンパイラが文句を言ってこないけど、もうちょっとメタ的なロジックを入れられる。

昔私的に書いていたものから取り上げます。

#define COMPARE_EXP_STRUCT(type_t, member_name, arg_name1, arg_name2) \
do { \
    if (((type_t*)arg_name1)->member_name > ((type_t*)arg_name2)->member_name) return -1; \
    if (((type_t*)arg_name1)->member_name < ((type_t*)arg_name2)->member_name) return 1; \
    return 0; \
} while (0)

このマクロ自体は複雑っぽくて「ナンジャコリャ?」って気になりますが、使う側はキレイにいけます。以下のように出来ます。qsortに比較関数を突っ込んで、中はマクロだからジェネリックに応用して一発です。

int compare_students(const void* a, const void* b) {
    COMPARE_EXP_STRUCT(Student, score, a, b);
}

size_t student_len = sizeof(students) / sizeof(students[0]);
qsort(students, student_len, sizeof(Student), compare_students);

もしかしたら同じようなことが出来るかもしれないけど、ドキュメントが不足しすぎていて全くチンプンカンプン。勘としてはcomptime機能がキーになる標準ライブラリが入ってて、それを応用する手法で実現できると予想してる。けど、良い感じの実装には至りませんでした。力不足です。

こういった操作をするだけでも結構骨が折れてしまいました。今後長い間で色々判明したら修正する可能性はあるかもしれません。

今後直したい点

build.zig.zonに対応したメタモジュールを呼び出して、ツールのバージョンに同期させることです。 他言語でもpyproject.tomlやCargo.tomlに対応したバージョン情報を読み出す仕掛けが、様々なライブラリについています。

これが出来ると何が良いかと言うと、コードのメインファイルをいじらずに「設定ファイル弄くったらバージョン更新です」とツールが言ってくれるようになるのです。管理が楽になると言うことですね。

確定した情報は持ち合わせていませんが、未実装で混乱しているっぽく見えます。

恐らくはzonの読み出し機能が今後付くことが計画されています。「0.13.0ではとりあえず実装されていないと推測でき、0.14.0でも実装されるか怪しい可能性が高い」と判断して、これを調査するタスクの時間をそそくさと飛ばすことにしました。

良かった所

1000KBサイズでWindows, Linuxで動くことを確認したので、目標はひとまず達成しました。これはzigの強みになりそうな部分です。今後自分のマシンで使い回せると思うとワクワクします。チェックも噛ませてアリーナでのドッと積んだメモリ確保・解放をやっているので、リークやセグメンテーションフォルトは基本出ないハズです。

このサイズ感はGoの1/4以下であり、C#のセルフホストバイナリの1/60と言う小ささです。同様のツールを配布されている方はGo製が多く、大凡5MB程度(機能も良いのですが)に対してzip配布で200kb程度。この値は素晴らしい。

一方で期待していた「WASI対応」がちょっと断念することになりました。理由はサードパーティの使っているAPIが「内部でwasiに対応したシステムコールのラッパー関数を使っていない」と言う問題で、根本には「zigが対応していないから」が要因になります。これではどんなにzigがwasiを簡単に吐ける言語と言えど、相当な知識がなければビルドに全く至りません。この課題を乗り越えれば、真にクロスプラットフォームになりますが…

上記のことは他言語でも似た状況があるかと思いますが、そもそも対応しているかどうかを判断するドキュメントがありません。恐らくRustの方が用意していますし、.NETのような技術であれば尚更整備されるでしょう。良くも悪くも、「zigの内部コードを掘るしかない」という結構高度な技を要求されます。流石に「無理だ!」と思いました。

しかしいやはや、本当にBunとかどうやったらこんな環境であんな膨大なモンが出来てるんだろう。こんな荒波みたいな言語でもハードル越えてるんだな。

ますます気になってしまった。。