rusteal-core 0.2.0

Safe Rust runtime for rusteal (object refs, lifecycle, dynamic calls)
Documentation
//! Generates `ffi_dispatch.rs` from rusteal-ffi's `api_table.rs`.
//!
//! For each sub-table function, emits a thin wrapper that dispatches through
//! the API table and centralizes the `unsafe` boundary, e.g.:
//!   `pub unsafe fn core_is_valid(p0: UObjectHandle) -> bool`.

use std::env;
use std::fmt::Write as FmtWrite;
use std::fs;
use std::path::Path;

use syn::{BareFnArg, Fields, File, Item, ItemStruct, Type};

fn main() {
    // In the workspace the definition is read from rusteal-ffi and copied next
    // to this build script; a published package carries only that copy, since
    // cargo cannot reach outside the package directory.
    let workspace_path = Path::new("../rusteal-ffi/src/api_table.rs");
    let packaged_path = Path::new("api_table.rs");
    println!("cargo:rerun-if-changed={}", workspace_path.display());

    if workspace_path.exists() {
        fs::copy(workspace_path, packaged_path).unwrap_or_else(|e| {
            panic!("Failed to copy {} to {}: {e}", workspace_path.display(), packaged_path.display())
        });
    } else if !packaged_path.exists() {
        panic!(
            "Neither {} nor {} exists: nothing to generate the FFI dispatch from.",
            workspace_path.display(),
            packaged_path.display()
        );
    }

    let source = fs::read_to_string(packaged_path).expect("Failed to read api_table.rs");
    let tables = parse_api_table(&source);

    let mut output = String::new();
    writeln!(output, "// Auto-generated by rusteal-core/build.rs — do not edit").unwrap();
    writeln!(output, "// Source: rusteal-ffi/src/api_table.rs").unwrap();
    writeln!(output).unwrap();

    writeln!(output, "use rusteal_ffi::handles::*;").unwrap();
    writeln!(output, "use rusteal_ffi::error::RustealErrorCode;").unwrap();
    writeln!(output).unwrap();

    // NativePtr: semantic alias for opaque payload buffers crossing the FFI
    // boundary (alloc_params, ProcessEvent params, reify callback params).
    writeln!(output, "/// Opaque native pointer for FFI payload buffers.").unwrap();
    writeln!(output, "pub type NativePtr = *mut u8;").unwrap();
    writeln!(output).unwrap();
    writeln!(output, "pub const NATIVE_PTR_NULL: NativePtr = core::ptr::null_mut();").unwrap();
    writeln!(output).unwrap();
    writeln!(output, "#[inline(always)]").unwrap();
    writeln!(output, "pub fn native_ptr_is_null(p: NativePtr) -> bool {{ p.is_null() }}").unwrap();
    writeln!(output).unwrap();

    writeln!(output, "/// Read `size_of::<T>()` bytes from native memory at `ptr + offset`.").unwrap();
    writeln!(output, "#[inline(always)]").unwrap();
    writeln!(output, "pub unsafe fn native_mem_read<T: Copy>(ptr: NativePtr, offset: usize) -> T {{").unwrap();
    writeln!(output, "    core::ptr::read_unaligned(ptr.add(offset) as *const T)").unwrap();
    writeln!(output, "}}").unwrap();
    writeln!(output).unwrap();

    writeln!(output, "/// Write `size_of::<T>()` bytes of `value` to native memory at `ptr + offset`.").unwrap();
    writeln!(output, "#[inline(always)]").unwrap();
    writeln!(output, "pub unsafe fn native_mem_write<T: Copy>(ptr: NativePtr, offset: usize, value: T) {{").unwrap();
    writeln!(output, "    core::ptr::write_unaligned(ptr.add(offset) as *mut T, value)").unwrap();
    writeln!(output, "}}").unwrap();
    writeln!(output).unwrap();

    for table in &tables {
        for func in &table.functions {
            generate_dispatch_wrapper(&mut output, table, func);
        }
    }

    let out_dir = env::var("OUT_DIR").unwrap();
    let out_path = Path::new(&out_dir).join("ffi_dispatch.rs");
    fs::write(&out_path, &output).expect("Failed to write ffi_dispatch.rs");
}

// ---------------------------------------------------------------------------
// Dispatch wrapper generation
// ---------------------------------------------------------------------------

fn generate_dispatch_wrapper(out: &mut String, table: &SubTable, func: &ApiFn) {
    let fn_name = format!("{}_{}", table.module_name, func.name);

    let rust_params: Vec<(String, String)> = func
        .params
        .iter()
        .enumerate()
        .map(|(i, p)| (format!("p{i}"), rust_type_str(&p.ty)))
        .collect();
    let rust_ret = match &func.return_type {
        None => "()".into(),
        Some(ty) => rust_type_str(ty),
    };

    writeln!(out, "#[inline(always)]").unwrap();
    write!(out, "pub unsafe fn {fn_name}(").unwrap();
    for (i, (name, ty)) in rust_params.iter().enumerate() {
        if i > 0 {
            write!(out, ", ").unwrap();
        }
        write!(out, "{name}: {ty}").unwrap();
    }
    writeln!(out, ") -> {rust_ret} {{").unwrap();

    write!(out, "    ((*crate::api::api().{}).{})(", table.module_name, func.name).unwrap();
    for (i, (name, _ty)) in rust_params.iter().enumerate() {
        if i > 0 {
            write!(out, ", ").unwrap();
        }
        // NativePtr (*mut u8) at the wrapper boundary is passed straight to the
        // FFI as *mut u8 — no cast needed. CVoidPtr params need a cast back.
        if matches!(func.params[i].ty, ApiType::CVoidPtr { .. }) {
            write!(out, "{name} as *mut core::ffi::c_void").unwrap();
        } else {
            write!(out, "{name}").unwrap();
        }
    }
    writeln!(out, ")").unwrap();
    writeln!(out, "}}").unwrap();
    writeln!(out).unwrap();
}

fn rust_type_str(ty: &ApiType) -> String {
    match ty {
        ApiType::UObjectHandle => "UObjectHandle".into(),
        ApiType::UClassHandle => "UClassHandle".into(),
        ApiType::FPropertyHandle => "FPropertyHandle".into(),
        ApiType::UFunctionHandle => "UFunctionHandle".into(),
        ApiType::UStructHandle => "UStructHandle".into(),
        ApiType::FNameHandle => "FNameHandle".into(),
        ApiType::FWeakObjectHandle => "FWeakObjectHandle".into(),
        ApiType::RustealErrorCode => "RustealErrorCode".into(),
        ApiType::Bool => "bool".into(),
        ApiType::U8 => "u8".into(),
        ApiType::U16 => "u16".into(),
        ApiType::U32 => "u32".into(),
        ApiType::U64 => "u64".into(),
        ApiType::I32 => "i32".into(),
        ApiType::I64 => "i64".into(),
        ApiType::F32 => "f32".into(),
        ApiType::F64 => "f64".into(),
        // Opaque payload pointers (*mut c_void / *mut u8) → NativePtr.
        // The wrapper casts back to the original pointer type at the call site.
        ApiType::CVoidPtr { mutability: Mutability::Mut } => "NativePtr".into(),
        ApiType::CVoidPtr { mutability: Mutability::Const } => "*const core::ffi::c_void".into(),
        ApiType::NamedStructPtr { mutability, name } => {
            format!("{} rusteal_ffi::reify_types::{name}", mut_qual(*mutability))
        }
        ApiType::Ptr { mutability, pointee } => {
            if matches!(**pointee, ApiType::U8) && *mutability == Mutability::Mut {
                return "NativePtr".into();
            }
            format!("{} {}", mut_qual(*mutability), rust_type_str(pointee))
        }
    }
}

fn mut_qual(m: Mutability) -> &'static str {
    match m {
        Mutability::Const => "*const",
        Mutability::Mut => "*mut",
    }
}

// ---------------------------------------------------------------------------
// Minimal inline parser for api_table.rs (was previously rusteal-api-parse crate)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
struct SubTable {
    module_name: String,
    functions: Vec<ApiFn>,
}

#[derive(Debug, Clone)]
struct ApiFn {
    name: String,
    params: Vec<ApiParam>,
    return_type: Option<ApiType>,
}

#[derive(Debug, Clone)]
struct ApiParam {
    ty: ApiType,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ApiType {
    UObjectHandle,
    UClassHandle,
    FPropertyHandle,
    UFunctionHandle,
    UStructHandle,
    FNameHandle,
    FWeakObjectHandle,
    RustealErrorCode,
    Bool,
    U8,
    U16,
    U32,
    U64,
    I32,
    I64,
    F32,
    F64,
    Ptr { mutability: Mutability, pointee: Box<ApiType> },
    NamedStructPtr { mutability: Mutability, name: String },
    CVoidPtr { mutability: Mutability },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mutability {
    Const,
    Mut,
}

fn parse_api_table(source: &str) -> Vec<SubTable> {
    let file: File = syn::parse_str(source).expect("Failed to parse api_table.rs");
    let mut tables = Vec::new();
    for item in &file.items {
        if let Item::Struct(s) = item {
            let name = s.ident.to_string();
            if name.starts_with("Rusteal") && name.ends_with("Api") && name != "RustealApiTable"
                && let Some(table) = parse_sub_table(s)
            {
                tables.push(table);
            }
        }
    }
    tables
}

fn parse_sub_table(s: &ItemStruct) -> Option<SubTable> {
    let struct_name = s.ident.to_string();
    let module_name = derive_module_name(&struct_name);
    let fields = match &s.fields {
        Fields::Named(f) => &f.named,
        _ => return None,
    };
    let mut functions = Vec::new();
    for field in fields {
        let field_name = field.ident.as_ref()?.to_string();
        if field_name.starts_with('_') {
            continue;
        }
        if let Some(api_fn) = parse_fn_field(&field_name, &field.ty) {
            functions.push(api_fn);
        }
    }
    if functions.is_empty() {
        return None;
    }
    Some(SubTable { module_name, functions })
}

fn derive_module_name(struct_name: &str) -> String {
    let name = struct_name
        .strip_prefix("Rusteal")
        .unwrap_or(struct_name)
        .strip_suffix("Api")
        .unwrap_or(struct_name);
    to_snake_case(name)
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() {
            if i > 0 {
                result.push('_');
            }
            result.push(c.to_lowercase().next().unwrap());
        } else {
            result.push(c);
        }
    }
    result
}

fn parse_fn_field(_name: &str, ty: &Type) -> Option<ApiFn> {
    let bare_fn = match ty {
        Type::BareFn(f) => f,
        _ => return None,
    };
    let params: Vec<ApiParam> = bare_fn.inputs.iter().filter_map(parse_bare_fn_arg).collect();
    let return_type = match &bare_fn.output {
        syn::ReturnType::Default => None,
        syn::ReturnType::Type(_, ty) => parse_api_type(ty),
    };
    Some(ApiFn {
        name: _name.to_string(),
        params,
        return_type,
    })
}

fn parse_bare_fn_arg(arg: &BareFnArg) -> Option<ApiParam> {
    let ty = parse_api_type(&arg.ty)?;
    Some(ApiParam { ty })
}

fn parse_api_type(ty: &Type) -> Option<ApiType> {
    match ty {
        Type::Path(tp) => {
            let seg = tp.path.segments.last()?;
            let name = seg.ident.to_string();
            match name.as_str() {
                "UObjectHandle" => Some(ApiType::UObjectHandle),
                "UClassHandle" => Some(ApiType::UClassHandle),
                "FPropertyHandle" => Some(ApiType::FPropertyHandle),
                "UFunctionHandle" => Some(ApiType::UFunctionHandle),
                "UStructHandle" => Some(ApiType::UStructHandle),
                "FNameHandle" => Some(ApiType::FNameHandle),
                "FWeakObjectHandle" => Some(ApiType::FWeakObjectHandle),
                "RustealErrorCode" => Some(ApiType::RustealErrorCode),
                "bool" => Some(ApiType::Bool),
                "u8" => Some(ApiType::U8),
                "u16" => Some(ApiType::U16),
                "u32" => Some(ApiType::U32),
                "u64" => Some(ApiType::U64),
                "i32" => Some(ApiType::I32),
                "i64" => Some(ApiType::I64),
                "f32" => Some(ApiType::F32),
                "f64" => Some(ApiType::F64),
                _ => None,
            }
        }
        Type::Ptr(ptr) => {
            let mutability = if ptr.mutability.is_some() {
                Mutability::Mut
            } else {
                Mutability::Const
            };
            if let Type::Path(tp) = &*ptr.elem
                && let Some(seg) = tp.path.segments.last()
            {
                if seg.ident == "c_void" {
                    return Some(ApiType::CVoidPtr { mutability });
                }
                let name = seg.ident.to_string();
                if name.starts_with("Rusteal") || name.starts_with('F') || name.starts_with('U') {
                    if let Some(inner) = parse_api_type(&ptr.elem)
                        && (is_handle_type(&inner) || is_scalar_type(&inner)) {
                            return Some(ApiType::Ptr {
                                mutability,
                                pointee: Box::new(inner),
                            });
                        }
                    return Some(ApiType::NamedStructPtr { mutability, name });
                }
            }
            let pointee = parse_api_type(&ptr.elem)?;
            Some(ApiType::Ptr {
                mutability,
                pointee: Box::new(pointee),
            })
        }
        _ => None,
    }
}

fn is_handle_type(ty: &ApiType) -> bool {
    matches!(
        ty,
        ApiType::UObjectHandle
            | ApiType::UClassHandle
            | ApiType::FPropertyHandle
            | ApiType::UFunctionHandle
            | ApiType::UStructHandle
            | ApiType::FNameHandle
    )
}

fn is_scalar_type(ty: &ApiType) -> bool {
    matches!(
        ty,
        ApiType::Bool
            | ApiType::U8
            | ApiType::U16
            | ApiType::U32
            | ApiType::U64
            | ApiType::I32
            | ApiType::I64
            | ApiType::F32
            | ApiType::F64
    )
}