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

e3bfsja2  于 2023-04-06  发布在  其他
关注(0)|答案(1)|浏览(138)

上下文:我正在用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, ...
}

平台代码(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必须实现SerializeDeserialize,即使它从未(反)序列化
  • 我还使用了typetag,这会导致一个编译错误,因为“还不支持泛型imps的反序列化”。
  • 缓存资产将非常困难,因为我没有AssetLoaderImpl的示例可以将它们缓存在成员变量中
  • Texture(或其他资产)嵌套得更深时,四处传递AssetLoader类型参数会变得很笨拙
  • 感觉不太对,主要是因为PhantomData和滥用泛型

这让我觉得我的方法不对,但我正在努力想出一个更好的解决方案。(也许是lazy_static),但这似乎也不对。(Box<dyn AssetLoader>可能)到serde的反序列化时,我可以在impl Deserialize for Texture中访问。我还没有找到任何方法来做到这一点,我真的很感激,如果有人可以指出我在正确的方向。

xxe27gdn

xxe27gdn1#

为了将状态传递给反序列化,你应该使用DeserializeSeed trait。DeserializeSeed的文档解决了这个用例:
DeserializeSeedDeserialize trait的有状态形式。如果你发现自己在寻找一种将数据传递到Deserialize impl的方法,这个trait就是实现它的方法。

状态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反序列化

现在我们可以在反序列化过程中使用DeserializeSeed trait来使用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,我们定义了一个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,
        })
    }
}

还有一个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,
            },
        )
    }
}

请注意,上面的这些DeserializeSeed定义与#[derive(Deserialize)]生成的定义(在Scene的情况下)以及serdeVec<T>定义的定义非常相似。然而,定义这些自定义实现允许状态通过整个过程传递到Texture的反序列化中。
∮ ∮把所有的都放在一起∮
现在我们可以使用serde_json从我们的JSON输入进行反序列化。注意,serde_json没有提供任何帮助方法来使用DeserializeSeed进行反序列化(过去有discussion),所以我们必须手动使用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);

    // ...
}

现在我们可以用一个有状态的AssetLoader来反序列化一个Scene。这可以很容易地扩展到包括其他资源,供Scene的其他成员在反序列化过程中访问。最棒的是,它保持了反序列化状态与实际反序列化结构体的解耦,这意味着你不需要关心在反序列化之外使用了什么AssetLoader

相关问题