//! Evaluates a Rhai script that describes an Animator Controller, and hands the result to C# as JSON. //! //! The C# side never sees Rhai: it receives a `ControllerGraph` and drives the Animator As Code //! modification API with it. pub mod builder; pub mod export; pub mod graph; pub mod motion_api; pub mod rhai_api; use builder::Aac; use rhai::{Dynamic, Engine, Scope}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::ptr; pub use graph::ControllerGraph; /// Opaque to C#. Owns the graph, the Rhai engine, and the last error message. pub struct AacContext { aac: Aac, engine: Engine, last_error: Option, } impl AacContext { fn set_error(&mut self, message: String) { // Interior NUL bytes would make CString::new fail; they can only come from a script string. self.last_error = Some(CString::new(message.replace('\0', "\\0")).unwrap_or_default()); } } /// Evaluate a script and serialize the resulting graph. The testable core of the FFI. pub fn evaluate_to_json(script: &str) -> Result { let aac = Aac::new(); let engine = rhai_api::engine(aac.clone()); let mut scope = Scope::new(); let _ = engine .eval_with_scope::(&mut scope, script) .map_err(|error| error.to_string())?; let graph = aac.write(|graph| graph.clone()); export::to_json(&graph) } /// Create a context. The returned pointer owns everything; release it with `aac_destroy`. #[no_mangle] pub extern "C" fn aac_create() -> *mut AacContext { let aac = Aac::new(); let engine = rhai_api::engine(aac.clone()); Box::into_raw(Box::new(AacContext { aac, engine, last_error: None, })) } /// Evaluate a Rhai script. Returns 0 on success; on failure, call `aac_last_error`. #[no_mangle] pub extern "C" fn aac_eval_rhai(handle: *mut AacContext, script: *const c_char) -> i32 { if handle.is_null() || script.is_null() { return 1; } // SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed. let context = unsafe { &mut *handle }; let script = match unsafe { CStr::from_ptr(script) }.to_str() { Ok(script) => script, Err(error) => { context.set_error(format!("script is not valid UTF-8: {error}")); return 1; } }; let outcome = { let engine = &context.engine; catch_unwind(AssertUnwindSafe(|| { let mut scope = Scope::new(); engine.eval_with_scope::(&mut scope, script) })) }; match outcome { Ok(Ok(_)) => { context.last_error = None; 0 } Ok(Err(error)) => { context.set_error(error.to_string()); 1 } Err(_) => { context.set_error("internal error: the evaluator panicked".to_string()); 2 } } } /// Serialize the graph to JSON. Returns null on error; release the result with `aac_free_string`. #[no_mangle] pub extern "C" fn aac_to_json(handle: *mut AacContext) -> *mut c_char { if handle.is_null() { return ptr::null_mut(); } // SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed. let context = unsafe { &mut *handle }; let outcome = catch_unwind(AssertUnwindSafe(|| { let graph = context.aac.write(|graph| graph.clone()); export::to_json(&graph) })); match outcome { Ok(Ok(json)) => match CString::new(json) { Ok(json) => { context.last_error = None; json.into_raw() } Err(_) => { context.set_error("internal error: serialized JSON contained a NUL byte".to_string()); ptr::null_mut() } }, Ok(Err(error)) => { context.set_error(error); ptr::null_mut() } Err(_) => { context.set_error("internal error: serialization panicked".to_string()); ptr::null_mut() } } } /// The last error message, or null if the last call succeeded. Borrowed: valid until the next call /// on this context, and must not be freed. #[no_mangle] pub extern "C" fn aac_last_error(handle: *mut AacContext) -> *const c_char { if handle.is_null() { return ptr::null(); } // SAFETY: the caller guarantees `handle` came from `aac_create` and has not been destroyed. let context = unsafe { &*handle }; match &context.last_error { Some(message) => message.as_ptr(), None => ptr::null(), } } /// Free a string returned by `aac_to_json`. #[no_mangle] pub extern "C" fn aac_free_string(string: *mut c_char) { if string.is_null() { return; } // SAFETY: `string` must come from `aac_to_json`, which uses `CString::into_raw`. unsafe { drop(CString::from_raw(string)) }; } /// Destroy a context created by `aac_create`. #[no_mangle] pub extern "C" fn aac_destroy(handle: *mut AacContext) { if handle.is_null() { return; } // SAFETY: `handle` must come from `aac_create`, which uses `Box::into_raw`. unsafe { drop(Box::from_raw(handle)) }; }