Skip to content

Compiling and running in WASM

Eduardo Bart edited this page Mar 21, 2024 · 20 revisions

How to compile for WASM

The Cartesi machine emulator library can be compiled for WASM and used in the frontend of web applications. Here we will guide step-by-step on how to do this.

1. Prerequisites

First make sure you have:

  • Git, wget, and other build utilities.
  • Emscripten toolchain installed and ready for use

You can install them on Ubuntu with:

apt-get install git wget build-essential emscripten

If you are not downloading uarch/uarch-pristine-hash.c and uarch/uarch-pristine-ram.c files for your Emulator release, you will need Docker to build them.

2. Clone:

git clone --recurse-submodules https://github.com/cartesi/machine-emulator.git
cd machine-emulator

3. Download boost and other dependencies:

make downloads bundle-boost

4. Compile libcartesi.a

make CC=emcc CXX=em++ AR="emar rcs" slirp=no release=yes libcartesi.a

5. Generate a WASM package

make STRIP=emstrip EMU_TO_LIB_A=src/libcartesi.a DESTDIR=wasm-pkg install-headers install-static-libs

Now you should have a directory with all required files in wasm-pkg for building C applications targeting WASM with the Emscripten toolchain:

$ tree wasm-pkg
wasm-pkg/
└── usr
    ├── include
    │   └── cartesi-machine
    │       ├── htif-defines.h
    │       ├── jsonrpc-machine-c-api.h
    │       ├── machine-c-api.h
    │       ├── machine-c-defines.h
    │       ├── machine-c-version.h
    │       ├── pma-defines.h
    │       ├── rtc-defines.h
    │       └── uarch-defines.h
    └── lib
        └── libcartesi.a

You should use libcartesi.a to link to your WASM application, and the cartesi-machine/machine-c-api.h header for using the cartesi machine C API.

Testing

Create a test.c file with the following contents:

#include <stdio.h>
#include <cartesi-machine/machine-c-api.h>

int main() {
    // Initialize a new machine config from defaults
    const cm_machine_config* def_config = cm_new_default_machine_config();
    if (!def_config) {
        printf("failed to get default machine config\n");
        return -1;
    }
    cm_machine_config config = *def_config;

    // Adjust initial machine config
    config.ram.image_filename = "linux.bin";
    config.ram.length = 128 * 1024 * 1024;
    config.dtb.bootargs = "no4lvl quiet earlycon=sbi console=hvc0 rootfstype=ext2 root=/dev/pmem0 rw init=/usr/sbin/cartesi-init";
    config.dtb.entrypoint = "uname -a; echo Hello from Cartesi Machine!";

    // Adjust flash drive configs
    cm_memory_range_config flash_drives[1] = {0};
    flash_drives[0].image_filename = "rootfs.ext2";
    flash_drives[0].start = 0x80000000000000;
    flash_drives[0].length = UINT64_MAX;
    config.flash_drive.entry = flash_drives;
    config.flash_drive.count = 1;

    // Set machine runtime config
    cm_machine_runtime_config runtime_config = {0};

    // Create the machine
    cm_machine *machine = NULL;
    char *err_msg = NULL;
    int err = cm_create_machine(&config, &runtime_config, &machine, &err_msg);
    if (err != 0) {
        printf("failed to create machine: %s\n", err_msg);
        cm_delete_cstring(err_msg);
        cm_delete_machine_config(def_config);
        return -1;
    }

    // Run the machine until it halts
    CM_BREAK_REASON break_reason;
    err = cm_machine_run(machine, UINT64_MAX, &break_reason, &err_msg);
    if (err != 0) {
        printf("failed to run machine: %s\n", err_msg);
        cm_delete_cstring(err_msg);
        cm_delete_machine(machine);
        cm_delete_machine_config(def_config);
        return -1;
    }

    // Cleanup
    cm_delete_machine(machine);
    cm_delete_machine_config(def_config);
    return 0;
}

Before compiling, make sure you have linux.bin and rootfs.ext2 files available, these files will be used to boot the machine. You can download them with:

wget -O rootfs.ext2 https://github.com/cartesi/machine-emulator-tools/releases/download/v0.14.1/rootfs-tools-v0.14.1.ext2
wget -O linux.bin https://github.com/cartesi/image-kernel/releases/download/v0.19.1/linux-6.5.9-ctsi-1-v0.19.1.bin

Now you can compile with:

emcc test.c -o test.html \
    -O3 \
    -I./wasm-pkg/usr/include -L./wasm-pkg/usr/lib -lcartesi \
    --embed-file linux.bin@linux.bin \
    --embed-file rootfs.ext2@rootfs.ext2 \
    -sSTACK_SIZE=4MB \
    -sTOTAL_MEMORY=512MB \
    -sNO_DISABLE_EXCEPTION_CATCHING

The files test.html, test.js and test.wasm will be compiled, and be ready for testing in a web application:

ls -la test.*
-rwxr-xr-x 1 user user 100M Feb  2 12:56 test.wasm
-rw-r--r-- 1 user user  84K Feb  2 12:56 test.js
-rw-r--r-- 1 user user  20K Feb  2 12:56 test.html
-rw-r--r-- 1 user user 1.8K Feb  2 12:56 test.c

You can quickly test in your browser with emrun tool, which is a very simple HTTP server:

emrun test.html

If everything works, a page will open in your web browser, and after a few seconds the machine will boot with a message like:

Linux localhost.localdomain 6.5.9-ctsi-1 #1 Tue, 05 Dec 2023 15:20:56 +0000 riscv64 riscv64 riscv64 GNU/Linux

Hello from Cartesi Machine!

Here is a screenshot:

image

Compile options

When compiling with emcc a few options were used, here is the reason for each one:

  • -O3 optimizes the build. It's recommended to always generate optimized builds for WASM, otherwise the WASM file will be too big and slow, some browsers may even fail to load.
  • -I./wasm-pkg/usr/include -L./wasm-pkg/usr/lib -lcartesi exposes the libcartesi headers and library for linking.
  • --embed-file options will bundle kernel and rootfs files used by the machine.
  • -sSTACK_SIZE=4MB is mandatory, otherwise the "stack size" will be too small for running the emulator.
  • -sTOTAL_MEMORY=512MB is mandatory, otherwise WASM memory will be too small for running the machine, this can be adjusted though depending on the size of kernel, rootfs and machine memory.
  • -sNO_DISABLE_EXCEPTION_CATCHING helps on giving tracebacks for any error in case of any.

Machine interoperability with HTML5

All this is only useful if you can interact with environment inside the machine in the HTML5 page, but how can you do with?

You can pass messages between the C code and anything running inside the emulator with the yield and rollup devices. By yielding from inside the machine cm_machine_run will break, and you can read/write to rollup buffers to exchange messages, check the official Cartesi Machine documentation on how to do this.

Once your messages are in the C code, you can propagate to javascript and HTML5 web page, please check Emscripten documentation on how to call between C and JavaScript code.