Tutorial: Hello Thread, Hello File

This tutorial creates a simple app that accepts files via drag-and-drop, and prints their contents to the console.

We build on the Hello World tutorial by modifying it in pieces. You can follow along by modifying those files, or via the completed zaplib/examples/tutorial_hello_thread example.

Universal interfaces

Many standard Rust functions don't work in WebAssembly out-of-the-box, such as threading or reading files. Zaplib provides a number of universal interfaces that work both natively and in WebAssembly. This tutorial demonstrates two cross-platform Zaplib abstractions:

  1. universal_thread
  2. UniversalFile

1. A Simple Loop

Let's start by logging to the console multiple times:

for i in 0..3 {
    log!("Hello, world! {i}");
}

You can run these examples either natively or in the browser. How to run a Zaplib app is covered in Getting Started and the Hello World tutorial.

The output is as expected:

zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 0
zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 1
zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 2

2. universal_thread

Let's put each log! in a thread:

for i in 0..3 {
    universal_thread::spawn(move || {
        log!("Hello, world! {i}");
    });
}

Now the ordering of the print statements is non-deterministic:

zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 2
zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 0
zaplib/examples/tutorial_hello_world_console/src/main.rs:22 - Hello, world! 1

Zaplib's universal_thread was designed to be a drop-in replacement for Rust's std::thread with added support for WebAssembly. Natively it uses std::thread. In WebAssembly it uses Web Workers.

If you're familiar with Web Workers, you'll notice how much easier Zaplib makes working with threads! The equivalent JavaScript using Web Workers and postMessage would be significantly more verbose, across multiple files. While threading is fairly advanced Rust, we think it is ultimately easier than threading in JavaScript.

3. UniversalFile

Let's read some files!

In Rust, you'd normally use std::file::File, but it is not available in WebAssembly, so we use UniversalFile. Natively it uses std::file::File. In WebAssembly it makes an HTTP request for the file. The following example reads the project's Cargo.toml file. It works both natively by reading the file locally, like normal. It works in WebAssembly if your file server also serves your Cargo.toml file.

First, add the following import to the top of your file:

use std::io::Read;

Next, let's write replace the loop of logging threads with:

let path = "zaplib/examples/tutorial_hello_world_console/Cargo.toml";
let mut file = UniversalFile::open(path).unwrap(); // open the file
let mut contents = String::new();                  // a string to hold the contents
file.read_to_string(&mut contents).unwrap();       // read the file into the string
log!("Contents of Cargo.toml: {contents}");        // log the file's contents

Note that this is a synchronous API: read_to_string will block until the whole file is read. JavaScript typically solves this via Promise, async, and await. While Rust does have async capabilities, we find that it quickly land you in "async hell" with Rust's borrow checker. In Zaplib — and low-level programming in general — we instead use threads to do things in an unblocking way:

universal_thread::spawn(move || {
    let path = "zaplib/examples/tutorial_hello_world_console/Cargo.toml";
    let mut file = UniversalFile::open(path).unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    log!("Contents of Cargo.toml: {contents}");
});

Since we're using a standard API interface, this code will work with any library that accepts a std::io::Read object, as opposed to WebAssembly libraries that expose more exotic asynchronous APIs.

Drag & drop files

Now let's put it all together. This might be a bit overwhelming all at once, but it gives you a glimpse into how various APIs work, such as drawing, event handling, threading, and file reading.

use std::io::Read;

use zaplib::*;

#[derive(Default)]
struct App {
    window: Window,
}

impl App {
    fn new(_cx: &mut Cx) -> Self {
        Self { window: Window { create_add_drop_target_for_app_open_files: true, ..Window::default() } }
    }

    fn handle(&mut self, _cx: &mut Cx, event: &mut Event) {
        if let Event::AppOpenFiles(aof) = event {
            // Get a copy of the file handle for use in the thread.
            let mut file = aof.user_files[0].file.clone();

            universal_thread::spawn(move || {
                let mut contents = String::new();
                file.read_to_string(&mut contents).unwrap();
                log!("Contents of dropped file: {contents}");
            });
        }
    }

    fn draw(&mut self, cx: &mut Cx) {
        self.window.begin_window(cx);
        self.window.end_window(cx);
    }
}

main_app!(App);

This code is from zaplib/examples/tutorial_hello_thread, so you can just run cargo run -p tutorial_hello_thread to run it natively.

Run this either natively or in WebAssembly, and then drag in a small text file onto the screen. It should print the contents to the console. Since we did the file reading in a thread, it won't block any other code; though in this example it's hard to tell the difference both because there's no other compute happening and because Zaplib runs your Rust code in a Web Worker anyway. 😉

If you're actually going to do file reading, be sure to read up on the std::file::File documentation, since the advice there still applies (e.g. it's often a good idea to wrap things in a BufReader).