Last active
May 24, 2024 09:37
-
-
Save nickav/b371ba3769160cd321063eac5503c9c7 to your computer and use it in GitHub Desktop.
WASM from Scratch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<script src="./hello.js"> | |
</script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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