Vibecoded Rust bindings for the Dynarmic C++ library in the form of Unicorn's API
  • Rust 78.3%
  • C++ 19.5%
  • Nix 1.6%
  • CMake 0.6%
Find a file
2026-08-09 01:12:51 +01:00
dynarm dynarm: jit-owned tick budget with safe timing defaults 2026-08-08 22:46:27 +01:00
dynarm-fuzz dynarm-fuzz: differential harness vs unicorn + full CI matrix 2026-08-08 06:12:55 +01:00
dynarm-sys fix up warnings 2026-08-09 00:52:38 +01:00
.gitignore Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
.gitmodules Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
Cargo.lock Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
Cargo.toml Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
deny.toml Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
flake.lock Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
flake.nix flake: fix buildRustPackage phase hijack by cmake/ninja hooks 2026-08-08 06:24:23 +01:00
LICENSE Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
PLAN.md Scaffold workspace: vendored submodules, lint policy, flake, CI skeleton 2026-08-08 05:13:17 +01:00
README.md dynarm: jit-owned tick budget with safe timing defaults 2026-08-08 22:46:27 +01:00
RELEASING.md dynarm-fuzz: differential harness vs unicorn + full CI matrix 2026-08-08 06:12:55 +01:00

dynarm

Safe, zero-overhead Rust bindings for Dynarmic, the C++ ARM (AArch32 + AArch64) dynamic recompiler.

  • Zero unsafe on the happy path. Implement a safe Environment trait, build a Jit, call run(). The only unsafe in the public API is quarantined on expert raw-memory knobs (page tables, fastmem, shared TPIDR storage), each with a full # Safety contract.
  • Panic-proof callbacks. Dynarmic's JIT frames cannot be unwound through; a panicking callback is caught at the boundary, execution halts, and the panic resumes from run() with the jit still usable.
  • Thread-safety by construction. run(&mut self) makes state-access-during-execution unrepresentable; HaltHandle (Clone + Send + Sync) stops a running jit from any thread and becomes inert — never dangling — after drop; the ExclusiveMonitor is Arc-only so it provably outlives every jit pointing into it.
  • C++-parity performance. Trampolines are monomorphized over your environment type; the binding adds one indirect call and one flag load per callback, and nothing at all on page-table/fastmem paths (measured: ≈0.35 ns per callback-path guest load on x86_64).
  • Builds offline, everywhere. Vendored dynarmic + trimmed Boost headers; no bindgen/libclang, no pkg-config, no system Boost, no network. CMake 3.15 through 4.x (FindBoost removal handled by a config-mode shim).

Example

use dynarm::a64::{Environment, Exception, JitBuilder, XReg};
use dynarm::{CallbackHandle, HaltReason};

/// 64 KiB of flat guest memory holding code and data.
struct FlatMemory {
    mem: Vec<u8>,
    ticks: u64,
}

impl Environment for FlatMemory {
    fn memory_read_8(&mut self, _: CallbackHandle<'_>, vaddr: u64) -> u8 {
        self.mem.get(vaddr as usize).copied().unwrap_or(0)
    }
    fn memory_write_8(&mut self, _: CallbackHandle<'_>, vaddr: u64, value: u8) {
        if let Some(slot) = self.mem.get_mut(vaddr as usize) {
            *slot = value;
        }
    }
    fn call_svc(&mut self, jit: CallbackHandle<'_>, _swi: u32) {
        jit.halt(HaltReason::USER_DEFINED_1); // dispatch after run() returns
    }
    fn add_ticks(&mut self, _: CallbackHandle<'_>, ticks: u64) {
        self.ticks = self.ticks.saturating_sub(ticks);
    }
    fn get_ticks_remaining(&mut self, _: CallbackHandle<'_>) -> u64 {
        self.ticks
    }
    # fn memory_read_16(&mut self, h: CallbackHandle<'_>, v: u64) -> u16 { u16::from_le_bytes([self.memory_read_8(h, v), self.memory_read_8(h, v + 1)]) }
    # fn memory_read_32(&mut self, h: CallbackHandle<'_>, v: u64) -> u32 { u32::from(self.memory_read_16(h, v)) | u32::from(self.memory_read_16(h, v + 2)) << 16 }
    # fn memory_read_64(&mut self, h: CallbackHandle<'_>, v: u64) -> u64 { u64::from(self.memory_read_32(h, v)) | u64::from(self.memory_read_32(h, v + 4)) << 32 }
    # fn memory_read_128(&mut self, h: CallbackHandle<'_>, v: u64) -> u128 { u128::from(self.memory_read_64(h, v)) | u128::from(self.memory_read_64(h, v + 8)) << 64 }
    # fn memory_write_16(&mut self, h: CallbackHandle<'_>, v: u64, x: u16) { for (i, b) in x.to_le_bytes().iter().enumerate() { self.memory_write_8(h, v + i as u64, *b); } }
    # fn memory_write_32(&mut self, h: CallbackHandle<'_>, v: u64, x: u32) { for (i, b) in x.to_le_bytes().iter().enumerate() { self.memory_write_8(h, v + i as u64, *b); } }
    # fn memory_write_64(&mut self, h: CallbackHandle<'_>, v: u64, x: u64) { for (i, b) in x.to_le_bytes().iter().enumerate() { self.memory_write_8(h, v + i as u64, *b); } }
    # fn memory_write_128(&mut self, h: CallbackHandle<'_>, v: u64, x: u128) { for (i, b) in x.to_le_bytes().iter().enumerate() { self.memory_write_8(h, v + i as u64, *b); } }
    # fn exception_raised(&mut self, _: CallbackHandle<'_>, pc: u64, e: Exception) { panic!("{e:?} at {pc:#x}") }
    # fn get_cntpct(&mut self, _: CallbackHandle<'_>) -> u64 { 0 }
    // ... remaining width variants elided (16/32/64/128-bit reads/writes) ...
}

let mut env = FlatMemory { mem: vec![0; 0x10000], ticks: 1000 };
// mov x0, #42 ; svc #0
env.mem[..8].copy_from_slice(&[0x40, 0x05, 0x80, 0xd2, 0x01, 0x00, 0x00, 0xd4]);

let mut jit = JitBuilder::new().build(env).expect("valid config");
jit.set_pc(0);
let reason = jit.run();

assert!(reason.contains(HaltReason::USER_DEFINED_1));
assert_eq!(jit.x(XReg::new(0).unwrap()), 42);

Workspace

crate contents license
dynarm safe API: Environment, Jit, JitBuilder, ExclusiveMonitor, HaltHandle 0BSD
dynarm-sys vendored dynarmic C++ source, C shim, hand-written FFI 0BSD AND MIT AND BSD-3-Clause AND BSL-1.0

Vendored dynarmic pin

what value
repository azahar-emu/dynarmic
commit e77b1ba0b7da7cbe93021b01a663acfe7c4dd516
upstream version 6.7.0
ext-boost commit 6a85c3100499e886e11c87a5c2109eedacea0a61

Building

Requires a Rust toolchain (MSRV 1.85), CMake ≥ 3.15 (4.x supported), and a C++20 compiler. No Boost, pkg-config, libclang, or network access.

From a git checkout, fetch the vendored sources first (the crates.io package ships them, so cargo add dynarm users skip this):

$ git submodule update --init --recursive

On Nix, note the flake-submodule wart: nix build '.?submodules=1'. The flake also ships #dynarm-system, which builds the crate against a standalone #dynarmic-cpp derivation through the injected-mode escape hatch below.

Build-time environment overrides

variable effect
DYNARM_LIB_DIR Skip the CMake build; link archives from this directory (highest precedence)
DYNARM_INCLUDE_DIR Installed dynarmic headers for shim compilation (with DYNARM_LIB_DIR)
DYNARM_STATIC 0/1 — dynamic/static linking in injected mode (default static)
DYNARM_EXTRA_LIBS Extra -l names to emit (space-separated)
DYNARM_BOOST_INCLUDE_DIR Substitute a Boost header tree for the vendored ext-boost
DYNARM_CMAKE_PROFILE CMake build profile (default Release, independent of the cargo profile)
DYNARM_CMAKE_ARGS Extra -D... args passed through to CMake
CMAKE_GENERATOR Honored as usual; Ninja auto-selected when available
CXXSTDLIB Override the C++ runtime library linked

There is deliberately no pkg-config probing and no find_package system-dynarmic mode: no distribution ships a compatible dynarmic, and half-working probes cost more than they save.

Feature flags

feature default effect
a64 AArch64 frontend
a32 AArch32 frontend
vendored (dynarm-sys) build the vendored C++ via CMake
system (dynarm-sys) require DYNARM_LIB_DIR (hard error otherwise)

Caveats worth knowing

  • Constructing the first jit installs process-wide SIGSEGV/SIGBUS handlers on POSIX (chaining previous handlers) — dynarmic's fastmem machinery.
  • Timing has safe defaults: a jit-owned tick budget (Jit::set_tick_budget, starts effectively unlimited) backs add_ticks/get_ticks_remaining unless you override them — so simple environments need no timing code at all. If you do override, return a bounded scheduler slice (the yuzu/Citra convention): dynarmic compares the value signed, so budgets ≥ i64::MAX read as exhausted and run() returns immediately (regression-tested; the built-in default clamps for you). Never need timing? enable_cycle_counting(false) skips the callbacks entirely.
  • UserDefined8 is reserved for the crate's panic protocol and cannot be set or observed through the public HaltReason.