This crate is intentionally layered so most users can stay in cache/object/wrapper APIs while contributors can reason about the lower-level runtime boundary.
This page explains how the crate is organized. Use generated rustdoc for detailed API information on the public modules, structs, and methods referenced here.
init
-> runtime symbol loading
-> thread attachment
-> assembly cache load
-> class hydration
-> user callbacks
api
-> raw IL2CPP bindings
-> cache helpers
-> method invocation
-> thread management
-> dump helpers
-> small Unity wrappers
structs
-> metadata wrappers (Assembly, Class, Method, Field, Property, Type)
-> object wrappers (Object, GameObject, Transform, MonoBehaviour, ...)
-> collection and math helpers
memory
-> symbol resolution
-> image base lookup
-> raw memory read/write
init is the front door for the crate.
During initialization the crate:
- resolves exported IL2CPP functions
- loads the FFI function table
- attaches the worker thread to the IL2CPP VM
- enumerates assemblies into a global cache
- hydrates classes, methods, fields, and properties
- runs queued callbacks
If symbol resolution or cache initialization fails, the state resets so initialization can be attempted again.
The cache exists to make repeated metadata lookup cheap and predictable.
- assemblies are stored as
Arc<Assembly> - classes are stored in a global lookup map
- methods are indexed for fast reuse
- class hydration is guarded so it only runs once per successful initialization cycle
For most users, api::cache is the only cache surface they should touch directly.
Invocation is split across two layers:
api::invoke_methodis the raw pointer-level helperMethod::call<T>is the normal user-facing path
Method::call<T> validates the argument count, handles instance binding, differentiates reference vs value-type returns, and converts managed exceptions into Err(String).
IL2CPP is thread-sensitive. The crate exposes api::Thread as the explicit mechanism for attaching and detaching threads outside the initialization path.
This is a design choice, not just a convenience wrapper. The thread boundary is one of the main correctness constraints in the entire crate.
Wrappers under api::wrappers and structs::components exist to make common Unity tasks less repetitive:
ApplicationandTimeexpose familiar Unity conceptsGameObject,Transform,MonoBehaviour, and related types layer on top ofObjectandMethod
These wrappers should stay thin. If a behavior belongs to core runtime or metadata semantics, it should live in the lower layer first.
When you need exact signatures or item-level semantics for any layer described above, consult rustdoc rather than this architecture overview.