Zig on the Web

Compiling Zig to WASM

TCombinator published on
6 min, 1166 words

Zig is an actual modern alternative to C. It is a new systems programming language that provides native C inter-op with better type security. Learn how to build Zig applications for the Web and make them future-proof.

Table of contents

  1. Introduction
  2. Why WASM?
  3. Audio Visualisation
  4. Coding

Introduction

Let's start by doing a basic introduction to Zig, it's syntax and how it's different from other systems programming languages like C++ | Rust.

Hello World

// Import standard library which is referred through the std constant
const std = @import("std");

// function declaration: [pub] fn _name(arguements:type_annotations) return_types

pub fn main() void {
    // Contrary to C functions, Zig functions have a fixed number of arguments.
    // In C: "printf" takes any number of arguments.
    // In Zig: std.log.info takes a format and a list of elements to print.
    std.debug.print("hello world", .{});  // .{} is an empty anonymous tuple. Think of it as the arguement list in C's printf where programmatic values are provided and are then substituted at runtime
}

Arrays

// Arrays are a contiguous chunk of memory which contain data of the same type
// Unlike C arrays which are basically raw pointers to where the memory is stored, Zig arrays are structures which include information about its length

// If the elements are added at the time of declaration we can omit the size with _  
const Array = [_]u8{ 1, 2 ,4 };

// Array with unitialised elements should have its size known at compile time
const Array2: [3]u8=undefined

Control flow

// Conditional branching.

if (condition) {
    ...
}
else {
    ...
}

// Simple "while" loop.
while (i < 10) { i += 1; }

// For loops in Zig are mainly used for iterables like Arrays and lists
// They aren't like C's for loops where you can set conditions and the increment value, for those purposes use while loops

// Loop over every item of an array (or slice).
for (items) |value| { sum += value; }

What and Why WASM

On their official Website the intro is given as follows

"WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications."

In simpler terms its just a compilation target like X86_64 or ARM which can be run on Browsers. The number of people that have access to the internet is growing exponentially by the nigh and so businesses need to have their applications online so that a wider audience can interact with it. Rewriting Legacy application with 100's of lines of code can be a daunting task and not to forget about JavaScript sluggish performance which might not be optimal for intensive applications like Games or 3D applications. As WASM is just a compilation target we can compile our pre-existing code to it and get near native performances in browser. WASM is able to run at super high speeds because it is compile ahead of time and isn't shipped with a Runtime unlike JavaScript and can be executed directly y the stack machine in the Browser.

Compling C Code to WASM :


# {{main.c}}

#include <stdio.h>
#int main() {
#  printf("hello, world!\n");
#  return 0;
#}

emcc -s WASM=1  main.c

node a.out.js
> hello, world!

This is it !!! We used emcc and specified the C file which we want to convert to WASM

emcc is a compiler like gcc which can be used to make binaries for the WASM art

and here is the Zig Equivalent

zig build-lib pointer_arithmatic.zig -target wasm32-freestanding -dynamic -rdynamic -freference-trace

Gatcha

Before you go brewing all your code to Web you need to tweak it a bit because WASM follows a different memory model and the executable is ran in a sandbox so syscalls can't be made, it only supports a handful of data numeric types and some others which we will cover in the forthcoming sections.

Audio Visualiser

Fairy tales are more than real not because they tell us dragon exists but because they tell us that they can be defeated and the dragon we will be defeating is WASM by building a audio visualiser

If you are a 2000's kid you must have used the Audacity to mash up songs together to make remixes or Nightcores. One feature of Audacity is the ability to visualise the wave forms of the audio signal we are working with.

We will be replicating (not all) the waveform visualization feature of Audacity by using WASM to interpret the raw WAV Sound files and then drawing it on the HTML CANVAS.

The WAV format

This is not a blog on multimedia and sound formats so we will be keeping the intro to WAV short and focus on the bits that are important to us.

The data inside a WAV file is represented like this

Source: https://www.codeproject.com/KB/audio-video/Circular_Buffers

We will assume our file is not just a simple mono channel WAV file because they are really simple but will be working with stereo files.

Coding

Side note: Zig has filled a divorce with LLVM which means it will use an in-house optimizer and code-gen. This has got a lot of people excited about its future as majority of the languages are using LLVM as their code-gen backend. It might never be as proficient as LLVM but its interesting to see a change in the industry.

The first step is to be able to read a WAV file. Taking a look at the above byte charts we can see that the first 4 bytes in a WAV file read as RIFF i.e. the values ['R','I','F','F'] are the first 4 bytes in a valid WAV files . Let's build a Zig program that can read a stream of bytes and verify if its a WAV based on the first 4 bytes or not.

const std = @import("std");
const assert = std.debug.assert;

export fn RiffCheck(data: [*]u8) {

	var val1:u8 = (data[0]);
	var val2:u8 = (data[1]);
	var var3:u8 = (data[2]);
	var var4:u8 = (data[3]);
	var Riff:[4]u8=undefined;
	Riff[0]=val1;
	Riff[1]=val2;
	Riff[2]=val3;
	Riff[3]=val4;
	 
	assert(mem.eql(u8, Riff, "RIFF"))
}

The above function takes in an array of u8 which represent the data inside the WAV file. In each of the val variables declared we take read 1 byte from the data array, add that to a char-list and compare it's value to the String "RIFF". For a valid WAV file this would run successfully.