我想为插件系统外包一些代码.在我的项目中,我有一个特性叫做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>>
}
Run Code Online (Sandbox Code Playgroud)
程序发布时,文件树看起来像:
.
??? fancy_program.exe
??? providers
??? fp_awesomedude.dll
??? fp_niceplugin.dll
Run Code Online (Sandbox Code Playgroud)
如果将插件编译为共享库,那可能吗?这也会影响插件箱类型的决定.
你有其他想法吗?也许我走错路,以便共享的libs不是圣杯.
我首先在Rust论坛上发布了这个.一位朋友建议我试试Stack Overflow.
Lin*_*ope 13
以这种方式使用插件一段时间之后,我必须提醒一下,根据我的经验,事情确实不同步,调试非常令人沮丧(奇怪的段错误,奇怪的操作系统错误).即使在我的团队独立验证依赖项同步的情况下,在动态库二进制文件之间传递非原始结构在OS X上也会因某种原因而失败.我想重新审视一下,找到它发生的情况,也许还有Rust的问题,但我会建议谨慎对待这个问题.
LLDB和valgrind对于调试这些问题几乎是必不可少的.
我自己一直在研究这些问题,我发现这里的官方文档很少,所以我决定玩游戏!
首先让我注意一下,因为关于这些属性的官方消息很少,所以如果你试图让空中飞机或核导弹故意发射,请不要依赖任何代码,至少在没有进行比我弄完了.如果此处的代码删除了您的操作系统,并且通过电子邮件向您的当地警方发送了十二生肖杀人的错误含泪的供词,我不负责任; 我们这里处于Rust的边缘,事情可能会从一个版本或工具链转变为另一个版本.
您可以在以下Github存储库中查看我的实验:Rust Plugin Playground.这段代码不是特别健壮,但是通过对PLUGIN_DIR静态的微小调整,host/src/lib.rs您可以加载用于调试/发布的插件以及在每个操作系统之间切换.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]指令,事实证明,rustfmt自动改变extern "Rust" fn简单fn.我不确定在这种情况下我是否同意这一点,因为它们在这里肯定是"外部"功能,但我会选择遵守rustfmt.libloading(或不稳定的DynamicLib功能)不会为您检查符号.起初我以为我的Vec测试证明你无法在主机和插件之间传递Vecs,直到我意识到我有一端Vec<i32>而另一端我有Vec<usize>Foo::bar因为没有名称错误.此外,由于具有特征边界的函数是单形的,因此通用函数和结构也是如此.编译器无法知道您将要调用,foo<i32>因此不会foo<i32>生成任何内容.插件边界上的任何函数都必须只采用具体类型并仅返回具体类型.&'a真正返回时,Rust被迫相信你&'b.我进行的第一次测试没有定制结构; 只是纯粹的原生Rust类型.如果可能的话,这将给出一个基线.我选择了三个基准类型:&mut i32,&mut Vec,和Option<i32> -> Option<i32>.这些都是出于非常具体的原因选择的:&mut i32因为它测试引用,&mut Vec因为它测试从主机应用程序中分配的内存中增加堆,以及Option测试通过移动和匹配简单枚举的双重目的.
这三个都按预期工作.变异的变异参考价值,推动以A VEC正常工作,而选择正常工作是否Some还是None.
这是为了测试你是否可以在插件和主机之间传递一个带有通用定义的非内置结构.这可以按预期工作,但正如"常规说明"部分所述,不能保证 Rust不会无法优化和/或优化结构定义的一方而不是另一方.始终测试您的特定用例,并在发生变化时使用CI.
此测试使用一个结构,其定义仅在插件端定义,但实现在公共包中定义的特征,并返回一个Box<Trait>.这按预期工作.呼叫trait_obj.fun()正常工作.
起初我实际上预计会有一些问题,如果没有让trait明确地Drop作为一个绑定而丢弃,但事实证明Drop也被正确调用(这是通过设置通过原始指针在测试堆栈上声明的变量的值来验证的来自struct的drop功能).(当然,我知道drop在Rust中,即使使用trait对象也总是被调用,但我不确定动态库是否会使它复杂化).
注意:
我没有测试如果你加载插件,创建一个特征对象,然后删除插件(可能会关闭它)会发生什么.我只能假设这可能是灾难性的.我建议只要特征对象持续存在就保持插件处于打开状态.
尽管存在一些限制和陷阱,但插件的工作方式与您希望自然地连接箱子完全一样.只要你测试,我认为这是一种非常自然的方式.它使符号加载更具可忍性,例如,如果您只需要加载一个new函数然后接收一个实现接口的特征对象.它还避免了令人讨厌的C内存泄漏,因为您无法或忘记加载drop/ free函数.那说,小心,并经常测试!
没有官方的插件系统,您不能在运行时在纯Rust中加载插件。我看到了一些关于做本机插件系统的讨论,但是目前还没有决定,也许永远不会有这样的事情。您可以使用以下解决方案之一:
您可以使用FFI使用本地动态库扩展代码。要使用C ABI,必须使用repr(C),no_mangle属性extern等。通过在Internet上搜索Rust FFI ,您将找到更多信息。使用此解决方案,您必须使用原始指针:它们没有安全保证(即,您必须使用不安全的代码)。
当然,您可以在Rust中编写动态库,但是要加载它并调用函数,必须通过C ABI。这意味着Rust的安全保证不适用于此处。此外,您不能使用的最高水平防锈的功能为trait,enum等图书馆和二进制之间。
如果您不希望这种复杂性,则可以使用一种适合于扩展Rust的语言:通过该语言,您可以动态地向代码中添加函数并以与Rust中相同的保证来执行它们。我认为这是更简单的方法:如果可以选择,并且执行速度不是很关键,请使用它来避免棘手的C / Rust接口。
这是可以轻松扩展Rust的语言的列表(并非详尽无遗):
您也可以使用Python或Javascript,或在awesome-rust中查看列表。