Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to make plugin extension hooks like WordPress actions in Rust?

Tags:

rust

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?

like image 279
Dac0d3r Avatar asked Jan 23 '16 20:01

Dac0d3r


1 Answers

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.

like image 95
Lukas Kalbertodt Avatar answered Oct 17 '22 08:10

Lukas Kalbertodt