#![allow(unsafe_code)]
#![allow(non_camel_case_types)]
#![allow(dead_code)]
use std::collections::BTreeMap;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use evorule_tcb::JsonValue;
use crate::Reactor;
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum evorule_error_code {
EVORULE_OK = 0,
EVORULE_ERROR_OOM = 1,
EVORULE_ERROR_INVALID_ARG = 2,
EVORULE_ERROR_RUNTIME = 3,
EVORULE_ERROR_NOT_INITIALIZED = 4,
}
pub type evorule_reactor = c_void;
pub type evorule_result = c_void;
#[no_mangle]
pub extern "C" fn evorule_version() -> *const c_char {
const VERSION: &str = env!("CARGO_PKG_VERSION");
match CString::new(VERSION) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn evorule_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)) };
}
}
struct ReactorFfiHandle {
_handle: crate::ReactorHandle,
_runtime: tokio::runtime::Runtime,
command_tx: crate::FactSender,
snapshot: std::sync::Arc<std::sync::Mutex<crate::ReactorStateSnapshot>>,
fact_id_gen: crate::FactIdGenerator,
}
#[no_mangle]
pub extern "C" fn evorule_reactor_new() -> *mut evorule_reactor {
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(r) => r,
Err(_) => return std::ptr::null_mut(),
};
let reactor = Reactor::builder(Vec::new()).build();
let (command_tx, _event_rx, _event_tx, handle, facts_log) = reactor.spawn();
let snapshot =
std::sync::Arc::new(std::sync::Mutex::new(crate::ReactorStateSnapshot::default()));
let _facts_log = facts_log;
let ffi_handle = Box::new(ReactorFfiHandle {
_handle: handle,
_runtime: runtime,
command_tx,
snapshot,
fact_id_gen: crate::FactIdGenerator::new(),
});
Box::into_raw(ffi_handle) as *mut evorule_reactor
}
#[no_mangle]
pub extern "C" fn evorule_reactor_free(reactor: *mut evorule_reactor) {
if reactor.is_null() {
return;
}
unsafe {
let handle = Box::from_raw(reactor as *mut ReactorFfiHandle);
drop(handle);
}
}
#[no_mangle]
pub extern "C" fn evorule_reactor_send_command(
reactor: *mut evorule_reactor,
instruction_json: *const c_char,
) -> evorule_error_code {
if reactor.is_null() || instruction_json.is_null() {
return evorule_error_code::EVORULE_ERROR_INVALID_ARG;
}
let handle = unsafe { &mut *(reactor as *mut ReactorFfiHandle) };
let json_str = unsafe { CStr::from_ptr(instruction_json) };
let json_str = match json_str.to_str() {
Ok(s) => s,
Err(_) => return evorule_error_code::EVORULE_ERROR_INVALID_ARG,
};
let instruction = match parse_simple_json(json_str) {
Some(v) => v,
None => return evorule_error_code::EVORULE_ERROR_INVALID_ARG,
};
let fact = crate::Fact::Command {
id: handle.fact_id_gen.next_id(),
instruction,
};
match handle.command_tx.send(fact) {
Ok(()) => evorule_error_code::EVORULE_OK,
Err(_) => evorule_error_code::EVORULE_ERROR_RUNTIME,
}
}
fn parse_simple_json(s: &str) -> Option<JsonValue> {
let s = s.trim();
if s.starts_with('{') && s.ends_with('}') {
parse_json_object(s)
} else if s.starts_with('"') && s.ends_with('"') {
Some(JsonValue::string(&s[1..s.len() - 1]))
} else if s.starts_with('[') && s.ends_with(']') {
Some(JsonValue::Array(vec![]))
} else if s == "true" {
Some(JsonValue::bool(true))
} else if s == "false" {
Some(JsonValue::bool(false))
} else if s == "null" {
Some(JsonValue::null())
} else if let Ok(n) = s.parse::<i64>() {
Some(JsonValue::integer(n))
} else {
None
}
}
fn parse_json_object(s: &str) -> Option<JsonValue> {
let s = s.trim();
if !s.starts_with('{') || !s.ends_with('}') {
return None;
}
let inner = &s[1..s.len() - 1].trim();
if inner.is_empty() {
return Some(JsonValue::Object(BTreeMap::new()));
}
let mut obj = BTreeMap::new();
for part in inner.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
let colon_pos = part.find(':')?;
let key_part = part[..colon_pos].trim();
let val_part = part[colon_pos + 1..].trim();
if !key_part.starts_with('"') || !key_part.ends_with('"') {
return None;
}
let key = &key_part[1..key_part.len() - 1];
let val = parse_simple_json(val_part)?;
obj.insert(key.to_string(), val);
}
Some(JsonValue::Object(obj))
}
struct ResultHandle {
output: String,
}
#[no_mangle]
pub extern "C" fn evorule_result_get_output(result: *mut evorule_result) -> *const c_char {
if result.is_null() {
return std::ptr::null();
}
let handle = unsafe { &*(result as *mut ResultHandle) };
match CString::new(handle.output.clone()) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn evorule_result_free(result: *mut evorule_result) {
if result.is_null() {
return;
}
unsafe {
let handle = Box::from_raw(result as *mut ResultHandle);
drop(handle);
}
}
#[no_mangle]
pub extern "C" fn evorule_reactor_current_queue_size(reactor: *mut evorule_reactor) -> c_int {
if reactor.is_null() {
return -1;
}
let handle = unsafe { &*(reactor as *mut ReactorFfiHandle) };
handle
.snapshot
.lock()
.map(|s| s.queue_len as c_int)
.unwrap_or(-1)
}
#[cfg(test)]
mod tests {
#![allow(unsafe_code)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::Fact;
use std::time::{Duration, Instant};
#[test]
fn ffi_send_command_produces_distinct_fact_ids_in_facts_log() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let (command_tx, _event_rx, _event_tx, handle, facts_log) = {
let _guard = runtime.enter();
let reactor = Reactor::builder(Vec::new()).build();
reactor.spawn()
};
let snapshot =
std::sync::Arc::new(std::sync::Mutex::new(crate::ReactorStateSnapshot::default()));
let ffi_handle = Box::new(ReactorFfiHandle {
_handle: handle,
_runtime: runtime,
command_tx,
snapshot,
fact_id_gen: crate::FactIdGenerator::new(),
});
let ptr = Box::into_raw(ffi_handle) as *mut evorule_reactor;
let cmds = [r#"{"op":"a"}"#, r#"{"op":"b"}"#, r#"{"op":"c"}"#];
for c in &cmds {
let cstr = CString::new(*c).unwrap();
let rc = evorule_reactor_send_command(ptr, cstr.as_ptr());
assert_eq!(rc, evorule_error_code::EVORULE_OK);
}
let deadline = Instant::now() + Duration::from_secs(2);
loop {
let n = facts_log
.history()
.iter()
.filter(|f| matches!(f, Fact::Command { .. }))
.count();
if n >= 3 || Instant::now() > deadline {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let ids: Vec<u64> = facts_log
.history()
.iter()
.filter_map(|f| match f {
Fact::Command { id, .. } => Some(id.0),
_ => None,
})
.collect();
evorule_reactor_free(ptr);
assert_eq!(
ids,
vec![1, 2, 3],
"C4: Command FactIds in FactsLog must be [1, 2, 3], got {ids:?}"
);
}
}