受 Python 启发的 Rust API,由 Serde 驱动

May 7, 2025

多年前,我致力于用 Rust 重新实现一些 Python 代码 (又一次),这一次我需要将 Python 的动态能力 (也就是 __getattr__) 应用于 Rust 严格且编译后的世界。 经过一番深思熟虑,并配备了 serde (反)序列化 crate,我着手做了这件事。 因此,如果你想知道 Rust 可以从 Python 学习哪些技巧,以及 serde 是否可以用于反射(请跟着我一起探索),那么答案是肯定的!让我告诉你如何做,以及为什么。 注意:我将尽量使这篇文章对只有 Rust 经验的 Python 程序员来说是可以理解的,这将会很有趣。 🦀🐍 Rick and Morty - Let's go. In and out. 20 minutes adventure, with complex Rust code with many types in the wormhole

Python 的启发

在 Windows 上使用 Python 时,有一个神奇的包,可以用来获取关于系统的各种信息。 无需过多介绍,它归结为以下几点:假设你想列出所有已安装的 物理风扇。你可以使用以下代码来实现:

import wmi
c = wmi.WMI()
for fan in c.Win32_Fan():
  if fan.ActiveCooling:
    print(f"Fan `{fan.Name}` is running at {fan.DesiredSpeed} RPM")

这使用了一些巧妙的 __getattr__ 实现,最终在后台运行了以下等效代码:

for fan in c.query("SELECT * FROM Win32_Fan"):
  if fan.wmi_property("ActiveCooling").value is True:
    print(f"Fan `{fan.wmi_property('Name').value}` is running at {fan.wmi_property('DesiredSpeed').value} RPM")

( wmi_property 方法返回另一个对象,该对象保存最终值及其类型。) 暂且不谈 WMI 到底是什么,这正是 Python 非常擅长的事情:一个清晰、简洁且直观的接口,抽象了复杂且令人头疼的实现。 在 Python 中,你只需要覆盖一些魔法方法即可实现这一点,但 Rust 完全是另一回事。 如果我们要创建一个 Rust crate,为用户提供一个类似于上面的不错的 API,我们该怎么做?

我们正在抽象什么

首先用纯 Rust 表达 "原始" API 会有所帮助。 它是原始的,因为我们可以用它从系统中获取 原始数据 ,但仅此而已。 我们有两种类型: Value 类型,可以保存不同类型的值,以及 Object 类型,可以提供对命名属性的访问 (这些属性本身就是 Value 实例)。 我们最终得到类似这样的东西 (完整的代码可以在 GitHub 上找到):

mod raw_api {
  pub struct Object { .. }
  pub enum Value {
    Bool(bool),
    I1(i8),
    // ..
    UI8(u64),
    String(String),
    Object(Object),
  }
  impl Object {
    pub fn get_attr(&self, name: &str) -> Value { .. }
  }
  pub fn query(query: &str) -> Vec<Object> { .. }
}

很简单,但用户使用起来相当痛苦:

let res = raw_api::query("SELECT * FROM Win32_Fan");
for obj in res {
  if obj.get_attr("ActiveCooling") == Value::Bool(true) {
    if let Value::String(name) = obj.get_attr("Name") {
      if let Value::UI8(speed) = obj.get_attr("DesiredSpeed") {
        println!("Fan `{name}` is running at {speed} RPM");
      }
    }
  }
}

由于任何字段都可以是任何类型,因此用户必须手动检查 (可以使用 match ,也可以像我们一样使用 if let ) 他们每次与之交互时得到的 Value 枚举的变体。当想要查询许多不同类型的对象 ( Win32_Battery 🔋, Win32_UserAccount 💁, Win32_Printer 🖨️,…) 时,这尤其麻烦。 Gordon Ramsay yelling IT'S RAW 注意:这是我们必须使用的 底层 API 的简化,但它足够接近,我们可以基于它设计我们的更高级别的 API。 你可以查看 wmi-rs crate 源代码以获取完整详细信息。

一种可能的设计

受到 Pythonic API 的启发,如果我们能做这样的事情会怎样:

// 1. 用户为要查询的对象类型定义一个自定义结构体。
struct Fan {
  name: String,
  active_cooling: bool,
  desired_speed: u64,
}
// 2. 指定 `query` 应该返回 `Fan` 的实例。
let res: Vec<Fan> = api::query();
// 3. Profit.
for fan in res {
  if fan.active_cooling {
    println!("Fan `{}` is running at {} RPM", fan.name, fan.desired_speed);
  }
}

好多了! Rust 的一个显着特点是 泛型返回类型 可以更改函数的行为 (其中 .collect() 是最著名的例子),那么我们如何实现类似的东西? 想象一下,我们继续更新 query 以接受一些泛型类型 T 。 我们可以使用什么 trait 来约束 T ,以便我们可以实现该函数?

fn query<T>() -> Vec<T> where T: ??? { ??? }

标准库提供 any::type_name ,它可以 (实际上并没有) 帮助我们构建 SELECT 查询,但在不诉诸 暴力 的情况下,我们似乎只能靠自己了。

走路之前先爬

让我们从一个相对简单的解决方案开始:让我们 定义一个新的 trait ,该 trait:

  1. 提供要查询的对象的名称。
  2. 处理从 Object 构建例如 Fan

这将为我们提供一个坚实的基础,然后我们才能 深入研究

trait Queryable {
  fn object_name() -> &'static str;
  fn from(obj: Object) -> Self;
}
fn query<T: Queryable>() -> Vec<T> {
  let name = T::object_name();
  let mut res = vec![];
  for obj in raw_api::query(&format!("SELECT * FROM Win32_{name}")) {
    res.push(T::from(obj))
  }
  res
}

一旦用户为 Fan 实现 2 这个新 trait:

impl Queryable for Fan {
  fn object_name() -> &'static str {
    "Fan"
  }
  fn from(obj: Object) -> Self {
    let name = if let Value::String(name) = obj.get_attr("Name") {
      name
    } else {
      panic!()
    };
    // .. repeat for the other fields ..
    Fan {
      name,
      active_cooling,
      desired_speed,
    }
  }
}

然后他们可以通过指定返回类型来使用我们改进的 query 函数:

let res: Vec<Fan> = api::query();

与我们之前使用的 raw_api:query 版本相比,这是一个实质性的改进。 虽然从库的角度来看更容易做到这一点,但这种方法迫使用户为他们想要使用的每种新类型手动实现 Queryable trait,即使没有正确的错误处理或对嵌套对象的支持,这也是冗长且容易出错的,并且不是真正的人体工程学。 我们可以使用 dtolnay 的 指南并学习编写过程宏,创建一个宏来自动生成此实现 3 并将其公开给用户,但是......我们可以使用 dtolnay 的 serde ,它基本上已经完成了所有这些!

Serde 来救援

Serde 是一个用于在 Rust 中序列化和反序列化数据的 框架 ,这意味着它定义了 trait ( SerializeDeserialize ),以及使用 derive 在编译时生成这些 trait 实现的能力。 正是这种生成 Deserialize 实现并使用它来创建类型实例的能力对我们很有用。 我应该先说一下,虽然我们将学到很多关于 Serde 内部结构的知识 (公平警告,即使像我们将要做的那样专注于特定部分,这也是一个复杂的主题),但我并不是试图声称这是 Serde 最 常规的 用例 (有关更多信息,请参阅 替代方案 部分)。 我确实认为结果非常棒。 要使用 Serde,我们通常需要一个额外的库,该库利用这些 trait 与不同的数据格式进行交互。 例如,对 Fan 使用 serde_json 看起来像这样:

use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Fan {
  name: String,
  active_cooling: bool,
  desired_speed: u64,
}
let fan: Fan = serde_json::from_str(r#"{ "Name": "CPU1", "ActiveCooling": true, "DesiredSpeed": 1100 }"#)?;
println!("Fan `{}` is running at {} RPM", fan.name, fan.desired_speed);

注意:已经,使用 Serde 开始获得回报: rename_all = "PascalCase" 正是我们需要的,以使我们的字段名称保持 snake_case 由于 Serde 能够从不同的 "数据格式" (JSON,YAML,Postcard,…) 创建用户的 struct ( Fan ), 我们 可以假装是另一种数据格式,并搭上 derive(Deserialize) 提供的编译时生成的 Deserialize impl 的顺风车。 如果我们能做到这一点,我们将能够创建一个接受任何实现 Deserialize 的 struct 的 query 函数,这对我们的用户来说会很方便:他们不仅可以简单地将 derive(Deserialize) 添加到他们的类型中,还可以使用不同的 Serde 配置 (如 rename_all 选项) 和 Serde 周围丰富的生态系统 (如 serde_with crate)。 最重要的是,我们希望从我们的自定义 Queryable trait 切换到 Serde 的 Deserialize trait,它看起来有点像这样:

-fn query<T: Queryable>() -> Vec<T> { .. }
+fn query<T: Deserialize>() -> Vec<T> { .. }

现在,说起来 容易 ,做起来 ,所以现在可能是喝杯新鲜的茶或咖啡的好时机:我们还有很长的路要走 (有很多 很多 trait)。 注意:如果您不太熟悉 Rust,请紧紧抓住您的空格:这是深入研究的更深层次的开始,但您可以随意直接转到 摘要

窥视幕后

我们知道我们想在我们的 query 函数中使用 Deserialize trait,但因为它几乎从未直接实现或使用过,所以我们需要更多地了解它才能做到这一点。 因此,为了更好地了解上面的代码是如何工作的,让我们尝试用 Fan struct 的手动 (且不完整的)4 Deserialize 实现来替换 derive(Deserialize) 。 我们将在多个阶段进行此操作,但这归结为 (1) 定义新的实用程序 struct ,然后 (2) 为它们实现一些 trait。 我们将从查看 Deserialize trait 的定义开始,并为 Fan 添加一个 impl

// `Deserialize` trait:
pub trait Deserialize<'de> {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where D: Deserializer<'de>;
}
// 我们的实现:
impl<'de> serde::Deserialize<'de> for Fan { .. }

您可以安全地忽略我们用例的 'de lifetime (因为我们不会保留从 Deserializer 借用的任何数据),并将实现定义读取为:

对于 任何 lifetime de,这是 Fanserde::Deserialize trait (具有该 lifetime) 的实现。5 我们唯一需要实现的方法是 fn deserialize ,它接受一个 Deserializer。 同样, 您可以安全地忽略 de lifetime ,但您可以将 D: serde::Deserializer<'de> 读取为: 泛型类型 D 实现了 serde::Deserializer trait,并且它可能产生的任何 借用 数据都将具有 de lifetime。

fn deserialize<D>(
  deserializer: D,
) -> Result<Fan, D::Error>
where
  D: serde::Deserializer<'de> { .. }

出乎意料的是,大多数有趣的代码根本不驻留在 Deserialize impl 中 6

impl<'de> serde::Deserialize<'de> for Fan {
  fn deserialize<D>(
    deserializer: D,
  ) -> Result<Self, D::Error>
  where
    D: serde::Deserializer<'de>,
  {
    const FIELDS: &'static [&'static str] = &[
      "Name",
      "ActiveCooling",
      "DesiredSpeed",
    ];
    deserializer.deserialize_struct(
      "Fan",
      FIELDS,
      // 新类型!
      FanVisitor {},
    )
  }
}

让我们稍微分解一下。 deserializer 参数 (例如,可以是 serde_json::Deserializer ) 是知道如何从 "数据格式" (在本例中为 JSON 格式的字符串) 访问我们需要的数据的部分。 Deserializer trait 有一个舒适的 32 个方法,但我们只使用一个: deserialize_struct

trait Deserializer<'de> {
  type Error: Error;
  fn deserialize_struct<V>(
    self,
    name: &'static str,
    fields: &'static [&'static str],
    visitor: V,
  ) -> Result<V::Value, Self::Error>
    where V: Visitor<'de>;
}

关于此函数最有趣的是 Visitor 参数。 考虑一下 Deserializer 的观点,因为它处理 { .., "DesiredSpeed": 1100 }。 实际要解决的问题是什么? FanVisitor 正在构造一个 Fan ,但它可以构建一个 HashMap<String, i32> ,或者任意数量的不同事物。 反序列化器所知道的只是它当前正在查看一个映射,并且当前键是一个字符串,而值是一个数字。 它需要一种方式来告诉我们这一点,这就是 Visitor 的用武之地:通过定义我们自己的 FanVisitor ,我们将让反序列化器通过在其上调用不同的方法来 "驱动" 它,以便它看到的数据, FanVisitor 将决定如何处理该数据。 所以我们的工作是给反序列化器一个访问者,它可以用来将数据 返回给我们 。 这起初有点令人困惑,但看到它在起作用将有助于澄清问题。 从技术上讲,我们无法 确切地 知道不同的反序列化器将如何为不同的输入实现 deserialize_struct 函数,但 serde_json 反序列化器 调用 visitor.visit_map 用于上面的示例输入,所以这是我们需要实现的。

// 请记住: `derive(Deserialize)` 会根据 `Fan` struct 定义在编译时自动生成此代码
// (包括新 struct 和 impl)。
// 一个没有字段的 struct,只需要我们可以将 impl 附加到某些东西上。
struct FanVisitor;
impl<'de> serde::de::Visitor<'de> for FanVisitor {
  // 访问者指定它将要生成什么类型
  // (如 `visit_map` 的返回类型所示)。
  type Value = Fan;
  // `map` 是来自反序列化器的数据的来源。
  fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
  where
    A: serde::de::MapAccess<'de>,
  {
    let (mut name, mut active_cooling, mut desired_speed) = (None, None, None);
    // 使用我们从反序列化器获得的 `map` 。
    // `key` 的类型为 `&str` ,这意味着我们调用 `map.next_key::<&str>()` 。
    while let Ok(Some(key)) = map.next_key() {
      // 但是, `next_value()` 为不同的字段返回不同的类型。
      // 我们稍后将在实现我们自己的 `Deserializer` 和 `MapAccess` 时解释如何实现。
      match key {
        "Name" => {
          let val: String = map.next_value()?;
          name = Some(val);
        }
        "ActiveCooling" => {
          let val: bool = map.next_value()?;
          active_cooling = Some(val);
        }
        // ..
      }
    }
    Ok(Fan { .. })
  }
}

我们将在下一节中更深入地研究 MapAccess trait,但现在请注意,我们使用它来获取键和值,并且我们 ( Visitor ) 确定它们应该是什么类型 (或者更确切地说,它们应该 反序列化 成什么类型)。 还有各种其他的 visit_{u64,string,bool,...} 函数,但默认情况下它们只是返回一个错误,所以我们可以仅为此最小示例实现 visit_map 。 完成此操作后,之前的相同代码现在可以在没有 derive(Deserialize) 的情况下工作:

pub struct Fan { .. }
impl<'de> serde::Deserialize<'de> for Fan { .. }
let fan: Fan = serde_json::from_str(r#"{ "Name": "CPU1", "ActiveCooling": true, "DesiredSpeed": 1100 }"#)?;
println!("Manual Serde impl: Fan `{}` is running at {} RPM", fan.name, fan.desired_speed);

因此,让我们回顾一下我们对 Serde 的 trait 之家的了解:

  1. Deserialize::deserialize 接受一个 Deserializer ,并调用 (对于像 Fan 这样的 struct ) deserializer.deserialize_struct 函数,其中包含一个 Visitor (如 FanVisitor ),它处理 struct 的创建。
  2. Deserializer (在本例中为 serde_json::Deserializer ) 调用一个 visitor.visit_ 函数 (在本例中为 visit_map ),该函数提供来自 Deserializer 的数据 (在本例中,是一个实现了 MapAccessmap )。
  3. Visitor 现在调用 mapnext_{key,value} 函数,该函数返回构建 struct 所需的数据。
  4. 一旦 map 中没有 next_keyVisitor 完成并返回创建的值 (在本例中为 Fan 的实例)。

或者,更直观地说:

let fan = Fan::Deserialize(serde_json::Deserializer::from_str(r#"{ "Name": "CPU1", "Active.."#));
impl Deserialize for Fan              impl Deserializer for serde_json::Deserializer
┌ fn deserialize(deser)               │
│  let visitor = FanVisitor {}           │
│  deser.deserialize_struct(.., visitor) ─calls──► ├ fn deserialize_struct(.., visitor)
│                          │  let map = serde_json::de::MapAccess::new(..)
│  impl Visitor for FanVisitor           │
│  ┌ fn visit_map(map) ◄────────calls───────────────── return visitor.visit_map(map)
│  │  loop {                     impl MapAccess for serde_json::de::MapAccess
│  │   key = map.next_key() ──────calls────────────► ┌ fn next_key()  // { ..,▼"Name": ..
│  │   /* since key is "Name" */           │
│  │   name: String = map.next_value() ───calls────► └ fn next_value() // {     ..:▼"CPU1", ..
│  │  }
◄────── return Fan { ... }

嗯,这里的 "更多" 是一个关键词。 Charlie Conspiracy (always Sunny In Philadelphia) 现在我们对流程有了基本的了解,让我们把手动 Deserialize impl 和它的 FanVisitor 抛在脑后,专注于我们的实际目标:一个更好的 query API 。

-pub struct Fan { .. }
-impl<'de> serde::Deserialize<'de> for Fan { .. }
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+pub struct Fan { .. }

实现反序列化器

我们选择使用 Deserialize 的原因是 因为 Serde 可以为用户的类型 derive 它。 因此,我们的工作是以以下方式实现 query 函数:

  1. 接受实现 DeserializeT
  2. 通过调用 它们Deserialize::deserialize 函数从 raw_api::Object 创建 T

既然我们现在对 Deserialize::deserialize 函数的作用了解得更多了,让我们继续开始更新 query 以使用 Deserialize ,并进行一些小的调整:

pub fn query<T: DeserializeOwned>() -> Vec<T> {
  // 我们稍后会修复这个。
  let name = "Fan";
  let mut res = vec![];
  for obj in raw_api::query(&format!("SELECT * FROM Win32_{name}")) {
    // 新类型!
    let deser = ObjectDeserializer { obj };
    res.push(T::deserialize(deser).unwrap())
  }
  res
}

我们没有使用 Deserialize<'de> ,而是使用了 DeserializeOwned ,它类似但禁止用户使用借用数据的类型 (如 struct BorrowedFan<'a> { name: &'a str } )。7 它与我们的 Queryable trait 版本 之间的主要区别在于,Serde 引入了一个新的中间 struct 和 trait ( T::deserialize(ObjectDeserializer { obj }) vs T::from(obj) ):添加的间接级别使我们能够将 Deserializer 与用户的 T 解耦。 Serde 的文档描述了 实现反序列化器 如下:

反序列化器负责通过调用它收到的 Visitor 上的恰好一个方法将输入数据映射到 Serde 的数据模型中。 Deserializer 方法由 Deserialize impl 调用,作为提示,指示 Deserialize 类型期望在输入中看到什么 Serde 数据模型类型。 类似于我们的 自定义实现derive(Deserialize) 版本也将调用 deserialize_struct ,因此我们需要实现它:

struct ObjectDeserializer {
  obj: raw_api::Object,
}
impl<'de> Deserializer<'de> for ObjectDeserializer {
  // .. snip ..
  // 有各种各样的 `deserialize_{bool,str,enum,...}` ,
  // 但我们现在可以忽略它们,因为我们只想支持 `struct` 。
  // 我们的实现,将由 `T::Deserialize` 调用:
  fn deserialize_struct<V>(
    self,
    name: &'static str,
    fields: &'static [&'static str],
    visitor: V,
  ) -> Result<V::Value, Self::Error>
  where
    V: serde::de::Visitor<'de>,
  {
    todo!()
  }
}

鉴于我们迄今为止所看到的一切,我们的 deserialize_struct 方法将需要:

  1. 定义一个实现 serde::de::MapAccess 的新 struct 。
  2. 使用该 struct 调用 visitor.visit_map ,该 struct 将需要在每次调用 next_keynext_value 时返回正确的 keyvalue

实现 MapAccess trait 只需要两个函数: next_key_seednext_value_seed 。 它们与我们之前调用的 next_{key,value} 非常相似,但 DeserializeSeed 提供了一些额外的灵活性,我稍后会介绍它。

trait MapAccess<'de> {
  type Error: Error;
  fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
    where K: DeserializeSeed<'de>;
  fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
    where V: DeserializeSeed<'de>;
  // ..
}

因此,我们需要在 next_key_seed 中将每个字段作为键返回,然后在随后的 next_value_seed 调用中返回字段的值。 我们可以定义一个名为 ObjectMapAccess 的新 struct,并像这样使用它:

struct ObjectMapAccess {
  // 没什么特别的,这是我们通过调用 `fields.iter().peekable()` 获得的类型。
  fields: Peekable<Iter<'static, &'static str>>,
  obj: raw_api::Object,
}
// 在 deserialize_struct 的主体中:
let map = ObjectMapAccess {
  fields: fields.iter().peekable(),
  obj: self.obj,
};
visitor.visit_map(map)

ObjectMapAccessMapAccess 实现中的基本逻辑是在 next_key_seedpeek() 来自 fields 的下一个字段,然后在 next_value_seed 中通过 next() 再次获取它,并调用 obj.get_attr 。 但是,如果我们这样做并查看这两个函数的签名,我们将看到我们尚未完成:

impl<'de> serde::de::MapAccess<'de> for ObjectMapAccess {
  fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
  where
    K: serde::de::DeserializeSeed<'de>,
  {
    if let Some(field) = self.fields.peek() {
      // 嗯。
    } else {
      Ok(None)
    }
  }
  fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
  where
    V: serde::de::DeserializeSeed<'de>,
  {
    let current_field = self.fields.next().unwrap();
    let field_value = self.obj.get_attr(current_field);
    // 嗯。
  }
}

坏种子

问题是,如果我们查看这两个函数的返回类型,我们需要返回 K::ValueV::Value ,但我们只知道 KV 实现了 DeserializeSeed ,与 Deserialize 相比,它看起来像这样:

trait Deserialize<'de> {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where D: Deserializer<'de>;
}
trait DeserializeSeed<'de> {
  // 新的。
  type Value;
  // 返回类型使用 `Value` 。
  fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where D: Deserializer<'de>;
}

不要太担心,但粗略地说,这意味着 DeserializeSeed 值可以具有一些状态 (与不能的常规 Deserialize 相比)。 例如,当访问者调用 let name: String = map.next_value().unwrap() 时,类型替换会导致:

fn next_value_seed(
  &mut self,
  seed: PhantomData<String>,
) -> Result<String, Self::Error> { ... }

调用 seed.deserialize(..) 等效于 调用 String::deserialize(..)注意: PhantomData 是 Rust 的说法,表示 "我关心某物的类型,但没有该类型的实际值" 。 解决了这个问题,我们现在 终于 可以完成 trait 难题了!

一直都是 Deserializer

我们唯一能做的是在 Deserializer 上调用 seed.deserialize ,因为它是 Deserialize/DeserializeSeed trait 提供的唯一函数。 对于字段名称,这很容易:我们知道我们拥有的字段名称是 &str ,并且 Serde 提供了一个 StrDeserializer ,(顾名思义) 实现 Deserializer trait,并且可以从 &str 创建。 使用与之前相同的可视化,如果 调用 Visitor 期望一个 String 键 (例如在 HashMap<String, _> 中),我们将得到类似这样的东西:

// 与 `next_key_seed` 相同,在对 `String` 进行类型替换之后:
// `StrDeserializer` 定义为 `struct StrDeserializer { value: &str, .. }` 。
fn next_key() -> Result<String, _> { String::deserialize(StrDeserializer::new("Name")) }
let key: String = next_key();