如何将选项传递给 Rust 的 serde,以便可以在 Deserialize::deserialize() 中访问?

2024-03-16

对于上下文:我正在用 Rust 编写光线追踪器,但我正在努力寻找一种以与文件系统无关的方式加载场景的好方法。我在用着serde https://serde.rs/这样我就不必发明自己的文件格式(还)。资源(图像纹理和网格数据)单独存储到场景文件中。场景文件只存储这些文件的路径。因为光线追踪器本身应该是一个与平台无关的库(我希望能够将其编译为浏览器的 WebAssembly),所以光线追踪器本身不了解文件系统。我打算在反序列化场景时加载资产,但这现在给我带来了真正的问题:

我需要将文件系统接口代码的实现传递给 serde,我可以在其中使用Deserialize::deserialize()但似乎没有任何简单的方法可以做到这一点。我想出了一种使用泛型来实现这一点的方法,但我对此并不满意。

这是我目前正在做的方式,作为 MCVE 进行剥离(使用的包是serde and serde_json):

库代码(lib.rs):

use std::marker::PhantomData;
use serde::{Serialize, Serializer, Deserialize, Deserializer};

pub struct Image {}

pub struct Texture<L: AssetLoader> {
    path: String,
    image: Image,
    phantom: PhantomData<L>,
}

impl<L: AssetLoader> Serialize for Texture<L> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.path.serialize(serializer)
    }
}

impl<'de, L: AssetLoader> Deserialize<'de> for Texture<L> {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Texture<L>, D::Error> {
        let path = String::deserialize(deserializer)?;

        // This is where I'd much rather have an instance of AssetLoader
        let image = L::load_image(&path);

        Ok(Texture {
            path,
            image,
            phantom: PhantomData,
        })
    }
}

pub trait AssetLoader {
    fn load_image(path: &str) -> Image;
    // load_mesh(), load_hdr(), ...
}

#[derive(Serialize, Deserialize)]
pub struct Scene<L: AssetLoader> {
    textures: Vec<Texture<L>>,
    // meshes, materials, lights, ...
}

特定于平台的代码(main.rs):

use serde::{Serialize, Deserialize};
use assetloader_mcve::{AssetLoader, Image, Scene};

#[derive(Serialize, Deserialize)]
struct AssetLoaderImpl {}

impl AssetLoader for AssetLoaderImpl {
    fn load_image(path: &str) -> Image {
        println!("Loading image: {}", path);
        // Load the file from disk, the web, ...
        Image {}
    }
}

fn main() {
    let scene_str = r#"
    {
      "textures": [
        "texture1.jpg",
        "texture2.jpg"
      ]
    }
    "#;

    let scene: Scene<AssetLoaderImpl> = serde_json::from_str(scene_str).unwrap();

    // ...
}

我不喜欢这种方法的地方:

  • AssetLoaderImpl必须实施Serialize and Deserialize即使它从未(反)序列化
  • 我也在用typetag https://crates.io/crates/typetag这会导致编译错误,因为“尚不支持通用 impl 的反序列化”
  • 缓存资产将非常困难,因为我没有实例AssetLoaderImpl可以将它们缓存在成员变量中
  • 通过AssetLoader周围的类型参数变得难以使用Texture(或其他资产)嵌套得更深
  • 感觉不太对劲,主要是因为PhantomData以及仿制药的滥用

这让我觉得我没有以正确的方式解决这个问题,但我正在努力想出更好的解决方案。我考虑过在库中使用一个可变的全局变量来保存一个实例AssetLoader(也许与lazy_static)但这似乎也不正确。理想情况下我会传递一个实例AssetLoader (Box<dyn AssetLoader>可能)反序列化时到 serde 我可以在impl Deserialize for Texture。我还没有找到任何方法来做到这一点,如果有人能指出我正确的方向,我将非常感激。


为了将状态传递给反序列化,您应该使用DeserializeSeed https://docs.rs/serde/latest/serde/de/trait.DeserializeSeed.html特征。的文档DeserializeSeed解决这个用例:

DeserializeSeed是有状态的形式Deserialize特征。如果您发现自己正在寻找一种将数据传递到Deserialize暗示,这个特质就是做到这一点的方法。

有状态的AssetLoader

就像你说的,路过AssetLoader作为通用参数意味着您无法在其中存储缓存(或其他内容)。使用DeserializeSeed,我们可以传递我们的一个实例AssetLoader结构体,所以我们修改一下AssetLoader的函数来授予访问权限self:

pub trait AssetLoader {
    // Adding `&mut self` allows implementers to store data in a cache or 
    // whatever else they want to do.
    fn load_image(&mut self, path: &str) -> Image;
}

现在我们可以修改AssetLoaderImpl使用这个新定义:

struct AssetLoaderImpl {
    // cache, etc.
}

impl AssetLoader for AssetLoaderImpl {
    fn load_image(&mut self, path: &str) -> Image {
        // Access cache here.
        println!("Loading image: {}", path);
        Image {}
    }
}

反序列化AssetLoader

现在我们可以使用AssetLoader在反序列化期间使用DeserializeSeed特征。因为我们希望这适用于任何实施者AssetLoader(允许我们将文件系统逻辑与反序列化逻辑分开),我们仍然必须使用通用的L: AssetLoader,但它不再需要附加到Texture结构体(或任何包含Texture).

一个好的模式是引入一个单独的TextureDeserializer类型来处理有状态反序列化,并实现DeserializeSeed在那个结构上。我们可以设置Value关联类型指示反序列化应返回Texture.

pub struct Texture {
    path: String,
    image: Image,
}

struct TextureDeserializer<'a, L> {
    asset_loader: &'a mut L,
}

impl<'de, L> DeserializeSeed<'de> for TextureDeserializer<'_, L>
where
    L: AssetLoader,
{
    type Value = Texture;

    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        let path = String::deserialize(deserializer)?;

        let image = self.asset_loader.load_image(&path);

        Ok(Texture { path, image })
    }
}

请注意,通用AssetLoader不再由`Texture 直接使用。

我们现在必须定义DeserializeSeed一直到链条上Scene的反序列化逻辑,因为我们将有AssetLoader状态贯穿整个过程。这可能看起来非常冗长,不幸的是我们不能直接用serde-derive,但是不将反序列化状态绑定在我们正在反序列化的结构中的优点远远超过了额外的冗长。

反序列化一个Vec<Texture>,我们定义一个TexturesDeserializer:

struct TexturesDeserializer<'a, L> {
    asset_loader: &'a mut L,
}

impl<'de, L> DeserializeSeed<'de> for TexturesDeserializer<'_, L>
where
    L: AssetLoader,
{
    type Value = Vec<Texture>;

    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct TexturesVisitor<'a, L> {
            asset_loader: &'a mut L,
        }

        impl<'de, L> Visitor<'de> for TexturesVisitor<'_, L>
        where
            L: AssetLoader,
        {
            type Value = Vec<Texture>;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a sequence of Textures")
            }

            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
            where
                A: SeqAccess<'de>,
            {
                let mut textures = Vec::new();

                while let Some(texture) = seq.next_element_seed(TextureDeserializer {
                    asset_loader: self.asset_loader,
                })? {
                    textures.push(texture);
                }

                Ok(textures)
            }
        }

        deserializer.deserialize_seq(TexturesVisitor {
            asset_loader: self.asset_loader,
        })
    }
}

And a SceneDeserializer反序列化Scene itself:

pub struct Scene {
    textures: Vec<Texture>,
}

pub struct SceneDeserializer<'a, L> {
    pub asset_loader: &'a mut L,
}

impl<'de, L> DeserializeSeed<'de> for SceneDeserializer<'_, L>
where
    L: AssetLoader,
{
    type Value = Scene;

    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct SceneVisitor<'a, L> {
            asset_loader: &'a mut L,
        }

        impl<'de, L> Visitor<'de> for SceneVisitor<'_, L>
        where
            L: AssetLoader,
        {
            type Value = Scene;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct Scene")
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
            where
                A: MapAccess<'de>,
            {
                if let Some(key) = map.next_key()? {
                    if key != "textures" {
                        return Err(de::Error::unknown_field(key, FIELDS));
                    }
                } else {
                    return Err(de::Error::missing_field("textures"));
                }

                let textures = map.next_value_seed(TexturesDeserializer {
                    asset_loader: self.asset_loader,
                })?;

                Ok(Scene { textures })
            }
        }

        const FIELDS: &[&str] = &["textures"];
        deserializer.deserialize_struct(
            "Scene",
            FIELDS,
            SceneVisitor {
                asset_loader: self.asset_loader,
            },
        )
    }
}

注意上面这些DeserializeSeed定义与生成的非常相似#[derive(Deserialize)](如果是Scene)以及已经定义的内容serde for Vec<T>。然而,定义这些自定义实现允许状态通过整个过程传递到反序列化中Texture.

把它们放在一起

现在我们可以使用serde_json从我们的 JSON 输入反序列化。注意serde_json不提供任何反序列化的帮助方法DeserializeSeed(已经有讨论 https://github.com/serde-rs/json/issues/282过去对此进行过),所以我们必须使用serde_json::Deserializer手动。对我们来说幸运的是,它的使用非常简单:

fn main() {
    let mut asset_loader = AssetLoaderImpl {
        // cache, etc.
    };

    let scene_str = r#"
    {
      "textures": [
        "texture1.jpg",
        "texture2.jpg"
      ]
    }
    "#;

    let mut deserializer = serde_json::Deserializer::new(serde_json::de::StrRead::new(&scene_str));
    let scene = SceneDeserializer {
        asset_loader: &mut asset_loader,
    }.deserialize(&mut deserializer);

    // ...
}

现在我们可以反序列化Scene有状态的AssetLoader。这可以很容易地扩展到包括其他成员的其他资源Scene也可以在反序列化期间访问。最重要的是,它使反序列化状态与实际的反序列化结构分离,这意味着您不需要关心什么AssetLoader在反序列化之外使用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何将选项传递给 Rust 的 serde,以便可以在 Deserialize::deserialize() 中访问? 的相关文章

随机推荐