前段时间看了vector
(https://github.com/timberio/vector)的实现,其中的多组件通过配置文件来启用的实现方式非常有意思,主要用到了 inventory
和typetag
两个crate组合来实现。
typetag
先来看看typetag
,他的描述是Serde serializable and deserializable trait objects.也就上对 trait objects 的序列化和反序列化支持。利用此功能,可以非常轻松的实现将一个 trait object 通过 serde
框架序列化,传输之后再反序列化,听起来可以实现 RPC
,事实上也的确可以。但本文不讨论 RPC
,只讨论主题即静态注册工厂。
为了达到目的,需要首先定义一些 trait 来配合。假设需要实现一个过滤器模型,有多种过滤器的实现,每个过滤器由不同的参数初始化。
首先定义一个 trait
trait SomeFilter {
fn filter(&mut self, input: &()) -> bool;
}
非常的简洁,它只拥有一个成员 filter
,接受参数不重要,返回值象征性的用 bool
来表示。
tips
SomeFilter
只做演示作用,实际应用中可能更复杂, 甚至可以是 #[async_trait]
。
有了它还不够,还需要一个 SomeFilter
的 Builder
,于是再加一个新的 trait
trait SomeFilterConfig {
fn build(&self) -> Result<Box<dyn SomeFilter>, ()>;
}
同样的这个 trait
也非常简洁,只需要一个 build
方法即可,返回的当然应该是 trait object,所以用Box<dyn SomeFilter>
。
当完成这两个 trait
定义后,根据 typetag
的指导,要想完成 trait object 的序列化,需要给对应的 trait
加上一个属性宏,来完成一些魔法。于是SomeFilterConfig
就变成了
#[typetag::serde(tag = "type")]
trait SomeFilterConfig {
fn build(&self) -> Result<Box<dyn SomeFilter>, ()>;
}
如果你熟悉 serde
的话,那么 tag = "type"
的作用可以猜到。由于是 trait object,当进行反序列化的时候,必须知道原来的类型是什么,于是 tag
就是在序列化后的例如 json
格式当中,以 "type": "Foo"
的形式存在的键名。
tips
tag
不需要一定是 "type"
,他和 serde
在处理枚举时的作用一致,可以根据自己喜好设置。
当准备完成后,接下来就是实现我们的第一个过滤器的时候了。
struct FooFilter {
foo: u32,
bar: String
}
impl SomeFilter for FooFilter {
fn filter(&mut self, _input: &()) -> bool {
println!("foo filter!");
true
}
}
实现就和平常一样,没有特殊魔法的存在。
以及它对应的 Builder
的实现
#[derive(Serialize, Deserialize)]
struct FooFilterConfig {
foo: u32,
bar: String
}
#[typetag::serde(name = "foo")]
impl SomeFilterConfig for FooFilterConfig {
fn build(&self) -> Result<Box<dyn SomeFilter>, ()> {
Ok(Box::new(FooFilter {
foo: self.foo,
bar: self.bar.clone(),
}))
}
}
注意到 #[typetag::serde(name = "foo")]
了吗,这与之前的不同,它不需要指定 tag
,甚至这个 name
都是可选的,之所以这里加上,只是为了方便使用。
同时由于我们需要反序列化的 trait object 并不是直接的过滤器实现,而是它对应的 Builder
,所以这里需要给 FooFilterConfig
来实现 Serialize
和Deserialize
。
到这一步,关于typetag
的设置就完成了,非常的简单。那么如何使用呢,来编写一个测试用例来试试
#[test]
fn test_foo_filter() {
let c = r#"
{
"type": "foo",
"foo": 1,
"bar": "bar"
}
"#;
let c: Box<dyn SomeFilterConfig> = serde_json::from_str(c).unwrap();
let mut filter = c.build().unwrap();
filter.filter(&());
}
对于更多的实例,也可以用同样的方法添加 BarFilter
和 BarFilterConfig
。
inventory
前面的 typetag
解决的只是 trait object 的问题,而真正的静态注册,则是靠 inventory
来实现的。inventory
利用了全局构造器来完成注册的功能。
接上面的例子,对于注册这个动作,可以定义一个新的结构体,储存已注册好的组件的元信息
struct FilterSubscription {
name: &'static str
}
impl FilterSubscription {
pub fn new(name: &'static str) -> FilterSubscription {
FilterSubscription {
name
}
}
}
新的 FilterSubscription
只是简单的储存了一个 name
,用来表示组件的名字,也可以根据实际需要包含更多的属性。
要注册就非常简单了,只需要在任意的地方调用submit!
宏
struct FooFilter {
foo: u32,
bar: String
}
inventory::submit! {
FilterSubscription::new("foo")
}
然后还需要同样在任意地方调用 collect!
宏,来收集之前所有 submit!
的结果
inventory::collect!(FilterSubscription);
同样来写一个测试用例
#[test]
fn test_inventory() {
for (idx, sub) in inventory::iter::<FilterSubscription>.into_iter().enumerate() {
println!("{} - {}", idx, sub.name)
}
}
我就勉为其难的放个图吧
组合
看到这里,应该已经猜到了具体的静态注册工厂应该如何实现,即组合 typetag
和 inventory
。
当然,对于如何使用这些静态注册的实例,不同场景也不一样。
对于开头提到的 vector
来说,他的每个组件都是由外部配置文件来决定的,并不是由程序运行时来启用或关闭。对于这种情况,inventory
起的作用比较有限,例如 vector
用它来实现类似于rustc --print target-list
的效果,即输出当前已经编译期启用的组件,并提供一个生成配置文件参考的作用。而真正实例化组件的时候,是不需要用 inventory
配合的。
这种方式借助了一个独立的 trait
pub trait GenerateConfig {
fn generate_config() -> toml::Value;
}
同时他的Subscription
负责调用 generate_config
来储存一份配置文件示例。
impl ComponentDescription
{
/// Creates a new component plugin description.
/// Configuration example is generated by the `GenerateConfig` trait.
pub fn new<B: GenerateConfig>(type_str: &'static str) -> Self {
ComponentDescription {
type_str,
example_value: || Some(B::generate_config()),
component_type: PhantomData,
}
}
}
当然为了简单,删除了原来代码中关于泛型参数的定义。
利用这种方式,就可以在inventory::iter
中获取注册的组件的元信息已经一份示例配置。
然而,并不是所有的场景都是外部控制的,如果控制权交给程序,例如所有组件由程序决定是否需要实例化,上述方式就不太适合了。
只需要进行一点点小修改,首先 FooFilterConfig
需要去掉内部属性,因为不再是由 typetag
来填充。
#[derive(Serialize, Deserialize)]
struct FooFilterConfig;
其次,SomeFilterConfig
trait 也许要修改,所需要的额外配置项将在 build
中传入
#[typetag::serde(tag = "type")]
trait SomeFilterConfig {
fn build(&self, param: Option<&dyn Any>) -> Result<Box<dyn SomeFilter>, ()>;
}
关于参数
这里用了 &dyn Any
来临时演示,实际当中可以是例如 serde_json::Value
等具体类型。
当然,相对应的 impl
块也需要改动
struct FooFilterParam {
foo: u32,
bar: String
}
#[typetag::serde(name = "foo")]
impl SomeFilterConfig for FooFilterConfig {
fn build(&self, param: Option<&dyn Any>) -> Result<Box<dyn SomeFilter>, ()> {
let default = FooFilterParam {
foo: 1,
bar: "bar".into(),
};
let param = param.and_then(|it| it.downcast_ref()).unwrap_or(&default);
Ok(Box::new(FooFilter {
foo: param.foo,
bar: param.bar.clone(),
}))
}
}
新的结构体
由于用了 &dyn Any
,这里额外引用了新的结构体,实际上这是不合理的,在后续的实例化中,是不知道具体需要什么类型的,但为了演示妥协一下。
同时 Subscription
则需要记录一些额外信息,例如它需要包含一个 Builder
的实例,方便在后续枚举过程中实例化各个组件。
struct FilterSubscription {
name: &'static str,
builder: Box<dyn SomeFilterConfig>
}
impl FilterSubscription {
pub fn new(name: &'static str, builder: Box<dyn SomeFilterConfig>) -> FilterSubscription {
FilterSubscription {
name,
builder
}
}
}
而当注册时,则需要提供一个 Builder
实例,就像这样
inventory::submit! {
FilterSubscription::new("foo", Box::new(FooFilterConfig))
}
经过这一番小改动之后,实现由程序控制的组件加载方式则变成了
#[test]
fn test_inventory() {
let mut modules = HashMap::new();
for (idx, sub) in inventory::iter::<FilterSubscription>.into_iter().enumerate() {
println!("creating module {} - {}", idx, sub.name);
let module: Box<dyn SomeFilter> = sub.builder.build(None).unwrap();
modules.insert(sub.name, module);
}
}
首先获取到所有已注册的 FilterSubscription
,从中取得相对应的 Builder
,然后锵锵锵,所有编译期注册的组件就可以很方便的在运行时初始化了。
结尾
上面的所有代码都为了演示而有不同程度的简化,实际上更复杂的模式也是可以实现的。