Host a Wasm module easily on Raspberry Pi Part 2

Knoldus Blog Audio
Reading Time: 6 minutes

This blog is a continuation of one of my previous blogs, if you have not checked that out, then here is the link. You can read that and then continue on this blog. This blog is a part of a series on WebAssembly. I have written several other blogs on wasm, wabt , wasm-bindgen and wasmi, etc. Feel free to check them out also. WebAssembly provides web developers with a new way of making applications available online. Previously, the only option was JavaScript. The problem is that JavaScript is relatively slower and performance can be low in certain situations. Thus, the World Wide Web Consortium (W3C) came up with a new method – WebAssembly. In order for Wasm to function, the browser used must be able to handle the language. Now, C, C++ and Rust can now all be used to program web applications as they have wasm as one of the compile targets.

More than 20 companies reportedly use WebAssembly in their tech stacks, including Backend, Foretag, and Cubbit. WebAssembly is a widely used compile target for Rust. Rust also helps us to host wasm modules easily. This enables us to use an IoT device like Raspberry Pi as a host.

Overview

In the last blog, I described how to make a battery indicator to use on our Raspberry pi. In this blog, I will show you how to write a host code in Rust that will make our Raspberry Pi a suitable host for wasm. So let’s see how can we do this.

First of all, we will add the armv7 compilation target to our Rust because the Raspberry Pi 2+ devices are ARM v7 devices. So, we need to compile the host program that we would write in Rust to armv7. To add the target we will run this command in the terminal.

$ rustup target add armv7-unknown-linux-gnueabihf

Next, configure Cargo for cross-compilation using the instructions on the GitHub repository. For Ubuntu, you need to run the following commands.

mkdir -p ~/.cargo
$ cat >>~/.cargo/config <<EOF
> [target.armv7-unknown-linux-gnueabihf]
> linker = "arm-linux-gnueabihf-gcc"
> EOF

Now we are all set. Create a new binary Rust project and write the host code.

Add the following dependencies to the .toml file

[package]
name = "pihost"
version = "0.1.0"
authors = ["aman2909verma <aman.verma@knoldus.com>"]

[dependencies]
notify = "4.0.0"
wasmi = "0.4.1"
ctrlc = { version = "3.0", features = ["termination"] }
[target.'cfg(any(target_arch = "arm", target_arch = "armv7"))'.dependencies]
blinkt = "0.4.0"

The blinkt dependency will help us interact with the blinkt! hardware.

Watching new modules

We need to continuously watch for the new wasm module and notify the main runner program to reload the wasm module when a new module is available. Add the following code to the main.rs file in the pihost module.

#[cfg(any(target_arch = "armv7", target_arch = "arm"))]
extern crate blinkt;

extern crate ctrlc;
extern crate notify;
extern crate wasmi;

use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::sync::mpsc::{channel, RecvTimeoutError, Sender};
use std::thread;
use std::time::Duration;
use wasm::Runtime;
use wasmi::RuntimeValue;

const MODULE_FILE: &'static str = "/home/kevin/indicators/indicator.wasm";
const MODULE_DIR: &'static str = "/home/kevin/indicators";

enum RunnerCommand {
    Reload,
    Stop,
}

fn watch(tx_wasm: Sender<RunnerCommand>) -> notify::Result<()> {
    let (tx, rx) = channel(); 

    let mut watcher: RecommendedWatcher =
        Watcher::new(tx, Duration::from_secs(1))?;
    watcher.watch(MODULE_DIR, RecursiveMode::NonRecursive)?;

    loop {
        match rx.recv() { 
            Ok(event) => handle_event(event, &tx_wasm),
            Err(e) => println!("watch error: {:?}", e),
        }
    }
}

fn handle_event(event: DebouncedEvent, tx_wasm: &Sender<RunnerCommand>) {
    match event {
        DebouncedEvent::NoticeWrite(path) => {
            let path = Path::new(&path);
            let filename = path.file_name().unwrap();
            if filename == "indicator.wasm" {
                tx_wasm.send(RunnerCommand::Reload).unwrap(); 
            } else {
                println!("write (unexpected file): {:?}", path);
            }
        }
        _ => {}
    }
}

watch() method

In the watch method, we create a multi-producer, single-consumer communication channel and then use a loop to block the receive channel until a message arrives.

handle() method

This method sends a message on the channel, indicating that we should reload the WebAssembly module. The wasm module should have the name indicator.wasm.

In Rust, multi-producer-single-consumer channels are used as a safe way to communicate between threads. In the case of the watch() function, the main thread sits in a loop, awaiting file system notifications from the monitored
directory. When we see the NoticeWrite event, it’s time to tell another thread to reload the module from the disk.

Main Thread

Now, we create the main thread. This thread has a number of jobs. It obviously needs to listen for the RunnerCommand::Reload message, but it also needs to handle the RunnerCommand::Stop message.

fn main() {
    let (tx_wasm, rx_wasm) = channel();
    let _indicator_runner = thread::spawn(move || {
        let mut runtime = Runtime::new();
        let mut module = wasm::get_module_instance(MODULE_FILE);
        println!("Starting wasm runner thread...");
        loop {
            match rx_wasm.recv_timeout(Duration::from_millis(100)) { 
                Ok(RunnerCommand::Reload) => {
                    println!("Received a reload signal, sleeping for 2s");
                    thread::sleep(Duration::from_secs(2));
                    module = wasm::get_module_instance(MODULE_FILE);
                }
                Ok(RunnerCommand::Stop) => {
                    runtime.shutdown();
                    break;
                }
                Err(RecvTimeoutError::Timeout) => {
                    runtime.reduce_battery();
                    runtime.advance_frame();
                    module
                        .invoke_export(
                            "sensor_update",
                            &[
                                RuntimeValue::from(wasm::SENSOR_BATTERY),
                                RuntimeValue::F64(
                                 runtime.remaining_battery.into()),
                            ][..],
                            &mut runtime,
                        ).unwrap();

                    module
                        .invoke_export(
                            "apply",
                            &[RuntimeValue::from(runtime.frame)][..],
                            &mut runtime,
                        ).unwrap();
                }
                Err(_) => break,
            }
        }
    });

    let tx_wasm_sig = tx_wasm.clone();  

    ctrlc::set_handler(move || { 
        tx_wasm_sig.send(RunnerCommand::Stop).unwrap();
    }).expect("Error setting Ctrl-C handler");

    if let Err(e) = watch(tx_wasm) { 
        println!("error: {:?}", e)
    }
}

mod wasm;

rx_wasm.recv_timeout enforce the frame rate with a 100ms timeout value on receive. We can clone send channels and thus their presence in the multi-producer module as we did with tx_wasm.clone().

We use the ctrlc crate to trap SIGTERM and SIGINT, sending a Stop command in response. The watch() function blocks the main thread with an infinite loop.

WebAssembly Module Runtime

Now we make the host program using the wasmi crate that we saw in the previous blog. We create the wasm.rs file referred to before.

use std::fmt;
use std::fs::File;
use wasmi::{
    Error as InterpreterError, Externals, FuncInstance, FuncRef,
    HostError, ImportsBuilder, Module, ModuleImportResolver, ModuleInstance,
    ModuleRef, RuntimeArgs, RuntimeValue, Signature, Trap, ValueType,
};

#[cfg(any(target_arch = "armv7", target_arch = "arm"))]
use blinkt::Blinkt;

fn load_module(path: &str) -> Module {
    use std::io::prelude::*;
    let mut file = File::open(path).unwrap();
    let mut wasm_buf = Vec::new();
    file.read_to_end(&mut wasm_buf).unwrap();
    Module::from_buffer(&wasm_buf).unwrap()
}

pub fn get_module_instance(path: &str) -> ModuleRef {
    let module = load_module(path);
    let mut imports = ImportsBuilder::new();
    imports.push_resolver("env", &RuntimeModuleImportResolver);

    ModuleInstance::new(&module, &imports)
        .expect("Failed to instantiate module")
        .assert_no_start()
}

pub const SENSOR_BATTERY: i32 = 20;


#[derive(Debug)]
pub enum Error {
    Interpreter(InterpreterError),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl From<InterpreterError> for Error {
    fn from(e: InterpreterError) -> Self {
        Error::Interpreter(e)
    }
}

impl HostError for Error {}

pub struct Runtime {
    #[cfg(any(target_arch = "armv7", target_arch = "arm"))]
    blinkt: Blinkt, // (2)
    pub frame: i32,
    pub remaining_battery: f64,
}

impl Runtime {
    #[cfg(any(target_arch = "armv7", target_arch = "arm"))]
    pub fn new() -> Runtime {
        println!("Instiantiating WASM runtime (ARM)");
        Runtime {
            blinkt: Blinkt::new().unwrap(),
            frame: 0,
            remaining_battery: 100.0,
        }
    }

    #[cfg(not(any(target_arch = "armv7", target_arch = "arm")))]
    pub fn new() -> Runtime {
        println!("Instantiating WASM runtime (non-ARM)");
        Runtime {
            frame: 0,
            remaining_battery: 100.0,
        }
    }
}

impl Externals for Runtime {
    fn invoke_index(
        &mut self,
        index: usize,
        args: RuntimeArgs,
    ) -> Result<Option<RuntimeValue>, Trap> {

        match index { 
            0 => {
                let idx: i32 = args.nth(0);
                let red: i32 = args.nth(1);
                let green: i32 = args.nth(2);
                let blue: i32 = args.nth(3);
                self.set_led(idx, red, green, blue);
                Ok(None)
            }
            _ => panic!("Unknown function index!"),
        }
    }
}

impl Runtime {
    #[cfg(not(any(target_arch = "armv7", target_arch = "arm")))]
    fn set_led(&self, idx: i32, red: i32, green: i32, blue: i32) {
        println!("[LED {}]: {}, {}, {}", idx, red, green, blue);
    }

    #[cfg(any(target_arch = "armv7", target_arch = "arm"))]
    fn set_led(&mut self, idx: i32, red: i32, green: i32, blue: i32) {
        self.blinkt
            .set_pixel(idx as usize, red as u8, green as u8, blue as u8);
        self.blinkt.show().unwrap();
    }

    #[cfg(not(any(target_arch = "armv7", target_arch = "arm")))]
    pub fn shutdown(&mut self) {
        println!("WASM runtime shut down.");
        self.halt();
    }

    #[cfg(any(target_arch = "armv7", target_arch = "arm"))]
    pub fn shutdown(&mut self) {
        println!("WASM runtime shut down.");
        self.blinkt.clear();
        self.blinkt.cleanup().unwrap();
        self.halt();
    }

    fn halt(&self) {
        ::std::process::exit(0);
    }

    pub fn reduce_battery(&mut self) {
        self.remaining_battery -= 1.0;
        if self.remaining_battery < 0.0 {
            self.remaining_battery = 100.0;
        }
    }

    pub fn advance_frame(&mut self) {
        self.frame += 1;
        if self.frame > 1_000_000_000 {
            self.frame = 0;
        }
    }
}

struct RuntimeModuleImportResolver;

impl<'a> ModuleImportResolver for RuntimeModuleImportResolver {
    fn resolve_func(
        &self,
        field_name: &str,
        _signature: &Signature,
    ) -> Result<FuncRef, InterpreterError> {
        println!("Resolving {}", field_name);
        let func_ref = match field_name {
            "set_led" => FuncInstance::alloc_host(
                Signature::new(
                    &[
                        ValueType::I32,
                        ValueType::I32,
                        ValueType::I32,
                        ValueType::I32,
                    ][..],
                    None,
                ),
                0,
            ),
            _ => {
                return Err(InterpreterError::Function(format!(
                    "host module doesn't export function with name {}",
                    field_name
                )))
            }
        };
        Ok(func_ref)
    }
}

This is the same boilerplate code which we would use to make a Rust host. Resolving the imports by using the RuntimeModuleImportResolver structure that implements the ModuleImportResolver trait.

Then, creating a Runtime for Externals by implementing Externals trait for the Runtime struct. Each time the host invokes a module function, it has to pass something that implements the Externals trait. This is typically referred to as the runtime, and it’s what allows the module to invoke imported functions.

You may have noticed there are actually multiple functions with the same name in the implementation of the Runtime. If we’re building for ARM, then the Runtime struct gets an extra field called blinkt and the set_led() function uses that field to control real hardware, otherwise, the function just prints some text to the console.

Everything is set, you can now run this program by cargo run command and see it working. If you have the real hardware then you can use this code to host the wasm module on a Raspberry Pi as well.


If you want to read more content like this?  Subscribe to Rust Times Newsletter and receive insights and latest updates, bi-weekly, straight into your inbox. Subscribe to Rust Times Newsletter: https://bit.ly/2Vdlld7.


host raspberry pi

Leave a Reply