Unexpected behavior within FFI procedural macro

I am a little confused by the following code. I am attempting to create a procedural macro for my FFI code. My problem is that I want to provide a mutable buffer to the caller. The following code compiles ONLY when the assert_is_send() code is uncommented, otherwise I get the following error:
`*mut u8` cannot be sent between threads safely the trait `std::marker::Send` is not implemented for `*mut u8` required by a bound in `SessionGuard::execute.

pub trait SessionGuard: Sized {
    fn execute<F, Fut>(session: *const c_char, action: F) -> i32
    where
        F: FnOnce(Self) -> Fut + Send + 'static,
        Fut: Future<Output = Result<(), Error>> + Send + 'static;
}

pub struct OutBuffer(pub *mut u8);

unsafe impl Send for OutBuffer {} 

#[session_endpoint]
async fn session(user: SessionUser, buffer: OutBuffer) -> Result<(), Error> {

    // fn assert_is_send<T: Send>(_t: &T) {}
    // assert_is_send(&buffer);
    let outbuffer = unsafe { std::slice::from_raw_parts_mut(buffer.0, 10) };
    ...    
    Ok(())
}

Does session_endpoint generate a closure? If so the closure probably captures buffer.0 when the assert_is_send call is not present and the entire buffer if it is present. You can use let buffer = buffer; to force the entire OutBuffer to be moved into the closure.

1 Like

Yup, because I am writing extern "C" functions I have to use a tokio runtime to grab the session information for the user.

pub static RT: LazyLock<Runtime> = LazyLock::new(|| {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .expect("Failed to build Tokio runtime")
});

RT.block_on(async move {
  let user = Self::get(&session_id).await?;
  action(user, session_id).await
 })

All the async functions under the #[session_endpoint] macro essentially get rewritten as so:

extern "C" fn #fn_name(
   session: *const std::ffi::c_char,
   #(#ffi_arg_names: <#ffi_arg_types as FromFFI>::CInput),
) -> i32 {
// And then the async action gets moved inside 
   RT.block_on(async move {...})

Well that works!

Somewhat recently the compiler has changed its closure capture rules. Before, it always captured the whole struct if you mentioned it in the closure's body. Now, it tries to capture only the individual fields that are used, possibly with different capture modes depending on their use. async { } blocks use similar implicit capture rules.

That's what's hitting you here. Since the function is rewritten into a closure, it tries to do the weakest possible capture on buffer. Since you use only the inner buffer.0 field, by default it captures only that field, i.e. the raw pointer. And raw pointers aren't Send.

If you explicitly use the whole buffer struct in the body, in any way, it will capture the entire struct. The pointer is no longer held over .await, since it's used just in the slice expression, so there is no error.

Since the resulting error is quite obscure, and the syntax of an async fn (decorated with the macro) suggests that the parameters are owned by the function body, I would suggest you change the definition of the macro to unconditionally move all function parameters into the closure. You can use it with the let param = param; trick, or just use a move closure, if the macro doesn't create any other captures.