aether/
ffi.rs

1//! C-FFI interface for Aether language bindings
2//!
3//! This module provides C-compatible functions for use with other languages
4//! through Foreign Function Interface (FFI).
5
6use std::ffi::{CStr, CString};
7use std::os::raw::{c_char, c_int};
8use std::panic;
9
10use crate::{Aether, Value};
11
12/// Opaque handle for Aether engine
13#[repr(C)]
14pub struct AetherHandle {
15    _opaque: [u8; 0],
16}
17
18/// Error codes returned by C-FFI functions
19#[repr(C)]
20pub enum AetherErrorCode {
21    Success = 0,
22    ParseError = 1,
23    RuntimeError = 2,
24    NullPointer = 3,
25    Panic = 4,
26}
27
28/// Create a new Aether engine instance
29///
30/// Returns: Pointer to AetherHandle (must be freed with aether_free)
31#[unsafe(no_mangle)]
32pub extern "C" fn aether_new() -> *mut AetherHandle {
33    let engine = Box::new(Aether::new());
34    Box::into_raw(engine) as *mut AetherHandle
35}
36
37/// Create a new Aether engine with all IO permissions enabled
38///
39/// Returns: Pointer to AetherHandle (must be freed with aether_free)
40#[unsafe(no_mangle)]
41pub extern "C" fn aether_new_with_permissions() -> *mut AetherHandle {
42    let engine = Box::new(Aether::with_all_permissions());
43    Box::into_raw(engine) as *mut AetherHandle
44}
45
46/// Evaluate Aether code
47///
48/// # Parameters
49/// - handle: Aether engine handle
50/// - code: C string containing Aether code
51/// - result: Output parameter for result (must be freed with aether_free_string)
52/// - error: Output parameter for error message (must be freed with aether_free_string)
53///
54/// # Returns
55/// - 0 (Success) if evaluation succeeded
56/// - Non-zero error code if evaluation failed
57#[unsafe(no_mangle)]
58pub extern "C" fn aether_eval(
59    handle: *mut AetherHandle,
60    code: *const c_char,
61    result: *mut *mut c_char,
62    error: *mut *mut c_char,
63) -> c_int {
64    #![allow(clippy::not_unsafe_ptr_arg_deref)]
65    if handle.is_null() || code.is_null() || result.is_null() || error.is_null() {
66        return AetherErrorCode::NullPointer as c_int;
67    }
68
69    // Catch panics and convert them to errors
70    let panic_result = panic::catch_unwind(|| unsafe {
71        let engine = &mut *(handle as *mut Aether);
72        let code_str = match CStr::from_ptr(code).to_str() {
73            Ok(s) => s,
74            Err(_) => return AetherErrorCode::RuntimeError as c_int,
75        };
76
77        match engine.eval(code_str) {
78            Ok(val) => {
79                let result_str = value_to_string(&val);
80                match CString::new(result_str) {
81                    Ok(cstr) => {
82                        *result = cstr.into_raw();
83                        *error = std::ptr::null_mut();
84                        AetherErrorCode::Success as c_int
85                    }
86                    Err(_) => AetherErrorCode::RuntimeError as c_int,
87                }
88            }
89            Err(e) => {
90                let error_str = e.to_string();
91                match CString::new(error_str) {
92                    Ok(cstr) => {
93                        *error = cstr.into_raw();
94                        *result = std::ptr::null_mut();
95                        // Determine error type from message
96                        if e.contains("Parse error") {
97                            AetherErrorCode::ParseError as c_int
98                        } else {
99                            AetherErrorCode::RuntimeError as c_int
100                        }
101                    }
102                    Err(_) => AetherErrorCode::RuntimeError as c_int,
103                }
104            }
105        }
106    });
107
108    match panic_result {
109        Ok(code) => code,
110        Err(_) => {
111            unsafe {
112                let panic_msg = CString::new("Panic occurred during evaluation").unwrap();
113                *error = panic_msg.into_raw();
114                *result = std::ptr::null_mut();
115            }
116            AetherErrorCode::Panic as c_int
117        }
118    }
119}
120
121/// Get the version string of Aether
122///
123/// Returns: C string with version (must NOT be freed)
124#[unsafe(no_mangle)]
125pub extern "C" fn aether_version() -> *const c_char {
126    static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
127    VERSION.as_ptr() as *const c_char
128}
129
130/// Free an Aether engine handle
131#[unsafe(no_mangle)]
132pub extern "C" fn aether_free(handle: *mut AetherHandle) {
133    if !handle.is_null() {
134        unsafe {
135            let _ = Box::from_raw(handle as *mut Aether);
136        }
137    }
138}
139
140/// Free a string allocated by Aether
141#[unsafe(no_mangle)]
142pub extern "C" fn aether_free_string(s: *mut c_char) {
143    #![allow(clippy::not_unsafe_ptr_arg_deref)]
144    if !s.is_null() {
145        unsafe {
146            let _ = CString::from_raw(s);
147        }
148    }
149}
150
151/// Helper function to convert Value to string representation
152fn value_to_string(value: &Value) -> String {
153    match value {
154        Value::Number(n) => {
155            // Format number nicely - remove trailing zeros
156            if n.fract() == 0.0 {
157                format!("{:.0}", n)
158            } else {
159                n.to_string()
160            }
161        }
162        Value::String(s) => s.clone(),
163        Value::Boolean(b) => b.to_string(),
164        Value::Array(arr) => {
165            let items: Vec<String> = arr.iter().map(value_to_string).collect();
166            format!("[{}]", items.join(", "))
167        }
168        Value::Dict(map) => {
169            let items: Vec<String> = map
170                .iter()
171                .map(|(k, v)| format!("{}: {}", k, value_to_string(v)))
172                .collect();
173            format!("{{{}}}", items.join(", "))
174        }
175        Value::Null => "null".to_string(),
176        Value::Function { .. } => "<function>".to_string(),
177        Value::BuiltIn { name, .. } => format!("<builtin: {}>", name),
178        Value::Generator { .. } => "<generator>".to_string(),
179        Value::Lazy { .. } => "<lazy>".to_string(),
180        Value::Fraction(f) => f.to_string(),
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_ffi_basic_eval() {
190        let handle = aether_new();
191        assert!(!handle.is_null());
192
193        let code = CString::new("Set X 10\n(X + 20)").unwrap();
194        let mut result: *mut c_char = std::ptr::null_mut();
195        let mut error: *mut c_char = std::ptr::null_mut();
196
197        let status = aether_eval(handle, code.as_ptr(), &mut result, &mut error);
198
199        assert_eq!(status, AetherErrorCode::Success as c_int);
200        assert!(!result.is_null());
201        assert!(error.is_null());
202
203        unsafe {
204            let result_str = CStr::from_ptr(result).to_str().unwrap();
205            assert_eq!(result_str, "30");
206            aether_free_string(result);
207        }
208
209        aether_free(handle);
210    }
211
212    #[test]
213    fn test_ffi_error_handling() {
214        let handle = aether_new();
215        let code = CString::new("UNDEFINED_VAR").unwrap();
216        let mut result: *mut c_char = std::ptr::null_mut();
217        let mut error: *mut c_char = std::ptr::null_mut();
218
219        let status = aether_eval(handle, code.as_ptr(), &mut result, &mut error);
220
221        assert_ne!(status, AetherErrorCode::Success as c_int);
222        assert!(result.is_null());
223        assert!(!error.is_null());
224
225        #[allow(unused_unsafe)]
226        unsafe {
227            aether_free_string(error);
228        }
229
230        aether_free(handle);
231    }
232}