kronroe_android/
android_bindings.rs1use chrono::Utc;
2use kronroe::TemporalGraph;
3use std::cell::RefCell;
4
5pub(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 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
60mod 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 unsafe fn handle_ref(handle: jlong) -> &'static KronroeGraphHandle {
76 unsafe { &*(handle as *const KronroeGraphHandle) }
77 }
78
79 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 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#[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 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 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}