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}