习惯Rust插件系统
我想为一个插件系统外包一些代码。 在我的项目中,我有一个叫做Provider
的特性,它是我的插件系统的代码。 如果你激活了功能“消费者”,你可以使用插件; 如果你不这样做,你是一个插件的作者。
我希望插件的作者通过编译到共享库来将他们的代码加入到我的程序中。 共享库是一个很好的设计决策吗? 无论如何,插件的局限性是使用Rust。
插件主机是否必须使用C方式加载共享库:加载未加载的函数?
我只想让作者使用特质Provider
来实现他们的插件,就是这样。 在看完sharedlib和libloading之后,似乎不可能以惯用的Rust方式加载插件。
我只想将特征对象加载到我的ProviderLoader
:
// lib.rs
pub struct Sample { ... }
pub trait Provider {
fn get_sample(&self) -> Sample;
}
pub struct ProviderLoader {
plugins: Vec<Box<Provider>>
}
程序发货时,文件树将如下所示:
.
├── fancy_program.exe
└── providers
├── fp_awesomedude.dll
└── fp_niceplugin.dll
如果插件被编译为共享库,那么这可能吗? 这也会影响插件箱子类型的决定。
你有其他想法吗? 也许我在错误的道路上,以便共享库不是圣杯。
我首先在Rust论坛上发布了这个。 一位朋友建议我试试Stack Overflow。
更新3/27/2018:
以这种方式使用插件一段时间后,我必须警告,根据我的经验,事情会发生不同步,并且调试(奇怪的段错误,奇怪的操作系统错误)可能会非常令人沮丧。 即使在我的团队独立验证依赖项同步的情况下,由于某种原因,在动态库二进制文件之间传递非原始结构往往会在OS X上失败。 我想重新审视这个问题,找出它发生的情况,并且可能与Rust开一个问题,但我会建议谨慎处理这个问题。
LLDB和valgrind对于调试这些问题几乎是必不可少的。
介绍
我一直在调查这些事情,我发现这里有很少的官方文档,所以我决定玩一玩!
首先让我注意,因为有这些性能方面几乎没有官方的消息,请不要依赖任何代码在这里,如果你想保持空气中的飞机或核导弹的发射错误地,至少在没有做多更全面的测试我已经搞定了。 如果此处的代码删除了您的操作系统并通过电子邮件向您当地的警方发送了一个错误的含沙射杀的忏悔,我不负责任; 我们处于Rust的边缘,事情可能会从一个版本或工具链变为另一个。
您可以在以下Github存储库查看我的实验:Rust Plugin Playground。 这段代码并不是特别健壮,但是通过对host/src/lib.rs
的PLUGIN_DIR
static进行微小的调整,您可以加载用于调试/发布的插件以及在每个OS之间切换.so / .dylib / .dll。 我已经在Windows 10( stable-x86_64-pc-windows-msvc
)和Cent OS 7( stable-x86_64-unknown-linux-gnu
)的调试和发布配置中对Rust 1.20 stable进行了个人测试。 为了测试你必须手动cargo build (--release)
plugin
箱,然后进行cargo test (--release)
host
机箱。
途径
我采用的方法是将common
箱子作为定义公共struct
和trait
定义的依赖项列出。 起初,我还要测试具有相同结构的结构,或者具有相同定义的特征,这些结构在两个库中都是独立定义的,但是我选择了它,因为它太脆弱了,你不想在真正的设计。 也就是说,如果有人想测试这个,请随时在上面的仓库上做一个PR,我会更新这个答案。
另外,Rust插件被宣布为dylib
。 我不确定如何编译为cdylib
会进行交互,因为我认为这意味着加载插件时会有两个版本的Rust标准库挂在附近(因为我相信cdylib
将Rust stdlib静态链接到共享对象中)。
测试
一般注意事项
#repr(C)
。 这可以通过保证布局来提供额外的安全层,但是我对编写“纯粹的”Rust插件和尽可能少地“像C一样处理Rust”那样尽可能地摆弄好奇而感到好奇。 我们已经知道你可以通过FFI使用Rust,通过将事物包装在不透明的指针中,手动删除等等,所以测试它并不是很有启发性。 pub fn foo(args) -> output
#[no_mangle]
pub fn foo(args) -> output
使用#[no_mangle]
指令pub fn foo(args) -> output
,事实证明, rustfmt
自动将extern "Rust" fn
更改为fn
。 我不确定在这种情况下我是否同意这一点,因为这里肯定是“外部”功能,但我会选择遵守rustfmt
。 libloading
(或不稳定的DynamicLib
功能)将不会为您检查符号。 起初我以为我的Vec
测试证明你无法通过主机和插件之间的Vecs,直到我意识到一方面我有Vec<i32>
,另一方面我有Vec<usize>
Foo::bar
因为缺少名称修改。 另外,由于具有特征边界的函数是单态的,所以泛函和结构也出现了。 编译器不知道你打算调用foo<i32>
所以不会生成foo<i32>
。 插件边界上的任何函数都只能使用具体类型,并只返回具体类型。 &'a
真的是&'b
时,Rust会被迫相信你。 本地锈
我进行的第一次测试是没有定制结构; 只是纯粹的本地Rust类型。 如果这是可能的话,这将给出一个基准。 我选择了三种基线类型: &mut i32
, &mut Vec
和Option<i32> -> Option<i32>
。 这些都是由于非常具体的原因而选择的: &mut i32
因为它测试引用, &mut Vec
因为它测试从宿主应用程序中分配的内存中增长堆,而Option
作为测试通过移动和匹配简单枚举。
三人都按预期工作。 突变参考值会突变该值,推到Vec可以正常工作,并且该选项可以正常工作,无论是“ Some
还是“ None
。
共享结构定义
这是为了测试你是否可以在插件和主机之间传递一个通用定义的非内建结构。 这可以按预期工作,但正如“一般说明”部分所述,不能保证 Rust不会无法优化和/或优化一侧而不是另一侧的结构定义。 总是测试你的特定用例,并在它改变的情况下使用CI。
盒装特质对象
该测试使用一个结构,其定义只在插件端定义,但是实现了在一个通用包中定义的特征,并返回一个Box<Trait>
。 这按预期工作。 调用trait_obj.fun()
可以正常工作。
起初,我实际上预计会出现问题,而不会使该特征明显具有Drop
作为边界,但事实证明Drop也被正确调用(这已通过设置通过raw在测试堆栈上声明的变量的值进行验证来自结构drop
函数的指针)。 (当然,我知道drop
总是被称为即使特质对象在Rust中,但我不确定动态库是否会使其复杂化)。
注意 :
我没有测试如果你加载一个插件,创建一个特质对象,然后删除插件(这可能会关闭它),会发生什么。 我只能假设这可能是灾难性的。 我建议只要特质对象一直保持打开状态。
备注
插件的工作原理与您期望的一样,只需自然链接一个箱子,尽管存在一些限制和缺陷。 只要你测试,我认为这是一个非常自然的方式。 例如,如果你只需要加载一个new
函数,然后接收一个实现接口的trait对象,它就会使得符号加载更加可承受。 它还避免了令人讨厌的C内存泄漏,因为你无法或忘记加载drop
/ free
函数。 这就是说,要小心,并且经常测试!
没有正式的插件系统,你不能在纯粹的Rust中运行时加载插件。 我看了一些关于做本地插件系统的讨论,但现在没有什么决定,也许永远不会有这样的事情。 您可以使用以下解决方案之一:
您可以使用FFI使用本地动态库扩展您的代码。 要使用C ABI,您必须使用repr(C)
, no_mangle
属性, extern
等。您可以通过在互联网上搜索Rust FFI找到更多信息。 有了这个解决方案,你必须使用原始指针:它们没有安全保证(即你必须使用不安全的代码)。
当然,您可以在Rust中编写动态库,但要加载并调用函数,您必须通过C ABI。 这意味着Rust的安全保证不适用于此。 此外,你不能在库和二进制文件之间使用Rust的最高级别的功能作为trait
, enum
等。
如果你不想要这种复杂性,你可以使用一种适合于展开Rust的语言:你可以使用它来动态添加函数到你的代码中,并使用与Rust相同的保证来执行它们。 在我看来,这是更简单的方法:如果您有选择,并且执行速度不重要,可以使用它来避免棘手的C / Rust界面 。
这是一个可以轻松扩展Rust的语言列表(并非详尽无遗):
您也可以使用Python或Javascript,或者在真棒锈蚀中查看列表。
链接地址: http://www.djcxy.com/p/96499.html上一篇: Idiomatic Rust plugin system
下一篇: Does `lm` return `model` for reasons other than `predict`