- Rust 78.3%
- C++ 19.5%
- Nix 1.6%
- CMake 0.6%
| dynarm | ||
| dynarm-fuzz | ||
| dynarm-sys | ||
| .gitignore | ||
| .gitmodules | ||
| Cargo.lock | ||
| Cargo.toml | ||
| deny.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| PLAN.md | ||
| README.md | ||
| RELEASING.md | ||
dynarm
Safe, zero-overhead Rust bindings for Dynarmic, the C++ ARM (AArch32 + AArch64) dynamic recompiler.
- Zero
unsafeon the happy path. Implement a safeEnvironmenttrait, build aJit, callrun(). The onlyunsafein the public API is quarantined on expert raw-memory knobs (page tables, fastmem, shared TPIDR storage), each with a full# Safetycontract. - 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; theExclusiveMonitorisArc-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) backsadd_ticks/get_ticks_remainingunless 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::MAXread as exhausted andrun()returns immediately (regression-tested; the built-in default clamps for you). Never need timing?enable_cycle_counting(false)skips the callbacks entirely. UserDefined8is reserved for the crate's panic protocol and cannot be set or observed through the publicHaltReason.