Rusty plugin system for my project

Moin!

I'm facing a problem where I want to outsource some code for a plugin system. Inside my project I have a crate called "Provider" which is the code for my plugin system. If you activate the feature "consumer" you can use plugins; if you don't, you are an author of plugins.

What I want to achieve is: I want authors of plugins to get their code into my program by compiling to a shared library. (Is a shared library a good design decision? The limitation of the plugins is using Rust anyway.)

Does the plugin host has to go the C way for loading the shared library (loading an unmangled function)?
I just want authors to use the trait "Provider" for implementing their plugins and that's it.
After taking a look at sharedlib and libloading it's seems impossible to load plugins in a rusty way.
I'd just like to load trait objects into my ProviderLoader.

// lib.rs

pub struct Sample { ... }

pub trait Provider { 
    fn get_sample(&self) -> Sample;
}

pub struct ProviderLoader {
    plugins: Vec<Box<Provider>>
}

So when the program is shipped the file tree would like:

.
├── fancy_program.exe
└── providers
    ├── fp_awesomedude.dll
    └── fp_niceplugin.dll

Is that impossible if plugins are compiled to shared libs? This would also affect the decision of the plugins' crate-type.
Do you guys have some other ideas for me? Maybe I'm on the wrong path so that shared libs aren't the holy grail.

Alles sahneliebe
xnor

A friend adviced me to also ask on Stack Overflow.

4 Likes

I'd suggest combining the approach taken by plugin frameworks like Yapsy for Python with the approach that's used by crates like rust-cpython to hide the use of C APIs as glue.

That basically means that you'd want the following characteristics:

  • Python modules written in C handle finding the entry point by exposing a known symbol built from the plugin's filename (PyInit_foo for foo.so) which gets called when you import them. A Rust solution would do something similar under the hood to provide a known entry point for initializing the higher-level abstractions.
  • rust-cpython hides most of the complexity using macros like py_module_initializer! and traits like ToPyObject. (The PyO3 fork of it shows how things will get even nicer once feaures like procedural macros are stabilized.)
  • Yapsy allows custom plugin types by subclassing the IPlugin interface. You'd probably want to use the trait system to do something similar for type-safety. (ie. A project which can load plugins would be composed of at least two crates, with the second one defining the plugin interface to be used by both plugins and the plugin host.)
  • Yapsy works around "importing a Python module can run arbitrary code" by putting things that should be known before initializing a plugin (eg. metadata for the plugin list) into a ConfigParser-format (.ini format) file alongside the code. If you have similar needs, you could use TOML for this.
  • Yapsy hides the details of finding and loading plugins from the consumer by having a PluginLoader class that you configure and then call collectPlugins() on. Rust's type system would require something a little different, but the idea of abstracting away the "convert a plugin search path to a list of plugin references" still holds.

Worst case scenario, you require downstream users to call some of your code from a build.rs to generate part of the abstraction.

NOTE: In the interest of full disclosure, I'd like to use such a plugin system because I have a Yapsy-based project I'm migrating from Python to Rust, where I'd like to push the entire backend into the Rust side so it can be used with frontends other than the current PyQt one.

1 Like