Skip to main content

elara_ffi/
lib.rs

1#![allow(clippy::missing_safety_doc)]
2//! ELARA FFI - Foreign Function Interface
3//!
4//! C-compatible bindings for mobile SDKs (Android/iOS).
5//! This crate provides a stable ABI for Kotlin and Swift wrappers.
6
7pub 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/// Library version
20#[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/// Initialize the ELARA library
27/// Must be called before any other functions
28/// Returns 0 on success, negative on error
29#[no_mangle]
30pub extern "C" fn elara_init() -> c_int {
31    // Initialize logging, runtime, etc.
32    0
33}
34
35/// Shutdown the ELARA library
36/// Should be called when done using the library
37#[no_mangle]
38pub extern "C" fn elara_shutdown() {
39    // Cleanup resources
40}
41
42/// Free a string allocated by ELARA
43#[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/// Free a byte buffer allocated by ELARA
51#[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}