The C API requires the thread never terminates,
so the func argument must be a function that never returns
So there is no C API to join the thread.
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) -> !
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.
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.
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]
I wish rust had inferred type for static variable, at least in function scopes ↩︎
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:
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
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.