Rust 静态注册组件的实现

前段时间看了vector(https://github.com/timberio/vector)的实现,其中的多组件通过配置文件来启用的实现方式非常有意思,主要用到了 inventorytypetag两个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]

有了它还不够,还需要一个 SomeFilterBuilder,于是再加一个新的 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 来实现 SerializeDeserialize

到这一步,关于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(&());
}

对于更多的实例,也可以用同样的方法添加 BarFilterBarFilterConfig

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)
    }
}

我就勉为其难的放个图吧

组合

看到这里,应该已经猜到了具体的静态注册工厂应该如何实现,即组合 typetaginventory

当然,对于如何使用这些静态注册的实例,不同场景也不一样。

对于开头提到的 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,然后锵锵锵,所有编译期注册的组件就可以很方便的在运行时初始化了。

结尾

上面的所有代码都为了演示而有不同程度的简化,实际上更复杂的模式也是可以实现的。

发表评论

邮箱地址不会被公开。 必填项已用*标注