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::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 match api::load(|symbol| match resolve_symbol(symbol) {
91 Ok(addr) => addr as *mut c_void,
92 Err(e) => {
93 logger::error(&format!("{}", e));
94 std::ptr::null_mut()
95 }
96 }) {
97 Ok(_) => {}
98 Err(missing) => {
99 logger::error(&format!(
100 "Failed to load IL2CPP API symbols: {}",
101 missing.join(", ")
102 ));
103 }
104 }
105
106 logger::info("IL2CPP API loaded, waiting for cache initialization...");
107
108 let _thread = Thread::attach(true);
109
110 let mut cache_ready = false;
111 let mut asm_count = 0usize;
112 let mut cls_count = 0usize;
113
114 for attempt in 1..=CACHE_INIT_MAX_ATTEMPTS {
115 cache::clear();
116
117 match unsafe { cache::load_all_assemblies() } {
118 Ok(a) => {
119 asm_count = a;
120 match cache::hydrate_all_classes() {
121 Ok(c) => {
122 cls_count = c;
123 logger::info(&format!(
124 "Cache initialized: {} assemblies loaded, {} classes hydrated",
125 a, c
126 ));
127 cache_ready = true;
128 break;
129 }
130 Err(e) => {
131 logger::error(&format!(
132 "Cache init failed during class hydration: {}",
133 e
134 ));
135 }
136 }
137 }
138 Err(e) => {
139 logger::error(&format!("Cache init failed: {}", e));
140 }
141 }
142
143 if attempt < CACHE_INIT_MAX_ATTEMPTS {
144 logger::info(&format!(
145 "Cache initialization attempt {}/{} failed. Retrying in {}s...",
146 attempt,
147 CACHE_INIT_MAX_ATTEMPTS,
148 CACHE_INIT_RETRY_DELAY.as_secs()
149 ));
150 thread::sleep(CACHE_INIT_RETRY_DELAY);
151 }
152 }
153
154 let _ = (asm_count, cls_count);
155
156 if cache_ready {
157 logger::info("Cache ready, starting callback...");
158
159 let callbacks = {
160 let mut guard = STATE.lock().unwrap();
161 let old = std::mem::replace(&mut *guard, State::Done);
162 match old {
163 State::Running(cbs) => cbs,
164 _ => vec![],
165 }
166 };
167
168 std::thread::spawn(move || {
169 for cb in callbacks {
170 cb();
171 }
172 });
173 } else {
174 logger::error("Cache initialization failed after all retry attempts.");
175
176 let mut guard = STATE.lock().unwrap();
177 *guard = State::Idle;
178 }
179 });
180 }
181 }
182}