feature. See also
. The project being documented here (as the example) is the Zig library itself.
Thread.WasiThreadImpl
const WasiThreadImpl = struct
File
Code
const WasiThreadImpl = struct {
thread: *WasiThread,
pub const ThreadHandle = i32;
threadlocal var tls_thread_id: Id = 0;
const WasiThread = struct {
tid: std.atomic.Value(i32) = std.atomic.Value(i32).init(0),
memory: []u8,
allocator: std.mem.Allocator,
state: State = State.init(.running),
};
const Instance = struct {
thread: WasiThread,
tls_offset: usize,
stack_offset: usize,
raw_ptr: usize,
call_back: *const fn (usize) void,
original_stack_pointer: [*]u8,
};
const State = std.atomic.Value(enum(u8) { running, completed, detached });
fn getCurrentId() Id {
return tls_thread_id;
}
fn getCpuCount() error{Unsupported}!noreturn {
return error.Unsupported;
}
fn getHandle(self: Impl) ThreadHandle {
return self.thread.tid.load(.seq_cst);
}
fn detach(self: Impl) void {
switch (self.thread.state.swap(.detached, .seq_cst)) {
.running => {},
.completed => self.join(),
.detached => unreachable,
}
}
fn join(self: Impl) void {
defer {
// original allocator while freeing the memory.
var allocator = self.thread.allocator;
allocator.free(self.thread.memory);
}
while (true) {
const tid = self.thread.tid.load(.seq_cst);
if (tid == 0) break;
const result = asm (
\\ local.get %[ptr]
\\ local.get %[expected]
\\ i64.const -1 # infinite
\\ memory.atomic.wait32 0
\\ local.set %[ret]
: [ret] "=r" (-> u32),
: [ptr] "r" (&self.thread.tid.raw),
[expected] "r" (tid),
);
switch (result) {
0 => continue,
1 => continue,
2 => unreachable,
else => unreachable,
}
}
}
fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) SpawnError!WasiThreadImpl {
if (config.allocator == null) {
@panic("an allocator is required to spawn a WASI thread");
}
const Wrapper = struct {
args: @TypeOf(args),
fn entry(ptr: usize) void {
const w: *@This() = @ptrFromInt(ptr);
const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', 'void', or '!void'";
switch (@typeInfo(@typeInfo(@TypeOf(f)).@"fn".return_type.?)) {
.noreturn, .void => {
@call(.auto, f, w.args);
},
.int => |info| {
if (info.bits != 8) {
@compileError(bad_fn_ret);
}
_ = @call(.auto, f, w.args);
},
.error_union => |info| {
if (info.payload != void) {
@compileError(bad_fn_ret);
}
@call(.auto, f, w.args) catch |err| {
std.debug.print("error: {s}\n", .{@errorName(err)});
if (@errorReturnTrace()) |trace| {
std.debug.dumpErrorReturnTrace(trace);
}
};
},
else => {
@compileError(bad_fn_ret);
},
}
}
};
var stack_offset: usize = undefined;
var tls_offset: usize = undefined;
var wrapper_offset: usize = undefined;
var instance_offset: usize = undefined;
// - The actual stack for the thread
// - The TLS segment
// - `Instance` - containing information about how to call the user's function.
const map_bytes = blk: {
// other threads clobbering our new thread.
// Unfortunately, WebAssembly has no notion of read-only segments, so this
// is only a best effort.
var bytes: usize = std.wasm.page_size;
bytes = std.mem.alignForward(usize, bytes, 16);
stack_offset = bytes;
bytes += @max(std.wasm.page_size, config.stack_size);
bytes = std.mem.alignForward(usize, bytes, __tls_align());
tls_offset = bytes;
bytes += __tls_size();
bytes = std.mem.alignForward(usize, bytes, @alignOf(Wrapper));
wrapper_offset = bytes;
bytes += @sizeOf(Wrapper);
bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance));
instance_offset = bytes;
bytes += @sizeOf(Instance);
bytes = std.mem.alignForward(usize, bytes, std.wasm.page_size);
break :blk bytes;
};
const allocated_memory = try config.allocator.?.alloc(u8, map_bytes);
const wrapper: *Wrapper = @ptrCast(@alignCast(&allocated_memory[wrapper_offset]));
wrapper.* = .{ .args = args };
const instance: *Instance = @ptrCast(@alignCast(&allocated_memory[instance_offset]));
instance.* = .{
.thread = .{ .memory = allocated_memory, .allocator = config.allocator.? },
.tls_offset = tls_offset,
.stack_offset = stack_offset,
.raw_ptr = @intFromPtr(wrapper),
.call_back = &Wrapper.entry,
.original_stack_pointer = __get_stack_pointer(),
};
const tid = spawnWasiThread(instance);
// The values of such error are unspecified. WASI-Libc treats it as EAGAIN.
if (tid < 0) {
return error.SystemResources;
}
instance.thread.tid.store(tid, .seq_cst);
return .{ .thread = &instance.thread };
}
comptime {
if (!builtin.single_threaded) {
@export(&wasi_thread_start, .{ .name = "wasi_thread_start" });
}
}
fn wasi_thread_start(tid: i32, arg: *Instance) callconv(.c) void {
comptime assert(!builtin.single_threaded);
__set_stack_pointer(arg.thread.memory.ptr + arg.stack_offset);
__wasm_init_tls(arg.thread.memory.ptr + arg.tls_offset);
@atomicStore(u32, &WasiThreadImpl.tls_thread_id, @intCast(tid), .seq_cst);
arg.call_back(arg.raw_ptr);
switch (arg.thread.state.swap(.completed, .seq_cst)) {
.running => {
asm volatile (
\\ local.get %[ptr]
\\ i32.const 0
\\ i32.atomic.store 0
:
: [ptr] "r" (&arg.thread.tid.raw),
);
asm volatile (
\\ local.get %[ptr]
\\ i32.const 1 # waiters
\\ memory.atomic.notify 0
\\ drop # no need to know the waiters
:
: [ptr] "r" (&arg.thread.tid.raw),
);
},
.completed => unreachable,
.detached => {
// without having to worry about freeing the stack
__set_stack_pointer(arg.original_stack_pointer);
var allocator = arg.thread.allocator;
allocator.free(arg.thread.memory);
},
}
}
const spawnWasiThread = @"thread-spawn";
extern "wasi" fn @"thread-spawn"(arg: *Instance) i32;
extern fn __wasm_init_tls(memory: [*]u8) void;
inline fn __tls_base() [*]u8 {
return asm (
\\ .globaltype __tls_base, i32
\\ global.get __tls_base
\\ local.set %[ret]
: [ret] "=r" (-> [*]u8),
);
}
inline fn __tls_size() u32 {
return asm volatile (
\\ .globaltype __tls_size, i32, immutable
\\ global.get __tls_size
\\ local.set %[ret]
: [ret] "=r" (-> u32),
);
}
inline fn __tls_align() u32 {
return asm (
\\ .globaltype __tls_align, i32, immutable
\\ global.get __tls_align
\\ local.set %[ret]
: [ret] "=r" (-> u32),
);
}
inline fn __set_stack_pointer(addr: [*]u8) void {
asm volatile (
\\ local.get %[ptr]
\\ global.set __stack_pointer
:
: [ptr] "r" (addr),
);
}
inline fn __get_stack_pointer() [*]u8 {
return asm (
\\ global.get __stack_pointer
\\ local.set %[stack_ptr]
: [stack_ptr] "=r" (-> [*]u8),
);
}
}