#plugin-system #compile-time #dynamically #write #run-time #dll #safety

dynamic-plugin

为您的 Rust 软件编写编译时检查、动态加载的插件库

1 个不稳定版本

0.1.0 2024年5月18日

#687开发工具

MIT 许可证

11KB

Rust 的动态加载插件

目标

在许多软件中,允许其他开发者添加您未考虑到的功能可能是有益的。为此,通常会使用插件式系统。

Rust 是一种强大的语言,它非常注重安全性,然而这可能会使您在处理插件时面临挑战,因为您可能会迅速失去 Rust 提供的许多编译时检查。本库的目标是在运行时加载的插件库(DLLs、DyLibs 或 SOs)编写时重新引入 Rust 建立的基础安全性。

架构

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

插件主机

插件主机是系统的一部分,它将在运行时查找和加载插件,并调用它们的功能。这通常是您的主软件包。

插件客户端

插件客户端(们)是由您或其他开发者编写的插件。这些插件必须与主机提供的插件接口匹配,并允许以安全的方式调用其他代码。

编写插件系统

要编写插件系统,您首先需要决定您的接口。为此示例,我们将使用此接口进行演示

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

这里的 do_a_thing 函数将仅由插件作者决定触发,执行任何操作。 say_hello 函数应向指定的人显示消息,然后返回一个布尔值,表示是否成功。诚然,这不是一个复杂的接口!

编写插件主机

在您的项目中添加 dynamic-plugin

cargo add dynamic-plugin --features host

现在在您的 main.rs 文件中,您可以定义您的接口

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;
    }
}

请注意,我们不能只是发送字符串!因为这依赖于 FFI,我们需要使用与 C 兼容的数据。如果这样做,Rust 将会警告您!

这就快完成了!我们现在可以编写一些代码来实际使用这些插件

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(())
}

编写插件客户端

您现在可以为您的接口编写插件!创建一个新的库项目

cargo new --lib example-plugin

Cargo.toml 中,指定这应该构建为与 C 兼容的库

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

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

您现在可以定义您的插件实现

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
    }
}

插件现在已准备好构建和分发。

进一步探索...

您还可以通过将其放入自己的库中来避免重复使用插件定义。实现此功能的示例可在源存储库的 example-pluginexample-plugin-host 文件夹中找到。

尝试计算'0_usize - 1_usize',这将导致溢出

如果您遇到此编译时错误,这表明您正在编写的实现与插件定义的预期实现不匹配。请检查您

  • 是否使用了正确的定义。
  • 是否拥有满足定义所需的所有函数。
  • 所有函数的名称是否正确(与定义相同)。
  • 所有函数参数的顺序和类型是否与定义相同。
  • 所有函数的返回类型是否与定义相同。

依赖项

~0.4–5.5MB
~19K SLoC