authorgravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2023-06-20 22:19:51+02:00
committergravatar for luuk@degram.devLuuk de Gram <luuk@degram.dev> 2023-06-26 20:00:57+02:00
loga97dbdfa0b1246913ba90cd5c05ff633e9003cb9
tree39b0d61f4c6ee95e6d3ba39a9ce91c676398c929
parentea0d4c8377aeabe1ef588e82cbdd9aa729adbce0
signaturelock-open Commit is signed but in an unrecognized format.

std: implement `Thread` `spawn` for WASI

This implements a first version to spawn a WASI-thread. For a new thread to be created, we calculate the size required to store TLS, the new stack, and metadata. This size is then allocated using a user-provided allocator. After a new thread is spawn, the HOST will call into our bootstrap procedure. This bootstrap procedure will then initialize the TLS segment and set the newly spawned thread's TID. It will also set the stack pointer to the newly created stack to ensure we do not clobber the main thread's stack. When bootstrapping the thread is completed, we will call the user's function on this new thread.

1 files changed, 192 insertions(+), 0 deletions(-)

lib/std/Thread.zig+192
...@@ -28,6 +28,8 @@ else if (use_pthreads)...@@ -28,6 +28,8 @@ else if (use_pthreads)
28 PosixThreadImpl28 PosixThreadImpl
29else if (target.os.tag == .linux)29else if (target.os.tag == .linux)
30 LinuxThreadImpl30 LinuxThreadImpl
31else if (target.os.tag == .wasi)
32 WasiThreadImpl
31else33else
32 UnsupportedImpl;34 UnsupportedImpl;
3335
...@@ -266,6 +268,7 @@ pub const Id = switch (target.os.tag) {...@@ -266,6 +268,7 @@ pub const Id = switch (target.os.tag) {
266 .freebsd,268 .freebsd,
267 .openbsd,269 .openbsd,
268 .haiku,270 .haiku,
271 .wasi,
269 => u32,272 => u32,
270 .macos, .ios, .watchos, .tvos => u64,273 .macos, .ios, .watchos, .tvos => u64,
271 .windows => os.windows.DWORD,274 .windows => os.windows.DWORD,
...@@ -296,6 +299,8 @@ pub const SpawnConfig = struct {...@@ -296,6 +299,8 @@ pub const SpawnConfig = struct {
296299
297 /// Size in bytes of the Thread's stack300 /// Size in bytes of the Thread's stack
298 stack_size: usize = 16 * 1024 * 1024,301 stack_size: usize = 16 * 1024 * 1024,
302 /// The allocator to be used to allocate memory for the to-be-spawned thread
303 allocator: ?std.mem.Allocator = null,
299};304};
300305
301pub const SpawnError = error{306pub const SpawnError = error{
...@@ -733,6 +738,193 @@ const PosixThreadImpl = struct {...@@ -733,6 +738,193 @@ const PosixThreadImpl = struct {
733 }738 }
734};739};
735740
741const WasiThreadImpl = struct {
742 comptime {
743 // Sets the stack pointer, which is needed after creating a new thread
744 // to ensure the stack of the main thread isn't being poluted.
745 asm (
746 \\ .text
747 \\ .export_name __set_stack_pointer, __set_stack_pointer
748 \\ .globaltype __stack_pointer, i32
749 \\ .hidden wasi_thread_start
750 \\ .globl wasi_thread_start
751 \\ .type __set_stack_pointer, @function
752 \\
753 \\ __set_stack_pointer:
754 \\ .functype __set_stack_pointer (i32) -> ()
755 \\ local.get 0 # The raw pointer which replaces the stack pointer
756 \\ global.set __stack_pointer
757 \\ end_function
758 );
759 }
760 thread: *WasiThread,
761
762 pub const ThreadHandle = i32;
763 threadlocal var tls_thread_id: Id = 0;
764
765 const WasiThread = struct {
766 tid: Atomic(i32) = Atomic(i32).init(0),
767 memory: []u8,
768 };
769
770 /// A meta-data structure used to bootstrap a thread
771 const Instance = struct {
772 thread: WasiThread,
773 /// Address of this `Instance`
774 base: usize,
775 /// Contains the pointer of the new __tls_base.
776 tls_base: usize,
777 /// Contains the pointer to the stack for the newly spawned thread.
778 stack_pointer: usize,
779 /// Contains the pointer to the wrapper which holds all arguments
780 /// for the callback.
781 raw_ptr: usize,
782 /// Function pointer to a wrapping function which will call the user's
783 /// function upon thread spawn. The above mentioned pointer will be passed
784 /// to this function pointer as its argument.
785 call_back: *const fn (usize) void,
786 };
787
788 fn getCurrentId() Id {
789 return tls_thread_id;
790 }
791
792 fn getHandle(self: Impl) ThreadHandle {
793 return self.thread.tid;
794 }
795
796 fn detach(self: Impl) void {
797 _ = self;
798 }
799
800 fn join(self: Impl) void {
801 _ = self;
802 }
803
804 fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) !WasiThreadImpl {
805 if (config.allocator == null) return error.OutOfMemory; // an allocator is required to spawn a WASI-thread
806
807 // Wrapping struct required to hold the user-provided function arguments.
808 const Wrapper = struct {
809 args: @TypeOf(args),
810 fn entry(ptr: usize) void {
811 const w = @intToPtr(*@This(), ptr);
812 @call(.auto, f, w.args);
813 }
814 };
815
816 var guard_offset: usize = undefined;
817 var stack_offset: usize = undefined;
818 var tls_offset: usize = undefined;
819 var wrapper_offset: usize = undefined;
820 var instance_offset: usize = undefined;
821
822 // Calculate the bytes we have to allocate to store all thread information, including:
823 // - The actual stack for the thread
824 // - The TLS segment
825 // - `Instance` - containing information about how to call the user's function.
826 const map_bytes = blk: {
827 var bytes: usize = std.wasm.page_size;
828 guard_offset = bytes;
829
830 bytes = std.mem.alignForward(usize, bytes, 16); // align stack to 16 bytes
831 stack_offset = bytes;
832 bytes += @max(std.wasm.page_size, config.stack_size);
833
834 bytes = std.mem.alignForward(usize, bytes, __tls_align());
835 tls_offset = bytes;
836 bytes += __tls_size();
837
838 bytes = std.mem.alignForward(usize, bytes, @alignOf(Wrapper));
839 wrapper_offset = bytes;
840 bytes += @sizeOf(Wrapper);
841
842 bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance));
843 instance_offset = bytes;
844 bytes += @sizeOf(Instance);
845
846 bytes = std.mem.alignForward(usize, bytes, std.wasm.page_size);
847 break :blk bytes;
848 };
849
850 // Allocate the amount of memory required for all meta data.
851 const allocated_memory = try config.allocator.?.alloc(u8, map_bytes);
852
853 const wrapper = @ptrCast(*Wrapper, @alignCast(@alignOf(Wrapper), &allocated_memory[wrapper_offset]));
854 wrapper.* = .{ .args = args };
855
856 const instance = @ptrCast(*Instance, @alignCast(@alignOf(Instance), &allocated_memory[instance_offset]));
857 instance.* = .{
858 .thread = .{ .memory = allocated_memory },
859 .base = @ptrToInt(allocated_memory.ptr),
860 .tls_base = tls_offset,
861 .stack_pointer = stack_offset,
862 .raw_ptr = @ptrToInt(wrapper),
863 .call_back = &Wrapper.entry,
864 };
865
866 const tid = spawnWasiThread(instance);
867 // The specification says any value lower than 0 indicates an error.
868 // The values of such error are unspecified. WASI-Libc treats it as EAGAIN.
869 if (tid < 0) {
870 return error.SystemResources;
871 }
872 instance.thread.tid.store(tid, .SeqCst);
873
874 return .{ .thread = &instance.thread };
875 }
876
877 export fn wasi_thread_start(tid: i32, arg: *const Instance) void {
878 __set_stack_pointer(arg.thread.memory.ptr + arg.stack_pointer);
879 __wasm_init_tls(arg.thread.memory.ptr + arg.tls_base);
880 WasiThreadImpl.tls_thread_id = @intCast(u32, tid);
881
882 // finished bootstrapping, call user's procedure.
883 arg.call_back(arg.raw_ptr);
884 }
885
886 // Asks the host to create a new thread for us.
887 // Newly created thread wil lcall `wasi_tread_start` with the thread ID as well
888 // as the input `arg` that was provided to `spawnWasiThread`
889 const spawnWasiThread = @"thread-spawn";
890 extern "wasi" fn @"thread-spawn"(arg: *const Instance) i32;
891
892 /// Initializes the TLS data segment starting at `memory`.
893 /// This is a synthetic function, generated by the linker.
894 extern fn __wasm_init_tls(memory: [*]u8) void;
895 extern fn __set_stack_pointer(ptr: [*]u8) void;
896
897 /// Returns a pointer to the base of the TLS data segment for the current thread
898 inline fn __tls_base() [*]u8 {
899 return asm (
900 \\ .globaltype __tls_base, i32
901 \\ global.get __tls_base
902 \\ local.set %[ret]
903 : [ret] "=r" (-> [*]u8),
904 );
905 }
906
907 /// Returns the size of the TLS segment
908 inline fn __tls_size() u32 {
909 return asm volatile (
910 \\ .globaltype __tls_size, i32, immutable
911 \\ global.get __tls_size
912 \\ local.set %[ret]
913 : [ret] "=r" (-> u32),
914 );
915 }
916
917 /// Returns the alignment of the TLS segment
918 inline fn __tls_align() u32 {
919 return asm (
920 \\ .globaltype __tls_align, i32, immutable
921 \\ global.get __tls_align
922 \\ local.set %[ret]
923 : [ret] "=r" (-> u32),
924 );
925 }
926};
927
736const LinuxThreadImpl = struct {928const LinuxThreadImpl = struct {
737 const linux = os.linux;929 const linux = os.linux;
738930