How to create safe Rust wrapper for C API that creates a thread?

I am working on a embedded system that has no Rust std support.

It provides C API that start and runs a thread
likes this (simplified, written in Rust)

extern "C" {
    pub fn start_thread(
        func: unsafe extern "C" fn(data: *mut ::core::ffi::c_void) -> !,
        arg: *mut ::core::ffi::c_void,
    ) ;
}
  1. The C API requires the thread never terminates,
    so the func argument must be a function that never returns
  2. So there is no C API to join the thread.
  3. It is okay to require static lifetime everywhere in Rust API

How to implement a Safe Rust API that does not use pointer in its declaration?
If possible, the wrapper should be generic

Edit (2024/5/31): I made a mistake that func in C API was declared as *mut unsafe extern "C" fn(data: *mut ::core::ffi::c_void) -> !, this has been fixed to func: unsafe extern "C" fn(data: *mut ::core::ffi::c_void) -> !

I’m by no means an expert here, but I’d probably start with something like this and make adjustments according to the specific scenario:

use core::ffi;

extern "C" {
    pub fn start_thread(
        func: unsafe extern "C" fn(data: *mut ffi::c_void) -> !,
        arg: *mut ffi::c_void,
    ) ;
}

pub trait Thread: Send+Sync+Sized+'static {
    fn run(&mut self)->!;
}

/// Safety: `arg` must, in reality, be `&'static mut F`
unsafe extern "C" fn thread_main<F:Thread>(arg: *mut ffi::c_void)->! {
    let f:&mut F = unsafe { &mut *(arg as *mut F) };
    f.run()
}

pub fn spawn<F:Thread>(arg: &'static mut F) {
    unsafe { start_thread(thread_main::<F>, arg as *mut F as *mut ffi::c_void) }
}

Here's my attempt, using alloc if you have it:

#![feature(never_type)]

mod thread {
    pub fn spawn<F: FnOnce() -> ! + Send + 'static>(f: F) {
        use core::ffi::c_void;
    
        extern "C" {
            fn start_thread(
                thread: extern "C" fn(data: *mut c_void) -> !,
                data: *mut c_void,
            );
        }
        
        extern "C" fn thread<F: FnOnce() -> ! + Send + 'static>(data: *mut c_void) -> ! {
            let f = unsafe { Box::from_raw(data.cast::<F>()) };
            (f)()
        }
        
        unsafe {
            let data = Box::into_raw(Box::new(f)).cast();
            start_thread(thread::<F>, data)
        }
    }
}
1 Like

first off, this line in your example declares a pointer to a function pointer, which probably is not you want:

in rust, function pointer types are declared with fn keyword alone, e.g. fn (args) -> ret. see:

https://doc.rust-lang.org/book/ch19-05-advanced-functions-and-closures.html#function-pointers

note the C abi doesn't have the concept of "fat" pointers, so it needs two arguments. in rust, it is more idiomatic to use "fat" pointers for callback-like arguments. I'll use trait objects of the built-in FnMut trait in the example below.

first, since the rust api is generic, it must erase the type before being passed to ffi.

then you need create a "shim" function to feed into the ffi api. internally, it receives converts the argument back to the erased rust object.

in addition, if you want, you can make the "shim" function divergent so the rust callback can have arbitrary return type.

/// if `alloc` is not available, you can replace `Box` with `&'static dyn FnMut()`
fn safe_start_thread<F>(f: F)
where
    F: FnMut() -> !,
    F: Send,
    F: 'static
{
    // double box to get a thin pointer
    let f: Box<dyn FnMut()->!> = Box::new(f);
    let f = Box::new(f);
    // use the `thin` pointer and the "shim" function to call the ffi
    // safety: f is Send + 'static, and is a thin pointer
    unsafe {
        start_thread(thread_entry_point, Box::into_raw(f));
    }
}

/// the ffi shim 
unsafe extern "C" fn thread_entry_point(arg: *mut c_void) -> ! {
    // cast the argument
    // note because trait objects are themselves fat pointers,
    // an addition level of pointer indirection is involved
    let callback: *mut Box<dyn FnMut() -> !> = arg as _;
    // safety: ...
    let mut callback = Box::from_raw(callback);
    let _ = callback();
    // use panic or loop to ensure diverge if you use non-divergent callback
    loop {}
}

you can also use custom trait instead of FnMut, but it's the same principle.

2 Likes

You can avoid the double-boxing if you make your shim function generic instead of making a trait object; this is roughly equivalent to defining your own 1-function vtable:

    let f: Box<F> = Box::new(f);
    // safety: f is Send + 'static, and is a thin pointer
    unsafe {
        start_thread(thread_entry_point::<F>, Box::into_raw(f));
    }

This is a non-trivial transformation for this code, as you’ll need to come up with some static storage location for both the closure and the fat pointer (to have a thin pointer to it). Without an allocator, the space for these will need to be provided by the caller somehow.

2 Likes

that's nice. now you mentioned it, I have actually used the same technique for some win32 message loop wrapper code before, but I forget about it.

good point. if alloc is not available, it's easier to use a custom callback trait than the special Fn traits.

one of the main pros of using Fn traits is (IMO) the closure syntax (automatically captures context) is more natural to write than a named context type, but without alloc, the fact you cannot name the type of closures then causes the static storage problem. [1]


  1. I wish rust had inferred type for static variable, at least in function scopes ↩︎

feature(type_alias_impl_trait) should fix that, in theory: Permit impl Trait in type aliases by varkor · Pull Request #2515 · rust-lang/rfcs · GitHub

Here's some nightmarish code that at least compiles:

#![feature(type_alias_impl_trait, never_type)]

type MyThread = impl Fn() -> ! + Send + 'static;

static MY_THREAD: MyThread = || {
    panic!("Hello!");
};

fn main() {
    thread::spawn(&MY_THREAD)
}

mod thread {
    type ThreadCallback = dyn Fn() -> ! + Send + 'static;
    pub fn spawn(mut f: &'static ThreadCallback) {
        use std::ffi::c_void;
    
        extern "C" {
            fn start_thread(
                thread: extern "C" fn(data: *mut c_void) -> !,
                data: *mut c_void,
            );
        }
        
        extern "C" fn thread(data: *mut c_void) -> ! {
            let f = data.cast::<&'static ThreadCallback>();
            unsafe { (&*f)() }
        }
        
        unsafe {
            let data: *mut &'static ThreadCallback = &mut f;
            start_thread(thread, data.cast())
        }
    }
}
1 Like

Edit (2024/5/31): I made a mistake that func in C API was declared as *mut unsafe extern "C" fn(data: *mut ::core::ffi::c_void) -> !, this has been fixed to func: unsafe extern "C" fn(data: *mut ::core::ffi::c_void) -> !

Thanks all for the solutions:

TLDR:

  1. I don't know why fn() -> ! is ok, but FnOnce() -> ! is not stablized, so I work around this by FnOnce() -> core::convert::Infallible , return a enum without variant can also ensure the closure never returns
  2. It seems impossible to transmute generic extern "C" function to extern "C" fn(data: *mut c_void) -> !, with error "cannot transmute zero-sized type", so I make an intermediate function for workaround

Here is my current solution:

#![no_std]
extern crate alloc;
use alloc::boxed::Box;
use core::convert::Infallible;
use core::ffi::c_void;

// `FnOnce -> !` is not stablized yet. Use enum without variant for workaround
pub fn spawn<F>(f: F) where F: FnOnce() -> Infallible + Send + 'static {
    let f = Box::into_raw(Box::new(f));
    unsafe {
        start_thread_should_no_return(
            thread_helper::<F>,
            f.cast(),
        );
    }
}

extern "C" fn thread_helper<F>(data: *mut c_void) where F: FnOnce() -> Infallible + Send + 'static {
    let f = unsafe { Box::from_raw(data.cast::<F>()) };
    (f)();
    // This function cannot return because it returns a `Infallible` which is
    // an enum without variant without legal value
}

/// It seems Rust does not allow `thread_helper<F>` to be transformed directly to `extern "C" fn(*mut c_void) -> !`
/// This is a workaround for that
unsafe extern "C" fn start_thread_should_no_return(func: unsafe extern "C" fn(*mut c_void), data: *mut c_void) {
    start_thread(core::mem::transmute::<_, unsafe extern "C" fn(*mut c_void) -> !>(func), data);
}

extern "C" {
    // The C API requires that `func` never returns
    pub fn start_thread(
        func: unsafe extern "C" fn(data: *mut c_void) -> !,
        arg: *mut c_void,
    );
}

// Example to call the safe wrapper

fn example() {
    fn example_thread() -> ! {
        loop {
        }
    }
    spawn(|| example_thread() );
}

Never type requires nightly and #[feature(never_type)], yes. I wasn't aware that it was stable to use in signatures!

I'm not completely sure why you're trying to use transmute here, but yes, you can't transmute a function type (referencing a specific function, which therefore has no size) to a function pointer, transmute is a bit reinterpretation. If you can't fix the return types to match you could instead cast the specific thread_helper::<T> function to a function pointer first by assigning it to a variable with the pointer type ... fn(...) -> Infallible, if you want to simplify your code a bit.

See fn - Rust

In Rust 1.0.0, ! was only a piece of syntax designating a function that never returned. The plan to have it as a type too came later, in RFC 1216.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.