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    if handle.is_null() || code.is_null() || result.is_null() || error.is_null() {
65        return AetherErrorCode::NullPointer as c_int;
66    }
67
68    // Catch panics and convert them to errors
69    let panic_result = panic::catch_unwind(|| unsafe {
70        let engine = &mut *(handle as *mut Aether);
71        let code_str = match CStr::from_ptr(code).to_str() {
72            Ok(s) => s,
73            Err(_) => return AetherErrorCode::RuntimeError as c_int,
74        };
75
76        match engine.eval(code_str) {
77            Ok(val) => {
78                let result_str = value_to_string(&val);
79                match CString::new(result_str) {
80                    Ok(cstr) => {
81                        *result = cstr.into_raw();
82                        *error = std::ptr::null_mut();
83                        AetherErrorCode::Success as c_int
84                    }
85                    Err(_) => AetherErrorCode::RuntimeError as c_int,
86                }
87            }
88            Err(e) => {
89                let error_str = format!("{}", e);
90                match CString::new(error_str) {
91                    Ok(cstr) => {
92                        *error = cstr.into_raw();
93                        *result = std::ptr::null_mut();
94                        // Determine error type from message
95                        if e.contains("Parse error") {
96                            AetherErrorCode::ParseError as c_int
97                        } else {
98                            AetherErrorCode::RuntimeError as c_int
99                        }
100                    }
101                    Err(_) => AetherErrorCode::RuntimeError as c_int,
102                }
103            }
104        }
105    });
106
107    match panic_result {
108        Ok(code) => code,
109        Err(_) => {
110            unsafe {
111                let panic_msg = CString::new("Panic occurred during evaluation").unwrap();
112                *error = panic_msg.into_raw();
113                *result = std::ptr::null_mut();
114            }
115            AetherErrorCode::Panic as c_int
116        }
117    }
118}
119
120/// Get the version string of Aether
121///
122/// Returns: C string with version (must NOT be freed)
123#[unsafe(no_mangle)]
124pub extern "C" fn aether_version() -> *const c_char {
125    static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
126    VERSION.as_ptr() as *const c_char
127}
128
129/// Free an Aether engine handle
130#[unsafe(no_mangle)]
131pub extern "C" fn aether_free(handle: *mut AetherHandle) {
132    if !handle.is_null() {
133        unsafe {
134            let _ = Box::from_raw(handle as *mut Aether);
135        }
136    }
137}
138
139/// Free a string allocated by Aether
140#[unsafe(no_mangle)]
141pub extern "C" fn aether_free_string(s: *mut c_char) {
142    if !s.is_null() {
143        unsafe {
144            let _ = CString::from_raw(s);
145        }
146    }
147}
148
149/// Helper function to convert Value to string representation
150fn value_to_string(value: &Value) -> String {
151    match value {
152        Value::Number(n) => {
153            // Format number nicely - remove trailing zeros
154            if n.fract() == 0.0 {
155                format!("{:.0}", n)
156            } else {
157                n.to_string()
158            }
159        }
160        Value::String(s) => s.clone(),
161        Value::Boolean(b) => b.to_string(),
162        Value::Array(arr) => {
163            let items: Vec<String> = arr.iter().map(value_to_string).collect();
164            format!("[{}]", items.join(", "))
165        }
166        Value::Dict(map) => {
167            let items: Vec<String> = map
168                .iter()
169                .map(|(k, v)| format!("{}: {}", k, value_to_string(v)))
170                .collect();
171            format!("{{{}}}", items.join(", "))
172        }
173        Value::Null => "null".to_string(),
174        Value::Function { .. } => "<function>".to_string(),
175        Value::BuiltIn { name, .. } => format!("<builtin: {}>", name),
176        Value::Generator { .. } => "<generator>".to_string(),
177        Value::Lazy { .. } => "<lazy>".to_string(),
178        Value::Fraction(f) => f.to_string(),
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_ffi_basic_eval() {
188        let handle = aether_new();
189        assert!(!handle.is_null());
190
191        let code = CString::new("Set X 10\n(X + 20)").unwrap();
192        let mut result: *mut c_char = std::ptr::null_mut();
193        let mut error: *mut c_char = std::ptr::null_mut();
194
195        let status = aether_eval(handle, code.as_ptr(), &mut result, &mut error);
196
197        assert_eq!(status, AetherErrorCode::Success as c_int);
198        assert!(!result.is_null());
199        assert!(error.is_null());
200
201        unsafe {
202            let result_str = CStr::from_ptr(result).to_str().unwrap();
203            assert_eq!(result_str, "30");
204            aether_free_string(result);
205        }
206
207        aether_free(handle);
208    }
209
210    #[test]
211    fn test_ffi_error_handling() {
212        let handle = aether_new();
213        let code = CString::new("UNDEFINED_VAR").unwrap();
214        let mut result: *mut c_char = std::ptr::null_mut();
215        let mut error: *mut c_char = std::ptr::null_mut();
216
217        let status = aether_eval(handle, code.as_ptr(), &mut result, &mut error);
218
219        assert_ne!(status, AetherErrorCode::Success as c_int);
220        assert!(result.is_null());
221        assert!(!error.is_null());
222
223        #[allow(unused_unsafe)]
224        unsafe {
225            aether_free_string(error);
226        }
227
228        aether_free(handle);
229    }
230}