Skip to main content

syncular_runtime/native/
ffi.rs

1use crate::error::{ErrorKind, Result, SyncularError};
2use crate::native::{
3    native_runtime_manifest_json, NativeClientConfig, NativeClientOpenTask, NativeClientOptions,
4    NativeErrorInfo, NativeEventSubscription, NativeSyncularClient,
5};
6use std::any::Any;
7use std::ffi::{CStr, CString};
8use std::os::raw::{c_char, c_void};
9use std::panic::{catch_unwind, AssertUnwindSafe};
10use std::ptr;
11use std::sync::{Arc, Mutex};
12use std::thread::{self, JoinHandle};
13use std::time::Duration;
14
15pub struct SyncularNativeHandle {
16    client: Mutex<NativeSyncularClient>,
17}
18
19pub struct SyncularNativeOpenHandle {
20    task: Mutex<NativeClientOpenTask>,
21}
22
23pub struct SyncularNativeEventSubscription {
24    subscription: Arc<NativeEventSubscription>,
25    join: Mutex<Option<JoinHandle<()>>>,
26}
27
28pub struct SyncularNativePresenceHandle {
29    client: *mut SyncularNativeHandle,
30    scope_key: String,
31    active: Mutex<bool>,
32}
33
34pub type SyncularNativeEventCallback =
35    extern "C" fn(event_json: *const c_char, user_data: *mut c_void);
36pub type SyncularNativeEventErrorCallback =
37    extern "C" fn(error_json: *const c_char, user_data: *mut c_void);
38
39#[no_mangle]
40pub extern "C" fn syncular_string_free(value: *mut c_char) {
41    if value.is_null() {
42        return;
43    }
44
45    unsafe {
46        let _ = CString::from_raw(value);
47    }
48}
49
50#[no_mangle]
51pub extern "C" fn syncular_native_runtime_manifest_json(
52    error_out: *mut *mut c_char,
53) -> *mut c_char {
54    clear_error(error_out);
55    ffi_catch_string(error_out, native_runtime_manifest_json)
56}
57
58#[no_mangle]
59pub extern "C" fn syncular_native_client_open(
60    config_json: *const c_char,
61    auto_sync_local_writes: bool,
62    error_out: *mut *mut c_char,
63) -> *mut SyncularNativeHandle {
64    clear_error(error_out);
65    ffi_catch_ptr(error_out, || {
66        let config: NativeClientConfig = serde_json::from_str(&read_c_string(config_json)?)?;
67        let client = NativeSyncularClient::open_native_with_options(
68            config,
69            NativeClientOptions {
70                auto_sync_local_writes,
71            },
72        )?;
73        Ok(Box::into_raw(Box::new(SyncularNativeHandle {
74            client: Mutex::new(client),
75        })))
76    })
77}
78
79#[no_mangle]
80pub extern "C" fn syncular_native_client_open_async(
81    config_json: *const c_char,
82    auto_sync_local_writes: bool,
83    error_out: *mut *mut c_char,
84) -> *mut SyncularNativeOpenHandle {
85    clear_error(error_out);
86    ffi_catch_ptr(error_out, || {
87        let config: NativeClientConfig = serde_json::from_str(&read_c_string(config_json)?)?;
88        let task = NativeSyncularClient::open_native_async_with_options(
89            config,
90            NativeClientOptions {
91                auto_sync_local_writes,
92            },
93        );
94        Ok(Box::into_raw(Box::new(SyncularNativeOpenHandle {
95            task: Mutex::new(task),
96        })))
97    })
98}
99
100#[no_mangle]
101pub extern "C" fn syncular_native_client_open_async_command_id(
102    handle: *mut SyncularNativeOpenHandle,
103    error_out: *mut *mut c_char,
104) -> *mut c_char {
105    clear_error(error_out);
106    ffi_catch_string(error_out, || {
107        with_open_task(handle, |task| Ok(task.command_id().to_string()))
108    })
109}
110
111#[no_mangle]
112pub extern "C" fn syncular_native_client_open_async_is_finished(
113    handle: *mut SyncularNativeOpenHandle,
114    error_out: *mut *mut c_char,
115) -> bool {
116    clear_error(error_out);
117    ffi_catch_bool_value(error_out, || {
118        with_open_task(handle, |task| Ok(task.is_finished()))
119    })
120}
121
122#[no_mangle]
123pub extern "C" fn syncular_native_client_open_async_finish_timeout(
124    handle: *mut SyncularNativeOpenHandle,
125    timeout_ms: u64,
126    error_out: *mut *mut c_char,
127) -> *mut SyncularNativeHandle {
128    clear_error(error_out);
129    ffi_catch_ptr(error_out, || {
130        with_open_task(handle, |task| {
131            match task.take_client_timeout(Duration::from_millis(timeout_ms)) {
132                Some(Ok(client)) => Ok(Box::into_raw(Box::new(SyncularNativeHandle {
133                    client: Mutex::new(client),
134                }))),
135                Some(Err(error)) => Err(error),
136                None => Ok(ptr::null_mut()),
137            }
138        })
139    })
140}
141
142#[no_mangle]
143pub extern "C" fn syncular_native_client_open_async_close(
144    handle: *mut SyncularNativeOpenHandle,
145    error_out: *mut *mut c_char,
146) -> bool {
147    clear_error(error_out);
148    ffi_catch_bool(error_out, || {
149        if handle.is_null() {
150            return Ok(());
151        }
152
153        let _ = unsafe { Box::from_raw(handle) };
154        Ok(())
155    })
156}
157
158#[no_mangle]
159pub extern "C" fn syncular_native_client_close(
160    handle: *mut SyncularNativeHandle,
161    error_out: *mut *mut c_char,
162) -> bool {
163    clear_error(error_out);
164    ffi_catch_bool(error_out, || {
165        if handle.is_null() {
166            return Ok(());
167        }
168
169        let boxed = unsafe { Box::from_raw(handle) };
170        let mut client = boxed.client.into_inner().map_err(|_| {
171            SyncularError::message(ErrorKind::Internal, "native handle is poisoned")
172        })?;
173        client.close()
174    })
175}
176
177#[no_mangle]
178pub extern "C" fn syncular_native_client_trigger_sync(
179    handle: *mut SyncularNativeHandle,
180    error_out: *mut *mut c_char,
181) -> bool {
182    clear_error(error_out);
183    ffi_catch_bool(error_out, || {
184        with_client(handle, |client| client.trigger_sync())
185    })
186}
187
188#[no_mangle]
189pub extern "C" fn syncular_native_client_trigger_sync_websocket(
190    handle: *mut SyncularNativeHandle,
191    error_out: *mut *mut c_char,
192) -> bool {
193    clear_error(error_out);
194    ffi_catch_bool(error_out, || {
195        with_client(handle, |client| client.trigger_sync_websocket())
196    })
197}
198
199#[no_mangle]
200pub extern "C" fn syncular_native_client_pause_sync_worker(
201    handle: *mut SyncularNativeHandle,
202    error_out: *mut *mut c_char,
203) -> bool {
204    clear_error(error_out);
205    ffi_catch_bool(error_out, || {
206        with_client(handle, |client| client.pause_sync_worker())
207    })
208}
209
210#[no_mangle]
211pub extern "C" fn syncular_native_client_resume_sync_worker(
212    handle: *mut SyncularNativeHandle,
213    error_out: *mut *mut c_char,
214) -> bool {
215    clear_error(error_out);
216    ffi_catch_bool(error_out, || {
217        with_client(handle, |client| client.resume_sync_worker())
218    })
219}
220
221#[no_mangle]
222pub extern "C" fn syncular_native_client_resume_from_background(
223    handle: *mut SyncularNativeHandle,
224    error_out: *mut *mut c_char,
225) -> *mut c_char {
226    clear_error(error_out);
227    ffi_catch_string(error_out, || {
228        with_client(handle, |client| client.resume_from_background())
229    })
230}
231
232#[no_mangle]
233pub extern "C" fn syncular_native_client_sync_worker_running(
234    handle: *mut SyncularNativeHandle,
235    error_out: *mut *mut c_char,
236) -> bool {
237    clear_error(error_out);
238    ffi_catch_bool_value(error_out, || {
239        with_client(handle, |client| Ok(client.sync_worker_running()))
240    })
241}
242
243#[no_mangle]
244pub extern "C" fn syncular_native_client_start_realtime_worker(
245    handle: *mut SyncularNativeHandle,
246    error_out: *mut *mut c_char,
247) -> bool {
248    clear_error(error_out);
249    ffi_catch_bool(error_out, || {
250        with_client(handle, |client| client.start_realtime_worker())
251    })
252}
253
254#[no_mangle]
255pub extern "C" fn syncular_native_client_stop_realtime_worker(
256    handle: *mut SyncularNativeHandle,
257    error_out: *mut *mut c_char,
258) -> bool {
259    clear_error(error_out);
260    ffi_catch_bool(error_out, || {
261        with_client(handle, |client| client.stop_realtime_worker())
262    })
263}
264
265#[no_mangle]
266pub extern "C" fn syncular_native_client_join_presence(
267    handle: *mut SyncularNativeHandle,
268    scope_key: *const c_char,
269    metadata_json: *const c_char,
270    error_out: *mut *mut c_char,
271) -> bool {
272    clear_error(error_out);
273    ffi_catch_bool(error_out, || {
274        let scope_key = read_c_string(scope_key)?;
275        let metadata = read_optional_c_string(metadata_json)?
276            .map(|json| serde_json::from_str(&json))
277            .transpose()?;
278        with_client(handle, |client| client.join_presence(&scope_key, metadata))
279    })
280}
281
282#[no_mangle]
283pub extern "C" fn syncular_native_client_leave_presence(
284    handle: *mut SyncularNativeHandle,
285    scope_key: *const c_char,
286    error_out: *mut *mut c_char,
287) -> bool {
288    clear_error(error_out);
289    ffi_catch_bool(error_out, || {
290        let scope_key = read_c_string(scope_key)?;
291        with_client(handle, |client| client.leave_presence(&scope_key))
292    })
293}
294
295#[no_mangle]
296pub extern "C" fn syncular_native_client_update_presence_metadata(
297    handle: *mut SyncularNativeHandle,
298    scope_key: *const c_char,
299    metadata_json: *const c_char,
300    error_out: *mut *mut c_char,
301) -> bool {
302    clear_error(error_out);
303    ffi_catch_bool(error_out, || {
304        let scope_key = read_c_string(scope_key)?;
305        let metadata_json = read_c_string(metadata_json)?;
306        let metadata = serde_json::from_str(&metadata_json)?;
307        with_client(handle, |client| {
308            client.update_presence_metadata(&scope_key, metadata)
309        })
310    })
311}
312
313#[no_mangle]
314pub extern "C" fn syncular_native_client_presence_json(
315    handle: *mut SyncularNativeHandle,
316    scope_key: *const c_char,
317    error_out: *mut *mut c_char,
318) -> *mut c_char {
319    clear_error(error_out);
320    ffi_catch_string(error_out, || {
321        let scope_key = read_c_string(scope_key)?;
322        with_client(handle, |client| client.presence_json(&scope_key))
323    })
324}
325
326#[no_mangle]
327pub extern "C" fn syncular_native_client_join_presence_handle(
328    handle: *mut SyncularNativeHandle,
329    scope_key: *const c_char,
330    metadata_json: *const c_char,
331    error_out: *mut *mut c_char,
332) -> *mut SyncularNativePresenceHandle {
333    clear_error(error_out);
334    ffi_catch_ptr(error_out, || {
335        let scope_key = read_c_string(scope_key)?;
336        let metadata = read_optional_c_string(metadata_json)?
337            .map(|json| serde_json::from_str(&json))
338            .transpose()?;
339        with_client(handle, |client| client.join_presence(&scope_key, metadata))?;
340        Ok(Box::into_raw(Box::new(SyncularNativePresenceHandle {
341            client: handle,
342            scope_key,
343            active: Mutex::new(true),
344        })))
345    })
346}
347
348#[no_mangle]
349pub extern "C" fn syncular_native_presence_handle_scope_key(
350    handle: *mut SyncularNativePresenceHandle,
351    error_out: *mut *mut c_char,
352) -> *mut c_char {
353    clear_error(error_out);
354    ffi_catch_string(error_out, || {
355        with_presence_handle(handle, |presence| Ok(presence.scope_key.clone()))
356    })
357}
358
359#[no_mangle]
360pub extern "C" fn syncular_native_presence_handle_update_metadata(
361    handle: *mut SyncularNativePresenceHandle,
362    metadata_json: *const c_char,
363    error_out: *mut *mut c_char,
364) -> bool {
365    clear_error(error_out);
366    ffi_catch_bool(error_out, || {
367        let metadata_json = read_c_string(metadata_json)?;
368        let metadata = serde_json::from_str(&metadata_json)?;
369        with_presence_handle(handle, |presence| {
370            let active = presence.active.lock().map_err(|_| {
371                SyncularError::message(ErrorKind::Internal, "native presence handle is poisoned")
372            })?;
373            if !*active {
374                return Err(SyncularError::message(
375                    ErrorKind::Config,
376                    "native presence handle is inactive",
377                ));
378            }
379            with_client(presence.client, |client| {
380                client.update_presence_metadata(&presence.scope_key, metadata)
381            })
382        })
383    })
384}
385
386#[no_mangle]
387pub extern "C" fn syncular_native_presence_handle_leave(
388    handle: *mut SyncularNativePresenceHandle,
389    error_out: *mut *mut c_char,
390) -> bool {
391    clear_error(error_out);
392    ffi_catch_bool_value(error_out, || {
393        with_presence_handle(handle, leave_presence_handle_inner)
394    })
395}
396
397#[no_mangle]
398pub extern "C" fn syncular_native_presence_handle_close(
399    handle: *mut SyncularNativePresenceHandle,
400    error_out: *mut *mut c_char,
401) -> bool {
402    clear_error(error_out);
403    ffi_catch_bool(error_out, || {
404        if handle.is_null() {
405            return Ok(());
406        }
407
408        let presence = unsafe { Box::from_raw(handle) };
409        let _ = leave_presence_handle_inner(&presence)?;
410        Ok(())
411    })
412}
413
414#[no_mangle]
415pub extern "C" fn syncular_native_client_set_auth_headers_json(
416    handle: *mut SyncularNativeHandle,
417    headers_json: *const c_char,
418    error_out: *mut *mut c_char,
419) -> bool {
420    clear_error(error_out);
421    ffi_catch_bool(error_out, || {
422        let headers_json = read_c_string(headers_json)?;
423        with_client(handle, |client| client.set_auth_headers_json(&headers_json))
424    })
425}
426
427#[no_mangle]
428pub extern "C" fn syncular_native_client_set_subscriptions_json(
429    handle: *mut SyncularNativeHandle,
430    subscriptions_json: *const c_char,
431    error_out: *mut *mut c_char,
432) -> bool {
433    clear_error(error_out);
434    ffi_catch_bool(error_out, || {
435        let subscriptions_json = read_c_string(subscriptions_json)?;
436        with_client(handle, |client| {
437            client.set_subscriptions_json(&subscriptions_json)
438        })
439    })
440}
441
442#[no_mangle]
443pub extern "C" fn syncular_native_client_force_subscriptions_bootstrap_json(
444    handle: *mut SyncularNativeHandle,
445    subscription_ids_json: *const c_char,
446    error_out: *mut *mut c_char,
447) -> *mut c_char {
448    clear_error(error_out);
449    ffi_catch_string(error_out, || {
450        let subscription_ids_json = read_c_string(subscription_ids_json)?;
451        with_client(handle, |client| {
452            client.force_subscriptions_bootstrap_json(&subscription_ids_json)
453        })
454    })
455}
456
457#[no_mangle]
458pub extern "C" fn syncular_native_client_set_field_encryption_json(
459    handle: *mut SyncularNativeHandle,
460    config_json: *const c_char,
461    error_out: *mut *mut c_char,
462) -> bool {
463    clear_error(error_out);
464    ffi_catch_bool(error_out, || {
465        let config_json = read_c_string(config_json)?;
466        with_client(handle, |client| {
467            client.set_field_encryption_json(&config_json)
468        })
469    })
470}
471
472#[no_mangle]
473pub extern "C" fn syncular_native_client_set_encrypted_crdt_json(
474    handle: *mut SyncularNativeHandle,
475    config_json: *const c_char,
476    error_out: *mut *mut c_char,
477) -> bool {
478    clear_error(error_out);
479    ffi_catch_bool(error_out, || {
480        let config_json = read_c_string(config_json)?;
481        with_client(handle, |client| {
482            client.set_encrypted_crdt_json(&config_json)
483        })
484    })
485}
486
487#[no_mangle]
488pub extern "C" fn syncular_native_client_set_blob_encryption_json(
489    handle: *mut SyncularNativeHandle,
490    config_json: *const c_char,
491    error_out: *mut *mut c_char,
492) -> bool {
493    clear_error(error_out);
494    ffi_catch_bool(error_out, || {
495        let config_json = read_c_string(config_json)?;
496        with_client(handle, |client| {
497            client.set_blob_encryption_json(&config_json)
498        })
499    })
500}
501
502#[no_mangle]
503pub extern "C" fn syncular_native_encryption_helper_json(
504    method: *const c_char,
505    args_json: *const c_char,
506    error_out: *mut *mut c_char,
507) -> *mut c_char {
508    clear_error(error_out);
509    ffi_catch_string(error_out, || {
510        let method = read_c_string(method)?;
511        let args_json = read_c_string(args_json)?;
512        crate::encryption::encryption_helpers_json(&method, &args_json)
513    })
514}
515
516#[no_mangle]
517pub extern "C" fn syncular_native_client_apply_mutation_json(
518    handle: *mut SyncularNativeHandle,
519    mutation_json: *const c_char,
520    local_row_json: *const c_char,
521    error_out: *mut *mut c_char,
522) -> *mut c_char {
523    clear_error(error_out);
524    ffi_catch_string(error_out, || {
525        let mutation_json = read_c_string(mutation_json)?;
526        let local_row_json = read_optional_c_string(local_row_json)?;
527        with_client(handle, |client| {
528            client.apply_mutation_json(&mutation_json, local_row_json.as_deref())
529        })
530    })
531}
532
533#[no_mangle]
534pub extern "C" fn syncular_native_client_apply_leased_mutation_json(
535    handle: *mut SyncularNativeHandle,
536    mutation_json: *const c_char,
537    local_row_json: *const c_char,
538    error_out: *mut *mut c_char,
539) -> *mut c_char {
540    clear_error(error_out);
541    ffi_catch_string(error_out, || {
542        let mutation_json = read_c_string(mutation_json)?;
543        let local_row_json = read_optional_c_string(local_row_json)?;
544        with_client(handle, |client| {
545            client.apply_leased_mutation_json(&mutation_json, local_row_json.as_deref())
546        })
547    })
548}
549
550#[no_mangle]
551pub extern "C" fn syncular_native_client_enqueue_mutation_json(
552    handle: *mut SyncularNativeHandle,
553    mutation_json: *const c_char,
554    local_row_json: *const c_char,
555    error_out: *mut *mut c_char,
556) -> *mut c_char {
557    clear_error(error_out);
558    ffi_catch_string(error_out, || {
559        let mutation_json = read_c_string(mutation_json)?;
560        let local_row_json = read_optional_c_string(local_row_json)?;
561        with_client(handle, |client| {
562            client.enqueue_mutation_json(&mutation_json, local_row_json.as_deref())
563        })
564    })
565}
566
567#[no_mangle]
568pub extern "C" fn syncular_native_client_enqueue_leased_mutation_json(
569    handle: *mut SyncularNativeHandle,
570    mutation_json: *const c_char,
571    local_row_json: *const c_char,
572    error_out: *mut *mut c_char,
573) -> *mut c_char {
574    clear_error(error_out);
575    ffi_catch_string(error_out, || {
576        let mutation_json = read_c_string(mutation_json)?;
577        let local_row_json = read_optional_c_string(local_row_json)?;
578        with_client(handle, |client| {
579            client.enqueue_leased_mutation_json(&mutation_json, local_row_json.as_deref())
580        })
581    })
582}
583
584#[no_mangle]
585pub extern "C" fn syncular_native_client_enqueue_yjs_update_json(
586    handle: *mut SyncularNativeHandle,
587    update_json: *const c_char,
588    error_out: *mut *mut c_char,
589) -> *mut c_char {
590    clear_error(error_out);
591    ffi_catch_string(error_out, || {
592        let update_json = read_c_string(update_json)?;
593        with_client(handle, |client| {
594            client.enqueue_yjs_update_json(&update_json)
595        })
596    })
597}
598
599#[no_mangle]
600pub extern "C" fn syncular_native_client_open_crdt_field_json(
601    handle: *mut SyncularNativeHandle,
602    request_json: *const c_char,
603    error_out: *mut *mut c_char,
604) -> *mut c_char {
605    clear_error(error_out);
606    ffi_catch_string(error_out, || {
607        let request_json = read_c_string(request_json)?;
608        with_client(handle, |client| client.open_crdt_field_json(&request_json))
609    })
610}
611
612#[no_mangle]
613pub extern "C" fn syncular_native_client_apply_crdt_field_text_json(
614    handle: *mut SyncularNativeHandle,
615    request_json: *const c_char,
616    error_out: *mut *mut c_char,
617) -> *mut c_char {
618    clear_error(error_out);
619    ffi_catch_string(error_out, || {
620        let request_json = read_c_string(request_json)?;
621        with_client(handle, |client| {
622            client.apply_crdt_field_text_json(&request_json)
623        })
624    })
625}
626
627#[no_mangle]
628pub extern "C" fn syncular_native_client_apply_crdt_field_yjs_update_json(
629    handle: *mut SyncularNativeHandle,
630    request_json: *const c_char,
631    error_out: *mut *mut c_char,
632) -> *mut c_char {
633    clear_error(error_out);
634    ffi_catch_string(error_out, || {
635        let request_json = read_c_string(request_json)?;
636        with_client(handle, |client| {
637            client.apply_crdt_field_yjs_update_json(&request_json)
638        })
639    })
640}
641
642#[no_mangle]
643pub extern "C" fn syncular_native_client_enqueue_crdt_field_yjs_update_json(
644    handle: *mut SyncularNativeHandle,
645    request_json: *const c_char,
646    error_out: *mut *mut c_char,
647) -> *mut c_char {
648    clear_error(error_out);
649    ffi_catch_string(error_out, || {
650        let request_json = read_c_string(request_json)?;
651        with_client(handle, |client| {
652            client.enqueue_crdt_field_yjs_update_json(&request_json)
653        })
654    })
655}
656
657#[no_mangle]
658pub extern "C" fn syncular_native_client_enqueue_crdt_field_text_json(
659    handle: *mut SyncularNativeHandle,
660    request_json: *const c_char,
661    error_out: *mut *mut c_char,
662) -> *mut c_char {
663    clear_error(error_out);
664    ffi_catch_string(error_out, || {
665        let request_json = read_c_string(request_json)?;
666        with_client(handle, |client| {
667            client.enqueue_crdt_field_text_json(&request_json)
668        })
669    })
670}
671
672#[no_mangle]
673pub extern "C" fn syncular_native_client_enqueue_crdt_field_compaction_json(
674    handle: *mut SyncularNativeHandle,
675    request_json: *const c_char,
676    error_out: *mut *mut c_char,
677) -> *mut c_char {
678    clear_error(error_out);
679    ffi_catch_string(error_out, || {
680        let request_json = read_c_string(request_json)?;
681        with_client(handle, |client| {
682            client.enqueue_crdt_field_compaction_json(&request_json)
683        })
684    })
685}
686
687#[no_mangle]
688pub extern "C" fn syncular_native_client_materialize_crdt_field_json(
689    handle: *mut SyncularNativeHandle,
690    request_json: *const c_char,
691    error_out: *mut *mut c_char,
692) -> *mut c_char {
693    clear_error(error_out);
694    ffi_catch_string(error_out, || {
695        let request_json = read_c_string(request_json)?;
696        with_client(handle, |client| {
697            client.materialize_crdt_field_json(&request_json)
698        })
699    })
700}
701
702#[no_mangle]
703pub extern "C" fn syncular_native_client_crdt_document_snapshot_json(
704    handle: *mut SyncularNativeHandle,
705    request_json: *const c_char,
706    error_out: *mut *mut c_char,
707) -> *mut c_char {
708    clear_error(error_out);
709    ffi_catch_string(error_out, || {
710        let request_json = read_c_string(request_json)?;
711        with_client(handle, |client| {
712            client.crdt_document_snapshot_json(&request_json)
713        })
714    })
715}
716
717#[no_mangle]
718pub extern "C" fn syncular_native_client_crdt_update_log_json(
719    handle: *mut SyncularNativeHandle,
720    request_json: *const c_char,
721    error_out: *mut *mut c_char,
722) -> *mut c_char {
723    clear_error(error_out);
724    ffi_catch_string(error_out, || {
725        let request_json = read_c_string(request_json)?;
726        with_client(handle, |client| client.crdt_update_log_json(&request_json))
727    })
728}
729
730#[no_mangle]
731pub extern "C" fn syncular_native_client_snapshot_crdt_field_state_vector_json(
732    handle: *mut SyncularNativeHandle,
733    request_json: *const c_char,
734    error_out: *mut *mut c_char,
735) -> *mut c_char {
736    clear_error(error_out);
737    ffi_catch_string(error_out, || {
738        let request_json = read_c_string(request_json)?;
739        with_client(handle, |client| {
740            client.snapshot_crdt_field_state_vector_json(&request_json)
741        })
742    })
743}
744
745#[no_mangle]
746pub extern "C" fn syncular_native_client_compact_crdt_field_json(
747    handle: *mut SyncularNativeHandle,
748    request_json: *const c_char,
749    error_out: *mut *mut c_char,
750) -> *mut c_char {
751    clear_error(error_out);
752    ffi_catch_string(error_out, || {
753        let request_json = read_c_string(request_json)?;
754        with_client(handle, |client| {
755            client.compact_crdt_field_json(&request_json)
756        })
757    })
758}
759
760#[no_mangle]
761pub extern "C" fn syncular_native_client_apply_encrypted_crdt_update_json(
762    handle: *mut SyncularNativeHandle,
763    request_json: *const c_char,
764    error_out: *mut *mut c_char,
765) -> *mut c_char {
766    clear_error(error_out);
767    ffi_catch_string(error_out, || {
768        let request_json = read_c_string(request_json)?;
769        with_client(handle, |client| {
770            client.apply_encrypted_crdt_update_json(&request_json)
771        })
772    })
773}
774
775#[no_mangle]
776pub extern "C" fn syncular_native_client_enqueue_encrypted_crdt_update_json(
777    handle: *mut SyncularNativeHandle,
778    request_json: *const c_char,
779    error_out: *mut *mut c_char,
780) -> *mut c_char {
781    clear_error(error_out);
782    ffi_catch_string(error_out, || {
783        let request_json = read_c_string(request_json)?;
784        with_client(handle, |client| {
785            client.enqueue_encrypted_crdt_update_json(&request_json)
786        })
787    })
788}
789
790#[no_mangle]
791pub extern "C" fn syncular_native_client_apply_encrypted_crdt_checkpoint_json(
792    handle: *mut SyncularNativeHandle,
793    request_json: *const c_char,
794    error_out: *mut *mut c_char,
795) -> *mut c_char {
796    clear_error(error_out);
797    ffi_catch_string(error_out, || {
798        let request_json = read_c_string(request_json)?;
799        with_client(handle, |client| {
800            client.apply_encrypted_crdt_checkpoint_json(&request_json)
801        })
802    })
803}
804
805#[no_mangle]
806pub extern "C" fn syncular_native_client_enqueue_encrypted_crdt_checkpoint_json(
807    handle: *mut SyncularNativeHandle,
808    request_json: *const c_char,
809    error_out: *mut *mut c_char,
810) -> *mut c_char {
811    clear_error(error_out);
812    ffi_catch_string(error_out, || {
813        let request_json = read_c_string(request_json)?;
814        with_client(handle, |client| {
815            client.enqueue_encrypted_crdt_checkpoint_json(&request_json)
816        })
817    })
818}
819
820#[no_mangle]
821pub extern "C" fn syncular_native_client_enqueue_sync_now(
822    handle: *mut SyncularNativeHandle,
823    error_out: *mut *mut c_char,
824) -> *mut c_char {
825    clear_error(error_out);
826    ffi_catch_string(error_out, || {
827        with_client(handle, |client| client.enqueue_sync_now())
828    })
829}
830
831#[no_mangle]
832pub extern "C" fn syncular_native_client_enqueue_sync_websocket(
833    handle: *mut SyncularNativeHandle,
834    error_out: *mut *mut c_char,
835) -> *mut c_char {
836    clear_error(error_out);
837    ffi_catch_string(error_out, || {
838        with_client(handle, |client| client.enqueue_sync_websocket())
839    })
840}
841
842#[no_mangle]
843pub extern "C" fn syncular_native_client_app_tables_json(
844    handle: *mut SyncularNativeHandle,
845    error_out: *mut *mut c_char,
846) -> *mut c_char {
847    clear_error(error_out);
848    ffi_catch_string(error_out, || {
849        with_client(handle, |client| client.app_tables_json())
850    })
851}
852
853#[no_mangle]
854pub extern "C" fn syncular_native_client_app_table_metadata_json(
855    handle: *mut SyncularNativeHandle,
856    error_out: *mut *mut c_char,
857) -> *mut c_char {
858    clear_error(error_out);
859    ffi_catch_string(error_out, || {
860        with_client(handle, |client| client.app_table_metadata_json())
861    })
862}
863
864#[no_mangle]
865pub extern "C" fn syncular_native_client_app_schema_state_json(
866    handle: *mut SyncularNativeHandle,
867    error_out: *mut *mut c_char,
868) -> *mut c_char {
869    clear_error(error_out);
870    ffi_catch_string(error_out, || {
871        with_client(handle, |client| client.app_schema_state_json())
872    })
873}
874
875#[no_mangle]
876pub extern "C" fn syncular_native_client_list_table_json(
877    handle: *mut SyncularNativeHandle,
878    table: *const c_char,
879    error_out: *mut *mut c_char,
880) -> *mut c_char {
881    clear_error(error_out);
882    ffi_catch_string(error_out, || {
883        let table = read_c_string(table)?;
884        with_client(handle, |client| client.list_table_json(&table))
885    })
886}
887
888#[no_mangle]
889pub extern "C" fn syncular_native_client_query_json(
890    handle: *mut SyncularNativeHandle,
891    request_json: *const c_char,
892    error_out: *mut *mut c_char,
893) -> *mut c_char {
894    clear_error(error_out);
895    ffi_catch_string(error_out, || {
896        let request_json = read_c_string(request_json)?;
897        with_client(handle, |client| client.query_json(&request_json))
898    })
899}
900
901#[no_mangle]
902pub extern "C" fn syncular_native_client_enqueue_refresh_snapshot_json(
903    handle: *mut SyncularNativeHandle,
904    request_json: *const c_char,
905    error_out: *mut *mut c_char,
906) -> *mut c_char {
907    clear_error(error_out);
908    ffi_catch_string(error_out, || {
909        let request_json = read_c_string(request_json)?;
910        with_client(handle, |client| {
911            client.enqueue_refresh_snapshot_json(&request_json)
912        })
913    })
914}
915
916#[no_mangle]
917pub extern "C" fn syncular_native_client_store_blob_file_json(
918    handle: *mut SyncularNativeHandle,
919    file_path: *const c_char,
920    options_json: *const c_char,
921    error_out: *mut *mut c_char,
922) -> *mut c_char {
923    clear_error(error_out);
924    ffi_catch_string(error_out, || {
925        let file_path = read_c_string(file_path)?;
926        let options_json = read_optional_c_string(options_json)?;
927        with_client(handle, |client| {
928            client.store_blob_file_json(&file_path, options_json.as_deref())
929        })
930    })
931}
932
933#[no_mangle]
934pub extern "C" fn syncular_native_client_enqueue_store_blob_file_json(
935    handle: *mut SyncularNativeHandle,
936    file_path: *const c_char,
937    options_json: *const c_char,
938    error_out: *mut *mut c_char,
939) -> *mut c_char {
940    clear_error(error_out);
941    ffi_catch_string(error_out, || {
942        let file_path = read_c_string(file_path)?;
943        let options_json = read_optional_c_string(options_json)?;
944        with_client(handle, |client| {
945            client.enqueue_store_blob_file_json(&file_path, options_json.as_deref())
946        })
947    })
948}
949
950#[no_mangle]
951pub extern "C" fn syncular_native_client_retrieve_blob_file(
952    handle: *mut SyncularNativeHandle,
953    ref_json: *const c_char,
954    file_path: *const c_char,
955    error_out: *mut *mut c_char,
956) -> bool {
957    clear_error(error_out);
958    ffi_catch_bool(error_out, || {
959        let ref_json = read_c_string(ref_json)?;
960        let file_path = read_c_string(file_path)?;
961        with_client(handle, |client| {
962            client.retrieve_blob_file(&ref_json, &file_path)
963        })
964    })
965}
966
967#[no_mangle]
968pub extern "C" fn syncular_native_client_retrieve_blob_file_with_options(
969    handle: *mut SyncularNativeHandle,
970    ref_json: *const c_char,
971    file_path: *const c_char,
972    options_json: *const c_char,
973    error_out: *mut *mut c_char,
974) -> bool {
975    clear_error(error_out);
976    ffi_catch_bool(error_out, || {
977        let ref_json = read_c_string(ref_json)?;
978        let file_path = read_c_string(file_path)?;
979        let options_json = read_optional_c_string(options_json)?;
980        with_client(handle, |client| {
981            client.retrieve_blob_file_with_options(&ref_json, &file_path, options_json.as_deref())
982        })
983    })
984}
985
986#[no_mangle]
987pub extern "C" fn syncular_native_client_enqueue_retrieve_blob_file_json(
988    handle: *mut SyncularNativeHandle,
989    ref_json: *const c_char,
990    file_path: *const c_char,
991    options_json: *const c_char,
992    error_out: *mut *mut c_char,
993) -> *mut c_char {
994    clear_error(error_out);
995    ffi_catch_string(error_out, || {
996        let ref_json = read_c_string(ref_json)?;
997        let file_path = read_c_string(file_path)?;
998        let options_json = read_optional_c_string(options_json)?;
999        with_client(handle, |client| {
1000            client.enqueue_retrieve_blob_file_json(&ref_json, &file_path, options_json.as_deref())
1001        })
1002    })
1003}
1004
1005#[no_mangle]
1006pub extern "C" fn syncular_native_client_is_blob_local(
1007    handle: *mut SyncularNativeHandle,
1008    hash: *const c_char,
1009    error_out: *mut *mut c_char,
1010) -> bool {
1011    clear_error(error_out);
1012    ffi_catch_bool_value(error_out, || {
1013        let hash = read_c_string(hash)?;
1014        with_client(handle, |client| client.is_blob_local(&hash))
1015    })
1016}
1017
1018#[no_mangle]
1019pub extern "C" fn syncular_native_client_process_blob_upload_queue_json(
1020    handle: *mut SyncularNativeHandle,
1021    error_out: *mut *mut c_char,
1022) -> *mut c_char {
1023    clear_error(error_out);
1024    ffi_catch_string(error_out, || {
1025        with_client(handle, |client| client.process_blob_upload_queue_json())
1026    })
1027}
1028
1029#[no_mangle]
1030pub extern "C" fn syncular_native_client_enqueue_process_blob_upload_queue(
1031    handle: *mut SyncularNativeHandle,
1032    error_out: *mut *mut c_char,
1033) -> *mut c_char {
1034    clear_error(error_out);
1035    ffi_catch_string(error_out, || {
1036        with_client(handle, |client| client.enqueue_process_blob_upload_queue())
1037    })
1038}
1039
1040#[no_mangle]
1041pub extern "C" fn syncular_native_client_blob_upload_queue_stats_json(
1042    handle: *mut SyncularNativeHandle,
1043    error_out: *mut *mut c_char,
1044) -> *mut c_char {
1045    clear_error(error_out);
1046    ffi_catch_string(error_out, || {
1047        with_client(handle, |client| client.blob_upload_queue_stats_json())
1048    })
1049}
1050
1051#[no_mangle]
1052pub extern "C" fn syncular_native_client_blob_cache_stats_json(
1053    handle: *mut SyncularNativeHandle,
1054    error_out: *mut *mut c_char,
1055) -> *mut c_char {
1056    clear_error(error_out);
1057    ffi_catch_string(error_out, || {
1058        with_client(handle, |client| client.blob_cache_stats_json())
1059    })
1060}
1061
1062#[no_mangle]
1063pub extern "C" fn syncular_native_client_prune_blob_cache(
1064    handle: *mut SyncularNativeHandle,
1065    max_bytes: i64,
1066    error_out: *mut *mut c_char,
1067) -> i64 {
1068    clear_error(error_out);
1069    ffi_catch_i64(error_out, || {
1070        with_client(handle, |client| client.prune_blob_cache(max_bytes))
1071    })
1072}
1073
1074#[no_mangle]
1075pub extern "C" fn syncular_native_client_enqueue_prune_blob_cache(
1076    handle: *mut SyncularNativeHandle,
1077    max_bytes: i64,
1078    error_out: *mut *mut c_char,
1079) -> *mut c_char {
1080    clear_error(error_out);
1081    ffi_catch_string(error_out, || {
1082        with_client(handle, |client| client.enqueue_prune_blob_cache(max_bytes))
1083    })
1084}
1085
1086#[no_mangle]
1087pub extern "C" fn syncular_native_client_clear_blob_cache(
1088    handle: *mut SyncularNativeHandle,
1089    error_out: *mut *mut c_char,
1090) -> bool {
1091    clear_error(error_out);
1092    ffi_catch_bool(error_out, || {
1093        with_client(handle, |client| client.clear_blob_cache())
1094    })
1095}
1096
1097#[no_mangle]
1098pub extern "C" fn syncular_native_client_enqueue_clear_blob_cache(
1099    handle: *mut SyncularNativeHandle,
1100    error_out: *mut *mut c_char,
1101) -> *mut c_char {
1102    clear_error(error_out);
1103    ffi_catch_string(error_out, || {
1104        with_client(handle, |client| client.enqueue_clear_blob_cache())
1105    })
1106}
1107
1108#[no_mangle]
1109pub extern "C" fn syncular_native_client_compact_storage_json(
1110    handle: *mut SyncularNativeHandle,
1111    options_json: *const c_char,
1112    error_out: *mut *mut c_char,
1113) -> *mut c_char {
1114    clear_error(error_out);
1115    ffi_catch_string(error_out, || {
1116        let options_json = read_optional_c_string(options_json)?;
1117        with_client(handle, |client| {
1118            client.compact_storage_json(options_json.as_deref())
1119        })
1120    })
1121}
1122
1123#[no_mangle]
1124pub extern "C" fn syncular_native_client_enqueue_compact_storage_json(
1125    handle: *mut SyncularNativeHandle,
1126    options_json: *const c_char,
1127    error_out: *mut *mut c_char,
1128) -> *mut c_char {
1129    clear_error(error_out);
1130    ffi_catch_string(error_out, || {
1131        let options_json = read_optional_c_string(options_json)?;
1132        with_client(handle, |client| {
1133            client.enqueue_compact_storage_json(options_json.as_deref())
1134        })
1135    })
1136}
1137
1138#[no_mangle]
1139pub extern "C" fn syncular_native_client_register_query_json(
1140    handle: *mut SyncularNativeHandle,
1141    query_json: *const c_char,
1142    error_out: *mut *mut c_char,
1143) -> *mut c_char {
1144    clear_error(error_out);
1145    ffi_catch_string(error_out, || {
1146        let query_json = read_c_string(query_json)?;
1147        with_client(handle, |client| client.register_query_json(&query_json))
1148    })
1149}
1150
1151#[no_mangle]
1152pub extern "C" fn syncular_native_client_unregister_query(
1153    handle: *mut SyncularNativeHandle,
1154    query_id: *const c_char,
1155    error_out: *mut *mut c_char,
1156) -> bool {
1157    clear_error(error_out);
1158    ffi_catch_bool(error_out, || {
1159        let query_id = read_c_string(query_id)?;
1160        with_client(handle, |client| client.unregister_query(&query_id))
1161    })
1162}
1163
1164#[no_mangle]
1165pub extern "C" fn syncular_native_client_observed_queries_json(
1166    handle: *mut SyncularNativeHandle,
1167    error_out: *mut *mut c_char,
1168) -> *mut c_char {
1169    clear_error(error_out);
1170    ffi_catch_string(error_out, || {
1171        with_client(handle, |client| client.observed_queries_json())
1172    })
1173}
1174
1175#[no_mangle]
1176pub extern "C" fn syncular_native_client_diagnostic_snapshot_json(
1177    handle: *mut SyncularNativeHandle,
1178    error_out: *mut *mut c_char,
1179) -> *mut c_char {
1180    clear_error(error_out);
1181    ffi_catch_string(error_out, || {
1182        with_client(handle, |client| client.diagnostic_snapshot_json())
1183    })
1184}
1185
1186#[no_mangle]
1187pub extern "C" fn syncular_native_client_outbox_summaries_json(
1188    handle: *mut SyncularNativeHandle,
1189    error_out: *mut *mut c_char,
1190) -> *mut c_char {
1191    clear_error(error_out);
1192    ffi_catch_string(error_out, || {
1193        with_client(handle, |client| client.outbox_summaries_json())
1194    })
1195}
1196
1197#[no_mangle]
1198pub extern "C" fn syncular_native_client_upsert_auth_lease_json(
1199    handle: *mut SyncularNativeHandle,
1200    lease_json: *const c_char,
1201    error_out: *mut *mut c_char,
1202) -> bool {
1203    clear_error(error_out);
1204    ffi_catch_bool(error_out, || {
1205        let lease_json = read_c_string(lease_json)?;
1206        with_client(handle, |client| client.upsert_auth_lease_json(&lease_json))
1207    })
1208}
1209
1210#[no_mangle]
1211pub extern "C" fn syncular_native_client_issue_auth_lease_json(
1212    handle: *mut SyncularNativeHandle,
1213    request_json: *const c_char,
1214    error_out: *mut *mut c_char,
1215) -> *mut c_char {
1216    clear_error(error_out);
1217    ffi_catch_string(error_out, || {
1218        let request_json = read_c_string(request_json)?;
1219        with_client(handle, |client| client.issue_auth_lease_json(&request_json))
1220    })
1221}
1222
1223#[no_mangle]
1224pub extern "C" fn syncular_native_client_auth_lease_json(
1225    handle: *mut SyncularNativeHandle,
1226    lease_id: *const c_char,
1227    error_out: *mut *mut c_char,
1228) -> *mut c_char {
1229    clear_error(error_out);
1230    ffi_catch_string(error_out, || {
1231        let lease_id = read_c_string(lease_id)?;
1232        with_client(handle, |client| client.auth_lease_json(&lease_id))
1233    })
1234}
1235
1236#[no_mangle]
1237pub extern "C" fn syncular_native_client_active_auth_leases_json(
1238    handle: *mut SyncularNativeHandle,
1239    actor_id: *const c_char,
1240    now_ms: i64,
1241    error_out: *mut *mut c_char,
1242) -> *mut c_char {
1243    clear_error(error_out);
1244    ffi_catch_string(error_out, || {
1245        let actor_id = read_optional_c_string(actor_id)?;
1246        with_client(handle, |client| {
1247            client.active_auth_leases_json(actor_id.as_deref(), now_ms)
1248        })
1249    })
1250}
1251
1252#[no_mangle]
1253pub extern "C" fn syncular_native_client_set_outbox_auth_lease_json(
1254    handle: *mut SyncularNativeHandle,
1255    client_commit_id: *const c_char,
1256    provenance_json: *const c_char,
1257    error_out: *mut *mut c_char,
1258) -> bool {
1259    clear_error(error_out);
1260    ffi_catch_bool(error_out, || {
1261        let client_commit_id = read_c_string(client_commit_id)?;
1262        let provenance_json = read_optional_c_string(provenance_json)?;
1263        with_client(handle, |client| {
1264            client.set_outbox_auth_lease_json(&client_commit_id, provenance_json.as_deref())
1265        })
1266    })
1267}
1268
1269#[no_mangle]
1270pub extern "C" fn syncular_native_client_conflict_summaries_json(
1271    handle: *mut SyncularNativeHandle,
1272    error_out: *mut *mut c_char,
1273) -> *mut c_char {
1274    clear_error(error_out);
1275    ffi_catch_string(error_out, || {
1276        with_client(handle, |client| client.conflict_summaries_json())
1277    })
1278}
1279
1280#[no_mangle]
1281pub extern "C" fn syncular_native_client_resolve_conflict(
1282    handle: *mut SyncularNativeHandle,
1283    conflict_id: *const c_char,
1284    resolution: *const c_char,
1285    error_out: *mut *mut c_char,
1286) -> bool {
1287    clear_error(error_out);
1288    ffi_catch_bool(error_out, || {
1289        let conflict_id = read_c_string(conflict_id)?;
1290        let resolution = read_c_string(resolution)?;
1291        with_client(handle, |client| {
1292            client.resolve_conflict(&conflict_id, &resolution)
1293        })
1294    })
1295}
1296
1297#[no_mangle]
1298pub extern "C" fn syncular_native_client_enqueue_resolve_conflict(
1299    handle: *mut SyncularNativeHandle,
1300    id: *const c_char,
1301    resolution: *const c_char,
1302    error_out: *mut *mut c_char,
1303) -> *mut c_char {
1304    clear_error(error_out);
1305    ffi_catch_string(error_out, || {
1306        let id = read_c_string(id)?;
1307        let resolution = read_c_string(resolution)?;
1308        with_client(handle, |client| {
1309            client.enqueue_resolve_conflict(&id, &resolution)
1310        })
1311    })
1312}
1313
1314#[no_mangle]
1315pub extern "C" fn syncular_native_client_retry_conflict_keep_local(
1316    handle: *mut SyncularNativeHandle,
1317    conflict_id: *const c_char,
1318    error_out: *mut *mut c_char,
1319) -> *mut c_char {
1320    clear_error(error_out);
1321    ffi_catch_string(error_out, || {
1322        let conflict_id = read_c_string(conflict_id)?;
1323        with_client(handle, |client| {
1324            client.retry_conflict_keep_local(&conflict_id)
1325        })
1326    })
1327}
1328
1329#[no_mangle]
1330pub extern "C" fn syncular_native_client_subscribe_events_json(
1331    handle: *mut SyncularNativeHandle,
1332    capacity: u32,
1333    callback: Option<SyncularNativeEventCallback>,
1334    error_callback: Option<SyncularNativeEventErrorCallback>,
1335    user_data: *mut c_void,
1336    error_out: *mut *mut c_char,
1337) -> *mut SyncularNativeEventSubscription {
1338    clear_error(error_out);
1339    ffi_catch_ptr(error_out, || {
1340        let callback = callback.ok_or_else(|| {
1341            SyncularError::message(ErrorKind::Config, "native event callback is null")
1342        })?;
1343        let subscription = Arc::new(with_client(handle, |client| {
1344            Ok(client.subscribe_events(capacity as usize))
1345        })?);
1346        let thread_subscription = Arc::clone(&subscription);
1347        let user_data = user_data as usize;
1348        let join = thread::spawn(move || {
1349            while let Some(event) = thread_subscription.next_event_json() {
1350                match event {
1351                    Ok(event_json) => call_event_callback(callback, &event_json, user_data),
1352                    Err(error) => {
1353                        if let Some(error_callback) = error_callback {
1354                            let error_json = serde_json::to_string(&NativeErrorInfo::from_error(
1355                                &error,
1356                            ))
1357                            .unwrap_or_else(|_| {
1358                                r#"{"kind":"Internal","message":"failed to serialize native event error"}"#
1359                                    .to_string()
1360                            });
1361                            call_error_callback(error_callback, &error_json, user_data);
1362                        }
1363                    }
1364                }
1365            }
1366        });
1367
1368        Ok(Box::into_raw(Box::new(SyncularNativeEventSubscription {
1369            subscription,
1370            join: Mutex::new(Some(join)),
1371        })))
1372    })
1373}
1374
1375#[no_mangle]
1376pub extern "C" fn syncular_native_event_subscription_close(
1377    handle: *mut SyncularNativeEventSubscription,
1378    error_out: *mut *mut c_char,
1379) -> bool {
1380    clear_error(error_out);
1381    ffi_catch_bool(error_out, || {
1382        if handle.is_null() {
1383            return Ok(());
1384        }
1385
1386        let subscription = unsafe { Box::from_raw(handle) };
1387        subscription.subscription.close();
1388        if let Ok(mut join) = subscription.join.lock() {
1389            if let Some(join) = join.take() {
1390                let _ = join.join();
1391            }
1392        }
1393        Ok(())
1394    })
1395}
1396
1397fn call_event_callback(callback: SyncularNativeEventCallback, event_json: &str, user_data: usize) {
1398    let value = callback_c_string(event_json);
1399    callback(value.as_ptr(), user_data as *mut c_void);
1400}
1401
1402fn call_error_callback(
1403    callback: SyncularNativeEventErrorCallback,
1404    error_json: &str,
1405    user_data: usize,
1406) {
1407    let value = callback_c_string(error_json);
1408    callback(value.as_ptr(), user_data as *mut c_void);
1409}
1410
1411fn callback_c_string(value: &str) -> CString {
1412    CString::new(value.replace('\0', "\\u0000")).expect("sanitized string should not contain nul")
1413}
1414
1415fn with_client<T>(
1416    handle: *mut SyncularNativeHandle,
1417    f: impl FnOnce(&mut NativeSyncularClient) -> Result<T>,
1418) -> Result<T> {
1419    if handle.is_null() {
1420        return Err(SyncularError::message(
1421            ErrorKind::Config,
1422            "native handle is null",
1423        ));
1424    }
1425
1426    let handle = unsafe { &*handle };
1427    let mut client = handle
1428        .client
1429        .lock()
1430        .map_err(|_| SyncularError::message(ErrorKind::Internal, "native handle is poisoned"))?;
1431    f(&mut client)
1432}
1433
1434fn with_presence_handle<T>(
1435    handle: *mut SyncularNativePresenceHandle,
1436    f: impl FnOnce(&SyncularNativePresenceHandle) -> Result<T>,
1437) -> Result<T> {
1438    if handle.is_null() {
1439        return Err(SyncularError::message(
1440            ErrorKind::Config,
1441            "native presence handle is null",
1442        ));
1443    }
1444
1445    let handle = unsafe { &*handle };
1446    f(handle)
1447}
1448
1449fn leave_presence_handle_inner(presence: &SyncularNativePresenceHandle) -> Result<bool> {
1450    let mut active = presence.active.lock().map_err(|_| {
1451        SyncularError::message(ErrorKind::Internal, "native presence handle is poisoned")
1452    })?;
1453    if !*active {
1454        return Ok(false);
1455    }
1456    with_client(presence.client, |client| {
1457        client.leave_presence(&presence.scope_key)
1458    })?;
1459    *active = false;
1460    Ok(true)
1461}
1462
1463fn with_open_task<T>(
1464    handle: *mut SyncularNativeOpenHandle,
1465    f: impl FnOnce(&mut NativeClientOpenTask) -> Result<T>,
1466) -> Result<T> {
1467    if handle.is_null() {
1468        return Err(SyncularError::message(
1469            ErrorKind::Internal,
1470            "native async open handle is null",
1471        ));
1472    }
1473
1474    let task = unsafe { &*handle };
1475    let mut task = task.task.lock().map_err(|_| {
1476        SyncularError::message(ErrorKind::Internal, "native async open handle is poisoned")
1477    })?;
1478    f(&mut task)
1479}
1480
1481fn read_c_string(value: *const c_char) -> Result<String> {
1482    if value.is_null() {
1483        return Err(SyncularError::config("required string pointer is null"));
1484    }
1485
1486    let value = unsafe { CStr::from_ptr(value) };
1487    value
1488        .to_str()
1489        .map(str::to_string)
1490        .map_err(|err| SyncularError::config(format!("string is not valid UTF-8: {err}")))
1491}
1492
1493fn read_optional_c_string(value: *const c_char) -> Result<Option<String>> {
1494    if value.is_null() {
1495        return Ok(None);
1496    }
1497
1498    read_c_string(value).map(Some)
1499}
1500
1501fn ffi_catch_bool(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<()>) -> bool {
1502    match catch_unwind(AssertUnwindSafe(f)) {
1503        Ok(Ok(())) => true,
1504        Ok(Err(error)) => {
1505            write_error(error_out, error);
1506            false
1507        }
1508        Err(payload) => {
1509            write_error(error_out, panic_error(payload));
1510            false
1511        }
1512    }
1513}
1514
1515fn ffi_catch_bool_value(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<bool>) -> bool {
1516    match catch_unwind(AssertUnwindSafe(f)) {
1517        Ok(Ok(value)) => value,
1518        Ok(Err(error)) => {
1519            write_error(error_out, error);
1520            false
1521        }
1522        Err(payload) => {
1523            write_error(error_out, panic_error(payload));
1524            false
1525        }
1526    }
1527}
1528
1529fn ffi_catch_i64(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<i64>) -> i64 {
1530    match catch_unwind(AssertUnwindSafe(f)) {
1531        Ok(Ok(value)) => value,
1532        Ok(Err(error)) => {
1533            write_error(error_out, error);
1534            0
1535        }
1536        Err(payload) => {
1537            write_error(error_out, panic_error(payload));
1538            0
1539        }
1540    }
1541}
1542
1543fn ffi_catch_ptr<T>(error_out: *mut *mut c_char, f: impl FnOnce() -> Result<*mut T>) -> *mut T {
1544    match catch_unwind(AssertUnwindSafe(f)) {
1545        Ok(Ok(value)) => value,
1546        Ok(Err(error)) => {
1547            write_error(error_out, error);
1548            ptr::null_mut()
1549        }
1550        Err(payload) => {
1551            write_error(error_out, panic_error(payload));
1552            ptr::null_mut()
1553        }
1554    }
1555}
1556
1557fn ffi_catch_string(
1558    error_out: *mut *mut c_char,
1559    f: impl FnOnce() -> Result<String>,
1560) -> *mut c_char {
1561    ffi_catch_ptr(error_out, || f().map(alloc_c_string))
1562}
1563
1564fn panic_error(payload: Box<dyn Any + Send>) -> SyncularError {
1565    let message = payload
1566        .downcast_ref::<&str>()
1567        .map(|value| (*value).to_string())
1568        .or_else(|| payload.downcast_ref::<String>().cloned())
1569        .unwrap_or_else(|| "unknown panic payload".to_string());
1570    SyncularError::message(
1571        ErrorKind::Internal,
1572        format!("panic crossed FFI boundary: {message}"),
1573    )
1574}
1575
1576fn clear_error(error_out: *mut *mut c_char) {
1577    if !error_out.is_null() {
1578        unsafe {
1579            *error_out = ptr::null_mut();
1580        }
1581    }
1582}
1583
1584fn write_error(error_out: *mut *mut c_char, error: SyncularError) {
1585    if error_out.is_null() {
1586        return;
1587    }
1588
1589    let value = serde_json::to_string(&NativeErrorInfo::from_error(&error)).unwrap_or_else(|_| {
1590        r#"{"kind":"Internal","message":"failed to serialize native error"}"#.to_string()
1591    });
1592
1593    unsafe {
1594        *error_out = alloc_c_string(value);
1595    }
1596}
1597
1598fn alloc_c_string(value: String) -> *mut c_char {
1599    let sanitized = value.replace('\0', "\\u0000");
1600    CString::new(sanitized)
1601        .expect("sanitized string should not contain nul")
1602        .into_raw()
1603}
1604
1605#[cfg(test)]
1606mod tests {
1607    use super::*;
1608    use serde_json::Value;
1609
1610    #[test]
1611    fn ffi_boundary_converts_panics_to_structured_errors() {
1612        let previous_hook = std::panic::take_hook();
1613        std::panic::set_hook(Box::new(|_| {}));
1614        let mut error = ptr::null_mut();
1615        let ok = ffi_catch_bool(&mut error, || -> Result<()> {
1616            panic!("ffi boundary test panic");
1617        });
1618        std::panic::set_hook(previous_hook);
1619
1620        assert!(!ok);
1621        assert!(!error.is_null());
1622        let error_json = unsafe { CStr::from_ptr(error) }
1623            .to_str()
1624            .expect("utf8 error")
1625            .to_string();
1626        syncular_string_free(error);
1627
1628        let value: Value = serde_json::from_str(&error_json).expect("error json");
1629        assert_eq!(value["kind"], "Internal");
1630        assert!(value["message"]
1631            .as_str()
1632            .unwrap_or_default()
1633            .contains("panic crossed FFI boundary: ffi boundary test panic"));
1634    }
1635}