坚持生命周期:如何围绕共享库编写包装器结构?

Stuck with lifetimes: How to write a wrapper struct around a shared library?

我正在尝试围绕 Rust 中的共享库编写一个包装器结构:

pub struct LibraryWrapper<'lib> {
    lib: libloading::Library,
    func: libloading::Symbol<'lib, unsafe extern fn() -> u32>
}

impl<'lib> LibraryWrapper<'lib> { // lifetime `'lib` defined here
    pub fn new() -> libloading::Result<Self> {
        let lib: libloading::Library = libloading::Library::new(LIB_PATH)?;
        let func: libloading::Symbol<'lib, unsafe extern fn() -> u32> = unsafe { // type annotation requires that `lib` is borrowed for `'lib`
            lib.get(FUNC_NAME)? // borrowed value does not live long enough
        };

        Ok(Self {
            lib, func // `lib` dropped here while still borrowed
        })
    }
}

我如何告诉 Rust 编译器 func 不会比 lib 长,因为它们在同一个结构中?谢谢!

这无法完成,因为您无法确保稍后某些代码不会解构 LibraryWrapper 以保留 func 并删除 lib。例如:

fn this_is_bad<'lib>(wrapper: LibraryWrapper<'lib>) -> libloading::Symbol<'lib, unsafe extern fn() -> u32> {
    let LibraryWrapper { lib, func } = wrapper;
    func
}

要管理类似的东西,如果您需要发送,我建议您使用 RC 或 ARC。

如前所述,借用检查需要确保您不会破坏某些东西以保留功能并删除库。纠结了好几次,得出以下结论:

动态 linked libloading 东西是少数我认为可以变成 static 的东西之一。如果它是硬编码的 so/dll/dylib,我通常会为它创建一个带有 Once 的特殊静态,因此需要合理的努力才能删除它。对于编译时未知的加载项(例如,通过配置文件管理),我通常互斥保护一个 static 中央注册表,它只是一个 HashMap,带有库名称或路径的键,并且只允许在整个程序终止时删除库。是的,这基本上是一个单例,不,我一般不建议对几乎任何其他东西这样做,这种情况是一个特殊的例外。

为了增加安全性,我建议您隔离静态并仅使用函数或结构来引用它,尤其是使用哈希映射注册表,因为有人很容易对其执行 remove 或覆盖HashMap,或者不小心替换了一个同名的库。由于它是静态的,它仍然允许您将函数存储为另一个 static/singleton ,如果您真的需要,则不必每次都为它们查询库(但我不确定仅仅请求它是否会有很多性能开销每次都没有对它进行基准测试)。

我这样做有几个原因:

  1. 您通常希望像使用普通库一样使用这些函数,这与 linking FFI 或静态引入另一个库没有区别。无论如何,您必须知道函数名称,所以它实际上与通常具有这些函数的文件相同。
  2. 您不必使用 'lib 生命周期或库函数参数或其他成员来污染您想要引用功能的所有内容,而不是您是 link 的库ing 是 技术上 动态的,它只是引用一个依赖项。如果你的程序最终变得不可知,例如,稍后变成一个静态 linked 库(只需删除包装 static 动态库的文件中的设置行,并将它们更改为指向其他地方的函数)。
  3. 您通常只需要 to/want to/should link 在整个程序中给定的动态库一次,如果没有别的,这几乎就是 static 的目的。
  4. 在程序终止之前,您很少想要删除动态或非动态的库。
  5. 它将 unsafe 初始化和函数加载代码隔离到一个文件中(尽管包装器结构也承认这一点)。

这些原因的最常见例外是选项 4,比如如果你有一个高度可配置的插件系统,如果用户想要禁用旧的,插件占用的额外内存可能很重要一个并激活新的,在这种情况下,设计一个包装器方案可能是正确的调用,或者在你的库包装器中添加一个 unsafe fn close_dynamic_lib_no_seriously_do_you_really_want_to_do_this(self)

我能想到的唯一其他例外是​​,如果您正在为 crates.io 编写类似库的内容,您 确实 希望用户能够管理他们的动态库并为您将它们提供给您的库(例如,它可能位于非标准路径中,或者他们可以在多个实现之间进行选择),即便如此,修改这种方式仍然可能是有益的。

我最终使用了 lazy_static 板条箱。对于我的用例,我认为它比 std::sync::Once 更方便。感谢@LinearZoetrope 让我想到了这个想法!