我有一个Rust库,它有一些平台抽象形式的"后端"。该库使用build.rs
进行一些平台检查,并根据可以构建的后端设置一些编译时配置变量。然后在代码中,后端代码被保护如下:
#[cfg(backend1)]
struct Backend1 { ... }
#[cfg(backend2)]
struct Backend2 { ... }
...
此库的使用者想要实例化适用于当前平台的后端。理想情况下,您可以执行以下操作:
fn get_backend() -> Box<Backend> {
#[cfg(backend1)]
return mylib::backends::Backend1::new(...);
#[cfg(backend2)]
return mylib::backends::Backend2::new(...);
...
}
但是,mylib
中的配置变量不会分享给消费者,因此#[cfg(backend1)]
无法按预期工作。
有没有办法实现所需的行为,而无需为构建图书馆消费者的人员进行人工干预?我不希望用户必须手动传递可用后端列表。这似乎应该是可自动化的。
请注意,mylib
内置的后端结构完全不存在,这意味着消费者无法引用它们。消费者需要使用条件编译来确保仅引用内置于mylib
的后端。
任何给定平台可能有多个后端,在这种情况下,消费者应该能够选择哪一个。
答案 0 :(得分:1)
您无法从外部访问图书馆的配置。
您永远无法从消费者代码中了解后端的具体类型,因此您必须提出一些能够构建它们的机制,同时考虑到每个构造函数的不同需求。
这里的基本思想是引入一个上下文,类似于您可能在面向对象语言中使用的依赖注入上下文。上下文包含构造函数可能需要的值。
要创建特征对象,您需要一个特征:
pub trait Backend {
// all the common stuff for backends
}
用于构造后端的特性,以及用于保存这些后端所需的所有可能配置变量的结构。这可能与Backend
具有相同的特性,因为new
方法会阻止它成为对象。大多数变量都是可选的,因为并非所有后端都需要它们:
pub trait BackendContstruct {
fn new(ctx: &BackendContext) -> Result<Box<Backend>, BackendError>;
}
pub struct BackendContext<'a> {
var_1: Option<&'a str>,
var_2: Option<&'a str>,
another: Option<bool>,
// etc
}
如果提供错误的变量,则需要返回错误。不幸的是,构造动态意味着错误是运行时而不是编译时:
pub struct BackendError(String);
每个后端的可用性取决于平台支持。因此,使他们的定义依赖于平台:
#[cfg(platform1)]
mod backend1 {
pub struct Backend1;
impl ::Backend for Backend1 {}
impl ::BackendContstruct for Backend1 {
fn new(ctx: &::BackendContext) -> Result<Box<::Backend>, ::BackendError> {
if ctx.var_1.is_none() {
Err(::BackendError("Backend1 requires val_1 to initialize".to_string()))
} else {
Ok(Box::new(Backend1 {}))
}
}
}
}
#[cfg(platform1)]
#[cfg(platform2)]
mod backend2 {
pub struct Backend2;
impl ::Backend for Backend2 {}
impl ::BackendContstruct for Backend2 {
fn new(ctx: &::BackendContext) -> Result<Box<::Backend>, ::BackendError> {
Ok(Box::new(Backend2 {}))
}
}
}
没有具体类型是公开的,任何可能都不存在。因此,提供一个枚举,以便消费者可以指定他们想要的后端:
pub enum BackendType {
// these names are available in all configurations
Default, Backend1, Backend2, Backend3
}
构建后端的功能。请求不受支持的后端或错过上下文中的必需变量将是Err
。应鼓励消费者使用Default
变体,该变体应在任何平台上具有有效的后端:
pub fn create_backend(backend: BackendType, ctx: &BackendContext) -> Result<Box<Backend>, BackendError> {
match backend {
#[cfg(platform1)]
#[cfg(platform2)]
BackendType::Default => Backend2::new(ctx),
#[cfg(platform1)]
BackendType::Backend1 => Backend1::new(ctx),
#[cfg(platform1)]
#[cfg(platform2)]
BackendType::Backend2 => Backend2::new(ctx),
_ => Err(BackendError("Backend not available".to_string()))
}
}
答案 1 :(得分:0)
您可以使用一个返回可用后端列表的函数。
然后,当应用程序想要使用您的库时,它可以从其build.rs
调用该函数,选择一个可用的后端,并将其作为选项传递给编译器。