# Port To Rust

> Run a port into Rust without losing behaviour — define the parity contract, sequence the phases, migrate incrementally behind a stable boundary, and prove parity differentially. Use when moving, porting, rewriting, or migrating an existing codebase into Rust from any language, when deciding how to sequence or scope a rewrite, when a port needs to prove it matches its source, or when a partially-ported system needs both implementations running side by side.

- Skill: `rewrite-rs/port-to-rust` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add rewrite-rs/port-to-rust`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rewrite-rs/port-to-rust/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: rewrite-rs (https://skillmd.com/u/rewrite-rs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/rewrite-rs/port-to-rust

---


# Port to Rust

## A port is a behaviour-preserving move, and nothing else

The one rule this skill exists to enforce: during a port, behaviour changes
and code improvements are separate commits, separate reviews, and preferably
separate weeks. A port that also fixes bugs cannot be validated — every
difference is ambiguous between the fix and the regression, and the
differential harness reports both identically. Improvements are written down
as a follow-up list and shipped after parity is proven.

## Start from the parity contract, not from the code

Before any Rust is written, the contract answers four questions: what the
unit of parity is (a CLI invocation, a library function, an HTTP response, a
file artifact); which differences are acceptable (log wording, timing, map
iteration order, float formatting); which are not (any observable output a
caller can branch on); and how parity is measured — the full form in
`PARITY-CONTRACT.md`. A port without a written contract does not have a
definition of done, so it finishes when someone gets tired.

## Five phases, in order

| Phase | Ends when |
|---|---|
| 1. Inventory and seam | The boundary the Rust will live behind is named, and the call sites crossing it are counted |
| 2. Characterize | The existing behaviour is captured as executable tests — against the source implementation, not the intended one |
| 3. Port leaf-first | The lowest-dependency module is in Rust, called through the seam, with both implementations still present |
| 4. Run both | Every input reaching the seam goes through both implementations and the outputs are compared |
| 5. Cut over and delete | Traffic goes to Rust only, and the source implementation is deleted rather than left as a dead fallback |

Phase 2 characterizes the behaviour that exists, bugs included — a test that
asserts the intended behaviour is a bug report disguised as a test. Phase 5
is not optional: a source implementation left in place stops being maintained
and starts being a lie about what runs; the depth on each phase is in
`PHASES.md`.

## Decide the end state before the seam

Three end states; the choice governs everything downstream — the seam, the
contract, the verification, whether bindings are deliverable or scaffold:

| End state | What ships | Bindings |
|---|---|---|
| A. Replacement | A standalone Rust binary or crate; the source implementation is deleted | None, ever. The seam is a process, CLI, or network boundary |
| B. Rust core with a language binding as the product | A Rust engine plus a binding layer that stays — the package the existing consumers import is the deliverable | Permanent. The binding surface is public API: it gets semver, error-type mapping, packaging per platform, and its own docs |
| C. Bindings as scaffold | A standalone Rust binary or crate, reached through a binding layer that is deleted at cut-over | Temporary. Built to be thrown away, and phase 5 is not done until it is gone |

B and C are not the same work, and conflating them is the most expensive
mistake here. Scaffold bindings may be crude because they die; product
bindings are a public surface that outlives the port. Ask which one is
wanted — a user porting a CLI to Rust usually wants A, and handing them a
native extension module answers a question nobody asked. The answer goes in
the parity contract, because it changes what the unit of parity is: for A
the unit is an invocation or a request; for B it is the binding signature
the existing callers already call.

## Pick the incremental strategy from the seam, not from taste

| Strategy | Reach for it when |
|---|---|
| In-place FFI — Rust compiled into the existing build and called across a language boundary | The source language has a cheap, stable FFI story and the seam is a function call |
| Strangler at a process boundary — the Rust binary takes traffic for one route, endpoint, or subcommand at a time | The seam is already a process, socket, or CLI boundary; the languages do not interoperate cheaply |
| Whole-unit replacement | The unit is small enough to port and validate in one sitting — a single CLI tool, one library with a narrow surface |

The FFI mechanics — which crate, which build integration, which header
generator — sit in the per-language skills.

## Parity is proven, not asserted

The evidence is both implementations running over the same inputs and
agreeing. Building that harness is `/rust-testing`; this skill decides what
it has to cover — every input class the contract names, including the error
paths, which are the ones a port silently narrows — and the harness runs in
CI from phase 3 onward, not once at the end, because a difference found six
modules later cannot be attributed.

## What is deliberately not ported

Dead code, workarounds for limitations the source language had and Rust does
not, and abstraction layers that existed to work around the source type
system — each a judgment call written down in the contract, not made
silently mid-file. The inverse error is worse and more common: porting the
abstraction rather than the behaviour, so a five-layer class hierarchy
becomes five Rust traits nobody needs.

## Where the target-side judgment goes

This skill never decides what the Rust should look like. Expression shape is
`/idiomatic-rust`; a `clone` added to satisfy the borrow checker mid-port is
`/ownership-not-clone`; what a fallible path returns is `/rust-errors`;
whether a state should be constructible at all is `/type-driven-design`; the
public surface of the new crate is `/rust-api-design`; async and cancellation
are `/async-rust`; any `unsafe` in an FFI shim is `/unsafe-rust`; the review
pass over a ported diff is `/rust-code-review`. The deferral matters most
here: mid-port is exactly when an agent reaches for `clone` and `unwrap` to
make a translation compile, and those two are the signature of a port that
will read like its source forever. Separate the domain aspects from the
language aspects before writing the Rust; a construct that exists only for
a source-language limitation is not ported; structural similarity to the
source is a signal to re-examine — the entry is in `ANTIPATTERNS.md`.

## Anti-patterns

The top three: transliteration, big-bang rewrite, and improving while
porting; the full list, with the tell for each, is in `ANTIPATTERNS.md`.

## Verification

```bash
cargo test --all-features        # includes the differential and characterization tests
cargo clippy --all-targets --all-features   # add -- -D warnings only if the target repo has no lint config
cargo fmt --check
```

Plus the port-specific check the compiler cannot make: run the differential
harness over the recorded corpus and report the count of inputs compared and
the count of differences, not a pass/fail. Zero differences over nine inputs
is not parity, and a report that hides the denominator hides that.

