rust-port について
rust-port は、既存ソフトウェアの構造をそのまま写すのではなく、観察可能な動作を保って Rust に移植するためのスキルです。動作の分析、テストの移行、検証、移植ノートの作成までを案内します。
主な機能
- 01元のプログラムの観察可能な動作を整理
- 02既存テストを先に Rust テストへ移行
- 03Rust の型とエラーに基づく API 設計
- 04C、C++、Python、Go、JavaScript、TypeScript の対応指針
- 05ビルド、テスト、Clippy の実行
- 06API、テスト、差分の記録
活用シーン
- 01C または C++ のライブラリを Rust に移植
- 02Python または JavaScript のプログラムを Rust で書き直す
- 03境界ケースの動作や結果順序を維持
- 04コードレビュー向けの移植記録を作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ morganlinton/ beautifulrust --skill rust-portインストール後、Claude Code で/rust-portを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/port-c-config-parser/case.yamlplace-files.shprompt.mdevals/port-c-config-parser/files/config.cconfig.htest_config.cevals/port-c-config-parser/graders/01-builds-and-tests-pass.md02-clippy-clean.md03-no-unsafe.md04-every-test-ported.md05-errors-as-result.md06-error-implements-error.md07-option-for-absent.md08-typed-flags.md09-behavior-preserved.md10-porting-notes.md11-proportionate.mdevals/port-python-task-graph/case.yamlevals/port-python-task-graph/files/taskgraph.pytest_taskgraph.pyevals/port-python-task-graph/graders/01-builds-and-tests-pass.md02-clippy-clean.md03-every-test-ported.md04-exceptions-as-error-enum.md05-kind-is-enum.md06-priority-is-option.md07-definition-order-preserved.md08-order-and-cycle-match.md09-summary-every-kind-in-order.md10-porting-notes.md詳細
Rust への移植
目指すのは、同じ問題を解決するために Rust 開発者が書いたであろう Rust コードで、観測可能な動作も同じにすることです。一行ずつ置き換えた翻訳(Rust の構文で書かれた C や、Box<dyn Any> を使った Python)はコンパイルできますが、元の問題をすべて引き継いだうえに、借用チェッカーの問題まで加わります。構造ではなく、動作を移植してください。
1. 何かを書く前に、元のコードを理解する
- コードとテストを読み、あれば、それを呼び出すコードも読みます。
- 観測可能な動作をすべて列挙します。公開関数、その入力と出力、失敗するあらゆるケースを含めます。移植で見落としやすい、目立たない部分も含めてください。たとえば、結果の順序、同点の場合の処理、入力が空の場合の動作、空白や大文字・小文字の扱い、整数の範囲、エラーと有効な境界ケースの区別です。
- 偶発的な要素、つまり元の言語の都合でそうなっているだけの要素を記録します。手動メモリ管理、出力引数、
NULLの返却、グローバル状態、固定サイズバッファ、関数をまとめるためだけに存在するクラス、isinstanceチェックなどです。これらを引き継ぐ必要はありません。 - 元のコードにバグや曖昧な点がある場合は、その動作を維持して移植時の注記に記載し、ユーザーに判断を委ねます。移植で黙って修正してはいけません。
2. まずテストを移植する
実装を移植する前に、元のテストを Rust のテストに移植します。対応関係を確認できるよう、元のテストごとに Rust のテストを 1 つ用意し、元のテストと対応する名前を付けます。テストが「同じ動作」の定義です。元のコードにテストがない場合は、手順 1 の動作一覧を基に、動作を明らかにするテストを書きます。
3. Rust API を設計してから実装する
元のコードの形ではなく、動作一覧を基に公開型とシグネチャを決めます。その後、移植したテストに合わせて実装します。
C および C++ からの移植
| 元のコード | Rust での対応 |
|---|---|
malloc/free、new/delete、create/destroy のペア | 所有型(String、Vec、Box)は自動で解放されます。それ以外は Drop を使います |
「値がない」ことを表す NULL | Option<T> |
エラーコード(-1、ERR_* の定義)、errno | コードごとにバリアントを持つエラー列挙型を使った Result<T, E> とし、Display および std::error::Error を実装します |
出力引数(int *err_line、long *out) | 戻り値を使います。データは Ok の値またはエラーバリアントに入れます |
フラグのビットマスク(FLAG_A | FLAG_B) | 名前付きフィールドと Default を備えたオプション構造体、または bitflags クレートを使います。生の u32 は決して使いません |
長さを伴う char *、strcmp、strcasecmp | &str/String、==、eq_ignore_ascii_case |
| ポインターと要素数、インデックスによる反復処理 | スライス、イテレーター |
| 固定サイズバッファとその切り詰め | サイズ変更可能な型を使います。制限に動作上の意味がなければ、その制限はなくします |
#define 定数とマクロ | const、列挙型、関数 |
| グローバルまたは静的な可変状態 | 呼び出し元が所有する構造体 |
strtol など | 対応する整数型を使った str::parse とし、解析エラーをエラー列挙型に変換します |
FFI 経由で C と連携する必要がない限り、unsafe や生ポインターは使わないでください。その必要がある場合は、rust-best-practices スキルの説明に従って、使用箇所を限定してください。
Python からの移植
| 元の言語での表現 | Rust での表現 |
|---|---|
| 例外階層 | 例外クラスごとにバリアントを持つ単一のエラー enum(または領域ごとに 1 つ)。データ属性はバリアントのフィールドになります |
値としての None | Option<T> |
閉じた集合として使う文字列(kind in ("build", "test")) | enum を使い、テキスト形式には FromStr と Display を実装します |
| レコードとして使う dict やクラス | 名前付きフィールドを持つ構造体 |
dict の反復順序(挿入順が保証されます) | 順序が観測可能な場合は Vec または順序付きマップを使います。通常の HashMap では出力順が気付かないうちに変わります |
ダックタイピング、isinstance | トレイトまたは enum |
上限のない int | 意図して選んだビット幅の整数を使い、入力が大きくなり得る場合はオーバーフローを処理します(checked_*) |
sorted(..., key=...) | sort_by_key または sort_by。どちらも Python と同様に安定ソートです |
str.splitlines()、strip()、isalnum() | lines()、trim()、char::is_alphanumeric。\r\n、Unicode、空文字列などのエッジケースは、元の言語と照らし合わせて確認します |
パース処理を囲む try/except | エラーをマッピングした ?、または Result に対する match |
Go、JavaScript、TypeScript からの移植
- Go の
(value, error)による戻り値はResultに、nilはOptionに、インターフェースはトレイトになります。ゴルーチンとチャネルは、元の実装で必要なものに応じて、std::sync::mpscを使うスレッドまたは非同期タスクに置き換えます。 - JavaScript の
nullとundefinedはOptionに、スローされるエラーはResultに、プレーンオブジェクトは構造体になります。数値は、意図して選んだビット幅の整数またはf64にします。
4. 移植の規模を適切に保つ
考えられるすべての入力について元の実装と一致させることは、通常、適切な目標ではありません。 実際に受け取る入力に対して一致させ、それ以外には Rust の標準ライブラリを使います。
- 元の言語のランタイムを再実装しないでください。 Python の
splitlines、strip、int()、repr()や、C のロケール依存のisspace、strtolを手書きで複製するのではなく、str::lines、str::trim、str::parse、charのメソッドを使います。Unicode の数字、ASCII 以外の空白、上限のない整数など、Rust の動作との差が特殊な入力に限られる場合は、その差を回避するコードを書くのではなく、移植時の注意事項に記載します。そのような入力があるとユーザーから伝えられた場合に限り、互換性シムを作成します。 - API をむやみに増やさないでください。 元の実装が公開している API と、Rust の呼び出し側が期待する標準的なトレイト実装(コレクションの
Debug、Clone、Display、Error、FromStr、Default、IntoIterator)を移植します。元の実装の呼び出し側で必要とされない限り、ビルダー、Indexの実装、FromIterator、Extend、独自のイテレーター型は省略します。 - 問題がない限り、アルゴリズムは維持してください。 現実的な入力で二次計算量になるなど、実際の問題を解決する場合に限り、別のアルゴリズムに置き換え、その理由を注意事項に記載します。
- 規模を確認してください。 テスト、コメント、ドキュメントコメントを除いたコード行数を数えます。C または C++ からの移植は、通常、元の実装と同程度の規模になります。Python または JavaScript からの移植は、Rust では型、エラー enum、それらのトレイト実装を明示するため、通常は元の 2~3 倍になります。それを大きく超える場合は、上記 3 つの習慣のいずれかが原因であることがほとんどです。
元の実装とのディファレンシャルテストを行う価値はありますが、現実的な入力とエッジケースの入力を数十件試せば十分に判断できます。何百万件もの入力を使ったファジングハーネスでは、通常、注意事項に記載すべき特殊な差異が見つかるだけです。
5. 検証
Rust のツールチェーンを利用できる場合は、以下を実行し、報告前にすべての問題を修正してください。
cargo build
cargo test
cargo clippy --all-targets -- -D warnings
移植したテストはすべて合格する必要があります。元の実装のテストも実行できる場合は、それも実行し、同じ入力で結果を比較してください。
6. 移植メモを作成する
コードと併せて、レビュー担当者が数分で確認できる簡潔な PORTING.md を作成してください。
- API の対応付け: 元の各関数または型と、それに対応する Rust のものを表にまとめます。
- テストの対応付け: 元の各テストと、それをカバーする Rust のテストを示します。
- 意図的な違い: 動作が変わる点とその理由を記載します。たとえば、発生しなくなったメモリ不足エラー、なくなった固定バッファーの上限、または Rust の標準ライブラリが元の実装とは異なる動作をする特殊な入力などです。
- 維持した癖: 意図的に残した独特な動作を記載し、誤って「修正」されないようにします。
- 検証: 実行したコマンドとその結果、または実行できなかった旨を明記します。
Rust のスタイルについては、rust-best-practices スキルがインストールされていれば、それに従ってください。