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

13

背景:我正在使用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,导致编译错误,因为“尚不支持泛型impl的反序列化”
  • 缓存资产将非常困难,因为我没有AssetLoaderImpl的实例,可以在成员变量中缓存它们
  • Texture(或其他资源)嵌套更深时,传递AssetLoader类型参数变得笨拙
  • 感觉不太对,主要是因为PhantomData和滥用范型

这让我觉得我可能不是以正确的方式解决问题,但我正在苦苦挣扎想出更好的解决方案。我考虑过在库中使用一个可变的全局变量,保存AssetLoader的实例(也许使用lazy_static),但这也不太合适。理想情况下,我希望能够在反序列化时传递一个AssetLoader的实例(可能是Box<dyn AssetLoader>),以便我可以在impl Deserialize for Texture中访问它。我还没有找到任何方法可以做到这一点,如果有人能指点我正确的方向,我将不胜感激。

1个回答

1

为了将状态传递给反序列化,您应该使用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特性,在反序列化过程中使用AssetLoader。由于我们希望这适用于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,
        })
    }
}

还有一个 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 的情况下)生成的内容非常相似,并且 serde 已经为 Vec & lt; T&gt; 定义了。但是,定义这些自定义实现允许状态通过整个过程传递到 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

那似乎是一个稍微更好的解决方案,尽管我对大量样板代码感到有些不满意。 - Max Klein
1
正如serde文档所指出的那样,它是将数据传递到反序列化的解决方案。虽然有很多样板文件,但与手动实现Deserialize没有什么不同。不同之处在于,对于DeserializeSeed,没有像Deserialize那样的派生宏,因此目前只有这种繁琐的方式可供选择。您可以随时打开一个问题请求一个派生宏,或者找到一个现有的问题并在那里陈述您的用例。 - Anders Evensen

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