Toggle Menu

1.1 dmo.rs - State and content

Dmo holds the application state and data content we need to access when running the code.

The Vec<Operator> will be used to build the JIT fn. When calling it, the JIT fn will have access to Context, so this needs the data which we operate on – text sprites, print buffer and so on.

We expect the text sprites in Unicode (more fun characters), so we use Vec<char> instead of a Vec<u8>. A char is 4-bytes to represent UTF-32 encoding.

The Context and Vec<Operator> are private to make them only accessible through API calls which should remember to rebuild the JitFn as well.

// src/dmo.rs

pub struct Dmo {
    context: Context,
    operators: Vec<Operator>,
    jit_fn: JitFn,
}

pub struct Context {
    pub sprites: Vec<String>,
    pub buffer: Vec<char>,
    pub is_running: bool,
    pub time: f32,
}

pub enum Operator {
    /// No operation
    NOOP,
    /// Exit the main loop if time is greater than this value
    Exit(f32),
    /// Print the text buffer
    Print,
    /// Draw a sprite into the buffer: sprite idx, offset, time speed
    Draw(u8, u8, f32),
    /// Clear the text buffer with a character code, expect UTF-32 unicode
    Clear(u32),
}

The Dmo data could come from a GUI. Here we will just design our demo by writing a YAML file and deserializing it with serde.

Such as this for the earlier ASCII example:

# examples/fish-demo.yml

operators:
  - Clear: 32
  - Draw: [ 3, 0, 0.3 ]
  - Draw: [ 1, 5, 8.0 ]
  - Draw: [ 0, 2, 1.5 ]
  - Draw: [ 1, 0, 4.0 ]
  - Draw: [ 2, 3, 6.0 ]
  - Print
  - Exit: 30.0

context:
  sprites:
    - " ><(([°> "
    - " ><> "
    - " }-<ø> "
    - "¸¸,,¸¸,¸¸,,¸¸,¸¸,,¸¸,¸¸,,¸¸,¸¸,,¸¸,¸¸,,¸¸,¸¸,,¸¸,,"

This directly deserializes into our Dmo struct.

From the YAML we get the Dmo, then we will implement a Dmo::to_bytecode() which encodes it as a Vec<u8>.

// src/dmo.rs

impl Dmo {
    pub fn new_from_yml_str(text: &str) -> Result<Dmo, Box<Error>> {
        let dmo: Dmo = try!(serde_yaml::from_str(text));
        Ok(dmo)
    }

    pub fn write_to_blob(&self, path: &PathBuf) -> Result<(), Box<Error>> {
        let mut f: File = try!(File::create(&path));
        let bytecode = self.to_bytecode();
        f.write_all(bytecode.as_slice()).unwrap();
        Ok(())
    }
}

Two steps are necessary: First, the Dmo has to be assigned to a variable, then the JIT function is assembled.

After assignment, the Context and Operators have a memory address which the JIT function will be able to use.

let mut dmo = Dmo::from_bytecode(bytecode);
dmo.build_jit_fn();

The bytecode is a Vec<u8> which represents the whole demo and can be replayed by a small utility. We write the bytecode to disk, but the standalone replay doesn’t even need to read files, it is enough to include_bytes!() at compile time, reconstruct the Dmo and it’s ready to go.

// examples/fish-standalone.rs

pub fn main() {
    // Read in at compile time, include path is relative to the .rs file.
    let bytecode = include_bytes!("./fish-demo.dmo").to_vec();

    let mut dmo = Dmo::from_bytecode(bytecode);
    dmo.build_jit_fn();

    print!("\n");

    while dmo.get_is_running() {
        dmo.run_jit_fn();
        sleep(Duration::from_millis(10));
        dmo.add_to_time(0.01);
    }

    print!("\n");
}