Mark entire struct as unsafe

Hello hello,

Dynamic Libraries in almost all cases not guarded against memory safety, making their use in Rust discouraged at best. But I'm in the process of implementing a plugin system, which I'd like to be able to use various scripting languages, as well as dynlibs. I'm doing this via a trait, I'll call Plugin.

Plugin handlers wrapping scripting languages are implemented safely of course, as their usage can be inferred from their type definitions. However this is not the case for DynLibs. Consequently, the implementation of Plugin should be unsafe as well. However, the compiler seems to not allow me to unsafely implement the trait, with the error that implementing the trait 'Plugin' is not unsafe.

My question is about the best way to annotate the usage of this struct is entirely unsafe, and not guarded. Any suggestions?

I don't really get the problem. Structs can't be marked unsafe because that operation does not make any sense. There's nothing unsafe about a struct per se.

If the act of implementing a trait for a type is unsafe (ie., the existence of the impl Trait for Type is relied upon for soundness), then use an unsafe trait. If the act of calling a function of that trait is unsafe, then declare that function as unsafe.

1 Like

A struct itself can never be unsafe, doing something with it is (including creating it, if it has safety & validity invariants).
I suggest that you make the creation of this struct unsafe, and all relevant methods of that struct.

If the implementation of the trait methods are unsafe, then I can simply add the unsafe keyword to the function. if I understand correctly, you recommend marking each implemented function as unsafe, rather than the entire trait?

It seems this is the best approach. I will rework my system to account for this

I'm receiving the following error expected normal fn, found unsafe fn, because the trait does not define the functions to be unsafe

No, these are different things, as @H2CO3 said.

  • unsafe trait: The fact that this trait is implemented at all is unsafe. Send is such an example. Safe code can write <T: UnsafeTrait> and is allowed to assume certain things about T that are promised for implementations of UnsafeTrait.
  • unsafe fn: Calling this function requires the caller to uphold preconditions that are documented by the function[1].

I am suggesting that you think about what operations are unsafe and could potentically cause UB and who is responsible for checking/upholding the relevant preconditions.

Also, if the trait Plugin is safe, all its implementation also needs be safe. After all, callers of the trait cannot know whether they call an unsafe impl or not. This means that all relevant preconditions need to be checked in the trait's implementation. If you want to that, then do unsafe-blocks in its implementation.

If you can't check preconditions there, where are these checked? If you want to link to "arbitrary dynlibs" and cant make them honor some contracts, your architecture is unsound.


  1. The implementor can only be more lenient than the trait's documentation ↩︎

2 Likes

That's exactly the same error. You can't unsafe impl a trait that is not unsafe; it shouldn't be surprising that you can't mark a function as unsafe if it's not declared as unsafe in the trait. The solution is to also declare it as unsafe in the trait definition.

Implementations can't refine unsafe methods to be not-unsafe for the implementing type, but maybe it will be possible in the future.

In the meanwhile...


If the idea is that consumers of a P: Plugin need to uphold safety in the general case (so the trait methods should be unsafe), but there's a subset of implementors that are inherently safe to use, you could

pub trait Plugin {
    unsafe fn unsafe_foo(&self);
}

impl<T: ?Sized + SafePlugin> Plugin for T {
    // SAFETY: We only forward to the safe `SafePlugin::foo`
    unsafe fn unsafe_foo(&self) {
        self.foo()
    }
}

pub trait SafePlugin: Plugin {
    fn foo(&self);
}
1 Like

Note that in Rust, unsafe does not mean

This unsafe is required to In order to make the compiler happy and let's hope that this does not cause UB.

In instead means

The body of this unsafe block calls functions that require preconditions that cannot be checked by the compiler. I know the preconditions, these are A, B and C. And I uphold them for reasons X, Y and Z.

In this context "preconditions" mean: this is sufficient to not cause UB. Functions that are can cause logic errors on invalid inputs are not maked as unsafe.

Thank you all for your input. I'd like to clarify a few points here,

As @samuelpilz mentioned, the unsafe keyword is not a error-suppressing tool for the compiler, it's meant to explicitly indicate the manual checking of UB on the programmer's side. I'm aware of this, but in order to continue with this project, there are no other paths other than hoping the plugin is sound with which I can progress, meaning I would need to mark the entire trait or a subset of its functions as unsafe.

@quinedot This is a good approach, and in fact one I've considered, however this doesn't seem to address the issue to me, in that certian implementations are guaranteed to be safe, while others are not, and instead simply hides the fact that some aren't. I believe this to be neglegible, especially in this case, but I understand that this might not carry over to all occurrences of issues like this.

@H2CO3 The fact that these errors are the same is the basis of my question. I'm attempting to find a way to indicate that certain operations are unsafe, however I'm attempting to do so in a way which allows the most safety possible. I have obviously overstepped this line. Regardless, I'm grateful for your assistance and value your insight

Who does the checking? If the Plugin-impl can check the precondition, then you can write.

trait Plugin {
    fn foo(&self)
}
impl Foo for DylibPlugin {
    fn foo(&self) {
        if !self.is_call_valid() {panic!()} // or return Err(...)
        // then:

        // SAFETY: check happend
        unsafe { self.call_dylib() }
    }
}
1 Like

As far as I understand unsoundness, the Rust code can then be made sound by making the fn that loads the dylib as unsafe and document that the dylib as a safety-precondition, the dylibs are required to uphold the specific contract.

Edit: How to check the precondition then? I guess it is hope after all.

My example doesn't hide that some implementations have preconditions. Implementors who implement the unsafe-method version manually can't implement the safe-method version and don't have it's methods. Consumers can limit themselves to safe implementors if they choose.[1]

Completely possible it's not applicable to your situation though.

Pedantic non-contribution to the conversation

Soundness is generally defined as "safe code can't cause UB". So unsafe methods are always sound[2]... though perhaps not blame-free if the preconditions aren't properly documented.

In the following function, the blame for any resulting UB is on the caller.

// # Safety
// Some list of unsatisfiable requirements
pub unsafe fn foo() {
    *[].get_unchecked_mut(1337) = 420;
}

So you can always make your architecture sound and blame free, albeit possibly useless, with enough unsafe and documentation :upside_down_face:.


  1. If they opt in to generically accepting the unsafe trait, which are safe is "hidden"... but in that case they must uphold the preconditions anyway, naturally, so it doesn't matter if some concrete instantiations are safe. ↩︎

  2. safe code can't call them ↩︎

4 Likes

Okay thanks to everyone, my approach is as follows:

The trait will remain safe, as this allows cleaner usage of plugins which are not dynamic libraries.
The DynLib plugin will delegate the loading and usage of the library to a set of functions whose sole purpose is to ensure the integrity of values used internally, while making it explicitly clear that these functions operate on unsafe structures.

This is essentially a combination of @quinedot and @samuelpilz approaches, but in my opinion, is the most transparent, cleanest, and safest.

Thanks for your help everyone

I would say that for a dynamic library plugin system, the safety invariants are ones you document for people providing the plugins. Things like "the buffer must not be accessed after the function returns".

That sort of thing is standard for native code documentation (that isn't crap, at least), so that's how you can justify the unsafe block, better than "hope", at least. Sure, the plugin can break the documented requirements, but they don't have any room to complain about you crashing because of that then.

This is kind of a weird bit of trivia and only tangentially related to the question, but union objects with one field are kind of like an unsafe struct. You cannot read from them without unsafe.

1 Like

Yep, this is the approach I would be taking, but there is still no hard restriction on what the dynlib can do, more, hence hope but yes...

This is almost exactly what I had in mind! Plus if the union only has one variant, it's guaranteed to be safe, despite having to annotate it accordingly! yessir!