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