7

I'm about to rewrite a highly modular CMS in Rust, so my question is if it's even possible to have the "core" application set up extension points (actions/hooks), which other plugins / crates is able to "tab" into.

Something like this would suffice, but how would you do this in Rust? The architecture above uses a plugin registry and initiates each plugin's main method from the core by iterating over each of them. However in Rust, since you can't have a global "modules" variable in e.g. a plugin_registry lib crate, I guess this is not the correct thinking in Rust.

Is there a better and more flexible way to make "plugins" integrate seamlessly with a core application? For example, something like an event dispatcher like WordPress uses?

Community
  • 1
  • 1
Dac0d3r
  • 2,176
  • 6
  • 40
  • 76
  • Shepmaster why would you downvote and close this? Very constructive, I learned a lot. Thanks! – Dac0d3r Jan 23 '16 at 20:33
  • Sure it's *possible* - Rust is Turing complete and all that. Something like [iron](https://github.com/iron/iron) has a "middleware" stack - perhaps that would give you a place to start investigating? Then you can ask specific, targeted question here on Stack Overflow. – Shepmaster Jan 23 '16 at 20:33
  • I'm asking what the right approach would be to writeing a core application with extension points, from which 3rd party modules/crates/plugins would be able to hook into and add functionality, integrating seemsless ly with the core application.. Sorry for not being a rust expert and asking a stupid question – Dac0d3r Jan 23 '16 at 20:36
  • No offense intended, I just don't believe that the question *in it's current form* is [on-topic](http://stackoverflow.com/help/on-topic). There are all kinds of questions that are [not ideal to ask](http://stackoverflow.com/help/dont-ask). Don't worry, it will take 4 more people to agree before the question is actually closed; maybe they will disagree with me. – Shepmaster Jan 23 '16 at 20:36
  • I'll try to rewrite my question then. I'm pretty new to rust, but I really would love to know the "rust way" to make this kind of plugin structure – Dac0d3r Jan 23 '16 at 20:38
  • It has nothing to do with Rust or being an "expert" ^_^. I believe that if you asked the same question for any language here on SO it would merit the same response. As I see it, asking "how do I write a large program so that it has X and Y characteristics" goes far beyond the scope of SO questions and answers. A useful answer would have to cover so much ground. Again, this is all my opinion; others may agree with you. – Shepmaster Jan 23 '16 at 20:42
  • Fair enough, however if you take a look at the url I linked to in the question, you'd see a pretty awesome answer, when I asked a similar question in Golang. That's exactly what I'm looking for - but the rust way of course. – Dac0d3r Jan 23 '16 at 20:44

1 Answers1

10

As Shepmaster said, this is a very general question; hence there are many ways to do what you want. And as already mentioned, too, iron is a great example of a modular framework.

However, I'll try to give a useful example of how one could implement such a plugin system. For the example I will assume, that there is some kind of main-crate that can load the plugins and "configure" the CMS. This means that the plugins aren't loaded dynamically!


Structure

First, lets say we have four crates:

  • rustpress: the big main crate with all WordPress-like functionality
  • rustpress-plugin: needs to be used by plugin authors (is an own crate in order to avoid using a huge crate like rustpress for every plugin)
  • rustpress-signature: here we create our plugin which will add a signature to each post
  • my-blog: this will be the main executable that configures our blog and will run as a web server later

1. The trait/interface

The way to go in Rust are traits. You can compare them to interfaces from other languages. We will now design the trait for plugins which lives in rustpress-plugin:

pub trait Plugin {
    /// Returns the name of the plugin
    fn name(&self) -> &str;
    /// Hook to change the title of a post
    fn filter_title(&self, title: &mut String) {}
    /// Hook to change the body of a post
    fn filter_body(&self, body: &mut String) {}
}

Note that the filter_* methods already have a default implementation that does nothing ({}). This means that plugins don't have to override all methods if they only want to use one hook.

2. Write our plugin

As I said we want to write a plugin that adds our signature to each posts body. To do that we will impl the trait for our own type (in rustpress-signature):

extern crate rustpress_plugin;
use rustpress_plugin::Plugin;

pub struct Signature {
    pub text: String,
}

impl Plugin for Signature {
    fn name(&self) -> &str {
        "Signature Plugin v0.1 by ferris"
    }

    fn filter_body(&self, body: &mut String) {
        body.push_str("\n-------\n");   // add visual seperator 
        body.push_str(&self.text);
    }
}

We created a simple type Signature for which we implement the trait Plugin. We have to implement the name() method and we also override the filter_body() method. In our implementation we just add text to the post body. We did not override filter_title() because we don't need to.

3. Implement the plugin stack

The CMS has to manage all plugins. I assume that the CMS has a main type RustPress that will handle everything. It could look like this (in rustpress):

extern crate rustpress_plugin;
use rustpress_plugin::Plugin;

pub struct RustPress {
    // ...
    plugins: Vec<Box<Plugin>>,
}

impl RustPress {
    pub fn new() -> RustPress {
        RustPress {
            // ...
            plugins: Vec::new(),
        }
    }

    /// Adds a plugin to the stack
    pub fn add_plugin<P: Plugin + 'static>(&mut self, plugin: P) {
        self.plugins.push(Box::new(plugin));
    }

    /// Internal function that prepares a post
    fn serve_post(&self) {
        let mut title = "dummy".to_string();
        let mut body = "dummy body".to_string();

        for p in &self.plugins {
            p.filter_title(&mut title);
            p.filter_body(&mut body);
        }

        // use the finalized title and body now ...
    }

    /// Starts the CMS ...
    pub fn start(&self) {}
}

What we are doing here is storing a Vec full of boxed plugins (we need to box them, because we want ownership, but traits are unsized). When the CMS then prepare a blog-post, it iterates through all plugins and calls all hooks.

4. Configure and start the CMS

Last step is adding the plugin and starting the CMS (putting it all together). We will do this in the my-blog crate:

extern crate rustpress;
extern crate rustpress_plugin;
extern crate rustpress_signature;

use rustpress::RustPress;
use rustpress_plugin::Plugin;
use rustpress_signature::Signature;

fn main() {
    let mut rustpress = RustPress::new();

    // add plugin
    let sig = Signature { text: "Ferris loves you <3".into() };
    rustpress.add_plugin(sig);

    rustpress.start();
}

You also need to add the dependencies to the Cargo.toml files. I omitted that because it should be fairly easy.

And note again that this is one of many possibilities to create such a system. I hope this example is helpful. You can try it on playground, too.

Lukas Kalbertodt
  • 79,749
  • 26
  • 255
  • 305
  • 1
    Let me know if something is not clear enough or if some information is missing! – Lukas Kalbertodt Feb 13 '16 at 11:16
  • Wow finally. Just what I was looking for - what an amazing answer! Thank you so much. Although I already ended up implementing a slightly different architecture before reading your answer. I'm sure though, if I combine my solution with yours, I'll get the best of both and probably a close to optimal architecture, so again, Thanks a alot for taking the time to come up with this quality answer! – Dac0d3r Feb 14 '16 at 00:46
  • This is an amazing answer, but is it at all possible to automatically add plugins from a config file indicating the list of plugins to be used? – Vikash Balasubramanian Jul 10 '22 at 16:51