mono-rt 0.1.0

Dynamic bindings to the Mono runtime for process injection into Unity games and Mono-hosted applications on Windows
docs.rs failed to build mono-rt-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: mono-rt-0.2.0

mono-rt

Dynamic bindings to the Mono runtime, designed for process injection into Unity games and other Mono-hosted applications on Windows.

Rather than starting a new JIT domain, this crate attaches to a Mono runtime that is already running in the target process. All exports are resolved at runtime via GetModuleHandleW and GetProcAddress, so no import library or compile-time link to mono.dll is needed.

Platform support

Platform Status
Windows Supported
Linux Planned but contributions welcome!

The core binding layer is platform-agnostic; only init() uses a Windows-specific API to locate the loaded module. A Linux port would replace that with dlopen/dlsym and is a self-contained change.

Getting started

Add the dependency:

[dependencies]
mono-rt = "0.1.0"
# or, for the latest commit:
mono-rt = { git = "https://github.com/theo-abel/mono-rt" }

Then in your injected code:

use mono_rt::prelude::*;

// 1. Resolve exports from the already-loaded Mono DLL.
//    Common names: "mono.dll" (Unity <= 2017), "mono-2.0-bdwgc.dll" (Unity 2018+)
mono_rt::init("mono-2.0-bdwgc.dll")?;

// 2. Attach the current thread. Keep the guard alive for the duration of all Mono work.
let _guard = unsafe { MonoThreadGuard::attach()? };

// 3. Navigate the assembly graph.
let image = MonoImage::find("Assembly-CSharp")?.expect("assembly not loaded");
let class = image.class_from_name("", "PlayerController")?.expect("class not found");

// 4. Look up a method and invoke it.
let method = class.method("Respawn", None)?.expect("method not found");
let domain = MonoDomain::root()?.expect("no root domain");
let obj = class.new_object(domain)?.expect("allocation failed");
let result = unsafe { method.invoke_with(obj.as_ptr(), &[Value::Bool(true)])? };

// 5. Read a field value directly from a live instance.
let hp_field = class.field("m_health")?.expect("field not found");
let offset = hp_field.offset()?;
// let hp: f32 = unsafe { mono_rt::read_field(obj_ptr, offset) };

Threading model

Mono requires every thread that calls into the runtime to be registered with the garbage collector. MonoThreadGuard::attach() handles this registration, and the guard automatically deregisters the thread when dropped.

All handle types (MonoClass, MonoObject, MonoMethod, ...) are !Send + !Sync, they are bound to the thread on which they were obtained. The compiler prevents them from crossing thread boundaries silently. If you do need to transfer a handle to another thread where you can guarantee both threads are attached, you can opt in with an explicit unsafe impl Send.

Runtime API coverage

The table below reflects the current state. The goal is to cover the APIs most relevant to game modding and runtime inspection; lower-level or rarely-needed functions can be added as needed.

Area Covered Not yet covered
Initialization init, thread attach/detach, root domain domain unload, domain creation
Assembly open by path, enumerate loaded, get image get by name, get list from domain
Image find by name, class lookup enumerate all classes
Class field/method by name, field/method enumeration, type descriptor, vtable, object allocation parent class, interfaces, properties, events, nested types
Field offset, name, type, static read instance write, static write
Method invoke (raw + typed Value), name full signature, parameter types, return type, flags
Object unbox get class, get type, clone
String create from &str, convert to String
Array length, element address create, set element
Type kind (TypeKind enum), boxing is_valuetype, get_class
GC pinned handles (gc_handle_new/free)
Exceptions surface as MonoError::ManagedException inspect message, stack trace

Safety

unsafe appears in two places in the public API:

  • MonoThreadGuard::attach() : you assert that the returned guard will be dropped on the same thread that called attach.
  • MonoMethod::invoke / invoke_with : you assert that the object and argument types match the method's actual signature, which Mono does not validate at the call site.
  • read_field / write_field : you assert that the offset and type T are correct for the target field, and that the object pointer is live.

Everything else - null checks, CString conversion, error propagation - is handled by the library.

Credits

Some inspiration was drawn from the mono-rs project, particularly the public API design. Shoot out to Bartosz for paving the way!

License

GPL-3.0-only. See LICENSE.