ABI of (unsafe) fn() and can you (unsafely) create a safe pointer to an unsafe fn (if you know what you're doing)?

If you have an unsafe fn foo() {} and an fn bar() {}, you can pass bar to something that takes an unsafe fn(), but you can't pass foo to something that takes an fn()...

but since ABI is invariant, the fact that you can throw bar at unsafe fn() means they must have the same ABI. so... can you, or can you not, turn an unsafe fn() into an fn() when needed?

Before you go about transforming unsafe function pointers to safe function pointers, can you just wrap the unsafe function in a safe non-capturing closure, which can then be casted to a safe function pointer?

unsafe fn foo(x: usize) -> String { panic!() }

// The below is difficult...
let unsafe_fn_ptr = foo as unsafe fn(usize) -> String;

// But we can manually do this:
let safe_fn_closure = |x: usize| -> String {
    // Our unsafe contract appears here.
    unsafe {
        foo(x)
    }
};

// Use the closure here, or cast:
let safe_fn_ptr = safe_fn_closure as fn(usize) -> String;

// The closure transform can be abbreviated:
let safe_fn_closure = |x| unsafe { foo(x) };
1 Like

Ah sorry. This is more of a hypothetical question about Rust's ABIs and stuff. It would only come up specifically when you have an existing unsafe fn() pointer and you want to pass it to an API taking an fn() (not Fn()) - without knowing what the original function is. Probably not useful in practice but it's something we got curious about.

I'm going to be honest with you, I am not sure.

But here are my thoughts;

extern "Rust" fn()

Has the Rust abi, and similarly,

unsafe extern "Rust" fn()

also has the Rust abi (note that these are identical to not having the extern "Rust" modifier).

Now, I don't believe that Rust makes any guarantees about its ABI, so it's possible that it could be different for unsafe/safe variants of the functions.

However

C doesn't have a notion of unsafe/safe functions, and so I would have to assume that the following two type signatures are equivalent, abi-wise:

type Safe = extern "C" fn();
type Unsafe = unsafe extern "C" fn();

And, therefore, the following should be okay:

extern "C" fn foo() {}

let safe_fn = foo as fn();

let unsafe_fn = unsafe {
    let pointer = &safe_fn as *const extern "C" fn();
    
    *(reference as *const unsafe extern "C" fn())
};

However, I believe someone better acquainted with the internals of Rust would be able to better answer the question at hand.

1 Like

I believe that Soni's reasoning here is not quite sufficient: because you can assign fn() to a place with type unsafe fn(), that just means fn() coerces to unsafe fn().

A hypothetical compiler could emit a trampoline closure for this coercion to adapt ABI, therefore allowing for unsafe to impact ABI. (Closures with no state are coercable to function pointers.)

That said, it is true that currently this coercion is a noöp transmute, and it would not be unreasonable to guarantee. It's just not guaranteed at the moment (unless I've missed some line where it is).

1 Like

And how would such trampoline closure handle things such as recursion/reentracy, threads, etc?

Keeping in mind we're talking about e.g.

fn foo(x: fn()) {
  bar(x)
}

fn bar(x: unsafe fn()) {
}

vs

fn foo(x: fn()) {
}

fn bar(x: unsafe fn()) {
  foo(x)
}

Even if it's not explicitly guaranteed, as far as we can tell there's no way it can't be guaranteed, unless Rust ABI fn() (and/or unsafe fn()) suddenly became tagged pointers, or Rust got a JIT compiler.

I'm not versed in assembly, so I'll write pseudocode:

fn foo(x: unsafe fn()) {}

fn bar(x: fn()) {
    foo(x)
}

//turns into
fn change_abi(source: fn() , target: impl FnOnce(unsafe fn())) {
    let resulting = intrinsics::abi_change(source);
    target(resulting)
}

fn foo(x: unsafe fn()) {}

fn bar(x: fn()) {
    change_abi(x, foo);
}

And this is all inlined later.