Some applications let their users modify their functionality. In most cases, it is done via plugins - small libraries that are being loaded by the main program, and then called in some specific circumstances. A well-known example would probably be the instant-messaging programs like Pidgin. They can communicate using various protocols (Jabber, Facebook, ...), have custom themes or provide additional functions thanks to the plugins that are available for them. In the Orbiter simulator the users can add new spaceships in the form of plugins. There are a lot of possible use cases. In this blog entry I'm going to present a way of achieving a similar effect in the Rust language. My way isn't probably the only one or the best, but I find it simple and convenient :)
Introduction
The main mechanism we are going to use are so-called traits. Traits define functions that can be called for a given data structure. For example, numeric types implement the Add
trait, which allows the programmer to use the + operator on them. Many structures implement the Clone
trait, which provides a way of creating copies of them. There are a lot of similar examples.
Let's see what it looks like in a simple case:
1 2 3 |
pub trait SomeTrait { fn get_some_int(&self) -> u32; } |
The trait above defines the types implementing it to support the get_some_int
function. This function takes a borrowed reference to the structure and returns some unsigned 32-bit integer.
This trait can be implemented for various datatypes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
impl SomeTrait for u32 { fn get_some_int(&self) -> u32 { *self } } struct SomeStruct; impl SomeTrait for SomeStruct { fn get_some_int(&self) -> u32 { 1 } } println!("{}", 5u32.get_some_int()); // prints 5 println!("{}", SomeStruct.get_some_int()); // prints 1 |
As you can see, different datatypes can implement the same trait in different ways, adequate for them. Traits can be then used in generic functions, which are functions that can operate on many datatypes. It can look like this, for example:
1 2 3 |
fn add_one<T: SomeTrait>(x: T) -> u32 { x.get_some_int() + 1 } |
Here the add_one
function knows nothing about its parameter, except for the fact that it implements the SomeTrait
trait. This is enough to tell that we can call get_some_int()
on it.
Our API will be based on traits. The main application will call functions defined in the plugin, knowing only that those functions will return data implementing some trait. This will allow it to perform some operations, while knowing nothing about their details. As an example, an instant messaging app could ask a plugin supporting some protocol for an object representing a contact, which would implement a trait defining such operations like sending a message or getting the contact details. This would allow the app to communicate correctly without the need for caring about the details of the protocol.
The implementation
Let's write a trivial example implementation.
The libraries in Rust are packaged in so-called "crates". Crates can depend on other crates and use their functions, as can applications. We will create a crate that shall define a plugin interface, then. This crate will be imported by the main application (for it to know how to use the plugin) and by the plugin (for it to know what functions it should support).
The code of our interface crate will be as complicated as the example above:
1 2 3 |
pub trait PluginTrait { fn get_some_string(&self) -> String; } |
The end. The only thing our plugins will be able to do is returning some strings generated by them.
Now a simple plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
extern crate plugin_interface; use plugin_interface::PluginTrait; struct Whatever; impl PluginTrait for Whatever { fn get_some_string(&self) -> String { "whatever".to_string() } } #[no_mangle] pub extern fn object_factory() -> Box<PluginTrait> { Box::new(Whatever) } |
Not much more complex. First, we declare that we will use the interface crate (called "plugin_interface"). We import the trait from it - it will define the way of communication between the crate and the program. Then we define a Whatever
structure, which implements this trait. Whenever the get_some_string
function will be called on a Whatever
variable, it will return just "whatever".
The last fragment is something new. The reason is as follows: we will want to compile the plugin into a dynamically-linked library (a .dll file in Windows or an .so in Linux). Such libraries can only export variables or functions - they know nothing about traits and such. We need to get over this somehow. The way we do it is this: the library will export a "naked" function, returning a trait object - a pointer to a data structure allocated on the heap. The heap allocation is achieved in Rust via the Box
type. In this case we don't specify the concrete type though, but only that it will be something implementing PluginTrait
. This way we get a uniform interface, which can be used by different plugins.
The function is also decorated with two more pieces of information: first, that it is to be exported ("extern" takes care of that) and second, that its name is to be left untouched (this is what #[no_mangle]
does - the default is to mangle the names of exported symbols).
The only thing that's left is the main program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
extern crate plugin_interface; extern crate libloading; use plugin_interface::PluginTrait; use libloading::{Library, Symbol}; use std::env::current_dir; fn main() { let mut path = current_dir().unwrap(); path.push("libplugin.so"); println!("Path: {}", path.display()); let lib = Library::new(path.as_path()).unwrap(); let object_factory: Symbol<extern fn() -> Box<PluginTrait>> = unsafe { lib.get(b"object_factory").unwrap() }; let obj = object_factory(); println!("Result: {}", obj.get_some_string()); } |
In order to load the plugin we use the "libloading" crate, allowing for loading of dynamic libraries and reading symbols out of them. Rust had an unstable API for that in its standard library (which meant that using it required the nightly version of the compiler), but it was recently deprecated in favor of this crate. It is much more convenient than the deprecated API, too.
The program does the following: first, it tries to load the plugin from the file called "libplugin.so" in the current directory. If it fails, it panics. Then, it tries to load the "object_factory" symbol from the library, which is what our function in the plugin is called. Here we define the symbol type as a function taking no arguments and returning a PluginTrait
trait object. This is the place in which the main program has to know the plugin interface - to know what exactly is the PluginTrait
.
If the loading succeeded, we can try to call the function and display the result. If everything went well, we get "Result: whatever" on the screen.
Some final words
In this example, I only presented getting some objects from the plugin by the program. Communicating the opposite way can be done analogously: some plugin functions can take arguments in the form of trait objects, which will be passed to them by the main program. Other functions can then use those objects. So, there is nothing that prevents us from expanding the scheme to two-way communication.
I think that is all - comments and constructive critique are always welcome :) The full code from this entry is available on GitHub: click