# 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
| 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:
```toml
[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:
```rust
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.
| 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](https://github.com/b4rti/mono-rs) project, particularly the public API design. Shoot out to Bartosz for paving the way!
## License
GPL-3.0-only. See [LICENSE](LICENSE).