168 lines
5.2 KiB
Rust
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)) };
|
|
}
|