lazily 0.10.2

Lazy reactive signals with dependency tracking and cache invalidation
Documentation
//! C ABI adapter for carrying `lazily-ipc` messages across FFI.
//!
//! The FFI surface deliberately owns only opaque channel handles and byte
//! buffers. Foreign runtimes exchange serialized [`IpcMessage`](crate::IpcMessage)
//! values without receiving Rust contexts, closures, references, or typed
//! handles.

use crate::ipc::IpcMessage;
use std::collections::VecDeque;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::{ptr, slice};

/// Status code returned by FFI functions.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LazilyFfiStatus {
    /// Operation completed successfully.
    Ok = 0,
    /// The channel had no queued message.
    Empty = 1,
    /// A required pointer argument was null.
    NullPointer = 2,
    /// Input bytes were not a valid serialized `IpcMessage`.
    InvalidMessage = 3,
    /// An `IpcMessage` could not be encoded to the configured FFI codec.
    EncodeFailed = 4,
    /// A Rust panic was caught before crossing the ABI.
    Panic = 5,
}

/// Message variant observed in a serialized `IpcMessage`.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum LazilyFfiMessageKind {
    /// No message kind has been written.
    #[default]
    Unknown = 0,
    /// Full graph image.
    Snapshot = 1,
    /// Incremental graph update.
    Delta = 2,
}

/// Byte buffer allocated by Rust and returned across the FFI boundary.
///
/// Call [`lazily_ffi_bytes_free`] exactly once for each non-empty buffer
/// returned by this module.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LazilyFfiBytes {
    /// Pointer to the first byte, or null when `len == 0`.
    pub ptr: *mut u8,
    /// Number of initialized bytes at `ptr`.
    pub len: usize,
}

impl LazilyFfiBytes {
    /// Return an empty FFI buffer.
    pub const fn empty() -> Self {
        Self {
            ptr: ptr::null_mut(),
            len: 0,
        }
    }

    fn from_vec(bytes: Vec<u8>) -> Self {
        if bytes.is_empty() {
            return Self::empty();
        }

        let mut bytes = bytes.into_boxed_slice();
        let ptr = bytes.as_mut_ptr();
        let len = bytes.len();
        std::mem::forget(bytes);
        Self { ptr, len }
    }
}

impl Default for LazilyFfiBytes {
    fn default() -> Self {
        Self::empty()
    }
}

/// Opaque in-process FFI message channel.
///
/// The handle queues canonical JSON-encoded [`IpcMessage`] frames. It is a
/// local ownership/ABI adapter; it is not a second graph state model.
#[derive(Debug, Default)]
pub struct LazilyFfiChannel {
    queue: VecDeque<Vec<u8>>,
}

/// Create an empty FFI channel handle.
#[unsafe(no_mangle)]
pub extern "C" fn lazily_ffi_channel_new() -> *mut LazilyFfiChannel {
    Box::into_raw(Box::<LazilyFfiChannel>::default())
}

/// Free an FFI channel handle created by [`lazily_ffi_channel_new`].
///
/// # Safety
///
/// `channel` must be null or a pointer returned by `lazily_ffi_channel_new`
/// that has not already been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_channel_free(channel: *mut LazilyFfiChannel) {
    if channel.is_null() {
        return;
    }

    let _ = catch_unwind(AssertUnwindSafe(|| {
        // SAFETY: Guaranteed by the caller's ownership contract above.
        unsafe { drop(Box::from_raw(channel)) };
    }));
}

/// Send one JSON-encoded `IpcMessage` through the FFI channel.
///
/// The input frame is decoded and re-encoded before enqueueing so receivers get
/// canonical bytes owned by Rust.
///
/// # Safety
///
/// `channel` must be a live pointer returned by `lazily_ffi_channel_new`.
/// `ptr..ptr+len` must be readable for `len` bytes when `len > 0`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_channel_send_json(
    channel: *mut LazilyFfiChannel,
    ptr: *const u8,
    len: usize,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers both raw pointers.
        let channel = unsafe { channel_mut(channel) }?;
        let bytes = unsafe { bytes_from_raw(ptr, len) }?;
        let message = decode_message_json(bytes)?;
        let encoded = encode_message_json(&message)?;
        channel.queue.push_back(encoded);
        Ok(())
    })
}

/// Receive one JSON-encoded `IpcMessage` from the FFI channel.
///
/// Writes an empty buffer and returns [`LazilyFfiStatus::Empty`] when no message
/// is currently queued.
///
/// # Safety
///
/// `channel` must be a live pointer returned by `lazily_ffi_channel_new`.
/// `out` must point to writable storage for one [`LazilyFfiBytes`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_channel_recv_json(
    channel: *mut LazilyFfiChannel,
    out: *mut LazilyFfiBytes,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers both raw pointers.
        let out = unsafe { out_bytes_mut(out) }?;
        *out = LazilyFfiBytes::empty();

        let channel = unsafe { channel_mut(channel) }?;
        match channel.queue.pop_front() {
            Some(bytes) => {
                *out = LazilyFfiBytes::from_vec(bytes);
                Ok(())
            }
            None => Err(LazilyFfiStatus::Empty),
        }
    })
}

/// Return the number of queued messages in the FFI channel.
///
/// # Safety
///
/// `channel` must be a live pointer returned by `lazily_ffi_channel_new`.
/// `out_len` must point to writable storage for one `usize`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_channel_len(
    channel: *const LazilyFfiChannel,
    out_len: *mut usize,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers both raw pointers.
        let channel = unsafe { channel_ref(channel) }?;
        let out_len = unsafe { out_len.as_mut() }.ok_or(LazilyFfiStatus::NullPointer)?;
        *out_len = channel.queue.len();
        Ok(())
    })
}

/// Validate that a byte slice is a JSON-encoded `IpcMessage`.
///
/// # Safety
///
/// `ptr..ptr+len` must be readable for `len` bytes when `len > 0`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_ipc_message_validate_json(
    ptr: *const u8,
    len: usize,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers the raw byte range.
        let bytes = unsafe { bytes_from_raw(ptr, len) }?;
        decode_message_json(bytes)?;
        Ok(())
    })
}

/// Return the variant kind for a JSON-encoded `IpcMessage`.
///
/// # Safety
///
/// `ptr..ptr+len` must be readable for `len` bytes when `len > 0`. `out_kind`
/// must point to writable storage for one [`LazilyFfiMessageKind`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_ipc_message_kind_json(
    ptr: *const u8,
    len: usize,
    out_kind: *mut LazilyFfiMessageKind,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers all raw pointers.
        let out_kind = unsafe { out_kind.as_mut() }.ok_or(LazilyFfiStatus::NullPointer)?;
        *out_kind = LazilyFfiMessageKind::Unknown;

        let bytes = unsafe { bytes_from_raw(ptr, len) }?;
        let message = decode_message_json(bytes)?;
        *out_kind = match message {
            IpcMessage::Snapshot(_) => LazilyFfiMessageKind::Snapshot,
            IpcMessage::Delta(_) => LazilyFfiMessageKind::Delta,
        };
        Ok(())
    })
}

/// Validate and return canonical JSON bytes for a serialized `IpcMessage`.
///
/// # Safety
///
/// `ptr..ptr+len` must be readable for `len` bytes when `len > 0`. `out` must
/// point to writable storage for one [`LazilyFfiBytes`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_ipc_message_clone_json(
    ptr: *const u8,
    len: usize,
    out: *mut LazilyFfiBytes,
) -> LazilyFfiStatus {
    ffi_guard(|| {
        // SAFETY: The public function safety contract covers all raw pointers.
        let out = unsafe { out_bytes_mut(out) }?;
        *out = LazilyFfiBytes::empty();

        let bytes = unsafe { bytes_from_raw(ptr, len) }?;
        let message = decode_message_json(bytes)?;
        *out = LazilyFfiBytes::from_vec(encode_message_json(&message)?);
        Ok(())
    })
}

/// Free an FFI byte buffer returned by this module.
///
/// # Safety
///
/// `bytes` must be empty or a buffer returned by this module that has not
/// already been freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn lazily_ffi_bytes_free(bytes: LazilyFfiBytes) {
    if bytes.ptr.is_null() {
        return;
    }

    let _ = catch_unwind(AssertUnwindSafe(|| {
        // SAFETY: Guaranteed by the caller's ownership contract above.
        unsafe {
            drop(Box::from_raw(ptr::slice_from_raw_parts_mut(
                bytes.ptr, bytes.len,
            )))
        };
    }));
}

fn ffi_guard(operation: impl FnOnce() -> Result<(), LazilyFfiStatus>) -> LazilyFfiStatus {
    match catch_unwind(AssertUnwindSafe(operation)) {
        Ok(Ok(())) => LazilyFfiStatus::Ok,
        Ok(Err(status)) => status,
        Err(_) => LazilyFfiStatus::Panic,
    }
}

fn decode_message_json(bytes: &[u8]) -> Result<IpcMessage, LazilyFfiStatus> {
    serde_json::from_slice(bytes).map_err(|_| LazilyFfiStatus::InvalidMessage)
}

fn encode_message_json(message: &IpcMessage) -> Result<Vec<u8>, LazilyFfiStatus> {
    serde_json::to_vec(message).map_err(|_| LazilyFfiStatus::EncodeFailed)
}

unsafe fn channel_ref<'a>(
    channel: *const LazilyFfiChannel,
) -> Result<&'a LazilyFfiChannel, LazilyFfiStatus> {
    // SAFETY: The caller promises that `channel` is a live handle or null.
    unsafe { channel.as_ref() }.ok_or(LazilyFfiStatus::NullPointer)
}

unsafe fn channel_mut<'a>(
    channel: *mut LazilyFfiChannel,
) -> Result<&'a mut LazilyFfiChannel, LazilyFfiStatus> {
    // SAFETY: The caller promises that `channel` is a live handle or null.
    unsafe { channel.as_mut() }.ok_or(LazilyFfiStatus::NullPointer)
}

unsafe fn out_bytes_mut<'a>(
    out: *mut LazilyFfiBytes,
) -> Result<&'a mut LazilyFfiBytes, LazilyFfiStatus> {
    // SAFETY: The caller promises that `out` is writable or null.
    unsafe { out.as_mut() }.ok_or(LazilyFfiStatus::NullPointer)
}

unsafe fn bytes_from_raw<'a>(ptr: *const u8, len: usize) -> Result<&'a [u8], LazilyFfiStatus> {
    if ptr.is_null() {
        return if len == 0 {
            Ok(&[])
        } else {
            Err(LazilyFfiStatus::NullPointer)
        };
    }

    // SAFETY: The caller promises that the byte range is readable.
    Ok(unsafe { slice::from_raw_parts(ptr, len) })
}