h33p / cglue

Rust ABI safe code generator
MIT License
181 stars 11 forks source link

Preventing TraitObjects from handing out objects with 'static lifetime #16

Open kulst opened 4 months ago

kulst commented 4 months ago

Hi :) I am currently working on a Rust API for the Apache Celix framework and I am considering to utilize this amazing crate for it. As to make everything safe is quite a complex topic I would like to ask for some advice from you.

Apache Celix is a C written software that dynamically loads and unloads so called bundles which are basically dynamic libraries with a little metadata. Each bundle must define a set of predefined functions (create, start, stop, delete). So the framework can communicate to the bundle to allocate/deallocate necessary resources. This means the framework has full control over the loading/unloading of bundles.

Each bundle can register services at the framework that can then be used by other bundles. In the C world a service is just a struct with a handle and one or more function pointers that use the handle. I want to safely provide services from the Rust world.

In my eyes this concept maps very well to TraitObjects like your crate provides in a ffi safe manner. Also the ability to generate C/C++ code for such TraitObjects comes in very handy as this makes it possible to use such Rust services from C/C++ written bundles.

As services can be used from many bundles at the same time a service user is only provided with a MyTraitRef. If a service should mutate internal state thread safe interior mutability is necessary.

However there is one problem I could not find a solution to yet: A service/TraitObject could hand out a 'static reference to a bundle ressource. You also pointed out this problem in the readme. To be safe I need to make sure that a trait which has the #[cglue_trait] attribute does not provide a return type with 'static lifetime.

Would you see a way to make this sure? Do you see a different problem I may not have concidered?

h33p commented 4 months ago

Hey, thanks for contacting!

So, if I understand correctly, the lifecycle of the dynamic libraries (bundles) is completely controlled by Celix.

I believe what you're asking for is quite difficult to ensure. For one, primitive types, such as u64 are 'static, so a blanket !'static bound would not work. However, without more context, I have a hard time figuring out what to suggest.

Could you give more concrete examples? For instance, say you have a rust service. How would it interact with a C bundle, and in what cases would it lead to UB? Would it be possible for a rust service to hold onto some resource from that bundle? Would it be possible for the bundle to hold onto some resource from the rust service that later gets unloaded?

Is your API mainly about providing rust-written service bundles? Or does it include something else, like, interacting with Celix's bundle loading/unloading system?

kulst commented 4 months ago

Hi, thanks for having a look. In the meantime I further investigated my problem and now have a better understanding myself (and also might have found a solution).

As my problem is only peripherally related to your crate, this issue can be closed in my opinion. However I want to give an overview how I want to use your crate, as it might interest you.

I have one bundle that provides a service (Provider who implements the service) and another bundle that uses a service. So a trait for that could look like this:

#[cglue_trait]
trait Service {
    fn use_service<'a>(&'a self, user_data : &'a UserData) -> &'a UseResult;
}

To use a service a callback must be created that is called from C. A very simplified version could look like:

fn callback(user_data : &UserData, svc : &impl Service);

I do know that both instances UserData and the object implementing Service are present during the callback. But after the callback it is unknown if both of them are still present (the bundle of either of the two could be unloaded at any time). So I need to enforce that the lifetime of user_data and svc are bound to the callback duration. To do this the callback that is send to C gets wrapped and the lifetime of user_data and svc get coerced to the lifetime of a variable that only lifes during the callback:

fn wrapped_callback(user_data : &UserData, svc : &impl Service, cb : fn(&UserData, &impl Service)) {
    let lifetime = ();
    let user_data = coerce_lifetime(&lifetime, user_data);
    let svc = coerce_lifetime(&lifetime, user_data);
    cb(user_data, svc);
}

With that in place it is ensured that user_data and svc are only valid during the callback. However an internal borrow in user_data that has a longer lifetime than user_data could be placed in svc and vice versa (interior mutability assumed). Such contamination must also be prevented.

This can be achieved if

These requirements are hard to achieve. I am thinking about some derivable unsafe trait like the following:

unsafe trait LifetimeCoercible<'g> {
    type Coerced;
    fn coerce_lifetime<G>(self, _giver : &'g G) -> Self::Coerced;
}

unsafe impl<'a, 'g, T> LifetimeCoercible<'g> for &'a T // and for &'mut T
where 
    'a : 'g,
    T : LifetimeCoercible<'g>
{
    type Coerced = &'g T;
    fn coerce_lifetime<G>(self, _giver : &'g G) -> Self::Coerced {
        self
    }
}

unsafe impl<'g> LifetimeCoercible<'g> for usize // and for other primitive types
{
    type Coerced = usize;
    fn coerce_lifetime<G>(self, _giver : &'g G) -> Self::Coerced {
        self
    }
}

The requirement for deriving this trait would be that no &'static references are part of the type and that all types implement LifetimeCoercible itself.

I hope this makes my problem a bit more clear. If the Rust service gets used from C/C++ these safety requirements must be enforced by the programmer of course.

My API is mainly about using/providing Rust services. Probably it will also have an unsafe API to use C services. Bundle loading/unloading however is not part of the API.