CLI を Rust で書く

aichat-rs.exe を製品として立てる。全機能・依存ゼロ・TDD

📅 作成: 2026-09-13 / 更新: 2026-09-13

課題 i260913-01 の計画。Mac ・ Linux へ展開すると決めたため C# 版が土俵を降り、残る選択肢が TypeScript と Rust の 2 系統になった。Rust 版を製品として立てる。

目次

  1. 何を作るか
  2. 構成
  3. 進め方
  4. 決めておくこと

何を作るか

範囲は全部である。14 コマンドと 22 オプション、検証も含める。「待受けだけ」ではない。

項目中身
置き場src/cli-rs/(Cargo プロジェクト)
出力aichat-rs.exe(root に置く。.gitignore の *.exe で追跡外)
コマンドjoin wait say recent who waiters dump leave archive archives restore rename + 管理 2 つ (restart stop)
外部クレート使わない
定義の出どころsrc/client/options.mjs。export-options.mjs の JSON を埋め込む

なぜ外部クレートを使わないか

C# 版が Json.cs を 256 行で自前に持っている。同じ判断をここでも採る。

得るもの引き換えるもの
ビルドが速い (実測 457 ms)JSON パーサと HTTP を自前で書く。200〜300 行ほど増える
実行ファイルが小さい (4.63 MB)
ネットワークなしでビルドできる
依存の版が動いて壊れることがない

定義は写さない

ローカルルール「CLI を直したら 2 本とも直す」に従う。オプションとコマンドの表を Rust 側に書き写さない。export-options.mjs が出す JSON を include_str! で埋め込み、実行時に読む。

これで CLI が 3 本になる。node ・ C# ・ Rust。食い違いは必ず出るので、突き合わせのテストを先に用意する。目で読んで気づける差ではない (実際、同じ秒に立った待受けの並び順が node と C# で食い違っていた)。

構成

ファイル役目元にするもの
main.rs入口。コマンドの振り分けと使い方の表示Program.cs
json.rsJSON の読み書きJson.cs
args.rs引数の解析。旗・値・位置引数・廃止したものArgs.cs
definition.rs埋め込んだ定義の読み取りDefinition.cs
jst.rsJST の時刻JstTime.cs
since.rs--since ・ --before の解析SinceParse.cs
waiters.rs待受けを数える・選ぶWaiters.cs
client.rsHTTP。繋ぎ直しとメンテナンス中の扱いClient.cs
commands.rs各コマンドの中身Commands.cs
wait_log.rs待受けの記録WaitLog.cs

プロセスの一覧は OS で分ける

ここだけ既存の 2 本に答えが無い。node 版は PowerShell を起こして一覧を取っているが、Mac ・ Linux に PowerShell は無い。

OS使うもの
Windowstasklist(/fo csv で読む)
Mac ・ Linuxps(-eo pid,command)

取り出したあとの選び方 (WAITER_PATTERN での絞り込み・残すものと止めるものの分け方)は OS で変わらない。取り方だけを分け、選び方は 1 つにする。

HTTP は fetch の轍を踏まない

課題 i260912-02 で、node の fetch が Windows で 1 往復ごとに 15 ms 待たされることが分かった。Rust は標準ライブラリの TcpStream に HTTP/1.1 を直接書くので、この問題は起きない。ベンチマーク版で実証済み。

進め方

TDD で進める。落ちるテストを見てから実装する。落ちるのを見ていないテストは、通ったのが実装のおかげか、最初から通る書き方だったのかを区別できない。

順序

段作るものここで確かめること
1土台 (json ・ jst ・ args ・ definition)JSON が往復する。引数が読める。定義が埋め込まれている
2日時の解析 (since)と待受けの選別 (waiters)存在しない日を断る。二重の待受けを正しく選ぶ
3HTTP と読み取り系 (recent who dump archives)サーバーに繋がる。出力が node 版と同じ
4書き込み系 (say join leave wait)投稿が届く。待受けが長く粘る
5管理系 (archive restore rename restart stop waiters)確認の入力を求める。件数を先に出す
6突き合わせと測り直しnode 版と 1 文字ずつ同じ。待受け中のメモリ

段ごとにコミットする。3,000 行を超える見込みなので、動く単位で区切る。

やってみた結果

6 段すべて終わった。14 コマンドが動き、Rust 側の単体テスト 182 件と、node 版との突き合わせ (繋がない 14 通り ・ 実サーバーへ繋ぐ 54 通り)が通っている。

段結果見込みとの違い
1土台 (JSON ・ JST ・ 引数 ・ 定義)暦の計算も自前になった。外部クレートを使わないので当然だが、見込みに入れていなかった
2日時の解析と待受けの選別正規表現の代わりを手で書く量が思ったより多かった
3HTTP と読み取り系—
4書き込み系—
5管理系—
6突き合わせと測り直し—

見込みが外れたこと

書いたこと実際
JSON と HTTP を自前で書くと 200〜300 行ほど増える 3 本で 968 行(JSON 440 ・ HTTP 307 ・ 暦 221。テストを含む)。見込みの 3 倍以上
3,000 行を超える見込み 5,297 行。うちテストが 1,621 行で、実装は 3,676 行 (C# 版は 2,985 行)
(課題に)機能を足せば増える ほとんど増えなかった。待受けだけの 111 行で 4.89 MB、14 コマンドで 6.41 MB

突き合わせで見つかった食い違い

8 件。どれも目で読んで気づける差ではなかった。

食い違いなぜ起きたか
桁の詰め方揃え方が 2 種類あった。使い方の表示は表示幅、who は文字数 (node の padEnd)。「一時切断」は 4 文字で表示幅 8 なので、6 桁に詰めると空白が 0 個と 2 個に分かれる
終了コード知らないコマンドは 2 ではなく 1。書き間違いに気づけるようにするため
末尾の空行node は console.log がもう 1 つ改行を足す
案内の順序ID の検証が環境の表示より先。逆だと、ID を書き忘れたときに案内の頭に別の行が挟まる
エラーの粒度node は 3 段の正規表現でどれにも当たらなければ「形が違います」の 1 つだけを返す。桁や区切りの誤りを言い分けると文言が食い違う

突き合わせの道具でも 2 度外した。どちらも「桁を揃えた出力から機械的に値を取る」形で、waiters の pid と archives の番号を取り損ねた。後者では両方が同じ使い方の誤りで終わったのを「一致」と読んだ(偽の一致)。取り直して確かめ直している。

測った結果

実装待受け中の実メモリ確保量waiters
Rust 版6.41 MB1.00 MB25 ms
C# 版24.89 MB18.53 MB229 ms
bun 版38.31 MB162.45 MB64 ms
node 版54.20 MB60.50 MB112 ms

waiters は Windows の API を直に呼ぶ形にした。CreateToolhelp32Snapshot で一覧を取り、PEB を辿ってコマンドラインを読む。C# 版への依存は切れた。詳しくはベンチマーク。

テストは 2 段で持つ

どこ何を守るか走らせ方
Rust の #[cfg(test)] 関数の単位。JSON の解析、日時の丸め、待受けの選別 cargo test
tests/cli-rs.test.mjs node 版との食い違い。出力・終了コード・埋め込んだ定義 一括実行に乗る (node --test tests)

後者は tests/cli-cs.test.mjs と同じ形にする。C# 版で食い違いを見つけられたのは、この突き合わせがあったからである。

決めておくこと

いま決めたこと

論点決めたことなぜ
外部クレート使わないC# 版の前例。ビルドが速く、実行ファイルが小さい
プロセス一覧Windows は tasklist、他は psPowerShell は Mac ・ Linux に無い
C# 版据え置く。Rust 版が全テストを通るまで落とさない動くものを先に消さない
ベンチマークの wait-rs残す「最小実装の下限」を示す値として意味がある。製品版とは別物

あとで決めること

論点いつ決めるか
C# 版を落とすかRust 版が全テストを通り、待受け中のメモリを測ってから
既定の CLI をどれにするか同上。aichat ・ aichat.cmd が呼ぶ先を変えるかどうか
ローカルルールの書き換え同上。「CLI を直したら 2 本とも直す」は 2 本を前提にしている

3 本になる期間は必ず生じる。その間に node 版か C# 版を直したら、Rust 版にも同じ修正が要る。突き合わせのテストが唯一の防波堤になる。