this post was submitted on 22 Mar 2024
17 points (100.0% liked)

Rust Programming

8168 readers
1 users here now

founded 5 years ago
MODERATORS
 

I have a plugin trait that includes some heavy types that would be almost impossible to wrap into a single API. It looks like this:

pub struct PluginContext<'a> {
    pub query: &'a mut String,
    pub gl_window: &'a GlutinWindowContext,
    flow: PluginFlowControl,
    pub egui_ctx: &'a Context,
    disable_cursor: bool,
    error: Option<String>,
}
pub trait Plugin {
    fn configure(&mut self, builder: ConfigBuilder) -> Result<ConfigBuilder, ConfigError> {
        Ok(builder)
    }
    fn search(&mut self, ui: &mut Ui, ctx: &mut PluginContext<'_>);
    fn before_search(&mut self, _ctx: &mut PluginContext<'_>) {}
}

Here is what I considered:

  1. Keeping all plugins in-repo. This is what I do now, however I'd like to make a plugin that would just pollute the repository. So I need another option that would keep the plugins' freedom as it is right now, but with the possibility to move the plugin out to a separate repository.
  2. I tried to look into dynamic loading, and since rust doesn't have a stable ABI, I'm okay with restricting the rust versions for the plugin ecosystem. However, I don't think it's possible to compile this complex API into a dynamic lib and load it safely.
  3. I'm also ok with recompiling the app every time I need a new plugin, but I would like to load these plugins automatically, so I don't want to change the code every time I need a new plugin. For example, I imagine loading all plugins from a folder. Unfortunately, I didn't find an easy solution for this neither. I think I will write a build macro that checks the ~/.config/myapp/plugins and include all of them into the repo.

Do you have any better ideas, suggestions? Thanks in advance.

(For context, this the app I'm writing about: https://github.com/fxdave/vonal-rust)

you are viewing a single comment's thread
view the rest of the comments
[–] onlinepersona@programming.dev 4 points 8 months ago (1 children)

About dynamic library loading, is rust really that much of a pain? If you don't mangle the functions, then the ABI should be alright, no?

Also, you can support plugins using WASM. An option is wasmer. Then other languages can compile to WASM and the plugins can be loaded into your application.

CC BY-NC-SA 4.0

[–] Vorpal@programming.dev 1 points 7 months ago (1 children)

Yes, rust is that much of a pain in this case, since you can only safely pass plain C compatible types across the plugin boundary.

One reason is that rust doesn't have stable layouts of structs and enums, the compiler is free to optimise the to avoid padding by reordering, decide which parts to use as niches for Options etc. And yes, that changes every now and then as the devs come up with new optimisations. I think it changes most recently last summer.

[–] nashenas@hachyderm.io 1 points 7 months ago (1 children)

@Vorpal @onlinepersona the way this is typically done is to expose an extern "C" interface in rust which provides a wrapper around the ABI-unstable rust interface. The C ABI for a given system is stable.

Note that C++ also doesn't have a stable ABI either. The same patterns are used there.

Let me know if you want me to go into more detail on any of that. I've dealt with Rust and C++ FFIs for the last few years.

[–] Vorpal@programming.dev 2 points 7 months ago

Sure, but my point was that such a C ABI is a pain. There are some crates that help:

  • Rust-C++: cxx and autocxx
  • Rust-Rust: stabby or abi_stable

But without those and just plain bindgen it is a pain to transfer any types that can't easily just be repr(C), and there are quite a few such types. Enums with data for example. Or anything using the built in collections (HashMap, etc) or any other complex type you don't have direct control over yourself.

So my point still stands. FFI with just bindgen/cbindgen is a pain, and lack of stable ABI means you need to use FFI between rust and rust (when loading dynamically).

In fact FFI is a pain in most languages (apart from C itself where it is business as usual... oh wait that is the same as pain, never mind) since you are limited to the lowest common denominator for types except in a few specific cases.