Trait for unsafe functions?


#1

I can write a function that takes an unsafe fn, e.g. fn takes_unsafe(f: unsafe fn()), and the compiler will enforce that whatever is passed to take_unsafe is an unsafe function. Is there a way to achieve the same with traits? As in, something like fn takes_unsafe_trait<F: unsafe Fn()>(f: F)?


#2

Don’t think that’s possible - the closures are just anon structs implementing one of Fn, FnOnce, or FnMut traits, which don’t have any unsafe in them.

You can invent your own unsafe marker trait but you’d lose the closure sugar.

Why do you need this by the way?


#3

Ah, oh well.

Why do you need this by the way?

I’m working on an object allocator, which is an allocator for a particular type that initializes objects before handing them out. The safety/unsafety story is a bit tricky because you want to make it so that an unsafe datastructure that presents a safe API can take as an argument an object allocator, and that allocator should be guaranteed to allocate properly-initialized objects. Thus, the client must be prevented from getting access to an object allocator that violates this invariant without using unsafe code, or else the data structure would allocate objects that were not properly initialized but treat them as initialized, leading to unsound behavior.

In particular, I still want unsafe code to be able to skirt these rules if the author knows what they’re doing, and be able to provide an initializer function that is unsafe. But I want to do that without breaking the safety guarantees of the safe parts of the API.


#4

I don’t think this will work the way you expect it to. There isn’t really any way to create an unsafe closure (although unsafe fn() works, as you’ve already mentioned). According to the playground, this doesn’t compile:

use std::mem;

fn takes_closure<F: Fn()>(func: F) {
    func();
}

fn main() {
    let unsafe_lambda = || {
        let unsigned: u32 = 5;
        let signed: i32 = mem::transmute(unsigned);
    };
    
    takes_closure(unsafe_lambda);
}

Error message:

error[E0133]: call to unsafe function requires unsafe function or block
  --> src/main.rs:10:27
   |
10 |         let signed: i32 = mem::transmute(unsigned);
   |                           ^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function

error: aborting due to previous error(s)

Otherwise would similar to Vec's get() and unsafe get_unchecked() fit your use case? Where you’ve got a safe function, but also the ability to use unsafe to skip checks when the user knows certain assumptions are valid.

trait Allocator {
  fn allocate<T>(&mut self) -> T;
  unsafe fn allocate_unchecked<T>(&mut self) -> T;
}

#5

Unfortunately not; the allocation and deallocation methods themselves need to be unsafe - it’s acquiring the allocator itself that can be safe in certain circumstances. Essentially, the idea is that you’d write a data structure that says, “If you hand me a valid object allocator that properly initializes the objects it allocate, then I will provide a safe API.” Thus, what I’m trying to enforce is that safe Rust is unable to get access to an object allocator that doesn’t uphold that contract.

However, I think that unsafe fn() is pretty close! I’ll have to sacrifice a tiny bit in the way of performance because calls to the initializer will have to be indirected through a function pointer instead of being monomorphized, as a trait would allow (sometimes). But that’s OK - it’s really not a big performance hit.


#6

Could you have trait SafeAllocator and unsafe trait UnsafeAllocator traits to talk about the allocator itself?

The other option, I think, would be to punt on closures and require a custom factory/constructor trait; you could then have a marker trait to also encode an unsafe version.


#7

Can you mark your takes_unsafe_closure function as unsafe?

That would require an unsafe block around takes_unsafe_closure(|| {})


#8

I think that’s what @Michael-F-Bryan suggested above and @joshlf replied to :slight_smile:


#9

Yeah so what I ended up with was unsafe fn blah_unsafe<F: Fn(*mut T)>(init: F) -> .... The unsafe version of the initializer takes a *mut T and is responsible for initializing it, so even a safe function could fail at that job (e.g., fn no_init(t: *mut T) {}). Thus, the constructor itself is unsafe, and that does the trick.