diff --git a/src/wasm.zig b/src/wasm.zig index 96b53e425..d496e404e 100644 --- a/src/wasm.zig +++ b/src/wasm.zig @@ -1,5 +1,114 @@ //! This file contains helpers for wasm compilation. const std = @import("std"); +const builtin = @import("builtin"); + +comptime { + if (!builtin.target.isWasm()) { + @compileError("wasm.zig should only be analyzed for wasm32 builds"); + } +} /// The allocator to use in wasm environments. -pub const alloc = std.heap.page_allocator; +/// +/// The return values of this should NOT be sent to the host environment +/// unless toHostOwned is called on them. In this case, the caller is expected +/// to call free. If a pointer is NOT host-owned, then the wasm module is +/// expected to call the normal alloc.free/destroy functions. +/// +/// TODO: we should NOT be using page_allocator because we're getting +/// full 64kb pages for every allocation. I plan on changing this to the +/// new stdlib wasm allocator once that is merged and available. +pub const alloc = if (builtin.is_test) + std.testing.allocator +else + std.heap.page_allocator; + +/// For host-owned allocations: +/// We need to keep track of our own pointer lengths because Zig +/// allocators usually don't do this and we need to be able to send +/// a direct pointer back to the host system. A more appropriate thing +/// to do would be to probably make a custom allocator that keeps track +/// of size. +var allocs: std.AutoHashMapUnmanaged([*]u8, usize) = .{}; + +/// Allocate len bytes and return a pointer to the memory in the host. +/// The data is not zeroed. +pub export fn malloc(len: usize) ?[*]u8 { + return alloc_(len) catch return null; +} + +/// Free an allocation from malloc. +pub export fn free(ptr: ?[*]u8) void { + if (ptr) |v| { + if (allocs.get(v)) |len| { + const slice = v[0..len]; + alloc.free(slice); + _ = allocs.remove(v); + } + } +} + +/// Convert an allocated pointer of any type to a host-owned pointer. +/// This pushes the responsibility to free it to the host. The returned +/// pointer will match the pointer but is typed correctly for returning +/// to the host. +pub fn toHostOwned(ptr: anytype) ![*]u8 { + // Convert our pointer to a byte array + const info = @typeInfo(@TypeOf(ptr)).Pointer; + const T = info.child; + const size = @sizeOf(T); + const casted = @intToPtr([*]u8, @ptrToInt(ptr)); + + // Store the information about it + try allocs.putNoClobber(alloc, casted, size); + errdefer _ = allocs.remove(casted); + + return casted; +} + +/// Returns true if the value is host owned. +pub fn isHostOwned(ptr: anytype) bool { + const casted = @intToPtr([*]u8, @ptrToInt(ptr)); + return allocs.contains(casted); +} + +/// Convert a pointer back to a module-owned value. The caller is expected +/// to cast or have the valid pointer for alloc calls. +pub fn toModuleOwned(ptr: anytype) void { + const casted = @intToPtr([*]u8, @ptrToInt(ptr)); + _ = allocs.remove(casted); +} + +fn alloc_(len: usize) ![*]u8 { + // Create the allocation + const slice = try alloc.alloc(u8, len); + errdefer alloc.free(slice); + + // Store the size so we can deallocate later + try allocs.putNoClobber(alloc, slice.ptr, slice.len); + errdefer _ = allocs.remove(slice.ptr); + + return slice.ptr; +} + +test "basics" { + const testing = std.testing; + var buf = malloc(32).?; + try testing.expect(allocs.size == 1); + free(buf); + try testing.expect(allocs.size == 0); +} + +test "toHostOwned" { + const testing = std.testing; + + const Point = struct { x: u32 = 0, y: u32 = 0 }; + var p = try alloc.create(Point); + errdefer alloc.destroy(p); + const ptr = try toHostOwned(p); + try testing.expect(allocs.size == 1); + try testing.expect(isHostOwned(p)); + try testing.expect(isHostOwned(ptr)); + free(ptr); + try testing.expect(allocs.size == 0); +}