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}