Skip to content

lilopkins/dynamic-plugins-rs

Repository files navigation

Dynamically Loaded Plugins for Rust

Goal

In many pieces of software, it can be beneficial to allow other developers to add functionality that you have not considered. In order to do this, plugin-like systems are often used.

Rust is a powerful language with a strong emphasis on safety, however this can make working with plugins quite challenging as you can quickly lose a lot of the compile-time checking that Rust offers. The goal of this library is to reintroduce the safety that Rust is built upon when writing plugin libraries (DLLs, DyLibs or SOs) that are loaded at runtime.

Architecture

             ┌───────────┐                
    Safe     │           │  Compile-time  
    Interface│ Reusable  │  Checking      
     ┌──────►│  Plugin   │◄─────┬────────┐
     │       │ Interface │      │        │
     │       │           │      │        │
┌────┴───┐   └───────────┘ ┌────┴─────┐  │
│        │                 │          │  │
│ Plugin │◄───────────────►│ Plugin A │  │
│  Host  │  Runtime calls  │          │  │
│        │◄───────┐        └──────────┘  │
└────────┘        │                      │
                  │        ┌──────────┐  │
                  │        │          │  │
                  └───────►│ Plugin B ├──┘
                           │          │   
                           └──────────┘   

Plugin Host

The plugin host is the part of the system that will find and load the plugins at runtime and will call upon their functionality. This is usually your main software package.

Plugin Client

The plugin client(s) are the plugins that are written by yourself/other developers. These must be written to match the plugin interface provided by the host and allow a safe way to call upon other code.

Writing a Plugin System

To write a plugin system, you will first need to decide upon your interface. For this example, we'll demonstrate with this interface:

┌───────────────────────────────┐
│ExamplePlugin                  │
├───────────────────────────────┤
│do_a_thing()                   │
│say_hello(name: string) -> bool│
└───────────────────────────────┘

The do_a_thing function here will just be triggered to do whatever the plugin author decides. The say_hello function should display a message to the named person, then return a boolean as to whether that was successful. Admittedly, this isn't exactly a complex interface!

Writing a Plugin Host

In your project, add the dynamic-plugin library:

cargo add dynamic-plugin --features host

Now in your main.rs file, you can define your interface:

use dynamic_plugin::{libc::c_char, plugin_interface};

plugin_interface! {
    extern struct ExamplePlugin {
        /// Ask the plugin to do a thing
        fn do_a_thing();
        /// Say hello to a person
        fn say_hello(to: *const c_char) -> bool;
    }
}

Note that we can't just send strings around! As this depends upon FFI, we need to use C-compatible data. Rust will warn you if you do not do this!

That is almost it! We can now write some code to actually use these plugins:

fn main() -> dynamic_plugin::Result<()> {
    let plugins = ExamplePlugin::find_plugins("./plugins")?;
    for plugin in plugins {
         plugin.do_a_thing()?;
         let s = std::ffi::CString::new("Jens").unwrap();
         plugin.say_hello(s.as_ptr())?;
    }
    Ok(())
}

Writing a Plugin Client

You can now write plugins for your interface! Create a new library project:

cargo new --lib example-plugin

In Cargo.toml, specify that this should build as a C-compatible library:

[lib]
crate-type = [ "cdylib" ]

[dependencies]
dynamic-plugin = { version = "x.x.x", features = [ "client" ] }

You can now define your plugin implementation:

use std::ffi::CStr;
use dynamic_plugin::{libc::c_char, plugin_interface, plugin_impl};

plugin_interface! {
    extern struct ExamplePlugin {
        /// Ask the plugin to do a thing
        fn do_a_thing();
        /// Say hello to a person
        fn say_hello(to: *const c_char) -> bool;
    }
}

plugin_impl! {
    ExamplePlugin,

    fn do_a_thing() {
        println!("A thing has been done!");
    }

    fn say_hello(name: *const c_char) -> bool {
        unsafe {
            let name = CStr::from_ptr(name);
            println!("Hello, {}!", name.to_string_lossy());
        }
        true
    }
}

The plugin is now ready to build and distribute.

Taking this further...

You can also avoid reusing the plugin definition by putting it in it's own library. An implementation that does this is available in the example-plugin and example-plugin-host folders of the source repository.

attempt to compute '0_usize - 1_usize', which would overflow

If you come across this compile-time error, this indicates that the implementation you are writing does not match the expected implementation for the plugin definition. Please check that you:

  • Are using the correct definition.
  • Have all the functions you need to meet the definition.
  • That all the functions are named correctly (identically to the definition).
  • That all the function arguments are the same order and types as the definition.
  • That all the function return types are the same as the definition.

About

Dynamically loaded plugin libraries for Rust.

Resources

License

Stars

Watchers

Forks

Languages