Skip to main content

Crate future_form_ffi

Crate future_form_ffi 

Source
Expand description

FFI support for future_form: host-driven polling, opaque handles, and effect slots.

This crate provides the building blocks for FFI bridges that let foreign hosts (Go, Java, Python, C, Swift) drive Rust async state machines without an async runtime.

§Core types

TypePurpose
PollOnceExtension trait: poll a boxed future once with a no-op waker
HostHandleThin-pointer wrapper for passing boxed futures through C ABI
AtomicSlotLock-free, thread-safe slot for a single value
EffectSlotShared-state channel for the effect protocol (built on AtomicSlot)
EffectHandleFuture handle + stashed effect + context pointer

§Architecture

┌────────────┐                    ┌──────────────────────┐
│  FFI Host  │  poll_once()       │  HostHandle<F>       │
│  (Go, Java │ ──────────────────>│  or EffectHandle     │
│   Python…) │ <──────────────────│                      │
│            │  Poll<T>           │                      │
│            │                    │  ┌────────────────┐  │
│            │  take_effect()     │  │  EffectSlot    │  │
│            │ ──────────────────>│  │  <E, R>        │  │
│            │ <──────────────────│  └────────────────┘  │
│            │  fulfill(resp)     │                      │
│            │ ──────────────────>│                      │
└────────────┘                    └──────────────────────┘

§FFI bridge pattern

FFI hosts can’t receive a Rust Box<T> directly — they need a raw pointer. The standard pattern is:

  1. Create: heap-allocate the handle and convert to a raw pointer with Box::into_raw, transferring ownership to the host.
  2. Poll/inspect: the host passes the pointer back into Rust bridge functions that reconstruct a reference (&mut *ptr).
  3. Free: the host calls a destructor that reclaims ownership with Box::from_raw and drops the handle.
// 1. Create — ownership moves to the host
let handle = HostHandle::new(fut);
let ptr = Box::into_raw(Box::new(handle));  // thin *mut for C ABI

// 2. Poll — borrow from the raw pointer
let handle = unsafe { &mut *ptr };
let result = handle.poll_once();

// 3. Free — reclaim and drop
unsafe { drop(Box::from_raw(ptr)) };

EffectHandle follows the same pattern, adding a stashed effect and context pointer for the effect protocol.

§Example

Simple bridge using HostHandle:

use core::task::Poll;
use future_form::{FutureForm, Sendable};
use future_form_ffi::host_handle::HostHandle;

let fut = Sendable::from_future(async { 42u64 });
let mut handle = HostHandle::new(fut);
assert_eq!(handle.poll_once(), Poll::Ready(42));

Effect protocol using EffectSlot:

use core::task::Poll;
use future_form_ffi::effect_slot::EffectSlot;

#[derive(Clone, Debug, PartialEq)]
enum Fx { GetTime }

#[derive(Debug, PartialEq)]
enum Resp { Time(u64) }

let slot: EffectSlot<Fx, Resp> = EffectSlot::new();
assert_eq!(slot.request(&Fx::GetTime), Poll::Pending);
slot.fulfill(Resp::Time(1234));
assert_eq!(slot.request(&Fx::GetTime), Poll::Ready(Resp::Time(1234)));

§Slot ownership

An EffectSlot is a single request-response channel. If multiple futures share the same slot, they will overwrite each other’s pending effects and receive responses intended for other futures. This is memory-safe (all operations are atomic) but semantically incorrect.

Two ownership strategies are supported:

§Per-struct slot (simple, one future at a time)

Embed the slot in the struct. All futures share it. Only one future may be in flight at a time:

struct MyService {
    state: u64,
    effects: EffectSlot<Effect, Response>,
}

§Per-future Arc<EffectSlot> (unlimited concurrent futures)

Create a fresh slot for each future. Share it via Arc between the async closure and the EffectHandle. There is no limit on the number of in-flight futures — each one gets its own independent effect channel:

use alloc::sync::Arc;

let slot = Arc::new(EffectSlot::new());
let slot_for_future = Arc::clone(&slot);

let fut = Box::pin(async move {
    let resp = core::future::poll_fn(|_cx| {
        slot_for_future.request(&Effect::GetTimestamp)
    }).await;
    // ...
});

let handle = EffectHandle::new(HostHandle::new(fut), context);
// The bridge stores `slot` alongside the handle (e.g., in a wrapper struct)
// so the host can call slot.take_effect() / slot.fulfill().

The per-future approach has no capacity limit — the host can create as many concurrent futures as memory allows, each with its own effect channel. The cost is one Arc allocation per future, negligible alongside the Box::pin that already heap-allocates the future. The key_value_store example demonstrates this pattern with concurrent Get/Put/Delete/List operations. See the design docs for a full comparison of ownership strategies including slot registries and fixed-size arrays.

§Handle storage

Rust does not assign or manage future IDs. The EffectHandle is the identity — an opaque token bundling the future, its slot, and its context. The host stores these however its concurrency model demands:

§Host-side collection

The host keeps a map of handles, keyed however it likes:

  Host (Go / Java / Python)
  ┌─────────────────────────────────────────────┐
  │                                             │
  │  handles: Map<FutureId, *mut EffectHandle>  │
  │                                             │
  │  handle_1 ──► EffectHandle ──► Arc<slot_1>  │
  │  handle_2 ──► EffectHandle ──► Arc<slot_2>  │
  │  handle_3 ──► EffectHandle ──► Arc<slot_3>  │
  │                                             │
  └─────────────────────────────────────────────┘

The FutureId is whatever the host wants — an integer counter, a UUID, a task name. It is entirely host-side bookkeeping. Rust does not know or care about it.

§Inline in host tasks

Each host-side task/goroutine/thread holds its handle directly — no map needed:

  Go goroutine A                Go goroutine B
  ┌──────────────────┐          ┌──────────────────┐
  │ handle *C.Handle │          │ handle *C.Handle │
  └────────┬─────────┘          └────────┬─────────┘
           │                             │
           ▼                             ▼
     EffectHandle                  EffectHandle
     └─► Arc<slot_a>              └─► Arc<slot_b>

No future ID at all — the handle is the identity.

§Event loop with a vec

  Host event loop
  ┌──────────────────────────────┐
  │  active: Vec<EffectHandle>   │
  │                              │
  │  for handle in &mut active { │
  │      match handle.poll() {   │
  │          Pending => check fx │
  │          Ready   => remove   │
  │      }                       │
  │  }                           │
  └──────────────────────────────┘

The index in the vec is the implicit ID, or the host uses a slab allocator. When a future completes, the host removes it — the handle drops, its Arc<EffectSlot> drops, and the slot is freed.

All three patterns work because EffectHandle is self-contained: the host never needs to coordinate with Rust about which handle maps to which future. See the design docs for more detail.

§no_std support

This crate is #![no_std] and requires only core + alloc. No feature flags, no platform-specific dependencies.

EffectSlot is built on AtomicSlot, which uses lock-free AtomicPtr swaps for thread safety — no mutex, no poisoning, no global lock contention.

Modules§

atomic_slot
A lock-free, thread-safe slot that holds at most one value.
effect_handle
FFI handle that pairs a future with a stashed effect snapshot.
effect_slot
Shared effect channel between async state machines and FFI bridges.
host_handle
Opaque thin-pointer handle for passing boxed futures through C ABI.
poll_once
The PollOnce extension trait for host-driven polling.