zig-assembly-test
zig-assembly-test

ndrean/zig-assembly-test

MIT

Zig compiled to WebAssembly rendered in Phoenix Liveview

1 1 0 0
4
View on Github  
Updated: 8:47:15 PM Tue Nov 26 2024 Size: 335KB Created: 10:25:19 PM Sun Nov 24 2024
Dependencies:
No known dependencies
zig  fetch  --save  git+https://github.com/ndrean/zig-assembly-test

Zig-assembly-test

Zig compiled to WebAssembly rendered in Phoenix Liveview and as a standalone app.

  • Build the WebAssembly code:
cd zoomzig
zig build
  • Render in Phoenix LiveView:
cd mandelbrot
mix copy && mix phx.server
  • Render by GitHub pages as a standalone app:

https://ndrean.github.io/zig-assembly-test/

Zig

The code will return a slice that corresponds to the RGBA values of each pixel.

In the "build.zig", we set .max_memory = std.wasm.page_size * 128.

We set a variable global_colours. The Zig will populate this slice.

To compile to WebAssembly, we:

  • use export fn ...
  • pass only numbers as arguments
  • can't return error union, thus no try. Use catch unreached.
  • function returns void or numbers.
  • to return the "colours slice", we build a function to return the address of the first element of this memory block with getColoursPointer, and another one with its length with getColoursSize.
var global_colours: ?[]u8 = null;
const allocator = std.heap.wasm_allocator;

export fn allocMemory(len: usize) ?[*]u8 {
    return if (allocator.alloc(u8, len)) |slice|
        slice.ptr
    else |_|
        null;
}


export fn getColoursPointer() *u8 {
    // Expose the colours array to the host
    return &global_colours.?.ptr[0];
}

export fn getColoursSize() usize {
    return global_colours.?.len;
}

Elixir

Run a Mix task to copy the "zoom.wasm" file into the assets folder.

The call WebAssembly.instantiateStreaming asks for a content-type "application/wasm".

Serve the "wasm" file to the client

We serve the wasm file with an endpoint defined in the router.

pipeline :api do
    plug :accepts, ["wasm"]
end

scope "/", MandelzoomWeb do
    pipe_through :api
    get "/wasm", WasmController, :load
end

Phoenix appends by default sets "charset=utf8" to the Content-Type and WebAssembly does not want this.

We overwrite the resp_headers:

(https://elixirforum.com/t/content-type-for-custom-binary-format/60452)

 conn = 
    %Plug.Conn{conn | resp_headers: [{"content-type", "application/wasm"} | conn.resp_headers]}

Javascript - WebAssembly

The code is call via a hook, MandelbrotViewer.

The key points:

  • instantiate a "memory" for WebAssembly,
  • we let Javascript pass the quantity of memory to allocate to WebAssembly as we provided a function in Zig to allocate memory for the slice (with std.heap.wasm_alloator).
  • fetch the wasm file (Phoenix will serve it)
  • we called the WebAssembly module "instance" here". Call the WebAssembly functions with instance.exports.<function_name>
  • pass only numbers (integers, floats) to WebAssembly. We named our main Zig function "initilize" which receives only numbers and return void.
const WASM_PAGE_SIZE = 65536; // 64kB
const MAXIMUM_PAGES = 150;
const INITAIL_PAGES = 60;

const cols = this.canvas.width,
      rows = this.canvas.height,
      bytesNeeded = cols * rows * 4;

const pagesNeeded = Math.ceil(bytesNeeded / WASM_PAGE_SIZE);
this.memSize = pagesNeeded;
const initialPages = Math.max(pagesNeeded, INITIAL_PAGES);

memory = new WebAssembly.Memory({initial: initialPages, maximum: MAXIMUM_PAGES});

const {instance }  = await WebAssembly.instantiateStreaming(fetch("/wasm"), { env: {memory}});


instance.exports.allocMemory(this.memSize);

instance.exports.initilize(eows, cols...);

To fill in the canvas, we:

  • instanttiate a Javascript new Uint8ClampedArray that will receive the WebAssembly data from the memory address with a given length.
  • create an ImageData from this data
  • draw into the Canvas with createImageBitmap

Related

The Elixir library Orb can produce a WAT (text).

To compile to WASM binary, use WABT.

Wasmex can run Wasi in Elixir.

Create a Github pages from a subfolder

  • Create a folder, "pages" here.
  • create an "index.html" which calls a (eg) "scriptjs"
  • put your JS in it
  • copy "zoom.wasm" in the same folder or keep it sync with:
# /pages
ln -s ../zoomzig/zig-out/bin/zoom.wasm

Then:

git subtree push --prefix pages origin gh-pages

et voilà:

https://ndrean.github.io/zig-assembly-test/