Skip to main content

mono_rt/
lib.rs

1//! Dynamic bindings to the Mono runtime for Windows.
2//!
3//! This crate attaches to a Mono runtime that is **already loaded in the current process** — it
4//! does not start a new JIT domain. The intended use case is process injection into Unity games
5//! and other Mono-hosted applications, where the Mono DLL is already mapped into memory before
6//! any code in this crate runs.
7//!
8//! # Initialization
9//!
10//! Before using any type in this crate, call [`init`] with the name of the Mono module as it
11//! appears in the host process. Common values:
12//!
13//! - `"mono.dll"` — Unity 2017 and earlier (legacy Mono)
14//! - `"mono-2.0-bdwgc.dll"` — Unity 2018+ (Boehm GC)
15//! - `"mono-2.0.dll"` — standalone Mono installations
16//!
17//! [`init`] uses `GetModuleHandleW` internally, so the DLL must already be loaded; it does not
18//! call `LoadLibrary`.
19//!
20//! # Threading model
21//!
22//! Mono requires every thread that calls into the runtime to be registered with the garbage
23//! collector. Use [`MonoThreadGuard::attach`] to register the current thread before making any
24//! Mono API calls. The guard automatically deregisters the thread when dropped.
25//!
26//! All handle types (`MonoClass`, `MonoObject`, …) are `!Send + !Sync`. They are bound to the
27//! thread on which they were obtained and cannot be moved to another thread without explicit
28//! `unsafe` code. This mirrors the per-thread attachment requirement: a handle is only valid on
29//! an attached thread, and the compiler prevents it from silently crossing that boundary.
30//!
31//! See [`MonoThreadGuard`] for the full contract.
32//!
33//! # Usage
34//!
35//! ```no_run
36//! use mono_rt::prelude::*;
37//!
38//! // resolve exports from the already-loaded Mono DLL
39//! mono_rt::init("mono-2.0-bdwgc.dll")?;
40//!
41//! // attach the current thread — keep the guard live for the duration of all Mono work
42//! let _guard = unsafe { MonoThreadGuard::attach()? };
43//!
44//! // navigate the assembly graph
45//! let image = MonoImage::find("Assembly-CSharp")?.expect("assembly not loaded");
46//! let class = image.class_from_name("", "Player")?.expect("class not found");
47//!
48//! // enumerate all fields and print their names and types
49//! for field in class.fields()? {
50//!     let name = field.name()?;
51//!     let kind = field.mono_type()?.and_then(|t| t.kind().ok());
52//!     println!("{name}: {kind:?}");
53//! }
54//!
55//! // read a field value directly from a live object
56//! let hp_field = class.field("m_health")?.expect("field not found");
57//! let offset = hp_field.offset()?;
58//! // obj_ptr is a *mut c_void obtained from a previous MonoObject::as_ptr() call
59//! // let hp: f32 = unsafe { mono_rt::read_field(obj_ptr, offset) };
60//! # Ok::<(), MonoError>(())
61//! ```
62
63mod bindings;
64mod error;
65mod guard;
66mod types;
67
68use std::{ffi::c_void, sync::OnceLock};
69
70use windows::{Win32::System::LibraryLoader::GetModuleHandleW, core::PCWSTR};
71
72use bindings::MonoBindings;
73pub use error::{MonoError, Result};
74pub use guard::MonoThreadGuard;
75pub use types::{
76    MonoArray, MonoAssembly, MonoClass, MonoClassField, MonoDomain, MonoFunc, MonoImage,
77    MonoMethod, MonoObject, MonoString, MonoThread, MonoType, MonoVTable, TypeKind, Value,
78};
79
80/// Commonly used types, re-exported as a single glob import.
81///
82/// ```rust,no_run
83/// use mono_rt::prelude::*;
84/// ```
85pub mod prelude {
86    pub use crate::{
87        MonoArray, MonoAssembly, MonoClass, MonoClassField, MonoDomain, MonoError, MonoImage,
88        MonoMethod, MonoObject, MonoString, MonoThread, MonoThreadGuard, MonoType, MonoVTable,
89        Result, TypeKind, Value,
90    };
91}
92
93static BINDINGS: OnceLock<MonoBindings> = OnceLock::new();
94
95/// Resolves the Mono API by locating exports in the named module.
96///
97/// The module must already be loaded in the current process — this function calls
98/// `GetModuleHandleW`, not `LoadLibraryW`. Call this once, as early as possible in your
99/// injected code, before any thread attaches or any handle is created.
100///
101/// # Errors
102///
103/// - [`MonoError::DllNotFound`] if no module with `module_name` is currently loaded.
104/// - [`MonoError::FnNotFound`] if a required export is missing from the module (unexpected for
105///   standard Mono builds).
106/// - [`MonoError::AlreadyInitialized`] if `init` has already been called successfully.
107pub fn init(module_name: &str) -> Result<()> {
108    let wide: Vec<u16> = module_name
109        .encode_utf16()
110        .chain(std::iter::once(0))
111        .collect();
112    let module = unsafe { GetModuleHandleW(PCWSTR::from_raw(wide.as_ptr())) }
113        .map_err(|_| MonoError::DllNotFound(module_name.to_owned()))?;
114
115    let exports = MonoBindings::new(module)?;
116
117    BINDINGS
118        .set(exports)
119        .map_err(|_| MonoError::AlreadyInitialized)?;
120    Ok(())
121}
122
123/// Returns the resolved Mono API, or [`MonoError::Uninitialized`] if [`init`] has not been
124/// called.
125///
126/// This is an internal function called by every Mono operation. It is not exposed publicly so
127/// that callers cannot bypass the thread-attachment requirement.
128pub(crate) fn api() -> Result<&'static MonoBindings> {
129    BINDINGS.get().ok_or(MonoError::Uninitialized)
130}
131
132/// Reads a field of type `T` from a Mono object at the given byte offset.
133///
134/// Use [`MonoClassField::offset`](crate::MonoClassField::offset) to obtain the correct offset for
135/// a field. The read uses `read_unaligned` because Mono's field layout does not guarantee that
136/// fields are aligned to their natural boundary.
137///
138/// # Safety
139///
140/// - `obj` must be a valid, non-null `MonoObject*` whose class declares a field of type `T` at
141///   `offset`.
142/// - The current thread must be attached to the Mono runtime via [`MonoThreadGuard`] — the GC
143///   must not relocate `obj` while this function is executing.
144/// - `T` must have the same size and representation as the Mono field type (e.g. `f32` for a
145///   `System.Single` field).
146#[must_use]
147pub unsafe fn read_field<T: Copy>(obj: *mut c_void, offset: u32) -> T {
148    unsafe {
149        obj.cast::<u8>()
150            .add(offset as usize)
151            .cast::<T>()
152            .read_unaligned()
153    }
154}
155
156/// Writes a value of type `T` into a field of a Mono object at the given byte offset.
157///
158/// The mirror of [`read_field`]. Use [`MonoClassField::offset`](crate::MonoClassField::offset) to
159/// obtain the correct offset.
160///
161/// # Safety
162///
163/// Same requirements as [`read_field`]. Additionally, writing a reference-type field (e.g. a
164/// field whose [`TypeKind`] is [`Class`](TypeKind::Class) or [`Object`](TypeKind::Object))
165/// bypasses the GC write barrier and will cause memory corruption if the GC uses a generational
166/// or incremental collection scheme. For reference fields, prefer invoking a managed setter via
167/// [`MonoMethod::invoke`](crate::MonoMethod::invoke).
168pub unsafe fn write_field<T: Copy>(obj: *mut c_void, offset: u32, value: T) {
169    unsafe {
170        obj.cast::<u8>()
171            .add(offset as usize)
172            .cast::<T>()
173            .write_unaligned(value);
174    }
175}