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() {
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();
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");
}
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();
}
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(),
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",
}
}
#[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
)
}