Can `trait Foo<T>` have a method that returns `T`, `&T`, and `&mut T`? (Also a request for a code review of my plugin system prototype)

I'm working on a plugin system for Rust that is generated by a macro.

To give plugins access to data of other plugins (because of the macro, they don't have access to required types), I have a trait:

trait UsePlugin<T> {
    fn use_plugin_mut(&mut self) -> &mut T;
}

I try to avoid having to implement methods for every data type I want to offer (&mut T, &T, RefCell<T>, Option<T>).

However, everything I tried so far, failed.

(&mut T and &T make sense most of the time, and have the smallest overhead. RefCell<T> (or just MutRef) is required, if a plugin wants easy mutable access to two plugins. Option<T>, or similar, would be useful to take the plugin, which also could be used to overcome the "two mutable references" problem).

So what I try to do is to have:

trait UsePlugin<T> {
    fn use_plugin(&mut self) -> T;
}

...and then implementations like:

impl UsePlugin<&mut Plugin2> for Plugin1 {
    fn use_plugin(&mut self) -> &mut Plugin2 {
        todo!()
    }
}

impl UsePlugin<&Plugin2> for Plugin1 {
    fn use_plugin(&mut self) -> &Plugin2 {
        todo!()
    }
}

One problem is, that the type on which UsePlugin is implemented, is a NewType with a mutable reference to the plugin data inside (see the Playground below, for the full code). This lead to a problem, which I couldn't overcome.

Before I waste more time, I'd like to ask if the above is even possible.

I'd also like to use the opportunity to ask for a code review of the most essential part of my plugin system.

Here is a Playground with the code

Performance is very important to me, so I'd be grateful for every hint in that regard.

My plugin system doesn't allocate or use dynamic dispatch at the moment (unless I missed something). However, I don't have any clue about system programming, so I might do something that isn't optimal.

In general, you can't abstract over mutability in Rust. That's why the standard library is full of get, get_mut, and into_inner.

This is because it's not merely a choice of a type, but a restriction how code that uses this type is written. Since shape of the compiled code can't change, the best case-scenario you can achieve with abstracting over types that ensure memory safety is to require all code to assume the worst-case scenario and allow only the most restrictive usage of all of these types.

Specifically, a call that takes &mut self will "lock" all of self exclusively and keep it unusable for as long as the returned reference exists, even if you return a shared & reference from it (that's to prevent further calls to &mut self that could invalidate a previously-returned reference). OTOH &self call returning & would only put a shared lock on self, and allow much more flexible use of the object.

You should have separate methods. Use a macro if writing them is too much boilerplate.


However, I don't understand what you're trying to do with so many layers of abstraction here. Maybe try implementing something less generic first to get it working?

5 Likes

Thank you, @kornel!

In general, you can't abstract over mutability in Rust. That's why the standard library is full of get , get_mut , and into_inner .

I was fearing this would be true, but hoped a trait with a generic could do it... :slight_smile:

the best case-scenario you can achieve with abstracting over types that ensure memory safety is to require all code to assume the worst-case scenario and allow only the most restrictive usage of all of these types.

I don't understand this part. Do you mean, if I want to offer &T and &mut T, I should just offer &mut T because that can be downgraded to &T?

Specifically, a call that takes &mut self will "lock" all of self exclusively and keep it unusable for as long as the returned reference exists, even if you return a shared & reference from it (that's to prevent further calls to &mut self that could invalidate a previously-returned reference). OTOH &self call returning & would only put a shared lock on self , and allow much more flexible use of the object.

Yes, that's the reason, why I need all of these types (for different scenarios, different types are optimal).

However, I don't understand what you're trying to do with so many layers of abstraction here. Maybe try implementing something less generic first to get it working?

Do you mean, why I want to offer &T, &mut T, RefCell<T>, Option<T>?

I try to write a flexible plugin system for general use, with minimal overhead.

This part of the plugin system is for a middleware plugin system (basically a stream of data, that is transformed by plugins).

Some plugins just add data (don't depend on any other plugin data).

Some plugins only need read access to other plugin data (&T).

Other plugins need mutable access to one other plugin's data at a time (&mut T).

Other plugins need mutable access to several other plugin's data at the same time (RefCell<T>).

The last group also could use &mut Option<T> to take T out, drop all references (before getting the next plugin data), do the work, and the put the plugins back in (this probably is only useful, if the small overhead of RefCell is too much).

I think, all of these data access methods would be useful in a plugin system that's intended for general use, and tries to archive minimal overhead.

Use a macro if writing them is too much boilerplate.

So the code will already be written by macros, so that's not the problem. I just think what I try to archive would be a nicer API (ergonomics of my plugin system are already not very great, so far...).

You should have separate methods.

Thank you, I'll do this (if nobody else has an idea... :slight_smile: ).

I mean that generic types are always maximally pessimistic and maximally inflexible about what can be done with them.

Types like &str can be shared, types like String can be moved, types like u8 can be copied. But a type T that could be any of them doesn't get these features. Quite the opposite. There's no room to say that if T is shareable, then share it, but if it's exclusive, then don't share it. It's always the worst most restrictive case, so you can't add memory-management flexibility with generics.

1 Like

Rust has AsRef, AsMut and TryInto for these. So instead of a trait-for-getting-another-trait you should probably implement these directly? But I still don't understand what you're trying to do.

There's no AsRefCell in Rust, because that alone doesn't make sense. You'd typically have Rc<RefCell<T>>. But if you have that, you can't simply return any of the other types, because then each borrow has to be wrapped in a Ref guard. Nobody is abstracting over RefCell, because something either always is a RefCell or can't pretend that it isn't (beyond a limited scope of a .borrow()).

Use of these types is not a performance thing. Their use is dictated by hard requirements of program's architecture (ownership and sharing of data). It seems like you don't have any architecture for your plugins, and want to also make program's architecture abstract?

Type of ownership and data sharing has to be non-abstract in Rust. Even when ownership is variable at run time (like Cow) or sharing is unclear (like RefCell), that possibility still has to be hardcoded.

3 Likes

Oh, your "Plugin1 uses Plugin2" thing reminds me of ECS. See how Bevy does it. It can have an arbitrary number of components (your plugins?) and then has "systems" which can require access to any of these components.

Bevy does quite a bit of magic with Query in function arguments to let you pick the right components, with appropriate mutability where necessary:

But even then it's clear that Bevy's World owns all of the entities and components, and only lends you them temporarily. You can't arbitrarily take ownership of them or ask for a RefCell.

In general, this making me wonder if it accomplishes the goal to “recast” the input to something you now own. In most situations this likely involves generating a copy. I wonder how much of a performance hit this would in fact be. How much might be optimized-away could be a pleasant surprise to the degree it mostly serves as type-level accounting?

Something to keep in mind is that patterns you typically think of as "expensive" in Rust are often the default in other languages.

For example, we normally bend over backwards to avoid using trait objects, but virtual method calls are commonplace for something like C++. Similarly, C++ has copy-by-default semantics where we'll try everything we can to avoid unnecessary clone() calls.

Even then, C++ code which uses these "non-performant" patterns can still be really fast because they won't often be triggered in places you actually care about performance (e.g. a tight loop processing loads of data). I'm guessing your plugin system will be the same.

Agree.

Human nature: we want more whenever possible. Rust makes a lot more possible.

Where I was going with my previous post was grey going generic over owned types. Unify using copy where required. See how much the Rust compiler gets you what you want. And to your point, where it doesn’t, maybe copy is not so bad in relation to the norm set by C++ and the like.

@kornel:

I mean that generic types are always maximally pessimistic and maximally inflexible about what can be done with them.

Types like &str can be shared, types like String can be moved, types like u8 can be copied. But a type T that could be any of them doesn't get these features. Quite the opposite. There's no room to say that if T is shareable, then share it, but if it's exclusive, then don't share it. It's always the worst most restrictive case, so you can't add memory-management flexibility with generics.

I think, I get it now. Thank you.

Rust has AsRef, AsMut and TryInto for these. So instead of a trait-for-getting-another-trait you should probably implement these directly? But I still don't understand what you're trying to do.

I believe, this wouldn't be possible, or wouldn't bring benefits (if I understand it correctly).

In a different thread I tried to explain the requirements and limitations. Does this make my intent more clear?

Basically, we have this:

  • PluginSys, which holds all plugins (which hold their data)
  • Plugins have an execute method, which takes &mut PluginSys as argument
  • UsePlugin<PluginN> is implemented for PluginSystem, so plugins can use UsePlugin::get_plugin_mut to get other plugins
  • This indirection is required because PluginSystem is generated by a macro, so plugins don't know anything about PluginSystem, or the other plugins it contains (they only know the types of the plugins they depend on, which they can access by requiring UsePlugin<T> on PluginSys)
  • UsePlugin<T> needs to be implemented for all plugins, so it's not possible to forbid UsePlugin<Plugin1> when PluginSys executes Plugin1 (if I'm not missing anything)

My solution was to create a NewType Plugin1Sys for Plugin1, and then implement UsePlugin<T> on Plugin1Sys for all plugins but Plugin1.

PluginSys would then pass Plugin1Sys<&mut PluginSys> to Plugin1::execute (which introduced the problematic lifetime this thread initially was about).

It seems like you don't have any architecture for your plugins, and want to also make program's architecture abstract?

Yes, my plugin system should be a separate crate, that I can use in different other projects (and if I manage to create something useful, I'll probably publish the crate at some point).

Type of ownership and data sharing has to be non-abstract in Rust. Even when ownership is variable at run time (like Cow) or sharing is unclear (like RefCell), that possibility still has to be hardcoded.

So my thinking was, that the implementations of the UsePlugin<T> trait would represent hard-coded non-abstract types.

And indeed, this is working to a certain extent:

trait Trait<'a, T> {
    fn get(&'a mut self) -> T;
}

struct Struct {
    data: Option<String>,
}

impl<'a> Trait<'a, &'a str> for Struct {
    fn get(&'a mut self) -> &'a str {
        (&*self).data.as_ref().unwrap().as_str()
    }
}

impl<'a> Trait<'a, &'a String> for Struct {
    fn get(&'a mut self) -> &'a String {
        (&*self).data.as_ref().unwrap()
    }
}

impl<'a> Trait<'a, &'a mut String> for Struct {
    fn get(&'a mut self) -> &'a mut String {
        self.data.as_mut().unwrap()
    }
}

impl<'a> Trait<'a, Option<String>> for Struct {
    fn get(&'a mut self) -> Option<String> {
        self.data.take()
    }
}

fn main() {
    let mut s = Struct {
        data: Some(String::from("foo")),
    };

    // The following works, as long as only one line is uncommented:
    let a: &str = s.get();
    //let b: &String = s.get();
    //let c: &mut String = s.get();
    //let d: Option<String> = s.get();

    println!("{:?}", a);

    // Unfortunately, the following doesn't work, because there are two
    //  `&mut T` at the same time, even so I tried to "downgrade"
    // `&mut T` to `&T`:
    let a: &str = s.get();
    let b: &str = s.get();

    println!("{} {}", a, b);
}

Of course, this isn't a single method that returns different types, but different implementations of the same trait (my description in my first post wasn't great, I think).

Unfortunately, in the end, this isn't working, as I indicate at the end of the example.

This makes me fear, that my previous test with my previous implementation was incorrect, and will hit the same limitation...

On the other hand, after thinking about it more, using separate methods should work.

get_ref should work several times, if it takes &self, get_mut should work for a single concurrent &mut Plugin (hopefully, dropping this values makes it possible to get a different plugin. This wasn't possible with another experiment, where I used lots of lifetimes to get one step further). get_refcell also should work for several concurrent plugins, if it takes &self.

Oh, your "Plugin1 uses Plugin2" thing reminds me of ECS. See how Bevy does it. It can have an arbitrary number of components (your plugins?) and then has "systems" which can require access to any of these components.

I was considering ECSs. However, the benchmarks I saw, made me believe this wouldn't be efficient enough (adding components seems too slow). After thinking about it again, I might have misunderstood the benchmarks. I was thinking, adding components would be 'adding data' to be processed. But it might be actually 'adding plugins', which would be more than fast enough. Thanks for bringing ECSs to my attention again!

@EdmundsEcho

Thanks for your response!

In general, this making me wonder if it accomplishes the goal to “recast” the input to something you now own. In most situations this likely involves generating a copy. I wonder how much of a performance hit this would in fact be. How much might be optimized-away could be a pleasant surprise to the degree it mostly serves as type-level accounting?

So I'm not completely sure I understand you correctly.

What I'm building at the moment is a middleware plugin system. One use-case I have, is a static website generator.

A very simple workflow would be:

  • scan file system for files that match a file glob (e.g. "*.md") – this would be the input to the plugin system
  • FsPlugin: Read the file into memory (for simplicity), and store file content in itself
  • FrontmatterPlugin: Get the file content from FsPlugin (as &str) and extract metadata in the form of TOML at the beginning of the file. Store parsed metadata, and rest of file content (as slice if this is possible)
  • MarkdownPlugin: Get the rest of the file content (without metadata) and convert to Markdown
  • TemplateEnginePlugin: Get Markdown and metadata, and convert to HTML
  • save HTML to file

This should be possible without needing to copy any data in the plugin system, and with minimal copying data in the plugins themselves (excluding whatever the used Markdown/TOML/Template engine crates do, which is outside my influence).

Of course, 95% or more of the time will be spent in the Markdown/TOML parsers, the template engine, or IO. But I'd still like to make my plugin system as fast as possible (without killing ergonomics completely).

I think, a plugin system like the one I try to build, would be useful in most things I want to build. I already built a static website generator, but maintaining it is a nightmare because everything is hard-coded, and adding stuff means I need to consider everything else.

Besides that, I have some ideas for server-side projects, where performance is more critical.

So my thinking is, basically: the more efficient my plugin system is, in the more contexts I can use it.

Where I was going with my previous post was grey going generic over owned types. Unify using copy where required. See how much the Rust compiler gets you what you want. And to your point, where it doesn’t, maybe copy is not so bad in relation to the norm set by C++ and the like.

So normally, plugins only need temporary access to other plugin's data (either read-only, or mutable), and this data needs to stay within the plugin system (so other plugins can use the data as well). When the plugin system gives ownership of a plugin to another plugin, the plugin needs to give the ownership back later. This should only be required in very, very few occasions (and I'm not 100% sure if I even want to give the option to take ownership of plugins. But I anyway can't prevent it, so I as well could give a nice API). So Copy wouldn't work, because the data need to stay in the plugin system for other plugins. If I pass a copy/clone of plugin 1 to plugin 2, and plugin 2 changes the data of plugin 1 – which is the main reason for giving mutable access – then these changes wouldn't be visible to the other plugins.

Human nature: we want more whenever possible. Rust makes a lot more possible.

Haha, 100% agreed... :slight_smile:

The double get() is not going to work, because the borrow checker enforces rules based on function interfaces, not function bodies.

The interface of get(&mut self) allows code like:

fn get(&mut self) -> &str {
    if !self.already_called {
        self.already_called = true;
        self.data.as_deref().unwrap()
    } else {
        self.data = None; // drop the string
        "lol"
    }
}

and then:

let a = s.get();
let b = s.get();

would create a dangling pointer in a, and cause memory unsafety if it was ever used.

You don't have such self-destructing implementation, but your trait's interface allows it, so the borrow checker has to prevent it from hypothetically happening.

If you had separate get(&self) and get_mut(&self), then a double get would work, since &self doesn't allow destruction of self.data.

Of course you still couldn't mutably get two plugins with this interface that exclusively locks all of self on the first call.

Since you're already involving macros, and defining types, so the whole thing is not extensible at run time, and allowing .take() that removes plugins, why not:

pub struct AllPlugins {
   pub plugin1: Option<Plugin1>,
   pub plugin2: Option<Plugin2>,
   pub plugin3: Option<Plugin3>,
}

if it's a pub struct, then each field can be borrowed individually.

or a HashMap of them. iter_mut().filter() can be used to pick multiple fields mutably (iterators guarantee to the borrow checker that each item is disjoint).

@Michael-F-Bryan:

Thank you for your advice!

Something to keep in mind is that patterns you typically think of as "expensive" in Rust are often the default in other languages.

For example, we normally bend over backwards to avoid using trait objects, but virtual method calls are commonplace for something like C++. Similarly, C++ has copy-by-default semantics where we'll try everything we can to avoid unnecessary clone() calls.

Yes, this is definitely the worst kind of premature optimization that exists... :slight_smile:

But so far it's a lot of fun for me, and I learned already a ton of new things (the most amazing being what can be done with generic traits).

I'm definitely planing to create prototypes with other approaches, like trait objects, ECS, maybe WebAssembly, and other ideas I have, and create some benchmarks.

I guess one big motivator for me is to learn new things about Rust. I hate to build projects for learning purposes only, so my main way to learn new things, is to try them while building things I really need (which at the very least gives me additional insights into the domain I'm working in).

With all the hoops I have to jump through with my current prototype, I wouldn't be astonished if a trait object-based approach would be similar fast, or even faster (as it probably could be much simpler). This probably also would be much better for end-users and plugin authors, because I wouldn't need a macro that generates the plugin system, and everybody would have access to all involved types.

Even then, C++ code which uses these "non-performant" patterns can still be really fast because they won't often be triggered in places you actually care about performance (e.g. a tight loop processing loads of data). I'm guessing your plugin system will be the same.

So, "tight loop processing loads of data" is basically the definition of what my middleware plugin system will do (in some use-cases). So I think, it's worth to try to optimize this code as much as possible.

@kornel:

The double get() is not going to work, because the borrow checker enforces rules based on function interfaces, not function bodies.
[...] would create a dangling pointer in a, and cause memory unsafety if it was ever used.

Ah, that makes sense, thank you!

Of course you still couldn't mutably get two plugins with this interface that exclusively locks all of self on the first call.

So, you mean, this wouldn't work:

let p1: &mut Plugin1 = sys.use_plugin_mut();
let p2: &mut Plugin2 = sys.use_plugin_mut();

mutate_plugin1(p1);

Right?

But this would work:

let p1: &mut Plugin1 = sys.use_plugin_mut();
mutate_plugin1(p1);

And this, as well:

let p1: &mut Plugin1 = sys.use_plugin_mut();
mutate_plugin1(p1);
drop(p1); // probably not even required

let p2: &mut Plugin2 = sys.use_plugin_mut();
mutate_plugin2(p2);

Correct?

This is all I hope to archive with use_plugin_mut.

Then, for the cases, where this isn't enough, I thought RefCell would help.

So would this be possible?

// I don't use `RC` because I believe `&RefCell<Plugin1>`
// would work. And this way, there isn't an allocation.
// I think, even `&RefMut<T>` could be enough, but I'm
// not 100% sure. This is the first time I even had to
// think about something like `RefCell`

// `use_plugin_mut` only takes `&self:
let p1: &RefCell<Plugin1> = sys.use_plugin_mut(); `
let p2: &RefCell<Plugin2> = sys.use_plugin_mut();

let p1_mut = p1.borrow_mut();
let p2_mut = p2.borrow_mut();

mutate_plugin1(p1_mut);
mutate_plugin2(p2_mut);

println!("{} {}", p1_mut, p2_mut);

I just realized, I actually can't even come up with a good reason, why someone would need mutable access to two plugins at the same time. But there are probably some cases, where this would be useful.

Since you're already involving macros, and defining types, so the whole thing is not extensible at run time, and allowing .take() that removes plugins, why not:

pub struct AllPlugins {
  pub plugin1: Option<Plugin1>,
  pub plugin2: Option<Plugin2>,
  pub plugin3: Option<Plugin3>,
}

Do you mean, why I don't give &mut AllPlugins directly to Plugin1::execute?

The problem is, that AllPlugins is generated in the end-user crate, not in the plugin system crate (basically, by a macro call like create_plugin_system!(Plugin1, Plugin2, Plugin3), which enables some optimizations).

So plugin authors don't have access to AllPlugins. Basically, AllPlugins gets generated after the plugin code was written. I work around this problem, by requiring Plugin1::execute to accept a generic, which can implement traits like UsePlugin<Plugin2> to get data. Plugins know the type of the plugins they depend on, so this approach works.

If the end-user and the plugin author is the same person, this of course would be possible. But I'd like to create a plugin system for general use, that can be used for building plugin ecosystems. Static website generator like GatsbyJS, or build tools like WebPack are good examples for this.

I think, such a plugin system would be very useful for the Rust ecosystem (as I wrote, I believe most of my projects would benefit from something like this).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.