Porting librsvg's tree of nodes to Rust

Earlier I wrote about how librsvg exports reference-counted objects from Rust to C. That was the preamble to this post, in which I'll write about how I ported librsvg's tree of nodes from C to Rust.

There is a lot of talk online about writing recursive data structures in Rust, as there are various approaches to representing the links between your structure's nodes. You can use shared references and lifetimes. You can use an arena allocator and indices or other kinds of identifiers into that arena. You can use reference-counting. Rust really doesn't like C's approach of pointers-to-everything-everywhere; you must be very explicit about who owns what and for how long things live.

Librsvg keeps an N-ary tree of Node structures, each of which corresponds to an XML element in the SVG file. Nodes can be of various kinds: "normal" shapes that know how to draw themselves like Bézier paths and ellipses; "reference-only" objects like markers, which get referenced from paths to draw arrowheads and the like; "paint server" objects like gradients and fill patterns which also don't draw themselves, but only get referenced for a fill or stroke operation on other shapes; filter objects like Gaussian blurs which get referenced after rendering a sub-group of objects.

Even though these objects are of different kinds, they have some things in common. They can be nested — a marker contains other shapes or sub-groups or shapes so you can draw multi-part arrowheads, for example; or a filter can be a Gaussian blur of the alpha channel of the referencing shape, followed by a light-source operation, to create an embossed effect on top of a shape. Objects can also be referenced by name in different places: you can declare a gradient and give it a name like #mygradient , and then use it for the fill or stroke of other shapes without having to declare it again.

Also, all the objects maintain CSS state. This state gets inherited from an object's ancestors in the N-ary tree, and finally gets overriden with whatever specific CSS properties the object has for itself. You could declare a Group (a <g> element) with a black 4 pixel-wide outline, and then into it put a bunch of other shapes, but each with a fill of a different color. Those shapes will inherit the black outline, but use their own fill.

Librsvg's representation of tree nodes, in C

The old C code had a simple representation of nodes. There is an RsvgNodeType enum which just identifies the type of each node, and an RsvgNode structure for each node in the tree. Each RsvgNode also keeps a small vtable of methods.

typedef enum { RSVG_NODE_TYPE_INVALID = 0, RSVG_NODE_TYPE_CHARS, RSVG_NODE_TYPE_CIRCLE, RSVG_NODE_TYPE_CLIP_PATH, RSVG_NODE_TYPE_COMPONENT_TRANFER_FUNCTION, ... } RsvgNodeType; typedef struct { void (*free) (RsvgNode *self); void (*draw) (RsvgNode *self, RsvgDrawingCtx *draw_ctx, int dominate); void (*set_atts) (RsvgNode *self, RsvgHandle *handle, RsvgPropertyBag *pbag); } RsvgNodeVtable; typedef struct _RsvgNode RsvgNode; typedef struct _RsvgNode { RsvgState *state; RsvgNode *parent; GPtrArray *children; RsvgNodeType type; RsvgNodeVtable *vtable; };

What about memory management? A node keep an array pointers to its children, and also a pointer to its parent (which of course can be NULL for the root node). The master RsvgHandle object, which is what the caller gets from the public API, maintains a big array of pointers to all the nodes, in addition to a pointer to the root node of the tree. Nodes are created while reading an SVG file, and they don't get freed until the toplevel RsvgHandle gets freed. So, it is okay to keep shared references to nodes and not worry about memory management within the tree: the RsvgHandle will free all the nodes by itself when it is done.

Librsvg's representation of tree nodes, in Rust

In principle I could have done something similar in Rust: have the master handle object keep an array to all the nodes, and make it responsible for their memory management. However, when I started porting that code, I wasn't very familiar with how Rust handles lifetimes and shared references to objects in the heap. The syntax is logical once you understand things, but I didn't back then.

So, I chose to use reference-counted structures instead. It gives me a little peace of mind that for some time I'll need to keep references from the C code to Rust objects, and I am already comfortable with writing C code that uses reference counting. Once everything is ported to Rust and C code no longer has references to Rust objects, I can probably move away from refcounts into something more efficient.

I needed to have a way to hook the existing C implementations of nodes into Rust, so that I can port them gradually. That is, I need to have a way to have nodes implemented both in C and Rust, while I port them one by one to Rust. We'll see how this is done.

Here is the first approach to the C code above. We have an enum that matches RsvgNodeType from C, a trait that defines methods on nodes, and the Node struct itself.

/* Keep this in sync with rsvg-private.h:RsvgNodeType */ #[repr(C)] #[derive(Debug, Copy, Clone, PartialEq)] pub enum NodeType { Invalid = 0, Chars, Circle, ClipPath, ComponentTransferFunction, ... } /* A *const RsvgCNodeImpl is just an opaque pointer to the C code's * struct for a particular node type. */ pub enum RsvgCNodeImpl {} pub type RsvgNode = Rc<Node>; pub trait NodeTrait: Downcast { fn set_atts (&self, node: &RsvgNode, handle: *const RsvgHandle, pbag: *const RsvgPropertyBag); fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32); fn get_c_impl (&self) -> *const RsvgCNodeImpl; } impl_downcast! (NodeTrait); pub struct Node { node_type: NodeType, parent: Option<Weak<Node>>, // optional; weak ref to parent children: RefCell<Vec<Rc<Node>>>, // strong references to children state: *mut RsvgState, node_impl: Box<NodeTrait> }

The Node struct is analogous to the old C structure above. The parent field holds an optional weak reference to another node: it's weak to avoid circular reference counts, and it's optional because not all nodes have a parent. The children field is a vector of strong references to nodes; it is wrapped in a RefCell so that I can add children (i.e. mutate the vector) while the rest of the node remains immutable.

RsvgState is a C struct that holds the CSS state for nodes. I haven't ported that code to Rust yet, so the state field is just a raw pointer to that C struct.

Finally, there is a node_impl: Box<NodeTrait> field. This has a reference to a boxed object which implements NodeTrait . In effect, we are separating the "tree stuff" (the basic Node struct) from the "concrete node implementation stuff", and the Node struct has a reference inside it to the node type's concrete implemetation.

The vtable turns into a trait, more or less

Let's look again at the old C code for a node's vtable:

typedef struct { void (*free) (RsvgNode *self); void (*draw) (RsvgNode *self, RsvgDrawingCtx *draw_ctx, int dominate); void (*set_atts) (RsvgNode *self, RsvgHandle *handle, RsvgPropertyBag *pbag); } RsvgNodeVtable;

The free method is responsible for freeing the node itself and all of its inner data; this is common practice in C vtables.

The draw method gets called, well, to draw a node. It gets passed a drawing context, plus a magic dominate argument which we can ignore for now (it has to do with CSS cascading).

Finally, the set_atts method is just a helper at construction time: after a node gets allocated, it is initialized from its XML attributes by the set_atts method. The pbag argument is just a dictionary XML attributes for this node, represented as key-value pairs; the method pulls the key-value pairs out of the pbag and initializes its own fields from the values.

The NodeTrait in Rust is similar, but has a few differences:

pub trait NodeTrait: Downcast { fn set_atts (&self, node: &RsvgNode, handle: *const RsvgHandle, pbag: *const RsvgPropertyBag); fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32); fn get_c_impl (&self) -> *const RsvgCNodeImpl; }

You'll note that there is no free method. Rust objects know how to free themselves and their fields automatically, so we don't need that method anymore. We will need a way to free the C data that corresponds to the C implementations of nodes — those are external resources not managed by Rust, so we need to tell it about them; see below.

The set_atts and draw methods are similar to the C ones, but they also have an extra node argument. Read on.

There is a new get_c_impl method. This is a temporary thing to accomodate C implementations of nodes; read on.

Finally, what about the " NodeTrait: Downcast " in the first line? We'll get to it in the end.

Separation of concerns?

So, we have the basic Node struct, which forms the N-ary tree of SVG elements. We also have a NodeTrait with a few of methods that nodes must implement. The Node struct has a node_impl field, which holds a reference to an object in the heap which implements NodeTrait .

I think I saw this pattern in the source code for Servo; I was looking at its representation of the DOM to see how to do an N-ary tree in Rust. I *think* it splits things in the same way; or maybe I'm misremembering and using the pattern from another tree-of-multiple-node-types implementation in Rust.

How should things look from C?

This is easy to answer: they should look exactly as they were before the conversion to Rust, or as close to that as possible, since I don't want to change all the C code at once!

In the post about exposing reference-counted objects from Rust to C, we already saw the new rsvg_node_ref() and rsvg_node_unref() functions, which hand out pointers to boxed Rc<Node> .

Previously I had made accessor functions for all of RsvgNode 's fields, so the C code doesn't touch them directly. There are functions like rsvg_node_get_type() , rsvg_node_get_parent() , rsvg_node_foreach_child() , that the C code already uses. I want to keep them exactly the same, with only the necessary changes. For example, when the C code did not reference-count nodes, the implementation of rsvg_node_get_parent() simply returned the value of the node->parent field. The new implementation returns a strong reference to the parent (upgraded from the node's weak reference to its parent), and the caller is responsible for unref() ing it.

Rust implementation of Node

Let's look at two "trivial" methods of Node :

impl Node { ... pub fn get_type (&self) -> NodeType { self.node_type } pub fn get_state (&self) -> *mut RsvgState { self.state } ... }

Nothing special there; just accessor functions for the node's fields. Given that Rust makes those fields immutable in the presence of shared references, I'm not 100% sure that I actually need those accessors. If it turns out that I don't, I'll remove them and let the code access the fields directly.

Now, the method that adds a child to a Node :

pub fn add_child (&self, child: &Rc<Node>) { self.children.borrow_mut ().push (child.clone ()); }

The children field is a RefCell<Vec<Rc<Node>>> . We ask to borrow it mutably with borrow_mut() , and then push a new item into the array. What we push is a new strong reference to the child, which we get with child.clone() . Think of this as " g_ptr_array_add (self->children, g_object_ref (child)) ".

And now, two quirky methods that call into the node_impl :

impl Node { ... pub fn set_atts (&self, node: &RsvgNode, handle: *const RsvgHandle, pbag: *const RsvgPropertyBag) { self.node_impl.set_atts (node, handle, pbag); } pub fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32) { self.node_impl.draw (node, draw_ctx, dominate); } ... }

The &self argument is implicitly a &Node . But we also pass a node: &RsvgNode argument! Remember the type declaration for RsvgNode ; it is just " pub type RsvgNode = Rc<Node> ". What these prototypes mean is:

pub fn set_atts (&self: reference to the Node, node: refcounter for the Node, ...) { ... call the actual implementation in self.node_impl ... }

This is because of the following. In objects that implement NodeTrait , the actual implementations of set_atts() and draw() still need to call into C code for a few things. And the only view that the C code has into the Rust world is through pointers to RsvgNode , that is, pointers to the Rc<Node> — the refcounting wrapper for nodes. We need to be able to pass this refcounting wrapper to C from somewhere, but once we are down in the concrete implementations of the trait, we don't have the refcounts anymore. So, we pass them as arguments to the trait's methods.

This may look strange; at first sight it may look as if you are passing self twice to a method call, but not really! The self argument is implicit in the method call, and the first node argument is something rather different: it is a reference count to the node, not the node itself. I may be able to remove this strange argument once all the nodes are implemented in Rust and there is no interfacing to C code anymore.

Accomodating C implementations of nodes

Now we get to the part where the Node and NodeTrait , implemented in Rust, both need to accomodate the existing C implementations of node types.

Instead of implementing a node type in Rust (i.e. implement NodeTrait for some struct), we will implement a Rust wrapper for node implementations in C, which implements NodeTrait . Here is the declaration of CNode :

/* A *const RsvgCNodeImpl is just an opaque pointer to the C code's * struct for a particular node type. */ pub enum RsvgCNodeImpl {} type CNodeSetAtts = unsafe extern "C" fn (node: *const RsvgNode, node_impl: *const RsvgCNodeImpl, handle: *const RsvgHandle, pbag: *const RsvgPropertyBag); type CNodeDraw = unsafe extern "C" fn (node: *const RsvgNode, node_impl: *const RsvgCNodeImpl, draw_ctx: *const RsvgDrawingCtx, dominate: i32); type CNodeFree = unsafe extern "C" fn (node_impl: *const RsvgCNodeImpl); struct CNode { c_node_impl: *const RsvgCNodeImpl, set_atts_fn: CNodeSetAtts, draw_fn: CNodeDraw, free_fn: CNodeFree, }

This struct CNode has essentially a void* to the C struct that will hold a node's data, and three function pointers. These function pointers ( set_atts_fn , draw_fn , free_fn ) are very similar to the original vtable we had, and that we turned into a trait.

We implement NodeTrait for this CNode wrapper as follows, by just calling the function pointers to the C functions:

impl NodeTrait for CNode { fn set_atts (&self, node: &RsvgNode, handle: *const RsvgHandle, pbag: *const RsvgPropertyBag) { unsafe { (self.set_atts_fn) (node as *const RsvgNode, self.c_node_impl, handle, pbag); } } fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32) { unsafe { (self.draw_fn) (node as *const RsvgNode, self.c_node_impl, draw_ctx, dominate); } } ... }

Maybe this will make it easier to understand why we neeed that "extra" node argument with the refcount: it is the actual first argument ot the C functions, which don't get the luxury of a self parameter.

And the free_fn() ? Who frees the C implementation data? Rust's Drop trait, of course! When Rust decides to free CNode , it will see if it implements Drop . Our implementation thereof calls into the C code to free its own data:

impl Drop for CNode { fn drop (&mut self) { unsafe { (self.free_fn) (self.c_node_impl); } } }

What does it look like in memory?

This is the basic layout. A Node gets created and put on the heap. Its node_impl points to a CNode in the heap (or to any other thing which implements NodeTrait , really). In turn, CNode is our wrapper for C implementations of SVG nodes; its c_node_impl field is a raw pointer to data on the C side — in this example, an RsvgNodeEllipse . We'll see how that one looks like shortly.

So how does C create a node?

I'm glad you asked! This is the rsvg_rust_cnode_new() function, which is implemented in Rust but exported to C. The C code uses it when it needs to create a new node.

#[no_mangle] pub extern fn rsvg_rust_cnode_new (node_type: NodeType, raw_parent: *const RsvgNode, state: *mut RsvgState, c_node_impl: *const RsvgCNodeImpl, set_atts_fn: CNodeSetAtts, draw_fn: CNodeDraw, free_fn: CNodeFree) -> *const RsvgNode { assert! (!state.is_null ()); assert! (!c_node_impl.is_null ()); let cnode = CNode { // 1 c_node_impl: c_node_impl, set_atts_fn: set_atts_fn, draw_fn: draw_fn, free_fn: free_fn }; box_node (Rc::new (Node::new (node_type, // 2 node_ptr_to_weak (raw_parent), // 3 state, Box::new (cnode)))) // put the CNode in the heap; pass it to the Node }

1. We create a CNode structure and fill it in from the parameters that got passed to rsvg_rust_cnode_new() .

2. We create a new Node with Node::new() , wrap it with a reference counter with Rc::new() , box that ("put it in the heap") and return a pointer to the box's contents. The boxificator is just this; it's similar to what we used before:

pub fn box_node (node: RsvgNode) -> *mut RsvgNode { Box::into_raw (Box::new (node)) }

3. We create a weak reference to the parent node. Here, raw_parent comes in as a pointer to a strong reference. To obtain a weak reference, we do this:

pub fn node_ptr_to_weak (raw_parent: *const RsvgNode) -> Option<Weak<Node>> { if raw_parent.is_null () { None } else { let p: &RsvgNode = unsafe { & *raw_parent }; Some (Rc::downgrade (&p.clone ())) // 5 } }

5. Here, we take a strong reference to the parent with p.clone() . Then we turn it into a weak reference with Rc::downgrade() . We stuff that in an Option with the Some() — remember that not all nodes have a parent, and we represent this with an Option<Weak<Node>> .

Creating a C implementation of a node

This is the C code for rsvg_new_group() , the function that creates nodes for SVG's <g> element.

RsvgNode * rsvg_new_group (const char *element_name, RsvgNode *parent) { RsvgNodeGroup *group; group = g_new0 (RsvgNodeGroup, 1); /* ... fill in the group struct ... */ return rsvg_rust_cnode_new (RSVG_NODE_TYPE_GROUP, parent, rsvg_state_new (), group, rsvg_group_set_atts, rsvg_group_draw, rsvg_group_free); }

The resulting RsvgNode* , which from C's viewpoint is an opaque pointer to something on the Rust side — the boxed Rc<Node> — gets stored in a couple of places. It gets put in the toplevel RsvgHandle 's array of all-the-nodes. It gets hooked, as a child, to its parent node. It may get referenced in other places as well, for example, in the dictionary of string-to-node for nodes that have an id="name" attribute. All those are strong references created with rsvg_node_ref() .

Getting the implementation structs from C

Let's look again at the implementation of NodeTrait for CNode . This is one of the methods:

impl NodeTrait for CNode { ... fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32) { unsafe { (self.draw_fn) (node as *const RsvgNode, self.c_node_impl, draw_ctx, dominate); } } ... }

In self.draw_fn we have a function pointer to a C function. We call it, and we pass the self.c_node_impl . This gives the function access to its own implementation data.

But what about cases where we need to access an object's data outside of the methods, and so we don't have that c_node_impl argument? If you are cringing because This Is Not Something That Is Done in OOP, well, you are right, but this is old code with impurities. Maybe once it is Rustified thoroughly, I'll have a chance to clean up those inconsistencies. Anyway, here is a helper function that the C code can call to get ahold of its c_node_impl :

#[no_mangle] pub extern fn rsvg_rust_cnode_get_impl (raw_node: *const RsvgNode) -> *const RsvgCNodeImpl { assert! (!raw_node.is_null ()); let node: &RsvgNode = unsafe { & *raw_node }; node.get_c_impl () }

That get_c_impl() method is the temporary thing I mentioned above. It's one of the methods in NodeTrait , and of course CNode implements it like this:

impl NodeTrait for CNode { ... fn get_c_impl (&self) -> *const RsvgCNodeImpl { self.c_node_impl } }

You may be thinking that is is an epic hack: not only do we provide a method in the base trait to pull an obscure field from a particular implementation; we also return a raw pointer from it! And a) you are absolutely right, but b) it's a temporary hack, and it is about the easiest way I found to shove the goddamned C implementation around. It will be gone once all the node types are implemented in Rust.

Now, from C's viewpoint, the return value of that rsvg_rust_cnode_get_impl() is just a void* , which needs to be casted to the actual struct we want. So, the proper way to use this function is first to assert that we have the right type:

g_assert (rsvg_node_get_type (handle->priv->treebase) == RSVG_NODE_TYPE_SVG); RsvgNodeSvg *root = rsvg_rust_cnode_get_impl (handle->priv->treebase);

This is no different or more perilous than the usual downcasting one does when faking OOP with C. It's dangerous, sure, but we know how to deal with it.

Creating a Rust implementation of a node

Aaaah, the fun part! I am porting the SVG node types one by one to Rust. I'll show you the simple implementation of NodeLine , which is for SVG's <line> element.

struct NodeLine { x1: Cell<RsvgLength>, y1: Cell<RsvgLength>, x2: Cell<RsvgLength>, y2: Cell<RsvgLength> }

Just x1/y1/x2/y2 fields with our old friend RsvgLength, no problem. They are in Cell s to make them mutable after the NodeLine is constructed and referenced all over the place.

Now let's look at its three methods of NodeTrait .

impl NodeTrait for NodeLine { fn set_atts (&self, _: &RsvgNode, _: *const RsvgHandle, pbag: *const RsvgPropertyBag) { self.x1.set (property_bag::lookup_length (pbag, "x1", LengthDir::Horizontal)); self.y1.set (property_bag::lookup_length (pbag, "y1", LengthDir::Vertical)); self.x2.set (property_bag::lookup_length (pbag, "x2", LengthDir::Horizontal)); self.y2.set (property_bag::lookup_length (pbag, "y2", LengthDir::Vertical)); } }

The set_atts() method just sets the fields of the NodeLine to the corresponding values that it gets in the property bag. This pbag is of key-value pairs from XML attributes. That lookup_length() function looks for a specific key, and parses it into an RsvgLength . If the key is not available, or if parsing yields an error, the function returns RsvgLength::default() — the default value for lengths, which is zero pixels. This is where we "bump" into the C code that doesn't know how to propagate parsing errors yet. Internally, the length parser in Rust yields a proper Result value, with error information if parsing fails. Once all the code is on the Rust side of things, I'll start thinking about propagating errors to the toplevel RsvgHandle . For now, librsvg is a very permissive parser/renderer indeed.

I'm starting to realize that set_atts() only gets called immediately after a new node is allocated. After that, I *think* that nodes don't mutate their internal fields. So, it may be possible to move the "pull stuff out of the pbag and set the fields" code from set_atts() to the actual constructors, remove the Cell wrappers around all the fields, and thus get immutable objects.

Maybe it is that I'm actually going through all of librsvg's code, so I get to know it better; or maybe it is that porting it to Rust is actually clarifying my thinking on how the code works and how it ought to work. If all nodes are immutable after creation... and they are a recursive tree... which already does Porter-Duff compositing... which is associative... that sounds very parallelizable, doesn't it? (I would never dare to do it in C. Rust is making it feel quite feasible, without data races.)

impl NodeTrait for NodeLine { fn draw (&self, node: &RsvgNode, draw_ctx: *const RsvgDrawingCtx, dominate: i32) { let x1 = self.x1.get ().normalize (draw_ctx); let y1 = self.y1.get ().normalize (draw_ctx); let x2 = self.x2.get ().normalize (draw_ctx); let y2 = self.y2.get ().normalize (draw_ctx); let mut builder = RsvgPathBuilder::new (); builder.move_to (x1, y1); builder.line_to (x2, y2); render_path_builder (&builder, draw_ctx, node.get_state (), dominate, true); } }

The draw() method normalizes the x1/y1/x2/y2 values to the current viewport from the drawing context. Then it creates a path builder and feeds it commands to draw a line. It calls a helper function, render_path_builder() , which renders it using the appropriate CSS cascaded values, and which adds SVG markers like arrowheads if they were specified in the SVG file.

Finally, our little hack for the benefit of the C code:

impl NodeTrait for NodeLine { fn get_c_impl (&self) -> *const RsvgCNodeImpl { unreachable! (); } }

In this purely-Rust implementation of NodeLine , there is no c_impl. So, this method asserts that nobody ever calls it. This is to guard myself against C code which may be trying to peek at an impl when there is no longer one.

Downcasting to concrete types

Remember rsvg_rust_cnode_get_impl() , which the C code can use to get its implementation data outside of the normal NodeTrait methods?

Well, since I am doing a mostly straight port from C to Rust, i.e. I am not changing the code's structure yet, just changing languages — it turns out that sometimes the Rust code needs access to the Rust structs outside of the NodeTrait methods as well.

I am using downcast-rs, a tiny crate that lets one do exactly this: go from a boxed trait object to a concrete type. Librsvg uses it like this:

use downcast_rs::*; pub trait NodeTrait: Downcast { fn set_atts (...); ... } impl_downcast! (NodeTrait); impl Node { ... pub fn with_impl<T: NodeTrait, F: FnOnce (&T)> (&self, f: F) { if let Some (t) = (&self.node_impl).downcast_ref::<T> () { f (t); } else { panic! ("could not downcast"); } } ... }

The basic Node has a with_impl() method, which takes a lambda that accepts a concrete type that implements NodeTrait . The lambda gets called with your Rust-side impl, with the right type.

Where the bloody hell is this used? Let's take markers as an example. Even though SVG markers are implemented as a node, they don't draw themselves: they can get referenced from paths/lines/etc. and there is special machinery to decide just where to draw them.

So, in the implementation for markers, the draw() method is empty. The code that actually computes a marker's position uses this to render the marker:

node.with_impl (|marker: &NodeMarker| marker.render (c_node, draw_ctx, xpos, ypos, computed_angle, line_width));

That is, it calls node.with_impl() and passes it an anonymous function which calls marker.render() — a special function just for rendering markers.

This is kind of double plus unclean, yes. I can think of a few solutions:

Instead of assuming that every node type is really a NodeTrait , make Node know about element implementations that can draw themselves normally and those that can't. Maybe Node can have an enum to discriminate that, and NodeTrait can lose the draw() method. Maybe there needs to be Drawable trait which "normal" elements implement, and something else for markers and other elements which are only used by reference?

Instead of having a single dictionary of named nodes — those that have an id="name" attribute, which is later used to reference markers, filters, etc. — have separate dictionaries for every reference-able element type. One dict for markers, one dict for patterns, one dict for filters, and so on. The SVG spec already says, for example, that it is an error to reference a marker but to specify an id for an element that is not a marker — and so on for the other reference-able types. After doing a lookup by id in the dictionary-of-all-named-nodes, librsvg checks the type of the found Node . It could avoid doing this check if the dictionaries were separate, since each dictionary would only contain objects of a known type.

Conclusion

Apologies for the long post! Here's a summary:

Node objects are reference-counted. Librsvg uses them to build up the tree of nodes.

There is a NodeTrait for SVG element implementations. It has methods to set an element's attributes, and to draw the element.

SVG elements already implemented in Rust have no problems; they just implement NodeTrait and that's it.

For the rest of the SVG elements, which are still implemented in C, I have a CNode which implements NodeTrait . The CNode holds an opaque pointer to the implementation data on the C side, and function pointers to the C implementations of NodeTrait .

There is some temporary-but-clean hackery to let C code obtain its implementation pointers outside of the normal method implementations.

Downcasting to concrete types is a necessary evil, but it is the easiest way to transform this C code into Rust. It may be possible to eliminate it with some refactoring.