使用serde_json在编译时反序列化文件

13

在我的程序开头,我从一个文件中读取数据:

let file = std::fs::File::open("data/games.json").unwrap();
let data: Games = serde_json::from_reader(file).unwrap();

出于以下原因,我想知道如何在编译时完成此操作:

  1. 性能:无需在运行时进行反序列化
  2. 可移植性:程序可以在任何机器上运行,而无需随其一起使用包含数据的json文件。

还可能有用的是,数据只能读取,这意味着解决方案可以将其存储为静态数据。


1- 我认为反序列化性能不会成为问题(对于大数据集,可以使用更快的序列化方法,如bincode) 2- 为了提高可移植性,请使用include_bytes,然后您可以反序列化所需的字节(避免使用json,这会增加二进制大小而没有任何实际用处)。 - Asya Corbeau
@AsyaCorbeau 有没有使用 include_bytes! 而不是 include_str! 的原因? - Nils André
@NilsAndré 没有,因为JSON意味着有效的utf8,这是在Rust中将字节数组与字符串分开的唯一要求。 - Sébastien Renauld
确实,但是使用二进制编码后,可执行文件的大小会减小(没有任何压缩开销),当JSON不被人类阅读或没有编码限制时,它是无用的,并且存在开销:反序列化JSON需要比优化的二进制编码更多的工作,选择性地,这可以避免所有JSON限制,但代价是互操作性(您可以通过在编译时将JSON作为资产进行转码来恢复它们)。 - Asya Corbeau
2个回答

7

这很简单,但存在一些潜在问题。首先,我们要处理一些事情:我们想要从文件中加载对象树还是在运行时解析它?

99%的时间,将其解析为静态引用(static ref)足以满足大多数人的需求,所以我会给你提供这个解决方案;我会在最后指向另一个版本,但它需要更多的工作并且是面向特定领域的。

你要找的宏(因为必须用宏)可以在标准库中找到:std::include_str!。顾名思义,它会在编译时处理你的文件,然后生成一个&'static str让你使用。然后你就可以自由地做任何喜欢的事情(比如解析它)。

从那里开始,接下来只需要使用lazy_static!就可以为程序的每个部分生成一个指向我们的JSON Value(或你决定使用的其他内容)的静态引用(static ref)。在你的例子中,它可能是这样的:

const GAME_JSON: &str = include_str!("my/file.json");

#[derive(Serialize, Deserialize, Debug)]
struct Game {
    name: String,
}

lazy_static! {
    static ref GAMES: Vec<Game> = serde_json::from_str(&GAME_JSON).unwrap();
}

在进行此操作时,您需要注意以下两点:
  1. 这将极大地增加文件的大小,因为 &str 没有以任何方式进行压缩。考虑使用 gzip 进行压缩。
  2. 您需要担心多个线程同时访问同一个 static ref 的常规问题,但由于它不可变,因此您只需要担心其中的一部分。

另一种方法是使用过程宏在编译时动态生成对象。如上所述,除非您在解析 JSON 时有非常昂贵的启动成本(对于大多数人来说并非如此),否则我不建议使用这种方法;最后一次我需要这样做是处理深度嵌套的多 GB 的 JSON 文件。

您需要查看的 crate 是 proc_macro2syn 用于代码生成;其余部分与编写普通方法的方式非常相似。


对于编译时生成对象,我应该使用 serde 还是自己编写解析器? - Nils André
@NilsAndré 那就看你自己的选择了。上一次我需要做这个时,我有一个优势,可以将结构转换为占用更少空间且杀死中间解析步骤的东西,我使用了 protocol buffers(与 serde 结合使用)以及一个预处理步骤作为 proc 宏。除非你绝对必须这样做,否则我不建议采用这种方法。 - Sébastien Renauld
@NilsAndré 重申一下:虽然在应用程序启动时不需要进行JSON解析步骤可以带来一些好处,但是自定义解析步骤会让你损失巨大,因为数据将被某些东西解析,以便你能够使用它。收益微小,头痛的问题却很大。 - Sébastien Renauld
一个构建脚本可以实现与您的过程宏相同的功能,并且更简单。 - Shepmaster

4
当您在运行时反序列化某些内容时,实际上是从磁盘上的另一个表示构建程序内存中的某些表示。但是在编译时,还没有“程序内存”的概念 - 这些数据将反序列化到哪里呢?
然而,事实上,您要尝试实现的目标是可能的。主要思想如下:要在程序内存中创建某些东西,您必须编写一些将创建数据的代码。如果您能够根据序列化数据自动生成代码,那么怎么样?这就是 uneval crate 所做的(免责声明:我是作者,因此鼓励您查看源代码以查看是否可以做得更好)。
要使用此方法,您将需要创建具有以下大致内容的 build.rs
// somehow include the Games struct with its Serialize and Deserialize implementations
fn main() {
    let games: Games = serde_json::from_str(include_str!("data/games.json")).unwrap();
    uneval::to_out_dir(games, "games.rs");
}

在您的初始化代码中,您将拥有以下内容:

let data: Games = include!(concat!(env!("OUT_DIR"), "/games.rs"));

请注意,以符合人体工程学的方式进行此操作可能相当困难,因为必要的结构定义现在必须在build.rs和创建本身之间共享,正如我在评论中提到的那样。如果您将创建拆分成两个部分,则可能会更容易,将结构定义(仅限它们)保留在一个创建中,并将使用它们的逻辑保留在另一个创建中。还有其他一些方法-使用include!技巧,或者利用构建脚本是普通Rust二进制文件并且也可以包含其他模块的事实-但这将使问题更加复杂。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接