Files
av3-animation-as-crab/rust/src/lib.rs
T
2026-09-18 14:01:10 +01:00

168 lines
5.2 KiB
Rust

//! 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<CString>,
}
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<String, String> {
let aac = Aac::new();
let engine = rhai_api::engine(aac.clone());
let mut scope = Scope::new();
let _ = engine
.eval_with_scope::<Dynamic>(&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::<Dynamic>(&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)) };
}