我想为插件系统外包一些代码。在我的项目中,我有一个名为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
如果将插件编译为共享库,那可能吗?这也会影响插件'crate-type的决定。
你有其他想法吗?也许我走错路,以便共享的libs不是圣杯。
我首先在Rust forum上发布了此内容。一位朋友建议我试试Stack Overflow。
答案 0 :(得分:10)
以这种方式使用插件一段时间之后,我必须提醒一下,根据我的经验,事情确实不同步,调试非常令人沮丧(奇怪的段错误,奇怪的操作系统错误)。即使在我的团队独立验证依赖项同步的情况下,在动态库二进制文件之间传递非原始结构在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
crate,然后cargo test (--release)
host
crate。
我采用的方法是共享common
个箱子,这两个箱子被列为定义常见struct
和trait
定义的依赖项。起初,我还要测试具有相同结构的结构,或具有相同定义的特征,在两个库中独立定义,但我选择反对它因为它太脆弱而你不会想要在真实的设计中做到这一点。也就是说,如果有人想测试这个,请随意在上面的存储库中进行PR,我会更新这个答案。
此外,Rust插件已声明为dylib
。我不确定编译方式cdylib
会如何互动,因为我认为这意味着在加载插件时会有两个版本的Rust标准库闲置(因为我相信cdylib
静态将Rust stdlib链接到共享对象中。
#repr(C)
。这可以通过保证布局提供额外的安全层,但我最感兴趣的是写作&#34; pure&#34;像C&#34;处理Rust一样少锈的插件尽可能摆弄。我们已经知道你可以通过FFI将Rust包装在不透明的指针中,手动删除等等,因此测试它并不是很有启发性。pub fn foo(args) -> output
,带有#[no_mangle]
指令,结果rustfmt
自动将extern "Rust" fn
更改为fn
。在这种情况下,我不确定我同意这一点,因为他们肯定是&#34; extern&#34;功能在这里,但我会选择遵守rustfmt
。libloading
(或不稳定的DynamicLib
功能)不会为您检查符号。起初我以为我的Vec
测试证明你无法在主机和插件之间传递Vecs,直到我意识到我有Vec<i32>
而另一端我有Vec<usize>
< / LI>
Foo::bar
。此外,由于具有特征边界的函数是单形的,因此通用函数和结构也是如此。编译器无法知道您将调用foo<i32>
,因此不会生成foo<i32>
。插件边界上的任何函数都必须只采用具体类型,并仅返回具体类型。&'a
时,Rust会被迫相信你&'b
。我进行的第一次测试没有定制结构;只是纯粹的原生Rust类型。如果可能的话,这将给出一个基线。我选择了三种基线类型:&mut i32
,&mut Vec
和Option<i32> -> Option<i32>
。这些都是出于非常具体的原因选择的:&mut i32
因为它测试引用&mut Vec
,因为它测试从主机应用程序中分配的内存增加堆,而Option
作为测试通过移动和匹配简单枚举的双重目的。
这三个都按预期工作。变异引用会改变值,推送到Vec可以正常工作,并且无论Some
还是None
,选项都能正常工作。
这是为了测试你是否可以在插件和主机之间传递一个带有通用定义的非内置结构。这可以按预期工作,但正如&#34;一般说明&#34;你不能在一方面而不是另一方面优化和/或优化结构定义,而不能承诺。始终测试您的特定用例,并在发生变化时使用CI。
此测试使用一个结构,其定义仅在插件侧定义,但实现了在公共包中定义的特征,并返回Box<Trait>
。这按预期工作。调用trait_obj.fun()
可以正常工作。
起初我实际上预计会出现丢弃问题而不会让特性明确地将Drop
作为约束,但事实证明Drop也被正确调用(这通过设置变量的值来验证)通过struct drop
函数的原始指针在测试堆栈上声明。 (当然,我知道{> 1}总是在Rust中使用特征对象进行调用,但我不确定动态库是否会使其复杂化。)
注意强>:
我没有测试如果加载插件,创建特征对象,然后删除插件(可能会关闭它)会发生什么。我只能假设这可能是灾难性的。我建议只要特征对象持续存在就保持插件处于打开状态。
插件的工作方式与您完全一样,希望只是自然地连接一个箱子,虽然有一些限制和陷阱。只要你测试,我认为这是一种非常自然的方式。它使符号加载更具可忍性,例如,如果您只需要加载drop
函数然后接收实现接口的特征对象。它还避免了令人讨厌的C内存泄漏,因为您无法或忘记加载new
/ drop
函数。那就是说,小心,并且总是测试!
答案 1 :(得分:6)
没有正式的插件系统,你不能在纯Rust中运行时加载插件。我看到了一些关于做一个原生插件系统的讨论,但现在还没有决定,也许永远不会有任何这样的事情。您可以使用以下解决方案之一:
您可以使用本机动态库using FFI扩展代码。要使用C ABI,您必须使用repr(C)
,no_mangle
属性,extern
等。您可以通过在互联网上搜索Rust FFI来查找更多信息。使用此解决方案,您必须使用原始指针:它们没有安全保证(即您必须使用不安全的代码)。
当然,您可以在Rust中编写动态库,但要加载它并调用函数,您必须通过C ABI。这意味着Rust的安全保证不适用于那里。此外,您不能在库和二进制文件之间使用最高级别的Rust功能trait
,enum
等。
如果您不想要这种复杂性,可以使用适合扩展Rust的语言:使用该语言可以动态地向代码添加函数并使用与Rust相同的保证执行它们。在我看来,这是更简单的方法:如果你有选择,如果执行速度不重要,使用它来避免棘手的C / Rust接口。
这是一个(非详尽的)语言列表,可以轻松扩展Rust:
您也可以使用Python或Javascript,或查看awesome-rust中的列表。