- tmux-rs プロジェクトは、約6か月をかけて C で書かれた tmux の全コードを Rust に移植した取り組み
- C2Rust ツールを使って当初は自動変換を試みたが、成果物の保守性が低く、最終的に手作業での変換を進めた
- ビルド過程と Rust-C 相互連携でさまざまな バグや構造的な問題 を経験した
- goto 文、マクロによるデータ構造、yacc パーサ など、C 固有のパターンを Rust に移す際の特有の課題と解決法があった
- プロジェクトはまだ unsafe Rust ベースだが、事前コンパイルと実行を通じて Rust への完全移植を目指している
プロジェクト概要
- tmux-rs は tmux の全コードベース(約67,000行の C コード) を Rust(約81,000行、コメント・空行を除く) に移植したプロジェクト
- 開発者はこの作業を 趣味プロジェクト として進め、C から Rust への移植過程で多くの試行錯誤と学びを得た
C2Rust の使用と限界
- もともと C2Rust という自動変換ツールを使って tmux の C コードを Rust に移植しようとしていた
- 自動変換コードは 可読性が低く、元の C コードに比べて 3 倍以上に肥大化し、各種の不要な型変換や定数名の消失などによって 保守性が大きく低下 した
- 手作業でのリファクタリング過程で、最終的に C2Rust の成果物を破棄 し、C コードを参照しながら直接 Rust へ移す方式に切り替えた
- C2Rust を使って初期段階でもビルド・実行できた点は、プロジェクトの妥当性と実現可能性を確認するうえで大いに役立った
ビルドプロセス設計
- tmux は autotools ビルドシステムをベースにしており、Rust コードと既存の C コードを 静的ライブラリ として連携させた
- 初期は Rust ライブラリを C バイナリにリンクする形だったが、コードの大半が Rust に移植されて以降は、Rust バイナリから C ライブラリをリンクする構成に変更した(
cc crate を使用)
- ビルド自動化のために build.sh スクリプト と
build.rs スクリプトを作成し、翻訳中でも段階的にビルド検証できるよう設計した
- ヘッダー宣言の欠落、関数シグネチャの不一致 など、ビルド過程で頻発する問題を関数単位で段階的に解決した
移植中に遭遇したバグ事例
バグ 1: 暗黙宣言とポインタ戻り値
- Rust に変換した関数で、ポインタ戻り値型 が C コード側で 暗黙宣言 になっていたため、return 値の上位 4 バイトが切り落とされて誤って渡される問題が発生した
- 解決策は、C 側に 正確な関数プロトタイプ を追加して、コンパイラが正しく動作するようにすることだった
バグ 2: 構造体フィールド型の不一致
- client 構造体でフィールド型(ポインタ vs 整数)の誤訳により、メモリオフセット計算 がずれてデータ解釈エラーと segfault が発生した
- 正確な構造体定義を C と Rust で一致させることで解決した
C 固有パターンの Rust への移植
Raw pointer の活用
- C ポインタを Rust の参照へ直接マッピングすることは、null 許容や初期化保証など Rust の安全性ルール に反する可能性がある
- そのため、ほとんどのポインタ構造は raw pointer (
*mut, *const) に移し、安全でない領域でのみ使用した
goto 文の処理
- C2Rust では goto のフロー制御 をアルゴリズム的に変換するが、多くの場合は Rust の labeled block + break、labeled loop + continue で十分に実装可能
マクロによるデータ構造の移植
- tmux は 侵入型 red-black tree、linked list を C マクロで実装 している
- Rust では Generic trait とカスタム iterator を使って類似のインターフェースを実装した(単一 trait の重複実装問題は dummy 型で解決)
yacc パーサの変換
- tmux は設定ファイルパーサのために yacc(lex) を使用している
- Rust では構造が似た lalrpop crate を使って文法とアクションをそのまま移植し、C lexer との連携用アダプタ も作成した
- この過程で、lalrpop の raw pointer サポートの限界(
NonNull<T> 活用)なども経験した
開発環境とツール
- 主に neovim でカスタムマクロを活用し、反復的な変換作業を進めた
- 例:
ptr == NULL → ptr.is_null() / ptr->field → (*ptr).field などを手作業で対応
- 自動化ツール(Cursor)も試したが、欠落または誤ったコードが多く、コードレビューの負担 が大きくなった
- 指の疲労軽減には多少役立ったが、生産性の面では限定的だった
結論と今後の方向性
- 全コードの Rust への移植は完了し、バージョン 0.0.1 を公開
- C2Rust と比べて手作業コードのほうが一部で優れているが、依然として unsafe Rust ベースで、多数のバグが存在する
- 最終目標は safe Rust コードへの移行 と tmux 機能の完全な Rust 移植の達成
- Rust や tmux に関心のある開発者との協業やフィードバックを GitHub Discussions を通じて期待している
4件のコメント
おお……でも、Rust のほうがより軽いんですか?
おお……いいですね?
tmux プラグインの中では
resurectが地味にメモリをかなり食って妙な動きをすることもあって外していたのですが、tmux-rs と一緒ならもっと良くなるのか気になりますね。https://rosettalens.com/s/ja/tmux-rs-intro
Hacker Newsのコメント
本当に素晴らしいプロジェクト記録の読み物で、感動した気持ちを伝えたい。著者の粘り強さと執念には大きな敬意を表したい。「庭いじりに似ているけれど segfault がもっと多い」という表現には深く共感した。こういう真剣な趣味プロジェクトからこそ、いちばん多くを学べるものだと思う。
c2rust に関する経験は特に興味深かった。以前にも、言語間の自動コード変換器がもたらす似たような変化を見たことがある。こうしたツールはプロジェクトを素早く立ち上げて実現可能性を証明するにはとても有用だが、結局はターゲット言語らしくない、空虚な感じのコードになりがちだ。最終的に苦しくても手動移植へ切り替えた判断は本当に正しかったと思う。自動変換では、C コードの意図を安全で Rust らしいコードへ翻訳するのに限界がある。
「面白いバグ」セクションの #2 の struct layout mismatch の話を見て、昔の外部関数インターフェース(FFI)の悪夢を思い出した。私も一度、C++ と C# の間で struct のパッキングが食い違い、微妙なデータ破損を突き止めるのに1週間無駄にしたことがある。意味の上で頭がおかしくなりそうなバグで、本気で正気を疑う。こういうものを見つけ出すには、とてつもないデバッグの忍耐が必要だ。著者に拍手を送りたい。
全体として、このプロジェクトはコアなインフラコードをモダナイズする現実的な難易度とプロセスをよく示したケースだと思う。次の大きな目標は unsafe から safe Rust へ移ることらしいが、どんな戦略を取るのか本当に気になる。
raw pointer や goto などの複雑な制御フローをすべて idiomatic で安全な Rust に直しつつ、コード全体を崩壊させないようにするのは、実質的に最初の移植より難しいかもしれない。ライフタイムや borrow checker をモジュール単位で段階的に導入するつもりなのか、intrusive データ構造をどう扱うつもりなのか気になる。標準ライブラリの BTreeMap のようなものに置き換えると性能への影響がありそうだが、もともとの intrusive 設計はそれを意図していたのではないかとも思う。
とにかく驚くべき仕事だ。ここまで詳細に過程を共有してくれてありがとう。GitHub でプロジェクトを引き続きフォローするつもりだ。
今回の話題はまさに自分を引き寄せる感じだ。
数年前から Rust ベースの tmux セッションマネージャー rmuxinator(tmuxinator クローンのようなもの)を自作している。ほとんどはうまく動くが、生活が忙しくて進みが遅く、最近はバグ修正中心でまた手を入れている。最近追加した機能は、rmuxinator をライブラリとしても使えるようにしたことだ。tmux-rs をフォークして rmuxinator を依存関係として組み込み、プロジェクトごとの設定ファイルでセッションを開始する方法が実際に機能するか試してみたい。rmuxinator を upstream に取り込むべきだと主張するつもりはないが、こうしたセッションテンプレート機能が terminal multiplexer 自体に内蔵されていたら本当に便利だっただろうと思う。
逆に、rmuxinator が tmux-rs をライブラリとして使って、シェルコマンド生成なしにセッション管理をすべて解決できたらもっとよいのでは、とも考えた(もちろん tmux-rs がそれをサポートしているかはまだ分からない)。
今進めているバグ修正が終わったら、上のアイデアのどちらかは必ず試してみるつもりだ。
ともあれ richardscollin、見事な仕事だと思う。
「なぜ tmux を Rust で書き直したのかと聞かれても、これといった良い理由はなく、ただの趣味プロジェクトだ。庭いじりみたいなものだけど segfault がもっと多い。」この姿勢がとても好きだ。
新しいものを作るとき、必ずしも大義名分や実用性だけが必要なわけではない。趣味プロジェクトから思いがけない発見が生まれることもある。著者の詳しい文章には感心した。
ちなみに自分の庭には segfault があふれている。新しいプロジェクトを書くほうが、うちの庭より安全に感じる。
まったく同感だ。すべてのプロジェクトが世界を変えるために存在する必要はない。
最近 fzf を Rust で再実装したことがある rs-fzf-clone
特別な理由があったわけではなく、既存の fzf も非常によく動いていたし、主な目的は Rust のチャネルとあいまい検索アルゴリズムを自分で体験してみたかったからだ。本当に楽しい学習過程だったし、元の fzf のほうが優れているとしても、それは必ずしも重要ではなかった。新しいことを試し、実験すること自体が目的だった。
「庭いじりは哲学者になるための最高の言い訳だ」
誰かが Rust は C より無条件に優れている、というニュアンスを出すと、反射的に皮肉な反応をしてしまう性分だ。でも、人々がこういうプロジェクトを楽しみのためにやっているという事実をつい忘れてしまう。
「私たちは必ずしも新しいものを作るためにだけ理由を必要としているわけではない」という言葉が印象に残った。
でも tmux は実際には新しいものではない。
既存のソフトウェアを別の言語で書き直すことにも理由が必要なのか、考えさせられる。
「庭いじりみたいだけど segfault がもっと多い」という言葉は面白い。Rust にはまだ慣れていないのだが、具体的にどんな場面で unsafe が必要になるのか気になる。
このプロジェクトの姿勢と、ほとんどのコメントが前向きな雰囲気であることにとても感銘を受けた。
成熟したアプリケーションを別の言語で書き直すのは常に良いことではないと言われがちだが、実際に試みることで多くの学びが伴う。結果よりも過程こそが本当に重要だ。
ここで集めた関心と AI 発展の流れを考えると、Rust 初学者にとって非常に魅力的な趣味プロジェクトへ発展しうる気がする。簡単なバグから直し、新しい機能を追加したり最適化したりするのは大きな経験になる。
ひとつアイデアとして、Gemini CLI(あるいは好きな LLM)を scratch buffer のようにして、tmux セッション内のさまざまなウィンドウやペインと対話させる機能を提案したい。
私の場合、複数のサーバーに同期されたペインでコマンドを実行しつつ、失敗などを手動で管理しているが、AI にコマンド実行を任せ、リアルタイムで出力を分析して適応的にコマンドを再生成する機能があれば、まるで動的に生成される特注のシェルスクリプトのようだと思う。
たとえば毎日 gvim を使っているが、エディタを作るなら gvim のようなものを作る必要はなく、自分が欲しい機能だけを持った新しいものを創造的に作りたい派だ。これだけ時間を投資するなら、創造的でユニークなことを試すほうがより意味があると思う。
たった今、tmux を Fil-C で1時間以内に移植してみた(libevent の移植とテスト通過も含む)。非常によく動くし、完全なメモリ安全性が確保できた経験だった。
こういうプロジェクトは好きだ。自分も rust にハマってみたい気がしている。
参考までに zellij(Rust ベースのターミナルマルチプレクサ)を紹介したい。
自分は単なるユーザーだが、Rust ベースのソリューションを探し続け、移っていくのを楽しんでいる。
ちょうど今この動画「Oxidise Your Command Line」を見ていたところだった。
https://www.youtube.com/watch?v=rWMQ-g2QDsI
一部は Rust 開発者でなければ不要かもしれないが、コマンドライン環境に慣れている人なら誰にとってもかなり役立つヒントもある。
c2rust では、定数名の保持など、著者が指摘した情報損失を減らす改善は十分可能なのではないかと思う。最初の変換の負担が大きいので。
大規模言語モデルで C コード全体を Safe Rust に1時間で、しかも正確に自動変換してくれる時代が来たら、こういうプロジェクトはとても未来志向の代表例になりそうだ。
ただし、著者も最終段階で Cursor で試してみたものの(2025年半ば時点)、変換効率が著しく落ちたと言っていたので、現実的な性能はまだ先だと思う。
codemod.com などでは、これをすでに「codemods」という概念で試している最中だ。
codemods は AST(抽象構文木)を使って、高速かつ大量のコード変換やリファクタリングを可能にする。
codemods API リファクタリング紹介
「大規模言語モデルで複雑な C コードを1時間で Safe Rust に完璧に変換できる」という部分が具体的で印象に残った。
今後コードがもっときれいになっていくことを期待している。zellij は何度か試したが、何年も開発されているにもかかわらず、tmux で当たり前に使える機能がいくつかまだ足りない。
特に status bar の非表示/表示ができないのがいちばん不便だ。
zellij-org/zellij issue #694 を参照
自分が頻繁に使うキーバインドがセッションマネージャープラグインのデフォルトバインドと衝突してしまい、ディレクトリ選択などの主要機能が塞がれてしまう。
結局、セッション作成もプラグインではなくコマンドラインから直接やらなければならない構造だ。