il2cpp_bridge_rs/init.rs
1//! Initialization of the IL2CPP runtime and metadata cache.
2//!
3//! [`init`] is the front door to the crate. It resolves IL2CPP exports, loads
4//! the function table, attaches the worker thread to the runtime, initializes
5//! the cache, hydrates metadata, and then runs queued callbacks.
6use crate::api::{self, cache, Thread};
7use crate::memory::symbol::resolve_symbol;
8use std::ffi::c_void;
9use std::sync::{Mutex, OnceLock};
10use std::thread;
11use std::time::Duration;
12
13#[cfg(dev_release)]
14use crate::logger;
15
16/// The target image name, set once during [`init()`](super::init).
17pub(crate) static TARGET_IMAGE_NAME: OnceLock<String> = OnceLock::new();
18
19type Callback = Box<dyn FnOnce() + Send + 'static>;
20
21enum State {
22 /// No initialization has started yet.
23 Idle,
24 /// Background thread is running; callbacks queued here fire when it finishes.
25 Running(Vec<Callback>),
26 /// Initialization completed successfully; new callers fire immediately.
27 Done,
28}
29
30static STATE: Mutex<State> = Mutex::new(State::Idle);
31const CACHE_INIT_MAX_ATTEMPTS: u8 = 5;
32const CACHE_INIT_RETRY_DELAY: Duration = Duration::from_secs(3);
33
34/// Initializes IL2CPP symbol loading and cache hydration.
35///
36/// This function must be called before using cache-backed helpers such as
37/// [`crate::api::cache::csharp`] or class/method lookups that depend on the
38/// hydrated metadata cache.
39///
40/// Behavior:
41///
42/// - First call: spawns the initialization worker and queues `on_complete`.
43/// - Calls while initialization is running: queue additional callbacks.
44/// - Calls after successful initialization: execute `on_complete` immediately
45/// on a newly spawned thread.
46/// - On failure: the internal state resets to idle so initialization can be
47/// attempted again.
48///
49/// The `target_image` should be the loaded module used to compute image base
50/// addresses and method RVA/VA information. Common values include
51/// `UnityFramework` on iOS and `GameAssembly` on many desktop Unity builds.
52///
53/// # Example
54///
55/// ```no_run
56/// use il2cpp_bridge_rs::{api, init};
57///
58/// init("GameAssembly", || {
59/// let asm = api::cache::csharp();
60/// println!("Ready: {}", asm.name);
61/// });
62/// ```
63///
64/// # Parameters
65///
66/// - `target_image`: name of the loaded image backing RVA/VA calculations
67/// - `on_complete`: callback executed after a successful initialization pass
68pub fn init<F>(target_image: &str, on_complete: F)
69where
70 F: FnOnce() + Send + 'static,
71{
72 TARGET_IMAGE_NAME
73 .set(target_image.to_owned())
74 .unwrap_or_else(|_| {
75 #[cfg(dev_release)]
76 logger::info("TARGET_IMAGE_NAME already set, ignoring new value.");
77 });
78 let mut guard = STATE.lock().unwrap();
79
80 match &mut *guard {
81 State::Done => {
82 drop(guard);
83 std::thread::spawn(on_complete);
84 }
85 State::Running(callbacks) => {
86 callbacks.push(Box::new(on_complete));
87 }
88 State::Idle => {
89 *guard = State::Running(vec![Box::new(on_complete)]);
90 drop(guard);
91
92 std::thread::spawn(move || {
93 if let Err(missing) = api::load(|symbol| match resolve_symbol(symbol) {
94 Ok(addr) => addr as *mut c_void,
95 Err(_e) => {
96 #[cfg(dev_release)]
97 logger::error(&format!("{}", _e));
98 std::ptr::null_mut()
99 }
100 }) {
101 #[cfg(dev_release)]
102 logger::error(&format!(
103 "Failed to load IL2CPP API symbols: {}",
104 missing.join(", ")
105 ));
106
107 let mut guard = STATE.lock().unwrap();
108 *guard = State::Idle;
109 return;
110 }
111
112 #[cfg(dev_release)]
113 logger::info("IL2CPP API loaded, waiting for runtime...");
114
115 #[cfg(dev_release)]
116 logger::info("IL2CPP runtime ready, installing early hook...");
117
118 let _thread = Thread::attach(true);
119
120 let mut cache_ready = false;
121 for attempt in 1..=CACHE_INIT_MAX_ATTEMPTS {
122 if cache::init() {
123 cache_ready = true;
124 break;
125 }
126
127 if attempt < CACHE_INIT_MAX_ATTEMPTS {
128 #[cfg(dev_release)]
129 logger::info(&format!(
130 "Cache initialization attempt {}/{} failed. Retrying in {}s...",
131 attempt,
132 CACHE_INIT_MAX_ATTEMPTS,
133 CACHE_INIT_RETRY_DELAY.as_secs()
134 ));
135 thread::sleep(CACHE_INIT_RETRY_DELAY);
136 }
137 }
138
139 if cache_ready {
140 #[cfg(dev_release)]
141 logger::info("Cache ready, starting hooks...");
142
143 let callbacks = {
144 let mut guard = STATE.lock().unwrap();
145 let old = std::mem::replace(&mut *guard, State::Done);
146 match old {
147 State::Running(cbs) => cbs,
148 _ => vec![],
149 }
150 };
151
152 cache::ensure_hydrated();
153
154 std::thread::spawn(move || {
155 for cb in callbacks {
156 cb();
157 }
158 });
159 } else {
160 #[cfg(dev_release)]
161 logger::error("Cache initialization failed after all retry attempts.");
162
163 let mut guard = STATE.lock().unwrap();
164 *guard = State::Idle;
165 }
166 });
167 }
168 }
169}