Rust and Dynamically Loading Structs

Ferdinand de Antoni
3 min readDec 23, 2019

--

If you have developed on the JVM before, such as with Scala or Java, you will probably be familiar with the ability to create new instances from class names:

trait Message {
def message: Unit
}
class SimpleMessage extends Message {
def message: Unit = {
println("Hello!")
}
}
val test = new SimpleMessage
val testName = test.getClass().getName
println(testName)
val clazz = Class.forName(testName)
val instance: Message = clazz.newInstance().asInstanceOf[Message]
println(instance.message)

This is especially useful when you dynamically need to instantiate a class from a string (e.g. when doing deserialization). In the above Scala example we load the concrete class SimpleMessage, create a new instance of it, and cast it to the trait Message which we can use in a generic manner further down our code.

This ability, however, does not come out of the box with Rust. There is no JVM with class information, nor some internal inventory that stores all structs that have been defined.

Luckily there are crates that can help us!

The Typetag Crate

You can import the typetag crate which will build an inventory upon startup containing all the structs you have decorated with the typetag macro:

#[typetag::serde(tag = "type")]
trait WebEvent {
fn inspect(&self);
}
#[derive(Serialize, Deserialize)]
struct PageLoad;
#[typetag::serde]
impl WebEvent for PageLoad {
fn inspect(&self) {
println!("200 milliseconds or bust");
}
}
#[derive(Serialize, Deserialize)]
struct Click {
x: i32,
y: i32,
}
#[typetag::serde]
impl WebEvent for Click {
fn inspect(&self) {
println!(
"negative space between the ads: x={} y={}",
self.x,
self.y
);
}
}

With the above, you can then do the following:

fn main() -> serde_json::Result<()> {    
let page_load = PageLoad;
let event = &page_load as &dyn WebEvent;
let json = serde_json::to_string(event)?;
println!("PageLoad json: {}", json);
let de: Box<dyn WebEvent> = serde_json::from_str(&json)?;
de.inspect();
println!(); let click = Click { x: 10, y: 10 };
let event = &click as &dyn WebEvent;
let json = serde_json::to_string(event)?;
println!("Click json: {}", json);
let de: Box<dyn WebEvent> = serde_json::from_str(&json)?;
de.inspect();
Ok(())}

The JSON it generates for PageLoad, for example, looks as follows:

{"type":"PageLoad","string":"hello"}

With this JSON, the typetag crate can lookup which struct it needs to use for deserialization. More importantly, it can do this from a trait object too.

Great! Now we can deserialize to a trait object! How do we get back to the original struct though? To do this we need to downcast.

Downcasting from Trait Objects

To get from a trait object back to the concrete type, Rust has the utility trait Any which allows you to do this. For example:

use std::any::Any;

trait WebEvent {
fn as_any(&self) -> &dyn Any;
}

struct PageLoad;

impl WebEvent for PageLoad {
fn as_any(&self) -> &dyn Any {
self
}
}

fn main() {
let event: Box<dyn WebEvent> = Box::new(PageLoad);
let pageLoad: &PageLoad = {
match event.as_any().downcast_ref::<B>() {
Some(load) => load,
None => panic!("WebEvent is not a PageLoad!"),
}
}
println!("PageLoad: {:?}", pageLoad);
}

Basically we just need a function added to the WebEvent trait which will allow us to convert a WebEvent trait object to an Any trait object. Once converted, we can use the downcast methods provided by the Any trait to try to downcast it back to the original struct.

Better yet, instead of converting to an Any, you can also opt to use the mopa crate. This crate will allow you to add the various downcast methods to your own trait. We can apply the downcast method directly to the WebEvent trait so that we do not have to convert to an Any first:

use mopa::*;

use serde::{Deserialize, Serialize};

#[typetag::serde(tag = "type")]
trait WebEvent: mopa::Any {
fn inspect(&self);
}

mopafy!(WebEvent);

#[derive(Serialize, Deserialize)]
struct Click {
x: i32,
y: i32,
}

#[typetag::serde]
impl WebEvent for Click {
fn inspect(&self) {
println!(
"negative space between the ads: x={} y={}",
self.x,
self.y
);
}
}

fn main() {
let click = Click { x: 10, y: 10 };
let event = &click as &dyn WebEvent;
let json = serde_json::to_string(event).unwrap();
println!("Click json: {}", json);
let de: Box<dyn WebEvent> =
serde_json::from_str(&json).unwrap();
de.inspect();

let back: &Click = de.downcast_ref::<Click>().unwrap();
back.inspect();
}

Now we can serialize to JSON using a trait object, deserialize from JSON using a trait object, and downcast to a concrete struct when needed.

--

--