Slide 1

Slide 1 text

Armin @mitsuhiko Ronacher How MiniJinja Works Internally Runtime Objects in Rust

Slide 2

Slide 2 text

buckle up …

Slide 3

Slide 3 text

this will be rough

Slide 4

Slide 4 text

“ugh unsafe”

Slide 5

Slide 5 text

MiniJinja A Rust Implementation of Jinja2 • https://github.com/mitsuhiko/minijinja • https://docs.rs/minijinja/ • Follow me on Twitter^W 𝕏 : @mitsuhiko

Slide 6

Slide 6 text

The Problem What are we trying to do here? • “Implement Jinja2 Templates for Rust” • Sometimes you need to generate text/HTML etc. from templates • These templates are executed at runtime • How do you expose Rust objects into the template engine? • How do you extract Rust objects out of the engine again?

Slide 7

Slide 7 text

Hello Schikaneder! - 1 - 2 - 3 use minijinja :: {Value, render}; fn main() { let rv = render!(r#" Hello {{ name|title }}! {%- for item in seq %} - {{ item }} {%- endfor %} "#, name => "schikaneder", seq => Value : : from(vec![1, 2, 3])); println!("{}", rv); }

Slide 8

Slide 8 text

[1, 2, 3] (sequence) [1, 2, 3] use minijinja : : Value; fn main() { let value = Value : : from_object(vec![1i32, 2, 3]); println!("{} ({})", value, value.kind()); let v: &Vec = value.downcast_object_ref().unwrap(); println!("{:?}", value); }

Slide 9

Slide 9 text

Dynamic Language Lifetimes and Garbage Collection • Value is reference counted • Internally holds something similar to an Arc • Clone on Value -> Increment refcount • How to represent objects?

Slide 10

Slide 10 text

pub trait Object: Debug + Send + Sync { fn repr(self: &Arc) -> ObjectRepr { ObjectRepr : : Map } fn get_value(self: &Arc, key: &Value) - > Option { None } fn enumerate(self: &Arc) -> Enumerator { Enumerator : : NonEnumerable } . . . }

Slide 11

Slide 11 text

pub enum ObjectRepr { Plain, Map, Seq, Iterable, } pub enum Enumerator { NonEnumerable, Empty, Str(&'static [&'static str]), Iter(Box + Send + Sync>), Seq(usize), ... }

Slide 12

Slide 12 text

#[ derive(Debug)] struct Point(f32, f32); impl Object for Point { fn repr(self: &Arc) - > ObjectRepr { ObjectRepr : : Seq } fn get_value(self: &Arc, key: &Value) -> Option { match key.as_usize()? { 0 = > Some(Value : : from(self.0)), 1 = > Some(Value : : from(self.1)), _ = > None, } } fn enumerate(self: &Arc) -> Enumerator { Enumerator : : Seq(2) } } let seq_point = Value : : from_object(Point(1.0, 2.5));

Slide 13

Slide 13 text

#[ derive(Debug)] struct Point(f32, f32); impl Object for Point { fn repr(self: &Arc) - > ObjectRepr { ObjectRepr : : Map } fn get_value(self: &Arc, key: &Value) -> Option { match key.as_str()? { "x" = > Some(Value :: from(self.0)), "y" = > Some(Value :: from(self.1)), _ = > None, } } fn enumerate(self: &Arc) -> Enumerator { Enumerator : : Str(&["x", "y"]) } } let map_point = Value : : from_object(Point(1.0, 2.5));

Slide 14

Slide 14 text

{{ seq_point }} [1.0, 2.5] {{ seq_point[0] }} 1.0 {{ seq_point|list }} [1.0, 2.5] {{ map_point }} {"x": 1.0, "y": 2.5} {{ map_point.x }} 1.0 {{ map_point|list }} ["x", "y"]

Slide 15

Slide 15 text

Values and Objects In short • Values hold primitives • integers • strings (Arc) • etc. • Values hold objects • Arc

Slide 16

Slide 16 text

error[E0038]: the trait `Object` cannot be made into an object | 4 | fn do_something(self: &Arc); | ---------- help: consider changing method `do_something`'s `self` parameter to be `&self`: `&Self` ... 14 | let obj = Arc :: new(X) as Arc; | ^^^^^^^^^^^^^^^ `Object` cannot be made into an object | note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically | 3 | trait Object { | ------ this trait cannot be made into an object . .. 4 | fn do_something(self: &Arc); | ^^^^^^^^^^ . . . because method `do_something`'s `self` parameter cannot be dispatched on use std :: sync : : Arc; trait Object { fn do_something(self: &Arc); } struct X; impl Object for X { fn do_something(self: &Arc) { } } let obj = Arc : : new(X) as Arc;

Slide 17

Slide 17 text

Error is Revealing “for a trait to be "object safe" it needs to allow building a vtable” • Rust cannot build a vtable • A vtable is a struct of virtual functions • Can we do it ourselves? • Plan: replace Arc with a custom DynObject

Slide 18

Slide 18 text

pub struct DynObject { ptr: Arc, vtable: &'static DynObjectVTable, } struct VTable { repr: fn(&Arc) -> ObjectRepr, get_value: fn(&Arc, key: &Value) -> Option, enumerate: fn(&Arc) -> Enumerator, type_id: fn() -> TypeId, type_name: fn() - > &'static str, drop: fn(Arc), } too big, can we just store a pointer? and how do we accomplish this? and how do we auto generate this?!

Slide 19

Slide 19 text

type_erase! { pub trait Object = > DynObject { fn repr(&self) - > ObjectRepr; fn get_value(&self, key: &Value) -> Option; fn enumerate(&self) - > Enumerator; } }

Slide 20

Slide 20 text

pub struct DynObject { ptr: *const (), vtable: *const (), } const _: () = { struct VTable { repr: fn(*const ()) -> ObjectRepr, get_value: fn(*const (), key: &Value) - > Option, enumerate: fn(*const ()) -> Enumerator, __type_id: fn() -> TypeId, __type_name: fn() - > &'static str, __drop: fn(*const ()), } fn vt(e: &DynObject) -> &VTable { unsafe { &*(e.vtable as *const VTable) } } impl DynObject { . .. } } rust for "void raw pointer" trick to declare a hidden type in scope lots of raw pointers casts the void pointer to our vtable brace for impact

Slide 21

Slide 21 text

impl DynObject { pub fn new(v: Arc) -> Self { let ptr = Arc : : into_raw(v) as *const T as *const (); let vtable = &VTable { repr: |ptr| unsafe { Arc : : : : increment_strong_count(ptr as *const T); let arc = Arc :: :: from_raw(ptr as *const T); :: repr(&arc) }, __type_id: || TypeId :: of :: (), __type_name: || type_name :: (), __drop: |ptr| unsafe { Arc : : from_raw(ptr as *const T); }, }; Self { ptr, vtable: vtable as *const VTable as *const (), } } pub fn repr(&self) - > ObjectRepr { (vt(self).repr)(self.ptr) } } convert to raw pointer panic safety! reconstruct the Arc invoke drop invoke trampoline via vtable

Slide 22

Slide 22 text

impl DynObject { pub fn downcast_ref(&self) - > Option<&T> { if (vt(self).__type_id)() = = TypeId : : of : : () { unsafe { return Some(&*(self.ptr as *const T)); } } None } pub fn downcast(&self) -> Option > { if (vt(self).__type_id)() = = TypeId : : of : : () { unsafe { Arc : : :: increment_strong_count(self.ptr as *const T); return Some(Arc :: :: from_raw(self.ptr as *const T)); } } None } } only downcast if compatible type

Slide 23

Slide 23 text

impl Clone for DynObject { fn clone(&self) -> Self { unsafe { std :: sync :: Arc : : increment_strong_count(self.ptr); } Self { ptr: self.ptr, vtable: self.vtable, } } } impl Drop for DynObject { fn drop(&mut self) { (vt(self).__drop)(self.ptr); } }

Slide 24

Slide 24 text

So… how to codegen?

Slide 25

Slide 25 text

macro_rules! type_erase { ($v:vis trait $t_name:ident => $erased_t_name:ident { $(fn $f:ident(&self $(, $p:ident: $t:ty $(,)?)*) $( -> $r:ty)?;)* }) => { $v struct $erased_t_name { ptr: *const (), vtable: *const (), } const _: () = { struct VTable { $($f: fn(*const (), $($p: $t),*) $( -> $r)?,)* __type_id: fn() -> TypeId, __type_name: fn() -> &'static str, __drop: fn(*const ()), } fn vt(e: &$erased_t_name) - > &VTable { unsafe { &*(e.vtable as *const VTable) } } impl $erased_t_name { / / ... } }; }; } match on function declarations trait Object => DynObject our scope trick again cast to VTable we have in scope generate all the function pointer slots

Slide 26

Slide 26 text

impl $erased_t_name { $v fn new(v: Arc) - > Self { let ptr = Arc :: into_raw(v) as *const T as *const (); let vtable = &VTable { $( $f: |ptr, $($p),*| unsafe { Arc : : :: increment_strong_count(ptr as *const T); let arc = Arc : : :: from_raw(ptr as *const T); : : $f(&arc, $($p),*) }, )* __type_id: || TypeId : : of : : (), __type_name: || type_name : : (), __drop: |ptr| unsafe { Arc :: from_raw(ptr as *const T); }, }; Self { ptr, vtable: vtable as *const VTable as *const () } } } generate all trampolines

Slide 27

Slide 27 text

impl $erased_t_name { $( $v fn $f(&self, $($p: $t),*) $( -> $r)? { (vt(self).$f)(self.ptr, $($p),*) } )* $v fn type_name(&self) -> &'static str { (vt(self).__type_name)() } $v fn downcast_ref(&self) - > Option<&T> { if (vt(self).__type_id)() == TypeId :: of : : () { unsafe { Some(&*(self.ptr as *const T)) } } else { None } } $v fn downcast(&self) -> Option> { if (vt(self).__type_id)() == TypeId :: of : : () { unsafe { Arc :: : : increment_strong_count(self.ptr as *const T); Some(Arc : : :: from_raw(self.ptr as *const T)); } } else { None } } } generate wrapper methods type name accessor cast helper owned cast helper

Slide 28

Slide 28 text

Documentation

Slide 29

Slide 29 text

#[ doc = concat!("Type-erased version of [`", stringify!($t_name), "`]")] $v struct $erased_t_name { ptr: *const (), vtable: *const (), } $( # [ doc = concat!( "Calls [`", stringify!($t_name), " :: ", stringify!($f), "`] of the underlying boxed value." )] $v fn $f(&self, $($p: $t),*) $( -> $r)? { (vt(self).$f)(self.ptr, $($p),*) } )* generate documentation comments from pieces

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

Putting it Together

Slide 32

Slide 32 text

#[ derive(Clone)] pub(crate) enum ValueRepr { Undefined, Bool(bool), U64(u64), I64(i64), F64(f64), None, String(Arc, StringType), Bytes(Arc> ), Object(DynObject), } #[ derive(Clone)] pub struct Value(pub(crate) ValueRepr);

Slide 33

Slide 33 text

impl Value { pub fn from_object(value: T) - > Value { Value :: from(ValueRepr : : Object(DynObject :: new(Arc :: new(value)))) } pub fn downcast_object_ref(&self) -> Option<&T> { match self.0 { ValueRepr : : Object(ref o) => o.downcast_ref(), _ => None, } } pub fn downcast_object(&self) - > Option> { match self.0 { ValueRepr : : Object(ref o) => o.downcast(), _ => None, } } }

Slide 34

Slide 34 text

fin.