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 MonoImageOpenStatus, MonoMethod, MonoObject, MonoString, MonoThread, MonoType, MonoVTable,
78 TypeKind, Value,
79};
80
81/// Commonly used types, re-exported as a single glob import.
82///
83/// ```rust,no_run
84/// use mono_rt::prelude::*;
85/// ```
86pub mod prelude {
87 pub use crate::{
88 MonoArray, MonoAssembly, MonoClass, MonoClassField, MonoDomain, MonoError, MonoImage,
89 MonoImageOpenStatus, MonoMethod, MonoObject, MonoString, MonoThread, MonoThreadGuard,
90 MonoType, MonoVTable, Result, TypeKind, Value,
91 };
92}
93
94static BINDINGS: OnceLock<MonoBindings> = OnceLock::new();
95
96/// Resolves the Mono API by locating exports in the named module.
97///
98/// The module must already be loaded in the current process — this function calls
99/// `GetModuleHandleW`, not `LoadLibraryW`. Call this once, as early as possible in your
100/// injected code, before any thread attaches or any handle is created.
101///
102/// # Errors
103///
104/// - [`MonoError::DllNotFound`] if no module with `module_name` is currently loaded.
105/// - [`MonoError::FnNotFound`] if a required export is missing from the module (unexpected for
106/// standard Mono builds).
107/// - [`MonoError::AlreadyInitialized`] if `init` has already been called successfully.
108pub fn init(module_name: &str) -> Result<()> {
109 let wide: Vec<u16> = module_name
110 .encode_utf16()
111 .chain(std::iter::once(0))
112 .collect();
113 let module = unsafe { GetModuleHandleW(PCWSTR::from_raw(wide.as_ptr())) }
114 .map_err(|_| MonoError::DllNotFound(module_name.to_owned()))?;
115
116 let exports = MonoBindings::new(module)?;
117
118 BINDINGS
119 .set(exports)
120 .map_err(|_| MonoError::AlreadyInitialized)?;
121 Ok(())
122}
123
124/// Returns the resolved Mono API, or [`MonoError::Uninitialized`] if [`init`] has not been
125/// called.
126///
127/// This is an internal function called by every Mono operation. It is not exposed publicly so
128/// that callers cannot bypass the thread-attachment requirement.
129pub(crate) fn api() -> Result<&'static MonoBindings> {
130 BINDINGS.get().ok_or(MonoError::Uninitialized)
131}
132
133/// Reads a field of type `T` from a Mono object at the given byte offset.
134///
135/// Use [`MonoClassField::offset`](crate::MonoClassField::offset) to obtain the correct offset for
136/// a field. The read uses `read_unaligned` because Mono's field layout does not guarantee that
137/// fields are aligned to their natural boundary.
138///
139/// # Safety
140///
141/// - `obj` must be a valid, non-null `MonoObject*` whose class declares a field of type `T` at
142/// `offset`.
143/// - The current thread must be attached to the Mono runtime via [`MonoThreadGuard`] — the GC
144/// must not relocate `obj` while this function is executing.
145/// - `T` must have the same size and representation as the Mono field type (e.g. `f32` for a
146/// `System.Single` field).
147#[must_use]
148pub unsafe fn read_field<T: Copy>(obj: *mut c_void, offset: u32) -> T {
149 unsafe {
150 obj.cast::<u8>()
151 .add(offset as usize)
152 .cast::<T>()
153 .read_unaligned()
154 }
155}
156
157/// Writes a value of type `T` into a field of a Mono object at the given byte offset.
158///
159/// The mirror of [`read_field`]. Use [`MonoClassField::offset`](crate::MonoClassField::offset) to
160/// obtain the correct offset.
161///
162/// # Safety
163///
164/// Same requirements as [`read_field`]. Additionally, writing a reference-type field (e.g. a
165/// field whose [`TypeKind`] is [`Class`](TypeKind::Class) or [`Object`](TypeKind::Object))
166/// bypasses the GC write barrier and will cause memory corruption if the GC uses a generational
167/// or incremental collection scheme. For reference fields, prefer invoking a managed setter via
168/// [`MonoMethod::invoke`](crate::MonoMethod::invoke).
169pub unsafe fn write_field<T: Copy>(obj: *mut c_void, offset: u32, value: T) {
170 unsafe {
171 obj.cast::<u8>()
172 .add(offset as usize)
173 .cast::<T>()
174 .write_unaligned(value);
175 }
176}