Thank you for your interest in contributing! This document covers the development workflow, code conventions, and how to add new features.
- Getting Started
- Project Structure
- Development Workflow
- Rust Conventions
- Swift Conventions
- Adding a Decompiler Pattern
- Adding a New FFI Type
- Testing
- Submitting a Pull Request
- Fork and clone the repository.
- Install prerequisites: Rust, Xcode 16+,
xcodegen, and a C compiler (ships with Xcode CLT). - Run
make allto build everything and openFerrite.xcodeprojin Xcode.
brew install xcodegen
make all
open Ferrite.xcodeprojFerrite/
├── src/
│ ├── rust/
│ │ ├── ferrite-pe/ # PE parser and C# decompiler
│ │ └── ferrite-ffi/ # UniFFI boundary (staticlib)
│ └── swift/
│ ├── Ferrite/ # SwiftUI app
│ └── FerriteFFI/ # C module target (generated header)
├── scripts/
│ └── build-rust.sh # Compiles Rust + generates Swift bindings
├── libs/ # Built libferrite_ffi.a (gitignored output)
├── project.yml # XcodeGen spec
├── Makefile
└── CLAUDE.md # AI assistant context
make all # recompile + regenerate bindings + regen Xcode project
# Then Cmd+B in Xcodecd src/rust
cargo test # run unit tests
cargo clippy -- -D warnings
cargo fmtEdit Swift files and press Cmd+B in Xcode — no Rust rebuild needed unless the FFI changed.
- Run
cargo fmtbefore committing. CI enforces formatting with--check. cargo clippy -- -D warningsmust pass with zero warnings.- All public functions in
ferrite-peshould have doc comments. - No
unwrap()outside of tests; use?or explicit error mapping. - Error types live in
assembly/mod.rs(PeError) andferrite-ffi/src/types.rs(FerriteError).
- All UI state lives in
@Observableservices (DecompilerService,ProjectService,SearchService). - Views must remain side-effect-free; trigger mutations via service methods.
- Use
@MainActorfor all service methods that touch UI state. - Prefer
Task.detachedfor FFI calls; dispatch results back withawait MainActor.run. - Do not import
ferrite_ffidirectly in views — go throughDecompilerService.
Decompiler patterns are in src/rust/ferrite-pe/src/decompiler/patterns/. Each file implements a specific C# pattern recogniser (e.g. loops_foreach.rs, null_coalescing.rs).
- Create
src/rust/ferrite-pe/src/decompiler/patterns/my_pattern.rs. - Implement a function that takes a slice of
AstNodeand returns a replacement if the pattern matches. - Register it in
patterns/mod.rs. - Add a test in
patterns/tests.rsusing a real or synthetic IL byte slice.
Patterns run in order; put more specific patterns before more general ones.
- Add the Rust type (with
#[derive(uniffi::Record)]or#[derive(uniffi::Enum)]) insrc/rust/ferrite-ffi/src/types.rs. - Add conversion logic in
src/rust/ferrite-ffi/src/convert.rs. - Expose it through a method on
DecompilerSessioninsrc/rust/ferrite-ffi/src/lib.rs. - Run
make all— the Swift bindings regenerate automatically. - Consume the new type in
DecompilerService.swiftor the relevant view.
Field naming: Rust snake_case fields become Swift camelCase in generated bindings automatically.
cd src/rust
cargo test # all crates
cargo test -p ferrite-pe # just the parser/decompilerTests live in:
src/rust/ferrite-pe/src/assembly/tests.rs— metadata parsingsrc/rust/ferrite-pe/src/decompiler/tests.rs— end-to-end decompilationsrc/rust/ferrite-pe/src/decompiler/patterns/tests.rs— individual patterns
There are no automated Swift tests yet. Manual testing:
- Load a real-world
.dll(e.g. from the .NET SDK or NuGet). - Verify the sidebar tree, decompiled output, and search results.
- Branch off
main:git checkout -b feat/my-feature. - Make your changes. Ensure
cargo fmt --checkandcargo clippy -- -D warningspass. - Add tests for any new Rust logic.
- Open a PR against
mainwith a clear description of what changed and why. - A CI run will build the Rust crates and run tests; it must pass before merge.
Commit style: short imperative subject line, e.g. add foreach loop pattern, fix null-coalescing precedence.