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

Max*_*ein 15 generics serialization rust serde

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

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

这是我目前正在做的方式,作为 MCVE 进行剥离(使用的包是serdeserde_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, ...
}

Run Code Online (Sandbox Code Playgroud)

特定于平台的代码(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();

    // ...
}

Run Code Online (Sandbox Code Playgroud)

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

  • AssetLoaderImpl必须实现SerializeDeserialize即使它从未(反)序列化
  • I'm also using typetag which causes a compilation error because "deserialization of generic impls is not supported yet"
  • Caching assets will be very difficult because I don't have an instance of AssetLoaderImpl which could cache them in a member variable
  • Passing the AssetLoader type parameter around is getting unwieldy when Texture (or other assets) are nested deeper
  • It just doesn't feel right, mostly because of the PhantomData and the abuse of generics

This makes me think that I'm not going about this the right way but I'm struggling to come up with a better solution. I thought about using a mutable global variable in the library holding an instance of AssetLoader (maybe with lazy_static) but that also doesn't seem right. Ideally I'd pass an instance of AssetLoader (Box<dyn AssetLoader> probably) to serde when deserializing that I can access in the impl Deserialize for Texture. I haven't found any way to do that and I'd really appreciate if anybody could point me in the right direction.

And*_*sen 3

为了将状态传递给反序列化,您应该使用该DeserializeSeed特征。的文档DeserializeSeed解决了这个用例:

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

有状态的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;
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以修改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 {}
    }
}
Run Code Online (Sandbox Code Playgroud)

反序列化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 })
    }
}
Run Code Online (Sandbox Code Playgroud)

AssetLoader请注意, “纹理”不再直接使用泛型。

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

为了反序列化 a Vec<Texture>,我们定义 a 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,
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

SceneDeserializer反序列化Scene自身:

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,
            },
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,上述定义与(在 的情况下)DeserializeSeed生成的定义以及for已定义的定义非常相似。然而,定义这些自定义实现允许状态通过整个过程传递到.#[derive(Deserialize)]SceneserdeVec<T>Texture

把它们放在一起

现在我们可以使用serde_jsonJSON 输入进行反序列化。请注意,它serde_json没有提供任何用于反序列化的辅助方法(过去DeserializeSeed已经对此进行过讨论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);

    // ...
}
Run Code Online (Sandbox Code Playgroud)

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