Skip to content

Instantly share code, notes, and snippets.

@nickav
Last active May 24, 2024 09:37
Show Gist options
  • Save nickav/b371ba3769160cd321063eac5503c9c7 to your computer and use it in GitHub Desktop.
Save nickav/b371ba3769160cd321063eac5503c9c7 to your computer and use it in GitHub Desktop.
WASM from Scratch
// Types
#define NULL 0
typedef unsigned char u8;
typedef unsigned int u32;
typedef signed int i32;
typedef signed long long i64;
typedef double f64;
// Imports
__attribute__((import_module("env"), import_name("js__output")))
void js__output(char *str, i32 size);
__attribute__((import_module("env"), import_name("js__sin")))
f64 js__sin(f64 x);
// Math
f64 sqrt(f64 x)
{
return __builtin_sqrt(x);
}
f64 sin(f64 x)
{
return js__sin(x);
}
// Memory
u32 wasm__heap_size_in_bytes()
{
return __builtin_wasm_memory_size(0) << 16;
}
extern u32 __heap_base;
void *wasm__heap_pointer()
{
return (void *)&__heap_base;
}
static u32 offset = 0;
__attribute__((export_name("wasm__push")))
u8 *wasm__push(u32 size)
{
u8 *result = NULL;
// NOTE(nick): align up to the nearest 16 bytes
size = (size + 15) & ~15;
if (offset + size < wasm__heap_size_in_bytes())
{
result = (u8 *)(wasm__heap_pointer()) + offset;
offset += size;
}
return result;
}
__attribute__((export_name("wasm__pop")))
void wasm__pop(u32 size)
{
// NOTE(nick): align up to the nearest 16 bytes
size = (size + 15) & ~15;
if ((i64)offset - size > 0) {
offset -= size;
} else {
offset = 0;
}
}
//
// Exported API
//
typedef struct f64_Array f64_Array;
struct f64_Array
{
u32 count;
f64 *data;
};
__attribute__((export_name("add")))
i32 add(i32 x, i32 y)
{
return x + y;
}
__attribute__((export_name("secret_message")))
char *secret_message()
{
return (char *)"Hello, Sailor!";
}
__attribute__((export_name("ping")))
void ping(char *message, i32 count)
{
js__output(message, count);
}
__attribute__((export_name("compute_square_roots")))
void compute_square_roots(f64 *array, i32 count)
{
for (i32 i = 0; i < count; i += 1)
{
array[i] = sqrt(array[i]);
}
}
__attribute__((export_name("push_f64_array")))
f64_Array *push_f64_array(u32 count)
{
f64_Array *result = (f64_Array *)wasm__push(sizeof(f64_Array));
result->count = count;
result->data = (f64 *)wasm__push(sizeof(f64) * count);
for (i32 i = 0; i < count; i += 1)
{
result->data[i] = 0;
}
return result;
}
<!doctype html>
<html>
<script src="./hello.js">
</script>
</html>
const decoder = new TextDecoder("utf8");
const DecodeString = (u8Array, idx, length) => {
if (!idx) return "";
const endPtr = idx + length;
return decoder.decode(u8Array.subarray(idx, endPtr));
}
const StringLength = (u8Array, ptr) => {
let endPtr = ptr;
while (u8Array[endPtr]) ++endPtr;
return endPtr - ptr;
}
const encoder = new TextEncoder("utf8");
// NOTE(nick): the output space needed is >= s.length and <= s.length * 3
const EncodeString = (u8Array, base, string) => {
if (!string.length) return 0;
return encoder.encodeInto(string, u8Array.subarray(base)).written;
}
const Kilobytes = (x) => 1024 * x;
const Megabytes = (x) => 1024 * 1024 * x;
const load = async (wasmPath) => {
const response = await fetch(wasmPath);
const bytes = await response.arrayBuffer();
// NOTE(nick): WASM pages are 64K each
const numPages = Megabytes(64) / Kilobytes(64);
const memory = new WebAssembly.Memory({ initial: numPages });
const heap = new Uint8Array(memory.buffer);
const wasm = await WebAssembly.instantiate(bytes, {
env: {
memory,
js__output: (str, size) => {
const result = DecodeString(heap, str, size);
console.log("[hello]", result);
},
js__sin: Math.sin,
},
});
const { instance } = wasm;
return {
add: (x, y) => {
return instance.exports.add(x, y);
},
secret_message: () => {
const ptr = instance.exports.secret_message();
return DecodeString(heap, ptr, StringLength(heap, ptr));
},
ping: (message) => {
const ptr = instance.exports.wasm__push(3 * message.length);
const count = EncodeString(heap, ptr, message);
instance.exports.ping(ptr, count);
instance.exports.wasm__pop(3 * message.length);
},
compute_square_roots: (array) => {
const cArrayPtr = instance.exports.wasm__push(8 * array.length);
const cArray = new Float64Array(memory.buffer, cArrayPtr, array.length);
cArray.set(array);
instance.exports.compute_square_roots(cArrayPtr, array.length);
instance.exports.wasm__pop(8 * array.length);
return Array.from(cArray);
},
push_f64_array: (length) => {
const ptr = instance.exports.push_f64_array(length);
// NOTE(nick): read the memory directly
const dataView = new DataView(memory.buffer);
let at = ptr;
const count = dataView.getUint32(at, true);
at += 4;
// NOTE(nick): pointers are u32
const data = dataView.getUint32(at, true);
at += 4;
const result = new Float64Array(memory.buffer, data, count);
return result;
},
};
};
const main = async () => {
const hello = await load('/hello.wasm');
console.log("hello.add(42, 42):", hello.add(42, 42));
console.log("secret_message():", hello.secret_message());
console.log(`ping("hello, good sir"):`, hello.ping("hello, good sir"));
console.log(`compute_square_roots([1, 2, 3, 4, 5]):`, hello.compute_square_roots([1, 2, 3, 4, 5]));
console.log(`push_f64_array(10):`, hello.push_f64_array(10));
};
main();
clang -O1 --target=wasm32 --no-standard-libraries -mbulk-memory -Wl,--no-entry -Wl,--import-memory -o hello.wasm hello.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment