Skip to main content

kronroe_android/
android_bindings.rs

1use chrono::Utc;
2use kronroe::TemporalGraph;
3use std::cell::RefCell;
4
5// ---------------------------------------------------------------------------
6// Layer 1 — Pure Rust handle (testable on host without JVM or NDK)
7// ---------------------------------------------------------------------------
8
9pub(crate) struct KronroeGraphHandle {
10    graph: TemporalGraph,
11}
12
13thread_local! {
14    static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
15}
16
17fn set_last_error(msg: String) {
18    // Strip null bytes so JNI new_string never fails on embedded nulls.
19    let sanitized = msg.replace('\0', "\\0");
20    LAST_ERROR.with(|cell| {
21        *cell.borrow_mut() = Some(sanitized);
22    });
23}
24
25fn clear_last_error() {
26    LAST_ERROR.with(|cell| {
27        *cell.borrow_mut() = None;
28    });
29}
30
31impl KronroeGraphHandle {
32    fn open(path: &str) -> Result<Self, String> {
33        TemporalGraph::open(path)
34            .map(|graph| Self { graph })
35            .map_err(|e| e.to_string())
36    }
37
38    fn open_in_memory() -> Result<Self, String> {
39        TemporalGraph::open_in_memory()
40            .map(|graph| Self { graph })
41            .map_err(|e| e.to_string())
42    }
43
44    fn assert_text(&self, subject: &str, predicate: &str, object: &str) -> Result<bool, String> {
45        self.graph
46            .assert_fact(subject, predicate, object.to_string(), Utc::now())
47            .map(|_| true)
48            .map_err(|e| e.to_string())
49    }
50
51    fn facts_about_json(&self, entity: &str) -> Result<String, String> {
52        let facts = self
53            .graph
54            .all_facts_about(entity)
55            .map_err(|e| e.to_string())?;
56        serde_json::to_string(&facts).map_err(|e| e.to_string())
57    }
58}
59
60// ---------------------------------------------------------------------------
61// Layer 2 — JNI bridge (thin wrappers delegating to KronroeGraphHandle)
62// ---------------------------------------------------------------------------
63
64mod jni_bridge {
65    use super::*;
66    use jni::objects::{JClass, JString};
67    use jni::sys::{jboolean, jlong, jstring, JNI_FALSE, JNI_TRUE};
68    use jni::JNIEnv;
69
70    /// Convert a JNI handle (jlong) back to a reference.
71    ///
72    /// # Safety
73    /// Caller must guarantee `handle` was returned by `nativeOpen` or
74    /// `nativeOpenInMemory` and has not been closed.
75    unsafe fn handle_ref(handle: jlong) -> &'static KronroeGraphHandle {
76        unsafe { &*(handle as *const KronroeGraphHandle) }
77    }
78
79    /// Extract a Rust `String` from a JNI `JString`.
80    fn jstring_to_string(env: &mut JNIEnv, s: &JString) -> Result<String, String> {
81        env.get_string(s)
82            .map(|js| js.into())
83            .map_err(|e| e.to_string())
84    }
85
86    #[no_mangle]
87    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeOpenInMemory(
88        _env: JNIEnv,
89        _class: JClass,
90    ) -> jlong {
91        clear_last_error();
92        match KronroeGraphHandle::open_in_memory() {
93            Ok(handle) => Box::into_raw(Box::new(handle)) as jlong,
94            Err(msg) => {
95                set_last_error(msg);
96                0
97            }
98        }
99    }
100
101    #[no_mangle]
102    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeOpen(
103        mut env: JNIEnv,
104        _class: JClass,
105        path: JString,
106    ) -> jlong {
107        clear_last_error();
108        let path = match jstring_to_string(&mut env, &path) {
109            Ok(v) => v,
110            Err(msg) => {
111                set_last_error(msg);
112                return 0;
113            }
114        };
115        match KronroeGraphHandle::open(&path) {
116            Ok(handle) => Box::into_raw(Box::new(handle)) as jlong,
117            Err(msg) => {
118                set_last_error(msg);
119                0
120            }
121        }
122    }
123
124    #[no_mangle]
125    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeClose(
126        _env: JNIEnv,
127        _class: JClass,
128        handle: jlong,
129    ) {
130        if handle == 0 {
131            return;
132        }
133        unsafe {
134            drop(Box::from_raw(handle as *mut KronroeGraphHandle));
135        }
136    }
137
138    #[no_mangle]
139    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeAssertText(
140        mut env: JNIEnv,
141        _class: JClass,
142        handle: jlong,
143        subject: JString,
144        predicate: JString,
145        object: JString,
146    ) -> jboolean {
147        clear_last_error();
148        if handle == 0 {
149            set_last_error("graph handle is null".to_string());
150            return JNI_FALSE;
151        }
152
153        let subject = match jstring_to_string(&mut env, &subject) {
154            Ok(v) => v,
155            Err(msg) => {
156                set_last_error(msg);
157                return JNI_FALSE;
158            }
159        };
160        let predicate = match jstring_to_string(&mut env, &predicate) {
161            Ok(v) => v,
162            Err(msg) => {
163                set_last_error(msg);
164                return JNI_FALSE;
165            }
166        };
167        let object = match jstring_to_string(&mut env, &object) {
168            Ok(v) => v,
169            Err(msg) => {
170                set_last_error(msg);
171                return JNI_FALSE;
172            }
173        };
174
175        let graph = unsafe { handle_ref(handle) };
176        match graph.assert_text(&subject, &predicate, &object) {
177            Ok(_) => JNI_TRUE,
178            Err(msg) => {
179                set_last_error(msg);
180                JNI_FALSE
181            }
182        }
183    }
184
185    #[no_mangle]
186    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeFactsAboutJson(
187        mut env: JNIEnv,
188        _class: JClass,
189        handle: jlong,
190        entity: JString,
191    ) -> jstring {
192        clear_last_error();
193        if handle == 0 {
194            set_last_error("graph handle is null".to_string());
195            return std::ptr::null_mut();
196        }
197
198        let entity = match jstring_to_string(&mut env, &entity) {
199            Ok(v) => v,
200            Err(msg) => {
201                set_last_error(msg);
202                return std::ptr::null_mut();
203            }
204        };
205
206        let graph = unsafe { handle_ref(handle) };
207        match graph.facts_about_json(&entity) {
208            Ok(json) => match env.new_string(&json) {
209                Ok(js) => js.into_raw(),
210                Err(e) => {
211                    set_last_error(e.to_string());
212                    std::ptr::null_mut()
213                }
214            },
215            Err(msg) => {
216                set_last_error(msg);
217                std::ptr::null_mut()
218            }
219        }
220    }
221
222    #[no_mangle]
223    pub extern "system" fn Java_com_kronroe_KronroeGraph_nativeLastErrorMessage(
224        env: JNIEnv,
225        _class: JClass,
226    ) -> jstring {
227        let msg = LAST_ERROR.with(|cell| cell.borrow().clone());
228        match msg {
229            Some(s) => match env.new_string(&s) {
230                Ok(js) => js.into_raw(),
231                Err(_) => {
232                    // Fallback: try a plain error message so the caller at least
233                    // knows something went wrong, rather than returning null
234                    // (which is indistinguishable from "no error").
235                    env.new_string("kronroe: error message could not be encoded")
236                        .map(|js| js.into_raw())
237                        .unwrap_or(std::ptr::null_mut())
238                }
239            },
240            None => std::ptr::null_mut(),
241        }
242    }
243}
244
245// ---------------------------------------------------------------------------
246// Tests — exercise Layer 1 directly (no JVM needed)
247// ---------------------------------------------------------------------------
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn open_in_memory_assert_query_roundtrip() {
255        let handle = KronroeGraphHandle::open_in_memory().expect("open_in_memory");
256        handle
257            .assert_text("Freya", "attends", "Sunrise Primary")
258            .expect("assert");
259        let json = handle.facts_about_json("Freya").expect("facts_about");
260        let facts: serde_json::Value = serde_json::from_str(&json).expect("valid json");
261        let arr = facts.as_array().expect("json array");
262        assert_eq!(arr.len(), 1);
263        assert_eq!(arr[0]["subject"], "Freya");
264        assert_eq!(arr[0]["predicate"], "attends");
265        assert_eq!(arr[0]["object"]["value"], "Sunrise Primary");
266    }
267
268    #[test]
269    fn open_file_backed_roundtrip() {
270        let dir = tempfile::tempdir().expect("tempdir");
271        let path = dir
272            .path()
273            .join("test.kronroe")
274            .to_string_lossy()
275            .to_string();
276        let handle = KronroeGraphHandle::open(&path).expect("open");
277        handle
278            .assert_text("alice", "works_at", "Acme")
279            .expect("assert");
280        let json = handle.facts_about_json("alice").expect("facts_about");
281        let facts: serde_json::Value = serde_json::from_str(&json).expect("valid json");
282        let arr = facts.as_array().expect("json array");
283        assert_eq!(arr.len(), 1);
284        assert_eq!(arr[0]["subject"], "alice");
285    }
286
287    #[test]
288    fn error_propagation_empty_entity() {
289        let handle = KronroeGraphHandle::open_in_memory().expect("open_in_memory");
290        // Empty entity should return an empty array, not error
291        let json = handle.facts_about_json("").expect("facts_about");
292        let facts: serde_json::Value = serde_json::from_str(&json).expect("valid json");
293        let arr = facts.as_array().expect("json array");
294        assert!(arr.is_empty());
295    }
296
297    #[test]
298    fn last_error_set_and_cleared() {
299        // Verify the LAST_ERROR thread-local works the same as iOS
300        clear_last_error();
301        let msg = LAST_ERROR.with(|cell| cell.borrow().clone());
302        assert!(msg.is_none(), "error should be cleared");
303
304        set_last_error("graph handle is null".to_string());
305        let msg = LAST_ERROR.with(|cell| cell.borrow().clone());
306        assert_eq!(msg.as_deref(), Some("graph handle is null"));
307
308        clear_last_error();
309        let msg = LAST_ERROR.with(|cell| cell.borrow().clone());
310        assert!(msg.is_none(), "error should be cleared again");
311    }
312
313    #[test]
314    fn last_error_sanitizes_null_bytes() {
315        clear_last_error();
316        set_last_error("broken\0message".to_string());
317
318        let msg = LAST_ERROR.with(|cell| cell.borrow().clone());
319        assert_eq!(msg.as_deref(), Some("broken\\0message"));
320    }
321}