Wasm

If you wanted to run C code in the browser, you would choose Wasm (Webassembly).

I wanted to make Zeyn available in the web for a few reasons:

Wasi

I think of WebAssembly System Interface as a Posix subset implementation for Webassembly. (Wasi does not aim to fully support Posix, still, what else would you describe it as in less than 100 words?)

When targeting Wasi, you can compile traditional C applications that:

This means that I could just compile the existing main.c of Zeyn. To run a Wasi Wasm, the needed interfaces have to be provided by the Javascript caller.

I chose not to use Wasi for a few reasons:

If Wasi becomes more mature and turns out not to be bloated with things that I do not need, I will consider switching to it.

Freestanding Wasm

Alternatively, the lower level approach is using a freestanding Wasm.

A freestanding Wasm just exports some functions, in my case (basically) just one:

void _start();

That starts the Zeyn application.

This Zeyn application interacts with Javascript using the env:

Basically just:

extern unsigned read(int fd, void *p, unsigned n, struct Err *err);
extern unsigned write(int fd, void *p, unsigned n, struct Err *err);

This means that I can provide Javascript implementations of the read and write functions, then call _start, and Zeyn should start and work.

Passing numbers

By default you can only pass numbers between Javascript and C.

When passing a pointer in C, you get a number in Javascript. This number is an index/offset in the Webassembly instance's memory. This means, you can pass C-style (null terminated) strings to Javascript and decode them like so:

/**
This function can be called from C like so:

	print("hello world");

@param ntsp is a pointer to a C-style (null terminated) string.
*/
const print = function (ntsp) {
	const memory = new Uint8Array(instance.exports.memory.buffer);
	let nt;
	for (nt = ntsp; nt < memory.length; nt++) {
		if (memory[nt] === 0) {
			// found null terminator
			break;
		}
	}
	const nts = memory.subarray(ntsp, nt);
	return new TextDecoder().decode(nts);
};

For the read and write implementations I explicitly pass the number of bytes at the passed pointer, the size of the passed memory area, instead of using a terminating character.

Setting EOF

When implementing read, I need some way to set *err to io_EOF signifying the end of the Zeyn code. I could figure out the alignment of the type struct Err, figure out the address of io_EOF, figure out the address of the default writeTo and flee methods, and modify the instances memory appropriately.

But it's easier to just export a setEof function from C:

#include <c.datasha.re/cutilib/err.h>
#include <c.datasha.re/cutilib/io.h>
void
setEof(struct Err *err)
{
	*err = err_msg(io_EOF);
}

Now I can just call this function with a given struct Err * from Javascript to set it to io_EOF.

Sync

Calling a function exported from Webassembly is synchronous. Derivatively, calling a function from Webassembly is also synchronous.

IO is the classic example for asynchronous programming.

(Basically) the only interface Zeyn needs is IO.

What to do?

For the initial implementation, I "coerced" the IO into being synchronous:

To get asynchronous IO, we need the Webassembly instance to pause at read/write calls, and resume later when data is available/data has been processed.

This is not easily possible in Webassembly, but there are a few options:

Memory Allocation

The approach for memory allocation I chose was to implement my own memory allocator in C. That's a good feature to have in Cutilib anyway. It's called mem_allocFrom and allocates memory from a given memory area.

To get such an initial, rather large, memory area in my C program, I implemented another function in env:

extern void *getHeap(unsigned size);

It's implemented in Javascript by growing the Webassembly's memory.

/** the size of a Webassembly memory page */
const PAGE_SIZE = 64 * 1024;

const getHeap = function (size) {
	const memory = new Uint8Array(instance.exports.memory.buffer);
	const ptr = memory.length;
	const nPages = Math.ceil(size / PAGE_SIZE);
	console.log("adding", nPages, "memory pages of heap (", size, "bytes )");
	instance.exports.memory.grow(nPages);
	return ptr;
},

Farbfeld

Now Zeyn produces images.

Those images should be looked at.

Browsers support looking at images.

But not Farbfeld images.

But as already mentioned in ./reimpl, implementing Farbfeld is not a huge issue.

The Web API that maps best to Farbfeld is the ImageData API related to the Canvas API.

With https://gitlab.com/datasha.re/farbweb I'm implementing a library that bridges Farbfeld and the Web.

Amongst other things it implements a custom element that accepts Farbfeld input:

<img is=farb-img data-src=./image.ff>

<img is=farb-img data-src="data:image/farbfeld;base64,ZmFyYmZlbGQAAAAHAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////////////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA">

Now where can I look at this Zeyn Wasm?

Right now, a demo HTML site is in the Zeyn repository in a "wasm" folder: https://gitlab.com/datasha.re/zeyn/-/blob/v0.0.1/wasm/wasm.art.htm

Before it works you'll need to:

But I'm sure I'll deploy it somewhere here soon and link it.