cleat 0.1.0

Android IL2CPP game modding toolkit — safe Rust bindings for IL2CPP field access, method calls, and inline hooks
Documentation
use crate::{Error, Result};
#[cfg(target_os = "android")]
use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[cfg(target_os = "android")]
use std::thread;
#[cfg(target_os = "android")]
use std::time::Duration;

/// The app's private data directory, set once at startup.
static APP_DATA: OnceLock<PathBuf> = OnceLock::new();

/// Max retries waiting for the target image to be loaded.
#[cfg(target_os = "android")]
const MAX_RETRIES: u32 = 30;
#[cfg(target_os = "android")]
const RETRY_DELAY: Duration = Duration::from_secs(2);

/// Bootstrap the cleat runtime.
///
/// Sets up android_logger, then spawns a background thread that waits
/// for `target_image` (e.g. libil2cpp.so) to load before calling
/// il2cpp_bridge_rs::init. Returns immediately so JNI_OnLoad does not
/// block the main thread (which would prevent Unity from loading its
/// native libraries).
#[cfg(target_os = "android")]
pub fn init(target_image: &str, on_ready: impl FnOnce() + Send + 'static) {
    android_logger::init_once(
        android_logger::Config::default().with_max_level(log::LevelFilter::Debug),
    );

    log::info!("cleat init: waiting for {target_image}...");

    let c_name = CString::new(target_image).unwrap();
    let image = target_image.to_owned();

    thread::spawn(move || {
        for i in 0..=MAX_RETRIES {
            let handle = unsafe { libc::dlopen(c_name.as_ptr(), libc::RTLD_NOLOAD) };
            if !handle.is_null() {
                unsafe { libc::dlclose(handle) };
                log::info!("cleat init: {image} found (attempt {i})");
                il2cpp_bridge_rs::init(&image, on_ready);
                return;
            }
            if i < MAX_RETRIES {
                thread::sleep(RETRY_DELAY);
            }
        }
        log::error!("cleat init: {image} not loaded after {MAX_RETRIES} retries");
    });
}

/// Desktop stub — init is a no-op outside Android.
#[cfg(not(target_os = "android"))]
pub fn init(_target_image: &str, _on_ready: impl FnOnce() + Send + 'static) {
    log::info!("cleat init: skipped (not on Android)");
}

/// Tell cleat where your app's private files live.
///
/// Call this once at the top of your `#[cleat::main]` function. Subsequent
/// calls are ignored.
///
/// ```ignore
/// cleat::set_app_data("/data/data/com.example.game/files");
/// ```
pub fn set_app_data(path: impl AsRef<Path>) {
    let _ = APP_DATA.set(path.as_ref().to_path_buf());
}

/// Get the app data directory that was set with [`set_app_data`].
///
/// Returns `Err(Error::NotInitialized)` if `set_app_data` hasn't been
/// called yet.
///
/// ```ignore
/// let config = cleat::app_data()?.join("ModConfig/config.json");
/// let json = std::fs::read_to_string(config)?;
/// ```
pub fn app_data() -> Result<&'static Path> {
    APP_DATA
        .get()
        .map(|p| p.as_path())
        .ok_or(Error::NotInitialized)
}