前段时间看了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,然后锵锵锵,所有编译期注册的组件就可以很方便的在运行时初始化了。
结尾
上面的所有代码都为了演示而有不同程度的简化,实际上更复杂的模式也是可以实现的。