写给前端看的Rust教程(22)WebAssembly实战四

3,090 阅读2分钟

原文:24 days from node.js to Rust

前言

无法想象JavaScript没有JSON的样子,不过对于Rust这种类型化语言来说会有所区别,我们需要将JSON转化成JavaScript中的对象,转化成Rust中的结构体

正文

serde

serdeSerialization 和 Deserialization 的简写)是一个很神奇的包,仅需要一行代码就可以将你的数据结构转化成JSON格式。该包提供了SerializeDeserialize 两个trait,可以让你自己来定义如何将一种数据结构转化成JSON。它本身不做转换的实际工作,实际工作是其它包实现的。我们之前已经用到了一个类似的包rmp-serde,它会将数据编码成MessagePack格式。我们本身不需要对serde有过深的了解,因为这里用到的都是通用的数据结构,如果你想有点新创造,那你需要实现上述的trait

利用derive功能,serde使得数据转化变的十分简单,你可以通过SerializeDeserialize对大多数对象进行自动转化

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Author {
    first: String,
    last: String,
}

一旦你实现了上述trait中的一个或两个,你就拥有了对任意格式数据的自动支持

下面这段代码用serde_jsonrmp-serde将一种数据格式转换成JSONMessagePack格式

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Author {
    first: String,
    last: String,
}

fn main() {
    let mark_twain = Author {
        first: "Samuel".to_owned(),
        last: "Clemens".to_owned(),
    };

    let serialized_json = serde_json::to_string(&mark_twain).unwrap();
    println!("Serialized as JSON: {}", serialized_json);
    let serialized_mp = rmp_serde::to_vec(&mark_twain).unwrap();
    println!("Serialized as MessagePack: {:?}", serialized_mp);

    let deserialized_json: Author = serde_json::from_str(&serialized_json).unwrap();
    println!("Deserialized from JSON: {:?}", deserialized_json);
    let deserialized_mp: Author = rmp_serde::from_read_ref(&serialized_mp).unwrap();
    println!("Deserialized from MessagePack: {:?}", deserialized_mp);
}

输出:

$ cargo run -p day-22-serde
[snipped]
Serialized as JSON: {"first":"Samuel","last":"Clemens"}
Serialized as MessagePack: [146, 166, 83, 97, 109, 117, 101, 108, 167, 67, 108, 101, 109, 101, 110, 115]
Deserialized from JSON: Author { first: "Samuel", last: "Clemens" }
Deserialized from MessagePack: Author { first: "Samuel", last: "Clemens" }

注意:留意第20行和第22行中如何显式指定反序列化的类型。这是反序列化器知道要输出什么内容的唯一方法

拓展命令行程序

上一篇文章中我们用到的命令行对于输入的内容限制较大,只能输入字符串,今天我们对其进行一下拓展,让它可以接收JSON数据

Cargo.toml文件中添加serde_json,在这里我们不需要serde

crates/cli/Cargo.toml

[dependencies]
my-lib = { path = "../my-lib" }
log = "0.4"
env_logger = "0.9"
structopt = "0.3"
rmp-serde = "0.15"
anyhow = "1.0"
serde_json = "1.0"

JSON

当我们确定自己要表达的内容时,采用自定义结构体是很不错的选择,但并不是每次都知道的这么清楚,所以我们需要一个中介,进行一种更加泛化的表示。serde_json的内置JSON表示可以通过serde_json::Value枚举值得到,而不必创建一个有derive标记的自定义结构体。这样的好处是可以不必知晓JSON的具体结构,用一种更加通用的方式来进行表达

现在我们改写下命令行参数的配置,让其接收一个JSON文件的路径

crates/cli/src/main.rs

struct CliOptions {
    /// The WebAssembly file to load.
    #[structopt(parse(from_os_str))]
    pub(crate) file_path: PathBuf,

    /// The operation to invoke in the WASM file.
    #[structopt()]
    pub(crate) operation: String,

    /// The path to the JSON data to use as input.
    #[structopt(parse(from_os_str))]
    pub(crate) json_path: PathBuf,
}

现在我们得到了一个JSON文件路径,我们还需去读取它的内容。我们用fs::read方法将WASM文件以字节的方式读取,我们还可以用fs::read_to_string方法将文件以字符串的方式读取

crates/cli/src/main.rs

fn run(options: CliOptions) -> anyhow::Result<String> {
    // snipped

    let json = fs::read_to_string(options.json_path)?;

    // snipped
}

然后我们用serde_json::from_str方法将字符串解析为JSON

crates/cli/src/main.rs

fn run(options: CliOptions) -> anyhow::Result<String> {
    // snipped

    let json = fs::read_to_string(options.json_path)?;
    let data: serde_json::Value = serde_json::from_str(&json)?;
    debug!("Data: {:?}", data);

    // snipped
}

最后我们将返回值的数据类型改为serde_json::Value以便可以将结果以JSON形式来表达

crates/cli/src/main.rs

fn run(options: CliOptions) -> anyhow::Result<serde_json::Value> {
    let module = Module::from_file(&options.file_path)?;
    info!("Module loaded");

    let json = fs::read_to_string(options.json_path)?;
    let data: serde_json::Value = serde_json::from_str(&json)?;
    debug!("Data: {:?}", data);

    let bytes = rmp_serde::to_vec(&data)?;

    debug!("Running  {} with payload: {:?}", options.operation, bytes);
    let result = module.run(&options.operation, &bytes)?;
    let unpacked: serde_json::Value = rmp_serde::from_read_ref(&result)?;

    Ok(unpacked)
}

现在我们可以将输入存放在JSON文件中来运行我们的命令行程序了

cargo run -p cli -- crates/my-lib/tests/test.wasm hello hello.json
[snipped]
"Hello, Potter."

我们的命令行程序越来越实用了,现在可以尝试起个更有意义的名字,这里我们将其改名为wapc-runner

我们之前一直以debug形式来打包,现在用release模式运行下看看有什么不同

$ cargo build --release
[snipped]
    Finished release [optimized] target(s) in 6m 08s
$ cp ./target/release/wapc-runner .
$ ./wapc-runner ./blog.wasm render ./blog.json
"<html><head><title>The Adventures of Tom Sawyer</title></head><body><h1>The Adventures of Tom Sawyer</h1><h2>By Mark Twain</h2><p>“TOM!”\n\nNo answer.\n\n“TOM!”\n\nNo answer.\n\n“What’s gone with that boy,  I wonder? You TOM!”\n\nNo answer.</p></body></html>"

release模式所需的时间可能会更长,具体时间取决于你的机器

现在我们有了一个很棒的可移植WebAssembly运行器,如果你希望继续对其优化,这里有个建议:你可以判断在JSON文件参数缺失的情况下直接从STDIN读取JSON数据,atty包可以帮助到你判断是否在终端运行

相关阅读

总结

本教程已经接近尾声,后面我们将会进行一些总结,包括介绍一些有用的包