How to convert unsafe extern "system" fn() to unsafe extern "C" fn()?

I know this sounds weird, but I am on the same machine with the same ABI, performing two exactly the same things but one from Rust and another from C. Now, with Rust, I already have a crate which returns what I need but as unsafe extern "system" fn(), but I need to pass this value to another crate's function, which receives it as unsafe extern "C" fn(). There are no ABI incompatibilities since these functions are eventually precisely the same (Vulkan's get_instance_proc_addr()).

Whenever I try to cast, I get an invalid cast error due to a non-primitive cast. If it helps, the crate's function I need to pass the extern "C" to is function generated by "bindgen".

The system I am writing this is Linux, rustc 1.70. According to the docs:

  • extern "C" -- This is the same as extern fn foo(); whatever the default your C compiler supports.
  • extern "system" -- Usually the same as extern "C", except on Win32, in which case it's "stdcall", or what you should use to link to the Windows API itself

An example of the problem on the playground:

fn a(f: unsafe extern "C" fn()) {
    f();
}

unsafe extern "system" fn s() {
    println!("Hello");
}

fn b() -> unsafe extern "system" fn() {
    s
}

fn main() {
    a(b() as unsafe extern "C" fn());
}

transmute can do the job:

#[cfg(target_family = "unix")]
fn cast_sys_to_c(f:unsafe extern "system" fn())->unsafe extern "C" fn() {
    // Safety: These two calling conventions are identical on unices
    unsafe { std::mem::transmute(f) }
}

fn main() {
    a(cast_sys_to_c(b()));
}
2 Likes

On the other hand, if you want to support all platforms, you'll need to save the system pointer in a static somewhere and write an extern "C" function that reads it:

use std::sync::atomic::{AtomicPtr, Ordering::Relaxed};

static S_PTR: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
fn init_s(s: unsafe extern "system" fn()) {
    S_PTR.store(s as *mut (), Relaxed);
}

/// Safety: In addition to normal FFI concerns,
/// `init_s` MUST have been previously called
unsafe extern "C" fn s_extern_c (){
    let s: unsafe extern "system" fn() = std::mem::transmute(S_PTR.load(Relaxed));
    s()
}

fn main() {
    init_s(b());
    a(s_extern_c);
}
2 Likes

Tbh, ultimately this should be fixed by the crate fixing their definition of get_instance_proc_addr.

1 Like

You can't, unfortunately, force everyone to do this or that. Also, there are reasons why the system and C ABIs are there, I believe.

For sure, you can't force people to change and a workaround may be necessary. But it's still a bug in the crate either way and should hopefully be fixed as such.

IMHO there's no good reason I can see for using the wrong ABI string. At best it works, so it's just unnecessarily annoying for users. At worst it's flat out wrong for some platform.

2 Likes

Thanks for the neat solution. I'm wondering though whether AtomicPtr could be replaced with a OnceLock? I feel like this gives more safety though it might have drawbacks I'm not aware of? Or is using OnceLock in the context of FFI something to be discouraged? Also, what are the benefits of storing the function pointer as *mut () and not *mut unsafe extern "system" fn()? Just to safe you some typing?

use std::sync::OnceLock;

static S_PTR: OnceLock<unsafe extern "system" fn()> = OnceLock::new();

unsafe extern "C" fn s_extern_c() {
    S_PTR.get_or_init(|| b())();
}

fn main() {
    a(s_extern_c);
}

Playground

OnceLock would be slower but safer (it would check if you didn't initialize the pointer). Though since you're already dealing with FFI unsafe pointer, I don't think it really matters overall, considering it's a relatively simple invariant.

*mut unsafe extern "system" fn() would be "a pointer to a function pointer", which is semantically wrong because you just want to store a function pointer.

2 Likes

Of course, thanks for clarifying.

I was working under the assumption that b() might have some arguments that need to be determined at runtime, which precludes calling it inline here. I originally used OnceLock, but needed to use set() and get() to accommodate this possibility. That meant s_extern_c might panic if the initialization function wasn't called first, which is UB when called from foreign code.

Calling a null function pointer is UB too, of course, but seemed more likely to behave sensibly than panicing across an FFI boundary— The AtomicPtr version, then, has the same safety requirements, better performance, and (hopefully) a better failure mode when the safety requirement is violated.

1 Like