dtact-util 0.2.0

Async utilities for Dtact: I/O, filesystem, process, signal, stream and timer primitives with lock-free native (io_uring/IOCP/kqueue) and tokio backends. Designed for hardware-level control and non-blocking heterogeneous orchestration.
Documentation
//! C FFI (CFFI) layer for `dtact-util`'s six native primitives.
//!
//! Covers `io`, `fs`, `process`, `signal`, `stream`, and `timer`, exposed as
//! a **blocking, synchronous** `extern "C"` surface for callers in C, C++,
//! or any language with a C FFI.
//!
//! Enabled by the `ffi` Cargo feature (off by default; implies `native`).
//! Header files (`dtact_util.h` / `dtact_util.hpp`) are generated by
//! `build.rs` via cbindgen when the crate is built with `DEV=1` set.
//!
//! # Design
//!
//! Every handle this layer hands back to C is a **raw owning pointer**
//! produced by [`Box::into_raw`] and reclaimed by [`Box::from_raw`] (rather
//! than an opaque integer index into a handle table). A non-null pointer
//! returned by a `*_create` / `*_open` / `*_spawn` / `*_connect` / `*_bind`
//! function is owned by the caller and MUST eventually be handed back to
//! the matching `*_close` / `*_free` / `*_wait` function exactly once — that
//! call takes ownership and drops the underlying Rust object. Using a handle
//! after it has been freed, or freeing it twice, is undefined behavior.
//!
//! Blocking is genuine: each call drives the underlying async operation to
//! completion on the calling thread via a small internal [`block_on`] spin
//! executor before returning. A few operations also offer a non-blocking
//! flavor where blocking-only would be a real usability regression.
//!
//! # Error reporting
//!
//! One convention is used across all six modules: a **thread-local
//! last-error string**. When a fallible function fails it returns a
//! sentinel (a null pointer, or a negative length / `-1` byte count, or a
//! nonzero `int`) and stores a human-readable message retrievable via
//! [`dtact_util_last_error_message`]. The returned pointer is valid until
//! the next `dtact-util` FFI call *on the same thread*; copy it out if you
//! need to keep it. A successful call clears the thread-local error.
//!
//! # Safety
//!
//! This contract applies to **every** `unsafe extern "C"` function in this
//! module and its submodules; it is stated here once and referenced from
//! each function's own `# Safety` section:
//!
//! - Every non-null handle pointer passed in must have been returned by the
//!   corresponding `dtact-util` FFI constructor, must not have been freed,
//!   and must not be used concurrently from another thread unless that
//!   function is explicitly documented as thread-safe. Passing a null,
//!   dangling, or foreign pointer is undefined behavior (null is checked
//!   and reported as an error where a handle is required, but a non-null
//!   invalid pointer cannot be detected).
//! - Every `*const c_char` string pointer must be non-null and point to a
//!   valid NUL-terminated C string.
//! - Every `(buf, len)` pair must describe a single allocation of at least
//!   `len` bytes that stays valid for the duration of the call; read
//!   functions write up to `len` bytes into it, write functions read `len`
//!   bytes from it.
//! - Out-pointer parameters (`*mut *mut T`) must be non-null and point to
//!   writable storage for one pointer.

#![allow(clippy::missing_safety_doc)]

use std::cell::RefCell;
use std::ffi::{CStr, CString, c_char};
use std::future::Future;

pub mod fs;
pub mod io;
pub mod process;
pub mod signal;
pub mod stream;
pub mod timer;

thread_local! {
    /// Last-error message for the current thread. See the module doc's
    /// "Error reporting" section.
    static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}

/// Store `msg` as this thread's last-error string.
pub(crate) fn set_last_error(msg: impl Into<Vec<u8>>) {
    // Replace any interior NULs so `CString::new` can't fail on an
    // std::io::Error's Display text.
    let mut bytes = msg.into();
    for b in &mut bytes {
        if *b == 0 {
            *b = b'?';
        }
    }
    let c = CString::new(bytes).unwrap_or_default();
    LAST_ERROR.with(|e| *e.borrow_mut() = Some(c));
}

/// Clear this thread's last-error string (called at the start of a
/// successful operation).
pub(crate) fn clear_last_error() {
    LAST_ERROR.with(|e| *e.borrow_mut() = None);
}

/// Convenience: record an `std::io::Error` (or any `Display`) as the last
/// error.
pub(crate) fn set_io_error(err: &std::io::Error) {
    set_last_error(err.to_string());
}

/// Return the current thread's last-error message as a NUL-terminated C
/// string, or null if no error has been recorded since the last successful
/// call.
///
/// The returned pointer is owned by the library and remains valid only
/// until the next `dtact-util` FFI call on this thread. Do not free it.
///
/// # Safety
///
/// See the [`crate::ffi`] module-level Safety contract. This function reads
/// only thread-local state and takes no pointer arguments.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dtact_util_last_error_message() -> *const c_char {
    LAST_ERROR.with(|e| e.borrow().as_ref().map_or(std::ptr::null(), |c| c.as_ptr()))
}

/// Minimal single-threaded blocking executor: polls `fut` to completion on
/// the calling thread with a no-op waker, yielding between polls. Every
/// blocking FFI entry point drives its async work through this.
///
/// This is sound for this crate's futures because their completions are
/// driven by dedicated backend worker threads (`IOCP`/`io_uring` workers, the
/// timer wheel thread, the process/blocking pools, or the peer endpoint for
/// in-process streams), not by the waker — the waker is purely an
/// optimization we can safely elide by re-polling.
pub(crate) fn block_on<F: Future>(fut: F) -> F::Output {
    use std::pin::pin;
    use std::task::{Context, Poll, Waker};

    let waker = Waker::noop();
    let mut cx = Context::from_waker(waker);
    let mut fut = pin!(fut);
    loop {
        match fut.as_mut().poll(&mut cx) {
            Poll::Ready(v) => return v,
            Poll::Pending => std::thread::yield_now(),
        }
    }
}

/// Borrow a `*const c_char` as `&str`, recording an error and returning
/// `None` on null or invalid UTF-8.
///
/// # Safety
///
/// `ptr`, if non-null, must be a valid NUL-terminated C string.
pub(crate) unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
    if ptr.is_null() {
        set_last_error("null string pointer");
        return None;
    }
    unsafe { CStr::from_ptr(ptr) }.to_str().map_or_else(
        |_| {
            set_last_error("string is not valid UTF-8");
            None
        },
        Some,
    )
}