Generating an idiomatic wrapper interface


#1

Hello,

I am the author of the gltf crate and need some help generating a wrapper interface for some unfriendly data structures.

Context

glTF is a standard for loading 3D models which are described by JSON data. The
gltf crate deserialises this JSON into Rust data structures. I would like to
provide a wrapper interface around this data to make the crate more friendly to
use and to provide extra features.

The main problem I want to address is that the JSON references other data
using indices into arrays contained in the root JSON object. Dereferencing this
data is cumbersome and perhaps error-prone as demonstrated below.

// We have to pass the root object around whenever we access other data
fn process_node(gltf: &Gltf, node: &Node) -> Result {
    for child_node_id in &node.children {
        // What if `node` does not belong to `gltf`? (potential `panic!`)
        let child_node = gltf.nodes.get(child_node_id);
        process_node(gltf, child_node)?;
    }

    if let Some(mesh_id) = node.mesh.as_ref() {
        let mesh = gltf.meshes.get(mesh_id);
        process_mesh(gltf, mesh)?;
    }
    ...
}

fn process_gltf() -> Result {
    let gltf = import("Foo.gltf")?;
    for scene in &gltf.scenes {
        for node_id in &scene.nodes {
            let node = gltf.scenes.get(node_id);
            process_node(gltf, node)?;
        }
    }
    ...
}

Instead I would like to provide a wrapper interface that allows easy retrieval
of referenced data. The result might look like this:

fn process_node(node: &Node) -> Result {
    // `child` is guaranteed to belong to the same glTF as `node`
    for child in node.iter_child_nodes() {
        process_node(child)?;
    }

    // Easy and safe retrieval of referenced data
    if let Some(mesh) = node.mesh() {
        process_mesh(mesh)?;
    }
    ...
}

fn process_gltf() -> Result {
    let gltf = import("Foo.gltf")?;
    for scene in gltf.iter_scenes() {
        for node in scene.iter_nodes() {
            process_node(node)?;
        }
    }
    ...
}

Question

In order to write this interface I have to turn all the pub data members
into methods and the documentation must match exactly. For example, consider
buffer::View which holds a reference to a buffer::Buffer:

/// Doc A
pub struct View {
    /// Doc B
    pub buffer: Index<Buffer>,
    ...
}

A corresponding wrapper struct might look like this:

/// Doc A
pub struct View<'a> {
    root: &'a json::Root,
    view: &'a json::buffer::View,
}

impl<'a> View<'a> {
    /// Doc B
    pub fn buffer(&self) -> Buffer<'a> {
        Buffer {
            root: self.root,
            buffer: self.root.get(&self.view.buffer),
        }
    }
}

In the implemenation of the wrapper I want to avoid:

  1. Using Deref as a way of inheriting data items
  2. Manually (re-)writing all the code

Does anyone know a way of generating such code?


#2

Have I defined the problem clearly enough?


#3

The tool of choice for code-generation of arbitrary complexity such as this would no doubt be procedural macros; though I don’t know much about them myself.

Sleuthing around I’m pleasantly surprised to see that the old book has a chapter on them, a lot of which is probably still relevant since the feature is stabilized; although it looks like some important functionality is outsourced to the “third-party” crates syn and quote which I imagine are a bit more experimental. Speaking of which, the reverse deps of syn might help you hunt for examples.

At face value proc_macros look like they are limited to custom derive(Trait) attributes, but last I heard there are ways around that.

That’s all I got. Somebody please correct me. :slight_smile:


#4

Thanks for your reply.

I’m currently using the proc_macro feature to generate validation code on the JSON data (example). This has been very successful (although compile times have suffered!) but applying the same technique to generate a wrapper is a difficult task. The main problems are:

  1. Copying comments - I think they may be skipped by the token stream?
  2. Keeping the JSON-facing data structures separate in their own json module.
  3. Keeping the crate friendly to contributors.

Using #[doc("This is a comment")] might get around problem #1.

Problem #2 might be handled in one of two ways: a ‘bottom-up’ approach, i.e. generating the wrapper from attributes on the JSON structs, or a ‘top-down’ approach, i.e. generating the JSON structs from attributes on the wrapper. Both require quite gnarly code.

I am considering generating the wrapper code with a script just once and rewriting the special cases as necessary. The concern is then the documentation willl have to be changed in two locations whenever it’s updated. Perhaps if the docs could be written outside of the source code and injected in by rustdoc that would solve the issue. Is rustdoc able to do this?