Zigが実際のCLIツール開発でRustより実用的に感じられる理由
(dayvster.com)- メモリ管理において、ZigはRustよりもシンプルで直感的なアプローチを提供する
- Rustのborrow checkerは強力だが、小さなCLIツール開発には過剰な複雑さと開発者の負担を生みやすい
- Zigの手動メモリ管理は、適切なツールと多少の開発者規律があれば、効率的なメモリ安全性の確保が可能
- プログラムの安全性はメモリ安全性だけでなく、予測可能な動作、管理しやすい性能、データ保護など多様な要素が重要
- Rustは大規模システムに適している一方で、小さく実用的なCLIツールではZigのほうが開発生産性と保守性の面で有利
概要
最近CLIツールを作る際、RustよりもZigを優先して選ぶようになっている。
メモリ管理の基本: スタックとヒープ
- スタックは、関数パラメータ、ローカル変数、戻りアドレスなどの非常に一時的なデータを保存する、高速で固定サイズのメモリ領域である
- ヒープは動的メモリ割り当てのための領域で、データの寿命が長い場合やサイズが実行時に決まる場合に使われる
- スタックは構造的に単純だが空間に制限があり、ヒープは速度や断片化の面で気を配る点が多い
RustのBorrow Checker
- Rustのborrow checkerはコンパイル時にメモリ安全性を保証する
- 参照、所有権、ライフタイム(lifetime)といった規則を強制し、nullポインタ参照やダングリングポインタなどのエラーを未然に防ぐ
- ただし、メモリ安全性はコンパイル時の観点でのみ検査されるため、利用者のミスや複雑な所有権設計そのものをなくしてくれるわけではない
事例: 自作のNotes CLI
- 個人ノート管理用CLIをRustで書こうとしたが、borrow checkerのために構造を苦労して再設計しなければならなかった
- 一方Zigでは、アロケータを使うだけで、ポインタベースのインデックス生成や自由な変更・削除がはるかにシンプルに実現できる
- Rustのborrow checkerには明確な目的があるが、Zigは基礎的なメモリ管理の知識と規律だけでも高い水準の効率と安全性を確保できる
メモリ安全性だけではないCLIツールの安全性
- プロダクトの真の安全性には、予測可能な動作、エラー発生時の意味あるフィードバック、機密データ保護、攻撃耐性など多様な要素が含まれる
- RustでもZigでも、メモリ安全性以外の条件を満たせなければ「安全」とは言えない
- たとえば、CLIがエラー時に黙ってデータを上書きしたり、ファイル権限を誤って設定したりすれば、利用者は深刻な問題に直面しうる
-
CLIツールの安全性
- 予測可能な動作: 不正な入力や予期しない状況でも、一貫して明確な動作を保証する必要がある
- クラッシュとデータ破損の防止: エラーを適切に処理し、データ破損や未通知のクラッシュを防ぐ必要がある
- 性能管理: 大量のデータを処理しても、リソース消費や応答性低下が発生しないようにすべきである
- 機密情報の保護: 一時ファイルや権限設定への注意が必要である
- 攻撃耐性: 入力検証、メモリオーバーフロー、インジェクション攻撃などに対する強さが必要である
Rust Borrow Checkerの強みと限界
-
強み
- データレースと重複参照の防止: コンパイラが単一のmutable参照と複数のimmutable参照という規約を保証する
- 強力なコンパイル時保証: メモリ関連のバグの大半が実行前に防がれる
- 初期バグ発見: 商用サービスや並行性システムでは大きな利点となる
-
限界と不便さ
- 認知的オーバーヘッド: 小さなCLI作業でも、所有権・ライフタイム・参照管理を常に考える必要がある
- ボイラープレート/構造の歪み:
Rc、RefCellのようなラッパー、cloneの多用、構造の再設計など、「問題解決」ではなく「コンパイラを満足させること」に意識が向きやすい - 論理バグ/状態バグには無力: メモリ規則しか保証せず、予測可能性、論理エラー、データ完全性は保証しない
- エッジケースの複雑さ: キャッシュ、グローバル状態、mutable index などではライフタイム衝突が起きやすい
- 結果として、小さなCLIプロジェクトではRustのborrow checkerが開発者にとって「精神的な税」となり、実際に必要な以上に複雑になりうる
Zigの安全性とシンプルさへのアプローチ
- Zigは選択的な安全性チェックと手動メモリ管理を基盤としている
- アロケータ(allocator)の概念を内蔵しており、構造的で予測可能なメモリ使用を実現できる
- カスタムアロケータも作成でき、プロジェクト特性に合わせてメモリ管理方式を指定できる
- Zigの
defer構文により、スコープ終了時の自動解放やリソースクリーンアップもはるかに直感的である - Rustと違って開発者の責任が強調されるため規律は必要だが、構造的にうまく設計すればメモリ安全性の達成と維持は容易である
- Zigはコードが簡潔で、ポインタやリスト、インデックスなどの構造変更がRustに比べてはるかにシンプルである
- Rustのように強く縛られなくても、同水準の安全かつ効率的なコードを実装できる
- さらに、Zigのcomptime機能は、コンパイル時コード実行、テスト、最適化に大いに役立つ
開発者体験(Developer Ergonomics)の重要性
- **開発者体験(ergonomics)**は、言語の文法、ツーリング、ドキュメント、コミュニティまで含む要素である
- Rustは非常に厳格な規則によって最終的にメモリ安全を保証するが、過度な規則やceremonyは生産性を低下させる
- Zigは開発者主導の設計を重視しており、コードをより簡単に、速く、修正しやすく、理解しやすく書ける
- Zigは直感的なコード、速い反復、低い認知負荷によって、開発者がツールと戦わず問題解決に集中できるようにする
- Zigは開発者を信頼し、適切なツールと選択肢を与える一方、Rustは過度に監督的で制約が強いと感じられることがある
- 開発者を「ミスから守る」ことよりも、自らのミスを通じて学び成長する機会を保証することが、開発者に優しい環境である
結論
- 大規模・マルチスレッド・長時間稼働システムなど、Rustの強みが最大限に生きる分野では、Rustが依然として最良の選択である
- しかし、小さく実用的なCLIツールには、Zigの軽量さ、シンプルさ、素早い実装と保守性の高さのほうが適している
- メモリ安全性は安全性というパズルの一部にすぎず、予測可能な動作、保守性、堅牢性といったCLIツールに不可欠な要素は、Zigのほうがより達成しやすい
- 結局重要なのは「より良い言語」ではなく、**自分に合ったワークフローとプロジェクト特性に適した「ツールの選択」**である
- Zigは「メモリ安全性 + 低い認知コスト + 開発者親和性/生産性」が組み合わさった、小さなツール開発にぴったりの言語である
3件のコメント
まだRustよりエコシステムが安定していないように思います。
Zig は新しいバージョンでの破壊的変更がかなり頻繁なので……小さなプロジェクトでも、できるだけ CI を付けて継続的に管理したほうがよさそうですね。
Hacker Newsの意見
Zigの利点は、C開発者のような発想を保ったまま考え続けられる点にあると思うが、ある程度は単に慣れの問題でもあると思う
Rustに十分慣れた開発者は、もはやborrow checkerと格闘しない。すでにそういうコード構造で考えるようになっている
Rustでは「オブジェクトスープ(object soup)」のようなアプローチはうまく機能しないが、だからといってそれが本質的により簡単な方法だとは思わない。ただ、私たちが慣れているから簡単に感じるだけだ
ergonomics(使い勝手)は測定したり数値化したりしにくい、という前提を受け入れると、この種の議論はずっと曖昧なままになる
「borrow checkerとの戦い」という話は、Rustで語彙的(Lexical) lifetimeしか理解されていなかった時代に由来する
私の経験では、熟練したRust開発者はあちこちにArcをばらまいて、ほとんど自動的なgarbage collectionのように使っている
熟練したRust開発者でも、Arc、Clone、Copyなどをあちこちで使っているオープンソースのRustプロジェクトを多く見てきた
Zigの利点は、Cのように慣れた感覚で開発しながらも、言語やツールチェーン側で安全性を補う機能があることだ
私は元記事の内容の大半に同意しない
RustでもCやZigと同じくlifetime、ownership、borrow scopeについて考える必要はあるが、違いはコンパイラの支援があるかどうかだ
どれだけ賢くても、疲れていたり気が散っていたりするとミスをするのが人間だ。ミスを認めることこそ賢明さだ
Rustコンパイラが安全だと見なすプログラムの空間は十分広くなく、かなりの頻度でまともなプログラムを拒否する
例: 構造体Fooでbarとbazがそれぞれ文字列のとき、barを可変参照したあとにbazを不変参照しようとするとコンパイルできず、こういう場面ではコード構造を無理やり回避するしかない
反論するとすれば、「実際には問題ない状態なのにコンパイル拒否されるケースを避けるため」に、二番手三番手の設計へコードを変えなければならないこと自体が大きな負担だ
上の例は本当に素晴らしいケースだと思う。自分のブログや記事で取り上げてよいか聞きたい
上のコードを見て、むしろ自分の主張に前より自信がなくなった
すべてのプログラムが必ずしもそこまで「安全」である必要はないことを、私たちは覚えておくべきだ
私たちは多くのUnsafeなソフトウェアを楽しみながら育ってきた。Star Fox 64、MS Paint、FruityLoopsなどだ
Zigの作者Andrew Kelleyも、音楽制作ソフトウェア(DAW)の開発環境がなかったからZigを作った、と読んだことがある。Zigはそういう創造的なソフトウェアによく合うと思う
メモリバグに敏感な人はRustを使えばいい
Super Mario Worldも、メモリバグがあったからこそより面白かったと信じている
「安全」とは、「自分のプログラムが意図どおりに動作する」という意味の短縮形だ
少し混乱しているのだが、私の意見がよくないと言われる理由は、メモリ安全が重要ではないと言っているように聞こえるからなのか気になる
borrow checkerの価値が過小評価されているのが残念だった
Rustのborrow checkerは、無効なメモリアクセスが起きないことをコンパイル時に保証してくれる
もちろん、コンパイラの規則に従うコード構造へ変えなければならない不便さはある
Rustを個人的に使うとき、lifetime annotationが間違っているとは感じなかったし、面倒なchoreのようではあったが、すぐに慣れた
unsafeを使わない限り、Rustでは2つのスレッドが同時に同じメモリへ書き込めない
「なぜCLIツール開発でZigのほうがより実用的に感じられるのか」には共感できない。CVE(脆弱性)防止の面では、Rustは依然として強みがある
実際、私は大半の作業をGC言語でも十分こなしており、他言語に貢献するときもRust、Zig、C/C++のどれでも構わない
CLIツールが特別だというのか?
unsafeを使わなければ2つのスレッドが同時に同じメモリを書けない、という話はそこまできれいには割り切れない
Rustでbacklinksを実装するのが非常に複雑だという点には同意する
Rc、Weak、RefCell、.borrow()などを使えば可能だが、簡単ではない
短時間で終了するプログラムなら、arenaアロケーションも手だろう(CLIツールとはそういうものを指しているように見える)
Rustは巨大で、マルチスレッドで、長時間稼働するアプリケーションで本当に力を発揮する
実際、私はRustで大規模なメタバースクライアントを書いたことがあるが、数十本のスレッドを24時間回してもメモリリークもクラッシュもなかった
同じことをC++でやるなら、QAチームとValgrindのようなツールが必須で、スクリプト言語は性能面で遅すぎる
私もRustで、地球の曲率や重力偏差まで反映した物理シミュレーション飛行機を作った
Zigは魅力的だが、Dもまだ存在していて、個人的にはDのほうが自分の望むC/C++代替に感じる
Zigの文法はどこかしっくりこず、Rustはすでにエコシステムの中心的存在になっている
Goもさまざまな言語ツールで高いシェアを持っており、AI分野ではPythonの次によく使われている
Rust以前にはGo vs. D論争があり、私もDの教材まで買ったが、結局Goに流れた
Dは良いが、普及を後押しするキラーアプリが出てこない
RustやZigでわざわざCLIツールを書く理由がよく分からない
ボトルネックはI/Oであって、GCが遅いからではない
GCの問題は、ゲームやDBのようなメモリ集約型でない限り論点ではないと思う
メモリ安全性の議論よりも、GCのない言語を選ぶべき理由をもっと考えるべきだ、という点を強調したい
もし「ノーGCが楽しいから」で使っているなら、それだけで十分で、議論は不要だ
起動が即時であること(実行遅延がないこと)は非常に有益だ
GoでCLIを作るのはとても良かった(Go言語自体は好みではないが)
sum type、パターンマッチング、asyncサポートがある言語を最優先で選ぶ
GCなしの開発がゲーム分野に限られるという指摘について
GC論争は、ある種のバンドワゴンのように感じる
Rustの組み込みborrow/reference機能で簡単なノートツールを作ったが、思ったほど複雑ではなかった
notesリストのインデックスを保存し、マップでつなぐ構造を想像すれば、速度差もほとんどなく、安全性の欠点もない
インデックスを間違えても範囲外エラーで捕まえられるので、カーネルメモリを上書きするよりはるかにましだ
printfデバッグのときもずっと簡単で直感的だ
raw pointerやreferenceは、通常allocatorやasync runtimeのように本当に必要な場面にだけ使い、一般的なロジックではindex-basedのほうが向いている
よく知られているように、Rust asyncでself-referential structが使えずPinまわりの問題が生じるのも、ここに理由がある
vecに保存された値へのポインタは、reallocなどが起きると無効になるため、この場合はMiriですぐエラーになる
C++開発者の私が安全な言語を探すなら、Swiftがいちばん適していると感じる
馴染みのある、あるいは似た言語ほど適応は早い
Swiftは最近クロスプラットフォーム対応も強化されており、現役のC++標準委員会の人たちも複数参加している
ただしAppleとの結びつきや、ネイティブUIフレームワークの不在などから、Apple以外の陣営への広がりは相対的に弱い
Swiftがもっと人気を得てほしい
SwiftとZig/Cを比較できるリソースがあれば、紹介してもらえるとうれしい
Zigも、ある程度の注意を払えばメモリ安全なソフトウェアを作れると言われるが、実際にはCでも一定の規律を守れば同じ結果になる
結局、この「少しの規律(訓練)」が現実では足りないから問題が起きる
Zigはさらに次の問題も解決する
Cが50年以上にわたってこのdisciplineの問題で失敗してきたなら、それは「少林の道」以上に難しいことだ