enough_ffi/
lib.rs

1//! # enough-ffi
2//!
3//! FFI helpers for exposing cancellation across language boundaries.
4//!
5//! This crate provides C-compatible functions and types for use with
6//! C#/.NET, Python, Node.js, and other languages that can call C APIs.
7//!
8//! ## Safety Model
9//!
10//! This crate uses reference counting internally to prevent use-after-free:
11//!
12//! - Sources and tokens use `Arc` internally
13//! - Destroying a source while tokens exist is safe - tokens remain valid
14//!   but can never become cancelled (since no one can call cancel anymore)
15//! - Each token must be explicitly destroyed when no longer needed
16//!
17//! ## C# Integration Example
18//!
19//! ```csharp
20//! // P/Invoke declarations
21//! [DllImport("mylib")]
22//! static extern IntPtr enough_cancellation_create();
23//!
24//! [DllImport("mylib")]
25//! static extern void enough_cancellation_cancel(IntPtr source);
26//!
27//! [DllImport("mylib")]
28//! static extern void enough_cancellation_destroy(IntPtr source);
29//!
30//! [DllImport("mylib")]
31//! static extern IntPtr enough_token_create(IntPtr source);
32//!
33//! [DllImport("mylib")]
34//! static extern bool enough_token_is_cancelled(IntPtr token);
35//!
36//! [DllImport("mylib")]
37//! static extern void enough_token_destroy(IntPtr token);
38//!
39//! // Usage with CancellationToken
40//! public static byte[] Decode(byte[] data, CancellationToken ct)
41//! {
42//!     var source = enough_cancellation_create();
43//!     var token = enough_token_create(source);
44//!     try
45//!     {
46//!         using var registration = ct.Register(() =>
47//!             enough_cancellation_cancel(source));
48//!
49//!         return NativeMethods.decode(data, token);
50//!     }
51//!     finally
52//!     {
53//!         enough_token_destroy(token);
54//!         enough_cancellation_destroy(source);
55//!     }
56//! }
57//! ```
58//!
59//! ## Rust FFI Functions
60//!
61//! ```rust
62//! use enough_ffi::{enough_token_create, enough_token_destroy, FfiCancellationToken};
63//! use enough::Stop;
64//!
65//! #[no_mangle]
66//! pub extern "C" fn decode(
67//!     data: *const u8,
68//!     len: usize,
69//!     token: *const FfiCancellationToken,
70//! ) -> i32 {
71//!     let stop = unsafe { FfiCancellationToken::from_ptr(token) };
72//!
73//!     // Use stop with any library that accepts impl Stop
74//!     if stop.should_stop() {
75//!         return -1; // Cancelled
76//!     }
77//!
78//!     0
79//! }
80//! ```
81
82#![warn(missing_docs)]
83#![warn(clippy::all)]
84
85use std::sync::atomic::{AtomicBool, Ordering};
86use std::sync::Arc;
87
88use enough::{Stop, StopReason};
89
90// ============================================================================
91// Internal Types
92// ============================================================================
93
94/// Shared cancellation state, reference counted.
95struct CancellationState {
96    cancelled: AtomicBool,
97}
98
99impl CancellationState {
100    fn new() -> Self {
101        Self {
102            cancelled: AtomicBool::new(false),
103        }
104    }
105
106    #[inline]
107    fn cancel(&self) {
108        self.cancelled.store(true, Ordering::Relaxed);
109    }
110
111    #[inline]
112    fn is_cancelled(&self) -> bool {
113        self.cancelled.load(Ordering::Relaxed)
114    }
115}
116
117// ============================================================================
118// FFI Source
119// ============================================================================
120
121/// FFI-safe cancellation source.
122///
123/// This is the type that should be created and destroyed across FFI.
124/// It owns a reference to the shared cancellation state.
125///
126/// Create with [`enough_cancellation_create`], destroy with
127/// [`enough_cancellation_destroy`].
128///
129/// **Safety**: This type uses `Arc` internally. Destroying the source while
130/// tokens exist is safe - tokens will continue to work but can never become
131/// cancelled.
132#[repr(C)]
133pub struct FfiCancellationSource {
134    inner: Arc<CancellationState>,
135}
136
137impl FfiCancellationSource {
138    fn new() -> Self {
139        Self {
140            inner: Arc::new(CancellationState::new()),
141        }
142    }
143
144    /// Cancel this source.
145    #[inline]
146    pub fn cancel(&self) {
147        self.inner.cancel();
148    }
149
150    /// Check if cancelled.
151    #[inline]
152    pub fn is_cancelled(&self) -> bool {
153        self.inner.is_cancelled()
154    }
155
156    /// Create a token from this source.
157    fn create_token(&self) -> FfiCancellationToken {
158        FfiCancellationToken {
159            inner: Some(Arc::clone(&self.inner)),
160        }
161    }
162}
163
164// ============================================================================
165// FFI Token
166// ============================================================================
167
168/// FFI-safe cancellation token.
169///
170/// This token holds a reference to the shared cancellation state.
171/// It must be explicitly destroyed with [`enough_token_destroy`].
172///
173/// The token remains valid even after the source is destroyed - it will
174/// just never become cancelled.
175#[repr(C)]
176pub struct FfiCancellationToken {
177    inner: Option<Arc<CancellationState>>,
178}
179
180impl FfiCancellationToken {
181    /// Create a "never cancelled" token.
182    ///
183    /// This token will never report as cancelled.
184    #[inline]
185    pub fn never() -> Self {
186        Self { inner: None }
187    }
188
189    /// Create a token view from a raw pointer.
190    ///
191    /// This creates a non-owning view that can be used to check cancellation.
192    /// The original token must remain valid for the lifetime of this view.
193    ///
194    /// # Safety
195    ///
196    /// - If `ptr` is non-null, it must point to a valid `FfiCancellationToken`
197    /// - The pointed-to token must outlive all uses of the returned view
198    #[inline]
199    pub unsafe fn from_ptr(ptr: *const FfiCancellationToken) -> FfiCancellationTokenView {
200        FfiCancellationTokenView { ptr }
201    }
202}
203
204impl Stop for FfiCancellationToken {
205    #[inline]
206    fn check(&self) -> Result<(), StopReason> {
207        match &self.inner {
208            Some(state) if state.is_cancelled() => Err(StopReason::Cancelled),
209            _ => Ok(()),
210        }
211    }
212
213    #[inline]
214    fn should_stop(&self) -> bool {
215        self.inner
216            .as_ref()
217            .map(|s| s.is_cancelled())
218            .unwrap_or(false)
219    }
220}
221
222impl std::fmt::Debug for FfiCancellationToken {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.debug_struct("FfiCancellationToken")
225            .field("is_cancelled", &self.should_stop())
226            .field("is_never", &self.inner.is_none())
227            .finish()
228    }
229}
230
231// ============================================================================
232// Token View (for Rust code receiving token pointers)
233// ============================================================================
234
235/// A non-owning view of a cancellation token.
236///
237/// This is used by Rust FFI functions that receive a token pointer.
238/// It does not own the token and does not affect reference counts.
239#[derive(Clone, Copy)]
240pub struct FfiCancellationTokenView {
241    ptr: *const FfiCancellationToken,
242}
243
244// SAFETY: The view only reads through the pointer, and the underlying
245// Arc<CancellationState> is Send + Sync.
246unsafe impl Send for FfiCancellationTokenView {}
247unsafe impl Sync for FfiCancellationTokenView {}
248
249impl FfiCancellationTokenView {
250    /// Create a "never cancelled" view.
251    #[inline]
252    pub const fn never() -> Self {
253        Self {
254            ptr: std::ptr::null(),
255        }
256    }
257}
258
259impl Stop for FfiCancellationTokenView {
260    #[inline]
261    fn check(&self) -> Result<(), StopReason> {
262        if self.ptr.is_null() {
263            return Ok(());
264        }
265        // SAFETY: Caller guarantees ptr is valid
266        unsafe {
267            if (*self.ptr).should_stop() {
268                Err(StopReason::Cancelled)
269            } else {
270                Ok(())
271            }
272        }
273    }
274
275    #[inline]
276    fn should_stop(&self) -> bool {
277        if self.ptr.is_null() {
278            return false;
279        }
280        // SAFETY: Caller guarantees ptr is valid
281        unsafe { (*self.ptr).should_stop() }
282    }
283}
284
285impl std::fmt::Debug for FfiCancellationTokenView {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        f.debug_struct("FfiCancellationTokenView")
288            .field("ptr", &self.ptr)
289            .field("is_null", &self.ptr.is_null())
290            .finish()
291    }
292}
293
294// ============================================================================
295// C FFI Functions - Source Management
296// ============================================================================
297
298/// Create a new cancellation source.
299///
300/// Returns a pointer to the source. Must be destroyed with
301/// [`enough_cancellation_destroy`].
302///
303/// Returns null if allocation fails.
304#[no_mangle]
305pub extern "C" fn enough_cancellation_create() -> *mut FfiCancellationSource {
306    Box::into_raw(Box::new(FfiCancellationSource::new()))
307}
308
309/// Cancel a cancellation source.
310///
311/// After this call, any tokens created from this source will report
312/// as cancelled.
313///
314/// # Safety
315///
316/// `ptr` must be a valid pointer returned by [`enough_cancellation_create`],
317/// or null (which is a no-op).
318#[no_mangle]
319pub unsafe extern "C" fn enough_cancellation_cancel(ptr: *const FfiCancellationSource) {
320    if let Some(source) = ptr.as_ref() {
321        source.cancel();
322    }
323}
324
325/// Check if a cancellation source is cancelled.
326///
327/// # Safety
328///
329/// `ptr` must be a valid pointer returned by [`enough_cancellation_create`],
330/// or null (which returns false).
331#[no_mangle]
332pub unsafe extern "C" fn enough_cancellation_is_cancelled(
333    ptr: *const FfiCancellationSource,
334) -> bool {
335    ptr.as_ref().map(|s| s.is_cancelled()).unwrap_or(false)
336}
337
338/// Destroy a cancellation source.
339///
340/// This is safe to call even if tokens created from this source still exist.
341/// Those tokens will remain valid but will never become cancelled.
342///
343/// # Safety
344///
345/// - `ptr` must be a valid pointer returned by [`enough_cancellation_create`],
346///   or null (which is a no-op)
347/// - The pointer must not be used after this call
348#[no_mangle]
349pub unsafe extern "C" fn enough_cancellation_destroy(ptr: *mut FfiCancellationSource) {
350    if !ptr.is_null() {
351        drop(Box::from_raw(ptr));
352    }
353}
354
355// ============================================================================
356// C FFI Functions - Token Management
357// ============================================================================
358
359/// Create a token from a cancellation source.
360///
361/// The token holds a reference to the shared state and must be destroyed
362/// with [`enough_token_destroy`].
363///
364/// The token remains valid even after the source is destroyed.
365///
366/// # Safety
367///
368/// `source` must be a valid pointer returned by [`enough_cancellation_create`],
369/// or null (which creates a "never cancelled" token).
370#[no_mangle]
371pub unsafe extern "C" fn enough_token_create(
372    source: *const FfiCancellationSource,
373) -> *mut FfiCancellationToken {
374    let token = match source.as_ref() {
375        Some(s) => s.create_token(),
376        None => FfiCancellationToken::never(),
377    };
378    Box::into_raw(Box::new(token))
379}
380
381/// Create a "never cancelled" token.
382///
383/// This token will never report as cancelled. Must be destroyed with
384/// [`enough_token_destroy`].
385#[no_mangle]
386pub extern "C" fn enough_token_create_never() -> *mut FfiCancellationToken {
387    Box::into_raw(Box::new(FfiCancellationToken::never()))
388}
389
390/// Check if a token is cancelled.
391///
392/// # Safety
393///
394/// `token` must be a valid pointer returned by [`enough_token_create`],
395/// or null (which returns false).
396#[no_mangle]
397pub unsafe extern "C" fn enough_token_is_cancelled(token: *const FfiCancellationToken) -> bool {
398    token.as_ref().map(|t| t.should_stop()).unwrap_or(false)
399}
400
401/// Destroy a token.
402///
403/// # Safety
404///
405/// - `token` must be a valid pointer returned by [`enough_token_create`],
406///   or null (which is a no-op)
407/// - The pointer must not be used after this call
408#[no_mangle]
409pub unsafe extern "C" fn enough_token_destroy(token: *mut FfiCancellationToken) {
410    if !token.is_null() {
411        drop(Box::from_raw(token));
412    }
413}
414
415// ============================================================================
416// Tests
417// ============================================================================
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn source_create_cancel_destroy() {
425        unsafe {
426            let ptr = enough_cancellation_create();
427            assert!(!ptr.is_null());
428
429            assert!(!enough_cancellation_is_cancelled(ptr));
430
431            enough_cancellation_cancel(ptr);
432
433            assert!(enough_cancellation_is_cancelled(ptr));
434
435            enough_cancellation_destroy(ptr);
436        }
437    }
438
439    #[test]
440    fn token_lifecycle() {
441        unsafe {
442            let source = enough_cancellation_create();
443            let token = enough_token_create(source);
444
445            assert!(!enough_token_is_cancelled(token));
446
447            enough_cancellation_cancel(source);
448
449            assert!(enough_token_is_cancelled(token));
450
451            enough_token_destroy(token);
452            enough_cancellation_destroy(source);
453        }
454    }
455
456    #[test]
457    fn token_survives_source_destruction() {
458        unsafe {
459            let source = enough_cancellation_create();
460
461            // Cancel before creating token
462            enough_cancellation_cancel(source);
463
464            let token = enough_token_create(source);
465
466            // Destroy source while token exists - this is now safe!
467            enough_cancellation_destroy(source);
468
469            // Token should still report cancelled
470            assert!(enough_token_is_cancelled(token));
471
472            enough_token_destroy(token);
473        }
474    }
475
476    #[test]
477    fn token_from_destroyed_source_never_cancels() {
478        unsafe {
479            let source = enough_cancellation_create();
480            let token = enough_token_create(source);
481
482            // Destroy source without cancelling
483            enough_cancellation_destroy(source);
484
485            // Token should remain valid but never become cancelled
486            // (no one can call cancel anymore)
487            assert!(!enough_token_is_cancelled(token));
488
489            enough_token_destroy(token);
490        }
491    }
492
493    #[test]
494    fn token_never() {
495        unsafe {
496            let token = enough_token_create_never();
497            assert!(!enough_token_is_cancelled(token));
498            enough_token_destroy(token);
499        }
500    }
501
502    #[test]
503    fn null_safety() {
504        unsafe {
505            // All of these should be safe no-ops
506            enough_cancellation_cancel(std::ptr::null());
507            enough_cancellation_destroy(std::ptr::null_mut());
508            assert!(!enough_cancellation_is_cancelled(std::ptr::null()));
509
510            enough_token_destroy(std::ptr::null_mut());
511            assert!(!enough_token_is_cancelled(std::ptr::null()));
512
513            // Null source creates never-cancelled token
514            let token = enough_token_create(std::ptr::null());
515            assert!(!enough_token_is_cancelled(token));
516            enough_token_destroy(token);
517        }
518    }
519
520    #[test]
521    fn token_view_from_ptr() {
522        unsafe {
523            let source = enough_cancellation_create();
524            let token = enough_token_create(source);
525
526            // Rust code would receive the token pointer and create a view
527            let view = FfiCancellationToken::from_ptr(token);
528
529            assert!(!view.should_stop());
530            assert!(view.check().is_ok());
531
532            enough_cancellation_cancel(source);
533
534            assert!(view.should_stop());
535            assert_eq!(view.check(), Err(StopReason::Cancelled));
536
537            enough_token_destroy(token);
538            enough_cancellation_destroy(source);
539        }
540    }
541
542    #[test]
543    fn token_view_never() {
544        let view = FfiCancellationTokenView::never();
545        assert!(!view.should_stop());
546        assert!(view.check().is_ok());
547    }
548
549    #[test]
550    fn types_are_send_sync() {
551        fn assert_send_sync<T: Send + Sync>() {}
552        assert_send_sync::<FfiCancellationToken>();
553        assert_send_sync::<FfiCancellationTokenView>();
554    }
555
556    #[test]
557    fn multiple_tokens_same_source() {
558        unsafe {
559            let source = enough_cancellation_create();
560            let t1 = enough_token_create(source);
561            let t2 = enough_token_create(source);
562            let t3 = enough_token_create(source);
563
564            assert!(!enough_token_is_cancelled(t1));
565            assert!(!enough_token_is_cancelled(t2));
566            assert!(!enough_token_is_cancelled(t3));
567
568            enough_cancellation_cancel(source);
569
570            assert!(enough_token_is_cancelled(t1));
571            assert!(enough_token_is_cancelled(t2));
572            assert!(enough_token_is_cancelled(t3));
573
574            // Destroy in different order than creation
575            enough_token_destroy(t2);
576            enough_cancellation_destroy(source);
577            enough_token_destroy(t1);
578            enough_token_destroy(t3);
579        }
580    }
581
582    #[test]
583    fn interop_with_enough() {
584        // Both implement Stop
585        fn use_stop(stop: impl Stop) -> bool {
586            stop.should_stop()
587        }
588
589        // Test FfiCancellationToken with Stop trait
590        assert!(!use_stop(FfiCancellationToken::never()));
591        assert!(!use_stop(FfiCancellationTokenView::never()));
592
593        // Test with a real source
594        unsafe {
595            let source = enough_cancellation_create();
596            let token = enough_token_create(source);
597            let view = FfiCancellationToken::from_ptr(token);
598
599            assert!(!use_stop(view));
600
601            enough_cancellation_cancel(source);
602            assert!(use_stop(view));
603
604            enough_token_destroy(token);
605            enough_cancellation_destroy(source);
606        }
607    }
608
609    #[test]
610    fn concurrent_access_stress() {
611        use std::sync::atomic::{AtomicUsize, Ordering};
612        use std::sync::Arc;
613        use std::thread;
614
615        unsafe {
616            let source = enough_cancellation_create();
617            let cancelled_count = Arc::new(AtomicUsize::new(0));
618            let check_count = Arc::new(AtomicUsize::new(0));
619
620            // Create tokens upfront and convert to addresses
621            let tokens: Vec<_> = (0..10)
622                .map(|_| enough_token_create(source) as usize)
623                .collect();
624
625            // Spawn multiple threads that check cancellation
626            let handles: Vec<_> = tokens
627                .into_iter()
628                .map(|token_addr| {
629                    let cancelled_count = Arc::clone(&cancelled_count);
630                    let check_count = Arc::clone(&check_count);
631
632                    thread::spawn(move || {
633                        let token = token_addr as *mut FfiCancellationToken;
634                        let view = FfiCancellationToken::from_ptr(token);
635                        for _ in 0..10000 {
636                            check_count.fetch_add(1, Ordering::Relaxed);
637                            if view.should_stop() {
638                                cancelled_count.fetch_add(1, Ordering::Relaxed);
639                                break;
640                            }
641                            thread::yield_now();
642                        }
643                        enough_token_destroy(token);
644                    })
645                })
646                .collect();
647
648            // Cancel after threads have started
649            thread::sleep(std::time::Duration::from_millis(1));
650            enough_cancellation_cancel(source);
651
652            for h in handles {
653                h.join().unwrap();
654            }
655
656            // All threads should have detected cancellation
657            assert!(cancelled_count.load(Ordering::Relaxed) > 0);
658            assert!(check_count.load(Ordering::Relaxed) > 0);
659
660            enough_cancellation_destroy(source);
661        }
662    }
663
664    #[test]
665    fn cross_thread_cancellation() {
666        use std::thread;
667
668        unsafe {
669            let source = enough_cancellation_create();
670            let token = enough_token_create(source);
671
672            // Send token to another thread
673            let token_addr = token as usize;
674            let handle = thread::spawn(move || {
675                let token = token_addr as *const FfiCancellationToken;
676                let view = FfiCancellationToken::from_ptr(token);
677
678                // Spin until cancelled
679                let mut iterations = 0;
680                while !view.should_stop() && iterations < 1_000_000 {
681                    iterations += 1;
682                    thread::yield_now();
683                }
684
685                view.should_stop()
686            });
687
688            // Cancel from main thread
689            thread::sleep(std::time::Duration::from_millis(5));
690            enough_cancellation_cancel(source);
691
692            let was_cancelled = handle.join().unwrap();
693            assert!(was_cancelled);
694
695            enough_token_destroy(token);
696            enough_cancellation_destroy(source);
697        }
698    }
699
700    #[test]
701    fn rapid_create_destroy() {
702        // Stress test allocation/deallocation
703        unsafe {
704            for _ in 0..1000 {
705                let source = enough_cancellation_create();
706                let tokens: Vec<_> = (0..10).map(|_| enough_token_create(source)).collect();
707
708                enough_cancellation_cancel(source);
709
710                for token in tokens {
711                    assert!(enough_token_is_cancelled(token));
712                    enough_token_destroy(token);
713                }
714
715                enough_cancellation_destroy(source);
716            }
717        }
718    }
719
720    #[test]
721    fn idempotent_cancel() {
722        unsafe {
723            let source = enough_cancellation_create();
724            let token = enough_token_create(source);
725
726            // Cancel multiple times should be safe
727            enough_cancellation_cancel(source);
728            enough_cancellation_cancel(source);
729            enough_cancellation_cancel(source);
730
731            assert!(enough_token_is_cancelled(token));
732
733            enough_token_destroy(token);
734            enough_cancellation_destroy(source);
735        }
736    }
737
738    #[test]
739    fn token_view_copy_semantics() {
740        unsafe {
741            let source = enough_cancellation_create();
742            let token = enough_token_create(source);
743
744            let view1 = FfiCancellationToken::from_ptr(token);
745            let view2 = view1; // Copy
746            let view3 = view1; // Copy again
747
748            assert!(!view1.should_stop());
749            assert!(!view2.should_stop());
750            assert!(!view3.should_stop());
751
752            enough_cancellation_cancel(source);
753
754            assert!(view1.should_stop());
755            assert!(view2.should_stop());
756            assert!(view3.should_stop());
757
758            enough_token_destroy(token);
759            enough_cancellation_destroy(source);
760        }
761    }
762
763    #[test]
764    fn check_returns_correct_reason() {
765        unsafe {
766            let source = enough_cancellation_create();
767            let token = enough_token_create(source);
768            let view = FfiCancellationToken::from_ptr(token);
769
770            assert_eq!(view.check(), Ok(()));
771
772            enough_cancellation_cancel(source);
773
774            assert_eq!(view.check(), Err(StopReason::Cancelled));
775
776            enough_token_destroy(token);
777            enough_cancellation_destroy(source);
778        }
779    }
780
781    #[test]
782    fn debug_formatting() {
783        unsafe {
784            let source = enough_cancellation_create();
785            let token = enough_token_create(source);
786            let view = FfiCancellationToken::from_ptr(token);
787
788            let token_ref = &*token;
789            let token_debug = format!("{:?}", token_ref);
790            assert!(token_debug.contains("FfiCancellationToken"));
791            assert!(token_debug.contains("is_cancelled"));
792
793            let view_debug = format!("{:?}", view);
794            assert!(view_debug.contains("FfiCancellationTokenView"));
795
796            enough_token_destroy(token);
797            enough_cancellation_destroy(source);
798        }
799    }
800
801    #[test]
802    fn simulated_ffi_pattern() {
803        // Simulates how a C caller would use this API
804        unsafe {
805            // 1. C code creates source and token
806            let source = enough_cancellation_create();
807            let token = enough_token_create(source);
808
809            // 2. C code passes token pointer to Rust FFI function
810            fn rust_ffi_function(
811                token_ptr: *const FfiCancellationToken,
812            ) -> Result<i32, &'static str> {
813                let stop = unsafe { FfiCancellationToken::from_ptr(token_ptr) };
814
815                for i in 0..1000 {
816                    if i % 100 == 0 {
817                        stop.check().map_err(|_| "cancelled")?;
818                    }
819                }
820                Ok(42)
821            }
822
823            // 3. First call succeeds
824            let result = rust_ffi_function(token);
825            assert_eq!(result, Ok(42));
826
827            // 4. C code triggers cancellation (e.g., from callback)
828            enough_cancellation_cancel(source);
829
830            // 5. Next call detects cancellation
831            let result = rust_ffi_function(token);
832            assert_eq!(result, Err("cancelled"));
833
834            // 6. C code cleans up
835            enough_token_destroy(token);
836            enough_cancellation_destroy(source);
837        }
838    }
839}