1use chrono::Utc;
2use kronroe::TemporalGraph;
3use std::cell::RefCell;
4use std::ffi::{c_char, CStr, CString};
5use std::ptr;
6
7pub struct KronroeGraphHandle {
8 graph: TemporalGraph,
9}
10
11thread_local! {
12 static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
13}
14
15fn set_last_error(msg: String) {
16 let sanitized = msg.replace('\0', "\\0");
20 LAST_ERROR.with(|cell| {
21 *cell.borrow_mut() = CString::new(sanitized).ok();
22 });
23}
24
25fn clear_last_error() {
26 LAST_ERROR.with(|cell| {
27 *cell.borrow_mut() = None;
28 });
29}
30
31fn cstr_to_string(ptr: *const c_char, field: &str) -> Result<String, String> {
32 if ptr.is_null() {
33 return Err(format!("{field} is null"));
34 }
35 let s = unsafe { CStr::from_ptr(ptr) }
36 .to_str()
37 .map_err(|_| format!("{field} is not valid UTF-8"))?;
38 Ok(s.to_string())
39}
40
41#[no_mangle]
42pub extern "C" fn kronroe_graph_open_in_memory() -> *mut KronroeGraphHandle {
47 clear_last_error();
48 match TemporalGraph::open_in_memory() {
49 Ok(graph) => Box::into_raw(Box::new(KronroeGraphHandle { graph })),
50 Err(err) => {
51 set_last_error(err.to_string());
52 ptr::null_mut()
53 }
54 }
55}
56
57#[no_mangle]
58pub unsafe extern "C" fn kronroe_graph_open(path: *const c_char) -> *mut KronroeGraphHandle {
63 clear_last_error();
64 let path = match cstr_to_string(path, "path") {
65 Ok(v) => v,
66 Err(e) => {
67 set_last_error(e);
68 return ptr::null_mut();
69 }
70 };
71
72 match TemporalGraph::open(&path) {
73 Ok(graph) => Box::into_raw(Box::new(KronroeGraphHandle { graph })),
74 Err(err) => {
75 set_last_error(err.to_string());
76 ptr::null_mut()
77 }
78 }
79}
80
81#[no_mangle]
82pub unsafe extern "C" fn kronroe_graph_close(handle: *mut KronroeGraphHandle) {
88 if handle.is_null() {
89 return;
90 }
91 unsafe {
92 drop(Box::from_raw(handle));
93 }
94}
95
96#[no_mangle]
97pub unsafe extern "C" fn kronroe_graph_assert_text(
103 handle: *mut KronroeGraphHandle,
104 subject: *const c_char,
105 predicate: *const c_char,
106 object: *const c_char,
107) -> bool {
108 clear_last_error();
109 if handle.is_null() {
110 set_last_error("graph handle is null".to_string());
111 return false;
112 }
113
114 let subject = match cstr_to_string(subject, "subject") {
115 Ok(v) => v,
116 Err(e) => {
117 set_last_error(e);
118 return false;
119 }
120 };
121 let predicate = match cstr_to_string(predicate, "predicate") {
122 Ok(v) => v,
123 Err(e) => {
124 set_last_error(e);
125 return false;
126 }
127 };
128 let object = match cstr_to_string(object, "object") {
129 Ok(v) => v,
130 Err(e) => {
131 set_last_error(e);
132 return false;
133 }
134 };
135
136 let graph = unsafe { &*handle };
137 match graph
138 .graph
139 .assert_fact(&subject, &predicate, object, Utc::now())
140 {
141 Ok(_) => true,
142 Err(err) => {
143 set_last_error(err.to_string());
144 false
145 }
146 }
147}
148
149#[no_mangle]
150pub unsafe extern "C" fn kronroe_graph_facts_about_json(
157 handle: *mut KronroeGraphHandle,
158 entity: *const c_char,
159) -> *mut c_char {
160 clear_last_error();
161 if handle.is_null() {
162 set_last_error("graph handle is null".to_string());
163 return ptr::null_mut();
164 }
165 let entity = match cstr_to_string(entity, "entity") {
166 Ok(v) => v,
167 Err(e) => {
168 set_last_error(e);
169 return ptr::null_mut();
170 }
171 };
172 let graph = unsafe { &*handle };
173
174 match graph.graph.all_facts_about(&entity) {
175 Ok(facts) => match serde_json::to_string(&facts) {
176 Ok(s) => match CString::new(s) {
177 Ok(cs) => cs.into_raw(),
178 Err(_) => {
179 set_last_error("failed to encode facts JSON".to_string());
180 ptr::null_mut()
181 }
182 },
183 Err(err) => {
184 set_last_error(err.to_string());
185 ptr::null_mut()
186 }
187 },
188 Err(err) => {
189 set_last_error(err.to_string());
190 ptr::null_mut()
191 }
192 }
193}
194
195#[no_mangle]
196pub extern "C" fn kronroe_last_error_message() -> *mut c_char {
206 LAST_ERROR.with(|cell| match cell.borrow().as_ref() {
207 Some(msg) => {
208 msg.clone().into_raw()
211 }
212 None => ptr::null_mut(),
213 })
214}
215
216#[no_mangle]
217pub unsafe extern "C" fn kronroe_string_free(ptr: *mut c_char) {
223 if ptr.is_null() {
224 return;
225 }
226 unsafe {
227 drop(CString::from_raw(ptr));
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use std::time::{SystemTime, UNIX_EPOCH};
235
236 fn c(s: &str) -> CString {
237 CString::new(s).expect("test CString")
238 }
239
240 fn unique_db_path() -> String {
241 let nanos = SystemTime::now()
242 .duration_since(UNIX_EPOCH)
243 .expect("clock")
244 .as_nanos();
245 let mut p = std::env::temp_dir();
246 p.push(format!("kronroe-ios-ffi-{nanos}.kronroe"));
247 p.to_string_lossy().to_string()
248 }
249
250 #[test]
251 fn ffi_open_assert_query_roundtrip_file_backed() {
252 let path = c(&unique_db_path());
253 let subject = c("Freya");
254 let predicate = c("attends");
255 let object = c("Sunrise Primary");
256 let entity = c("Freya");
257
258 let handle = unsafe { kronroe_graph_open(path.as_ptr()) };
259 assert!(!handle.is_null(), "open should return a valid handle");
260
261 let ok = unsafe {
262 kronroe_graph_assert_text(
263 handle,
264 subject.as_ptr(),
265 predicate.as_ptr(),
266 object.as_ptr(),
267 )
268 };
269 assert!(ok, "assert should succeed");
270
271 let json_ptr = unsafe { kronroe_graph_facts_about_json(handle, entity.as_ptr()) };
272 assert!(!json_ptr.is_null(), "facts query should return JSON");
273
274 let json = unsafe { CStr::from_ptr(json_ptr) }
275 .to_str()
276 .expect("valid utf8");
277 let facts: serde_json::Value = serde_json::from_str(json).expect("valid json");
278 let arr = facts.as_array().expect("json array");
279 assert_eq!(arr.len(), 1);
280 assert_eq!(arr[0]["subject"], "Freya");
281 assert_eq!(arr[0]["predicate"], "attends");
282 assert_eq!(arr[0]["object"]["value"], "Sunrise Primary");
283
284 unsafe {
285 kronroe_string_free(json_ptr);
286 kronroe_graph_close(handle);
287 }
288 }
289
290 #[test]
291 fn ffi_open_in_memory_assert_query_roundtrip() {
292 let subject = c("alice");
293 let predicate = c("works_at");
294 let object = c("Acme");
295 let entity = c("alice");
296
297 let handle = kronroe_graph_open_in_memory();
298 assert!(
299 !handle.is_null(),
300 "open_in_memory should return a valid handle"
301 );
302
303 let ok = unsafe {
304 kronroe_graph_assert_text(
305 handle,
306 subject.as_ptr(),
307 predicate.as_ptr(),
308 object.as_ptr(),
309 )
310 };
311 assert!(ok, "assert should succeed");
312
313 let json_ptr = unsafe { kronroe_graph_facts_about_json(handle, entity.as_ptr()) };
314 assert!(!json_ptr.is_null(), "facts query should return JSON");
315
316 let json = unsafe { CStr::from_ptr(json_ptr) }
317 .to_str()
318 .expect("valid utf8");
319 let facts: serde_json::Value = serde_json::from_str(json).expect("valid json");
320 let arr = facts.as_array().expect("json array");
321 assert_eq!(arr.len(), 1);
322
323 unsafe {
324 kronroe_string_free(json_ptr);
325 kronroe_graph_close(handle);
326 }
327 }
328
329 #[test]
330 fn ffi_failure_path_null_handle_assert_sets_error() {
331 let subject = c("alice");
332 let predicate = c("works_at");
333 let object = c("Acme");
334
335 let ok = unsafe {
336 kronroe_graph_assert_text(
337 std::ptr::null_mut(),
338 subject.as_ptr(),
339 predicate.as_ptr(),
340 object.as_ptr(),
341 )
342 };
343 assert!(!ok, "assert should fail with null handle");
344
345 let msg_ptr = kronroe_last_error_message();
346 assert!(!msg_ptr.is_null(), "error message should be set");
347 let msg = unsafe { CStr::from_ptr(msg_ptr) }
348 .to_str()
349 .expect("valid utf8");
350 assert!(
351 msg.contains("graph handle is null"),
352 "expected null-handle error, got: {msg}"
353 );
354 unsafe { kronroe_string_free(msg_ptr) };
355 }
356
357 #[test]
358 fn ffi_last_error_sanitizes_null_bytes() {
359 clear_last_error();
360 set_last_error("broken\0message".to_string());
361
362 let msg_ptr = kronroe_last_error_message();
363 assert!(!msg_ptr.is_null(), "error message should be present");
364 let msg = unsafe { CStr::from_ptr(msg_ptr) }
365 .to_str()
366 .expect("valid utf8");
367 assert_eq!(msg, "broken\\0message");
368
369 unsafe { kronroe_string_free(msg_ptr) };
370 }
371}