1#![allow(clippy::missing_safety_doc)]
2pub mod error;
8pub mod identity;
9pub mod session;
10pub mod types;
11
12use std::ffi::{c_char, c_int, CString};
13
14pub use error::*;
15pub use identity::*;
16pub use session::*;
17pub use types::*;
18
19#[no_mangle]
21pub extern "C" fn elara_version() -> *const c_char {
22 static VERSION: &[u8] = b"0.1.0\0";
23 VERSION.as_ptr() as *const c_char
24}
25
26#[no_mangle]
30pub extern "C" fn elara_init() -> c_int {
31 0
33}
34
35#[no_mangle]
38pub extern "C" fn elara_shutdown() {
39 }
41
42#[no_mangle]
44pub unsafe extern "C" fn elara_free_string(s: *mut c_char) {
45 if !s.is_null() {
46 drop(CString::from_raw(s));
47 }
48}
49
50#[no_mangle]
52pub unsafe extern "C" fn elara_free_bytes(ptr: *mut u8, len: usize) {
53 if !ptr.is_null() {
54 drop(Vec::from_raw_parts(ptr, len, len));
55 }
56}
57
58#[cfg(target_os = "android")]
59use jni::objects::{JByteArray, JClass, JGlobalRef, JObject, JValue};
60#[cfg(target_os = "android")]
61use jni::sys::{jbyteArray, jfloatArray, jint, jlong, jobject, jstring};
62#[cfg(target_os = "android")]
63use jni::{JNIEnv, JavaVM};
64#[cfg(target_os = "android")]
65use std::collections::HashMap;
66#[cfg(target_os = "android")]
67use std::ffi::c_void;
68#[cfg(target_os = "android")]
69use std::ffi::CStr;
70#[cfg(target_os = "android")]
71use std::sync::{Mutex, OnceLock};
72
73#[cfg(target_os = "android")]
74struct AndroidCallbackState {
75 vm: JavaVM,
76 callback: JGlobalRef,
77}
78
79#[cfg(target_os = "android")]
80static ANDROID_CALLBACKS: OnceLock<Mutex<HashMap<usize, *mut AndroidCallbackState>>> =
81 OnceLock::new();
82
83#[cfg(target_os = "android")]
84fn android_callbacks() -> &'static Mutex<HashMap<usize, *mut AndroidCallbackState>> {
85 ANDROID_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()))
86}
87
88#[cfg(target_os = "android")]
89fn clear_android_callbacks(handle: *mut ElaraSessionHandle) {
90 if handle.is_null() {
91 return;
92 }
93 unsafe {
94 elara_session_clear_callbacks(handle);
95 }
96 let mut map = android_callbacks().lock().unwrap();
97 if let Some(ptr) = map.remove(&(handle as usize)) {
98 unsafe { drop(Box::from_raw(ptr)) };
99 }
100}
101
102#[cfg(target_os = "android")]
103unsafe extern "C" fn android_message_callback(
104 user_data: *mut c_void,
105 source: ElaraNodeId,
106 data: *const u8,
107 len: usize,
108) {
109 if user_data.is_null() || data.is_null() {
110 return;
111 }
112 let state = &*(user_data as *mut AndroidCallbackState);
113 let env = match state.vm.attach_current_thread() {
114 Ok(env) => env,
115 Err(_) => return,
116 };
117 let bytes = std::slice::from_raw_parts(data, len);
118 let array = match env.byte_array_from_slice(bytes) {
119 Ok(value) => value,
120 Err(_) => return,
121 };
122 let array_obj = JObject::from(array);
123 let _ = env.call_method(
124 state.callback.as_obj(),
125 "onMessage",
126 "(J[B)V",
127 &[
128 JValue::Long(source.value as jlong),
129 JValue::Object(&array_obj),
130 ],
131 );
132}
133
134#[cfg(target_os = "android")]
135unsafe extern "C" fn android_presence_callback(
136 user_data: *mut c_void,
137 node: ElaraNodeId,
138 presence: ElaraPresence,
139) {
140 if user_data.is_null() {
141 return;
142 }
143 let state = &*(user_data as *mut AndroidCallbackState);
144 let env = match state.vm.attach_current_thread() {
145 Ok(env) => env,
146 Err(_) => return,
147 };
148 let values = [
149 presence.liveness,
150 presence.immediacy,
151 presence.coherence,
152 presence.relational_continuity,
153 presence.emotional_bandwidth,
154 ];
155 let array = match env.new_float_array(values.len() as i32) {
156 Ok(value) => value,
157 Err(_) => return,
158 };
159 if env.set_float_array_region(array, 0, &values).is_err() {
160 return;
161 }
162 let array_obj = JObject::from(array);
163 let _ = env.call_method(
164 state.callback.as_obj(),
165 "onPresence",
166 "(J[F)V",
167 &[
168 JValue::Long(node.value as jlong),
169 JValue::Object(&array_obj),
170 ],
171 );
172}
173
174#[cfg(target_os = "android")]
175unsafe extern "C" fn android_degradation_callback(
176 user_data: *mut c_void,
177 level: ElaraDegradationLevel,
178) {
179 if user_data.is_null() {
180 return;
181 }
182 let state = &*(user_data as *mut AndroidCallbackState);
183 let env = match state.vm.attach_current_thread() {
184 Ok(env) => env,
185 Err(_) => return,
186 };
187 let _ = env.call_method(
188 state.callback.as_obj(),
189 "onDegradation",
190 "(I)V",
191 &[JValue::Int(level as jint)],
192 );
193}
194
195#[cfg(target_os = "android")]
196#[no_mangle]
197pub extern "system" fn Java_com_elara_sdk_Elara_nativeVersion(
198 env: JNIEnv,
199 _class: JClass,
200) -> jstring {
201 let c_str = unsafe { CStr::from_ptr(elara_version()) };
202 match env.new_string(c_str.to_string_lossy()) {
203 Ok(value) => value.into_raw(),
204 Err(_) => std::ptr::null_mut(),
205 }
206}
207
208#[cfg(target_os = "android")]
209#[no_mangle]
210pub extern "system" fn Java_com_elara_sdk_Elara_nativeInit(_env: JNIEnv, _class: JClass) -> jint {
211 elara_init() as jint
212}
213
214#[cfg(target_os = "android")]
215#[no_mangle]
216pub extern "system" fn Java_com_elara_sdk_Elara_nativeShutdown(_env: JNIEnv, _class: JClass) {
217 elara_shutdown();
218}
219
220#[cfg(target_os = "android")]
221#[no_mangle]
222pub extern "system" fn Java_com_elara_sdk_Identity_nativeGenerate(
223 _env: JNIEnv,
224 _class: JClass,
225) -> jlong {
226 let handle = elara_identity_generate();
227 handle as jlong
228}
229
230#[cfg(target_os = "android")]
231#[no_mangle]
232pub extern "system" fn Java_com_elara_sdk_Identity_nativeFree(
233 _env: JNIEnv,
234 _class: JClass,
235 handle: jlong,
236) {
237 if handle != 0 {
238 unsafe { elara_identity_free(handle as *mut ElaraIdentityHandle) };
239 }
240}
241
242#[cfg(target_os = "android")]
243#[no_mangle]
244pub extern "system" fn Java_com_elara_sdk_Identity_nativeNodeId(
245 _env: JNIEnv,
246 _class: JClass,
247 handle: jlong,
248) -> jlong {
249 if handle == 0 {
250 return 0;
251 }
252 let node_id = unsafe { elara_identity_node_id(handle as *const ElaraIdentityHandle) };
253 node_id.value as jlong
254}
255
256#[cfg(target_os = "android")]
257#[no_mangle]
258pub extern "system" fn Java_com_elara_sdk_Identity_nativePublicKey(
259 env: JNIEnv,
260 _class: JClass,
261 handle: jlong,
262) -> jbyteArray {
263 if handle == 0 {
264 return env
265 .byte_array_from_slice(&[])
266 .map(|v| v.into_raw())
267 .unwrap_or(std::ptr::null_mut());
268 }
269 let mut buf = [0u8; 32];
270 let written = unsafe {
271 elara_identity_public_key(
272 handle as *const ElaraIdentityHandle,
273 buf.as_mut_ptr(),
274 buf.len(),
275 )
276 };
277 if written <= 0 {
278 return env
279 .byte_array_from_slice(&[])
280 .map(|v| v.into_raw())
281 .unwrap_or(std::ptr::null_mut());
282 }
283 env.byte_array_from_slice(&buf[..written as usize])
284 .map(|v| v.into_raw())
285 .unwrap_or(std::ptr::null_mut())
286}
287
288#[cfg(target_os = "android")]
289#[no_mangle]
290pub extern "system" fn Java_com_elara_sdk_Identity_nativeExport(
291 env: JNIEnv,
292 _class: JClass,
293 handle: jlong,
294) -> jbyteArray {
295 if handle == 0 {
296 return env
297 .byte_array_from_slice(&[])
298 .map(|v| v.into_raw())
299 .unwrap_or(std::ptr::null_mut());
300 }
301 let bytes = unsafe { elara_identity_export(handle as *const ElaraIdentityHandle) };
302 if bytes.is_empty() {
303 return env
304 .byte_array_from_slice(&[])
305 .map(|v| v.into_raw())
306 .unwrap_or(std::ptr::null_mut());
307 }
308 let slice = unsafe { std::slice::from_raw_parts(bytes.data, bytes.len) };
309 let result = env.byte_array_from_slice(slice);
310 unsafe { elara_free_bytes(bytes.data, bytes.len) };
311 result.map(|v| v.into_raw()).unwrap_or(std::ptr::null_mut())
312}
313
314#[cfg(target_os = "android")]
315#[no_mangle]
316pub extern "system" fn Java_com_elara_sdk_Identity_nativeImport(
317 env: JNIEnv,
318 _class: JClass,
319 data: jbyteArray,
320) -> jlong {
321 let array = unsafe { JByteArray::from_raw(data) };
322 let bytes = match env.convert_byte_array(array) {
323 Ok(value) => value,
324 Err(_) => return 0,
325 };
326 let handle = unsafe { elara_identity_import(bytes.as_ptr(), bytes.len()) };
327 handle as jlong
328}
329
330#[cfg(target_os = "android")]
331#[no_mangle]
332pub extern "system" fn Java_com_elara_sdk_Session_nativeCreate(
333 _env: JNIEnv,
334 _class: JClass,
335 identity_handle: jlong,
336 session_id: jlong,
337) -> jlong {
338 if identity_handle == 0 {
339 return 0;
340 }
341 let session = unsafe {
342 elara_session_create(
343 identity_handle as *const ElaraIdentityHandle,
344 session_id as u64,
345 )
346 };
347 session as jlong
348}
349
350#[cfg(target_os = "android")]
351#[no_mangle]
352pub extern "system" fn Java_com_elara_sdk_Session_nativeFree(
353 _env: JNIEnv,
354 _class: JClass,
355 handle: jlong,
356) {
357 if handle != 0 {
358 clear_android_callbacks(handle as *mut ElaraSessionHandle);
359 unsafe { elara_session_free(handle as *mut ElaraSessionHandle) };
360 }
361}
362
363#[cfg(target_os = "android")]
364#[no_mangle]
365pub extern "system" fn Java_com_elara_sdk_Session_nativeSessionId(
366 _env: JNIEnv,
367 _class: JClass,
368 handle: jlong,
369) -> jlong {
370 if handle == 0 {
371 return 0;
372 }
373 let session_id = unsafe { elara_session_id(handle as *const ElaraSessionHandle) };
374 session_id.value as jlong
375}
376
377#[cfg(target_os = "android")]
378#[no_mangle]
379pub extern "system" fn Java_com_elara_sdk_Session_nativeNodeId(
380 _env: JNIEnv,
381 _class: JClass,
382 handle: jlong,
383) -> jlong {
384 if handle == 0 {
385 return 0;
386 }
387 let node_id = unsafe { elara_session_node_id(handle as *const ElaraSessionHandle) };
388 node_id.value as jlong
389}
390
391#[cfg(target_os = "android")]
392#[no_mangle]
393pub extern "system" fn Java_com_elara_sdk_Session_nativePresence(
394 env: JNIEnv,
395 _class: JClass,
396 handle: jlong,
397) -> jfloatArray {
398 if handle == 0 {
399 return env
400 .new_float_array(0)
401 .map(|v| v.into_raw())
402 .unwrap_or(std::ptr::null_mut());
403 }
404 let presence = unsafe { elara_session_presence(handle as *const ElaraSessionHandle) };
405 let values = [
406 presence.liveness,
407 presence.immediacy,
408 presence.coherence,
409 presence.relational_continuity,
410 presence.emotional_bandwidth,
411 ];
412 let array = match env.new_float_array(values.len() as i32) {
413 Ok(value) => value,
414 Err(_) => return std::ptr::null_mut(),
415 };
416 if env.set_float_array_region(array, 0, &values).is_err() {
417 return std::ptr::null_mut();
418 }
419 array.into_raw()
420}
421
422#[cfg(target_os = "android")]
423#[no_mangle]
424pub extern "system" fn Java_com_elara_sdk_Session_nativeDegradation(
425 _env: JNIEnv,
426 _class: JClass,
427 handle: jlong,
428) -> jint {
429 if handle == 0 {
430 return ElaraDegradationLevel::L5_LatentPresence as jint;
431 }
432 let level = unsafe { elara_session_degradation(handle as *const ElaraSessionHandle) };
433 level as jint
434}
435
436#[cfg(target_os = "android")]
437#[no_mangle]
438pub extern "system" fn Java_com_elara_sdk_Session_nativeSend(
439 env: JNIEnv,
440 _class: JClass,
441 handle: jlong,
442 dest: jlong,
443 data: jbyteArray,
444) -> jint {
445 if handle == 0 {
446 return ElaraErrorCode::InvalidArgument as jint;
447 }
448 let array = unsafe { JByteArray::from_raw(data) };
449 let bytes = match env.convert_byte_array(array) {
450 Ok(value) => value,
451 Err(_) => return ElaraErrorCode::InvalidArgument as jint,
452 };
453 unsafe {
454 elara_session_send(
455 handle as *mut ElaraSessionHandle,
456 ElaraNodeId { value: dest as u64 },
457 bytes.as_ptr(),
458 bytes.len(),
459 ) as jint
460 }
461}
462
463#[cfg(target_os = "android")]
464#[no_mangle]
465pub extern "system" fn Java_com_elara_sdk_Session_nativeReceive(
466 env: JNIEnv,
467 _class: JClass,
468 handle: jlong,
469 data: jbyteArray,
470) -> jint {
471 if handle == 0 {
472 return ElaraErrorCode::InvalidArgument as jint;
473 }
474 let array = unsafe { JByteArray::from_raw(data) };
475 let bytes = match env.convert_byte_array(array) {
476 Ok(value) => value,
477 Err(_) => return ElaraErrorCode::InvalidArgument as jint,
478 };
479 let result = unsafe {
480 elara_session_receive(
481 handle as *mut ElaraSessionHandle,
482 bytes.as_ptr(),
483 bytes.len(),
484 )
485 };
486 result as jint
487}
488
489#[cfg(target_os = "android")]
490#[no_mangle]
491pub extern "system" fn Java_com_elara_sdk_Session_nativeSetSessionKey(
492 env: JNIEnv,
493 _class: JClass,
494 handle: jlong,
495 session_id: jlong,
496 key: jbyteArray,
497) -> jint {
498 if handle == 0 {
499 return ElaraErrorCode::InvalidArgument as jint;
500 }
501 let array = unsafe { JByteArray::from_raw(key) };
502 let bytes = match env.convert_byte_array(array) {
503 Ok(value) => value,
504 Err(_) => return ElaraErrorCode::InvalidArgument as jint,
505 };
506 unsafe {
507 elara_session_set_session_key(
508 handle as *mut ElaraSessionHandle,
509 session_id as u64,
510 bytes.as_ptr(),
511 bytes.len(),
512 ) as jint
513 }
514}
515
516#[cfg(target_os = "android")]
517#[no_mangle]
518pub extern "system" fn Java_com_elara_sdk_Session_nativeTick(
519 _env: JNIEnv,
520 _class: JClass,
521 handle: jlong,
522) -> jint {
523 if handle == 0 {
524 return ElaraErrorCode::InvalidArgument as jint;
525 }
526 unsafe { elara_session_tick(handle as *mut ElaraSessionHandle) as jint }
527}
528
529#[cfg(target_os = "android")]
530#[no_mangle]
531pub extern "system" fn Java_com_elara_sdk_Session_nativeSetCallback(
532 env: JNIEnv,
533 _class: JClass,
534 handle: jlong,
535 callback: jobject,
536) -> jint {
537 if handle == 0 {
538 return ElaraErrorCode::InvalidArgument as jint;
539 }
540 let handle_ptr = handle as *mut ElaraSessionHandle;
541 if callback.is_null() {
542 clear_android_callbacks(handle_ptr);
543 return 0;
544 }
545 let vm = match env.get_java_vm() {
546 Ok(value) => value,
547 Err(_) => return ElaraErrorCode::InternalError as jint,
548 };
549 let callback_obj = unsafe { JObject::from_raw(callback) };
550 let global = match env.new_global_ref(callback_obj) {
551 Ok(value) => value,
552 Err(_) => return ElaraErrorCode::InternalError as jint,
553 };
554 clear_android_callbacks(handle_ptr);
555 let state = Box::new(AndroidCallbackState {
556 vm,
557 callback: global,
558 });
559 let state_ptr = Box::into_raw(state);
560 let user_data = state_ptr as *mut c_void;
561 let result = unsafe {
562 elara_session_set_message_callback(handle_ptr, android_message_callback, user_data)
563 };
564 if result != 0 {
565 unsafe { drop(Box::from_raw(state_ptr)) };
566 return result as jint;
567 }
568 let result = unsafe {
569 elara_session_set_presence_callback(handle_ptr, android_presence_callback, user_data)
570 };
571 if result != 0 {
572 unsafe { drop(Box::from_raw(state_ptr)) };
573 return result as jint;
574 }
575 let result = unsafe {
576 elara_session_set_degradation_callback(handle_ptr, android_degradation_callback, user_data)
577 };
578 if result != 0 {
579 unsafe { drop(Box::from_raw(state_ptr)) };
580 return result as jint;
581 }
582 let mut map = android_callbacks().lock().unwrap();
583 map.insert(handle_ptr as usize, state_ptr);
584 0
585}
586
587#[cfg(target_os = "android")]
588#[no_mangle]
589pub extern "system" fn Java_com_elara_sdk_Session_nativeFeedStream(
590 env: JNIEnv,
591 _class: JClass,
592 handle: jlong,
593 stream_id: jlong,
594) -> jbyteArray {
595 if handle == 0 {
596 return env
597 .byte_array_from_slice(&[])
598 .map(|v| v.into_raw())
599 .unwrap_or(std::ptr::null_mut());
600 }
601 let bytes =
602 unsafe { elara_session_feed_stream(handle as *mut ElaraSessionHandle, stream_id as u64) };
603 if bytes.is_empty() {
604 return env
605 .byte_array_from_slice(&[])
606 .map(|v| v.into_raw())
607 .unwrap_or(std::ptr::null_mut());
608 }
609 let slice = unsafe { std::slice::from_raw_parts(bytes.data, bytes.len) };
610 let result = env.byte_array_from_slice(slice);
611 unsafe { elara_free_bytes(bytes.data, bytes.len) };
612 result.map(|v| v.into_raw()).unwrap_or(std::ptr::null_mut())
613}
614
615#[cfg(target_os = "android")]
616#[no_mangle]
617pub extern "system" fn Java_com_elara_sdk_Session_nativeStreamMetadata(
618 env: JNIEnv,
619 _class: JClass,
620 handle: jlong,
621 stream_id: jlong,
622) -> jbyteArray {
623 if handle == 0 {
624 return env
625 .byte_array_from_slice(&[])
626 .map(|v| v.into_raw())
627 .unwrap_or(std::ptr::null_mut());
628 }
629 let bytes = unsafe {
630 elara_session_stream_metadata(handle as *mut ElaraSessionHandle, stream_id as u64)
631 };
632 if bytes.is_empty() {
633 return env
634 .byte_array_from_slice(&[])
635 .map(|v| v.into_raw())
636 .unwrap_or(std::ptr::null_mut());
637 }
638 let slice = unsafe { std::slice::from_raw_parts(bytes.data, bytes.len) };
639 let result = env.byte_array_from_slice(slice);
640 unsafe { elara_free_bytes(bytes.data, bytes.len) };
641 result.map(|v| v.into_raw()).unwrap_or(std::ptr::null_mut())
642}