Skip to content

Latest commit

 

History

History
275 lines (205 loc) · 8.77 KB

File metadata and controls

275 lines (205 loc) · 8.77 KB

send_cells

Thread-safe cell types for sending and sharing non-Send/non-Sync types across thread boundaries.

logo

This crate provides specialized cell types that allow you to work with types that don't normally implement Send or Sync traits, enabling their use in concurrent contexts while maintaining memory safety through either runtime checks or manual verification.

Overview

The send_cells crate offers two categories of wrappers:

  • Safe wrappers with runtime thread checking
  • Unsafe wrappers for performance-critical scenarios with manual safety verification

This crate may be considered an alternative to the fragile crate, but provides a more ergonomic API and additional unsafe variants for maximum performance.

Quick Start

# wasm_lite_std::worker_doctest!(|| {
use send_cells::{SendCell, SyncCell};
use send_cells::sys::thread;
use std::rc::Rc;
use std::sync::Arc;

// Wrap a non-Send type to make it Send
let data = Rc::new(42);
let send_cell = SendCell::new(data);

// Access is checked at runtime - panics if accessed from wrong thread
assert_eq!(**send_cell.get(), 42);

// Wrap a non-Sync type to make it Sync
let shared_data = std::cell::RefCell::new("shared");
let sync_cell = Arc::new(SyncCell::new(shared_data));

// Share between threads with automatic synchronization
let sync_clone = Arc::clone(&sync_cell);
let handle = thread::spawn(move || {
    sync_clone.with(|data| {
        println!("Data: {}", data.borrow());
    });
});
handle.join().unwrap();
# });

Safe Wrappers

Safe wrappers provide runtime-checked access to wrapped values:

SendCell<T>

Allows sending non-Send types between threads with runtime thread checking:

  • Remembers the thread it was created on
  • Panics if accessed from a different thread
  • Perfect for single-threaded async contexts

SyncCell<T>

Allows sharing non-Sync types between threads with mutex-based synchronization:

  • Uses internal mutex for thread-safe access
  • Closure-based API prevents holding locks across await points
  • Ideal for shared state in multi-threaded applications

SendFuture<T>

Wraps non-Send futures to make them Send:

  • Runtime checks ensure the future is only polled on the correct thread
  • Enables use of non-Send futures with thread pool executors

Unsafe Wrappers

Unsafe wrappers provide zero-cost abstractions when you can manually verify safety:

UnsafeSendCell<T>

Allows sending non-Send types without runtime checks:

  • No performance overhead
  • Requires unsafe blocks for all access
  • Suitable for platform-specific thread guarantees

UnsafeSendFuture<T>

Wraps non-Send futures without runtime checks:

  • Zero overhead compared to the underlying future
  • Requires manual verification of thread safety

UnsafeSyncCell<T>

Allows sharing non-Sync types without runtime checks:

  • No synchronization overhead
  • Requires unsafe blocks for all access
  • Suitable when external synchronization is guaranteed

When to Use Each Type

Type Use When Performance Safety
SendCell Moving non-Send types in async contexts Good Runtime checked
SyncCell Sharing non-Sync types between threads Good Mutex protected
SendFuture Using non-Send futures with Send requirements Good Runtime checked
UnsafeSendCell Platform guarantees thread safety Best Manual verification
UnsafeSyncCell External synchronization guarantees Best Manual verification
UnsafeSendFuture Maximum performance for futures Best Manual verification

Platform Support

Standard Platforms

Full support for all major platforms with standard library support.

WebAssembly

wasm32-unknown-unknown is supported with runtime thread checks for web workers. Thread IDs are properly tracked even in WASM environments.

On wasm, SyncCell uses a different lock — and that is load-bearing. SyncCell::with/with_mut take std::sync::Mutex natively and a data-less atomic lock on wasm. Under +atomics, std::sync::Mutex is futex-backed (memory.atomic.wait32) and the browser main thread may not wait on it, so a contended SyncCell used from the main thread trapped with Atomics.wait cannot be called in this context.

It was contention-dependent, so single-threaded tests passed: an uncontended lock() takes the CAS fast path and never reaches the futex. Contention is the designed case for this type, which is what made it a live hazard rather than a curiosity.

The wasm lock spins while contended and never calls memory.atomic.wait32. Its guard holds only a shared reference to the atomic lock, avoiding references to lock-internal data that could overlap during a wake-up. One consequence: there is no lock poisoning on wasm, so with/with_mut cannot panic for that reason there. SendCell was never affected — it guards with a thread id, not a mutex.

Reported and fixed 2026-08-04. The regression test is tests/synccell_main_thread_repro.rs, which was confirmed to fail against the old lock and pass against the new one, in both Firefox and Chrome.

Examples

Async Runtime Integration

use send_cells::SendCell;
use std::rc::Rc;

async fn process_data() -> i32 {
    // Rc is not Send, but we need to use it in an async context
    let data = Rc::new(vec![1, 2, 3]);
    let cell = SendCell::new(data);

    // Can be moved into async blocks that might run on different threads.
    // Polling it on one is what panics — this drives it where it was created.
    let task = async move {
        let data = cell.get();
        data.iter().sum::<i32>()
    };

    task.await
}

// Driven here rather than described. `async_doctest!` is what a doctest has
// instead of an executor: it polls on the browser's event loop on wasm32 and
// with `block_on` natively, and defers the verdict until the future finishes,
// so a body that panics or hangs fails rather than passing when `main` returns.
// A real application would hand `process_data()` to its own executor —
// `tokio::spawn(...).await` — which is the same shape.
# wasm_lite_std::async_doctest!(async {
assert_eq!(process_data().await, 6);
# });

Shared State with SyncCell

# wasm_lite_std::worker_doctest!(|| {
use send_cells::SyncCell;
use send_cells::sys::thread;
use std::cell::RefCell;
use std::sync::Arc;

let counter = RefCell::new(0);
let sync_counter = Arc::new(SyncCell::new(counter));

let mut handles = vec![];

for _ in 0..10 {
    let counter_clone = Arc::clone(&sync_counter);
    handles.push(thread::spawn(move || {
        counter_clone.with_mut(|counter| {
            *counter.borrow_mut() += 1;
        });
    }));
}

for handle in handles {
    handle.join().unwrap();
}

sync_counter.with(|counter| {
    assert_eq!(*counter.borrow(), 10);
});
# });

Platform-Specific Usage

use send_cells::UnsafeSendCell;
use std::rc::Rc;

// Platform API guarantees callbacks run on main thread
fn setup_main_thread_callback() {
    let data = Rc::new("main thread only");

    // SAFETY: Platform guarantees this callback runs on main thread
    let cell = unsafe { UnsafeSendCell::new_unchecked(data) };

    platform_specific_api(move || {
        // SAFETY: We're guaranteed to be on the main thread
        let data = unsafe { cell.get() };
        assert_eq!(**data, "main thread only");
    });
}

setup_main_thread_callback();
# // The mock runs the callback inline, on the calling thread, so this example
# // executes rather than merely compiling.
# fn platform_specific_api<F: FnOnce() + Send + 'static>(f: F) { f() }

Safety Considerations

Safe Wrappers

The safe wrappers (SendCell, SyncCell, SendFuture) provide memory safety through:

  • Runtime thread checking with clear panic messages
  • Automatic synchronization via mutexes
  • Prevention of common concurrency bugs

Unsafe Wrappers

The unsafe wrappers require manual verification of:

  • Thread-local state dependencies
  • Concurrent access patterns
  • Drop safety on different threads
  • External synchronization requirements

Always prefer safe wrappers unless you have specific performance requirements and can rigorously verify thread safety.

Performance

Runtime Overhead

  • Safe wrappers: Small overhead for thread ID checking or mutex operations
  • Unsafe wrappers: Zero runtime overhead

Memory Overhead

  • SendCell: One ThreadId + wrapped value
  • SyncCell: One Mutex<()> + wrapped value
  • UnsafeSendCell: No overhead (transparent wrapper)

Related Crates

  • fragile - Similar functionality with different API design
  • once_cell - Lazy initialization primitives
  • parking_lot - Alternative synchronization primitives