| ... | ... | @@ -28,6 +28,8 @@ else if (use_pthreads) |
| 28 | 28 | PosixThreadImpl |
| 29 | 29 | else if (target.os.tag == .linux) |
| 30 | 30 | LinuxThreadImpl |
| 31 | else if (target.os.tag == .wasi) |
| 32 | WasiThreadImpl |
| 31 | 33 | else |
| 32 | 34 | UnsupportedImpl; |
| 33 | 35 | |
| ... | ... | @@ -266,6 +268,7 @@ pub const Id = switch (target.os.tag) { |
| 266 | 268 | .freebsd, |
| 267 | 269 | .openbsd, |
| 268 | 270 | .haiku, |
| 271 | .wasi, |
| 269 | 272 | => u32, |
| 270 | 273 | .macos, .ios, .watchos, .tvos => u64, |
| 271 | 274 | .windows => os.windows.DWORD, |
| ... | ... | @@ -296,6 +299,8 @@ pub const SpawnConfig = struct { |
| 296 | 299 | |
| 297 | 300 | /// Size in bytes of the Thread's stack |
| 298 | 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 | }; |
| 300 | 305 | |
| 301 | 306 | pub const SpawnError = error{ |
| ... | ... | @@ -733,6 +738,291 @@ const PosixThreadImpl = struct { |
| 733 | 738 | } |
| 734 | 739 | }; |
| 735 | 740 | |
| 741 | const WasiThreadImpl = struct { |
| 742 | thread: *WasiThread, |
| 743 | |
| 744 | pub const ThreadHandle = i32; |
| 745 | threadlocal var tls_thread_id: Id = 0; |
| 746 | |
| 747 | const WasiThread = struct { |
| 748 | /// Thread ID |
| 749 | tid: Atomic(i32) = Atomic(i32).init(0), |
| 750 | /// Contains all memory which was allocated to bootstrap this thread, including: |
| 751 | /// - Guard page |
| 752 | /// - Stack |
| 753 | /// - TLS segment |
| 754 | /// - `Instance` |
| 755 | /// All memory is freed upon call to `join` |
| 756 | memory: []u8, |
| 757 | /// The allocator used to allocate the thread's memory, |
| 758 | /// which is also used during `join` to ensure clean-up. |
| 759 | allocator: std.mem.Allocator, |
| 760 | /// The current state of the thread. |
| 761 | state: State = State.init(.running), |
| 762 | }; |
| 763 | |
| 764 | /// A meta-data structure used to bootstrap a thread |
| 765 | const Instance = struct { |
| 766 | thread: WasiThread, |
| 767 | /// Contains the offset to the new __tls_base. |
| 768 | /// The offset starting from the memory's base. |
| 769 | tls_offset: usize, |
| 770 | /// Contains the offset to the stack for the newly spawned thread. |
| 771 | /// The offset is calculated starting from the memory's base. |
| 772 | stack_offset: usize, |
| 773 | /// Contains the raw pointer value to the wrapper which holds all arguments |
| 774 | /// for the callback. |
| 775 | raw_ptr: usize, |
| 776 | /// Function pointer to a wrapping function which will call the user's |
| 777 | /// function upon thread spawn. The above mentioned pointer will be passed |
| 778 | /// to this function pointer as its argument. |
| 779 | call_back: *const fn (usize) void, |
| 780 | /// When a thread is in `detached` state, we must free all of its memory |
| 781 | /// upon thread completion. However, as this is done while still within |
| 782 | /// the thread, we must first jump back to the main thread's stack or else |
| 783 | /// we end up freeing the stack that we're currently using. |
| 784 | original_stack_pointer: [*]u8, |
| 785 | }; |
| 786 | |
| 787 | const State = Atomic(enum(u8) { running, completed, detached }); |
| 788 | |
| 789 | fn getCurrentId() Id { |
| 790 | return tls_thread_id; |
| 791 | } |
| 792 | |
| 793 | fn getHandle(self: Impl) ThreadHandle { |
| 794 | return self.thread.tid.load(.SeqCst); |
| 795 | } |
| 796 | |
| 797 | fn detach(self: Impl) void { |
| 798 | switch (self.thread.state.swap(.detached, .SeqCst)) { |
| 799 | .running => {}, |
| 800 | .completed => self.join(), |
| 801 | .detached => unreachable, |
| 802 | } |
| 803 | } |
| 804 | |
| 805 | fn join(self: Impl) void { |
| 806 | defer { |
| 807 | // Create a copy of the allocator so we do not free the reference to the |
| 808 | // original allocator while freeing the memory. |
| 809 | var allocator = self.thread.allocator; |
| 810 | allocator.free(self.thread.memory); |
| 811 | } |
| 812 | |
| 813 | var spin: u8 = 10; |
| 814 | while (true) { |
| 815 | const tid = self.thread.tid.load(.SeqCst); |
| 816 | if (tid == 0) { |
| 817 | break; |
| 818 | } |
| 819 | |
| 820 | if (spin > 0) { |
| 821 | spin -= 1; |
| 822 | std.atomic.spinLoopHint(); |
| 823 | continue; |
| 824 | } |
| 825 | |
| 826 | const result = asm ( |
| 827 | \\ local.get %[ptr] |
| 828 | \\ local.get %[expected] |
| 829 | \\ i64.const -1 # infinite |
| 830 | \\ memory.atomic.wait32 0 |
| 831 | \\ local.set %[ret] |
| 832 | : [ret] "=r" (-> u32), |
| 833 | : [ptr] "r" (&self.thread.tid.value), |
| 834 | [expected] "r" (tid), |
| 835 | ); |
| 836 | switch (result) { |
| 837 | 0 => continue, // ok |
| 838 | 1 => continue, // expected =! loaded |
| 839 | 2 => unreachable, // timeout (infinite) |
| 840 | else => unreachable, |
| 841 | } |
| 842 | } |
| 843 | } |
| 844 | |
| 845 | fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) !WasiThreadImpl { |
| 846 | if (config.allocator == null) return error.OutOfMemory; // an allocator is required to spawn a WASI-thread |
| 847 | |
| 848 | // Wrapping struct required to hold the user-provided function arguments. |
| 849 | const Wrapper = struct { |
| 850 | args: @TypeOf(args), |
| 851 | fn entry(ptr: usize) void { |
| 852 | const w: *@This() = @ptrFromInt(ptr); |
| 853 | @call(.auto, f, w.args); |
| 854 | } |
| 855 | }; |
| 856 | |
| 857 | var stack_offset: usize = undefined; |
| 858 | var tls_offset: usize = undefined; |
| 859 | var wrapper_offset: usize = undefined; |
| 860 | var instance_offset: usize = undefined; |
| 861 | |
| 862 | // Calculate the bytes we have to allocate to store all thread information, including: |
| 863 | // - The actual stack for the thread |
| 864 | // - The TLS segment |
| 865 | // - `Instance` - containing information about how to call the user's function. |
| 866 | const map_bytes = blk: { |
| 867 | // start with atleast a single page, which is used as a guard to prevent |
| 868 | // other threads clobbering our new thread. |
| 869 | // Unfortunately, WebAssembly has no notion of read-only segments, so this |
| 870 | // is only a best effort. |
| 871 | var bytes: usize = std.wasm.page_size; |
| 872 | |
| 873 | bytes = std.mem.alignForward(usize, bytes, 16); // align stack to 16 bytes |
| 874 | stack_offset = bytes; |
| 875 | bytes += @max(std.wasm.page_size, config.stack_size); |
| 876 | |
| 877 | bytes = std.mem.alignForward(usize, bytes, __tls_align()); |
| 878 | tls_offset = bytes; |
| 879 | bytes += __tls_size(); |
| 880 | |
| 881 | bytes = std.mem.alignForward(usize, bytes, @alignOf(Wrapper)); |
| 882 | wrapper_offset = bytes; |
| 883 | bytes += @sizeOf(Wrapper); |
| 884 | |
| 885 | bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); |
| 886 | instance_offset = bytes; |
| 887 | bytes += @sizeOf(Instance); |
| 888 | |
| 889 | bytes = std.mem.alignForward(usize, bytes, std.wasm.page_size); |
| 890 | break :blk bytes; |
| 891 | }; |
| 892 | |
| 893 | // Allocate the amount of memory required for all meta data. |
| 894 | const allocated_memory = try config.allocator.?.alloc(u8, map_bytes); |
| 895 | |
| 896 | const wrapper: *Wrapper = @ptrCast(@alignCast(&allocated_memory[wrapper_offset])); |
| 897 | wrapper.* = .{ .args = args }; |
| 898 | |
| 899 | const instance: *Instance = @ptrCast(@alignCast(&allocated_memory[instance_offset])); |
| 900 | instance.* = .{ |
| 901 | .thread = .{ .memory = allocated_memory, .allocator = config.allocator.? }, |
| 902 | .tls_offset = tls_offset, |
| 903 | .stack_offset = stack_offset, |
| 904 | .raw_ptr = @intFromPtr(wrapper), |
| 905 | .call_back = &Wrapper.entry, |
| 906 | .original_stack_pointer = __get_stack_pointer(), |
| 907 | }; |
| 908 | |
| 909 | const tid = spawnWasiThread(instance); |
| 910 | // The specification says any value lower than 0 indicates an error. |
| 911 | // The values of such error are unspecified. WASI-Libc treats it as EAGAIN. |
| 912 | if (tid < 0) { |
| 913 | return error.SystemResources; |
| 914 | } |
| 915 | instance.thread.tid.store(tid, .SeqCst); |
| 916 | |
| 917 | return .{ .thread = &instance.thread }; |
| 918 | } |
| 919 | |
| 920 | /// Bootstrap procedure, called by the host environment after thread creation. |
| 921 | export fn wasi_thread_start(tid: i32, arg: *Instance) void { |
| 922 | if (builtin.single_threaded) { |
| 923 | // ensure function is not analyzed in single-threaded mode |
| 924 | return; |
| 925 | } |
| 926 | __set_stack_pointer(arg.thread.memory.ptr + arg.stack_offset); |
| 927 | __wasm_init_tls(arg.thread.memory.ptr + arg.tls_offset); |
| 928 | @atomicStore(u32, &WasiThreadImpl.tls_thread_id, @intCast(tid), .SeqCst); |
| 929 | |
| 930 | // Finished bootstrapping, call user's procedure. |
| 931 | arg.call_back(arg.raw_ptr); |
| 932 | |
| 933 | switch (arg.thread.state.swap(.completed, .SeqCst)) { |
| 934 | .running => { |
| 935 | // reset the Thread ID |
| 936 | asm volatile ( |
| 937 | \\ local.get %[ptr] |
| 938 | \\ i32.const 0 |
| 939 | \\ i32.atomic.store 0 |
| 940 | : |
| 941 | : [ptr] "r" (&arg.thread.tid.value), |
| 942 | ); |
| 943 | |
| 944 | // Wake the main thread listening to this thread |
| 945 | asm volatile ( |
| 946 | \\ local.get %[ptr] |
| 947 | \\ i32.const 1 # waiters |
| 948 | \\ memory.atomic.notify 0 |
| 949 | \\ drop # no need to know the waiters |
| 950 | : |
| 951 | : [ptr] "r" (&arg.thread.tid.value), |
| 952 | ); |
| 953 | }, |
| 954 | .completed => unreachable, |
| 955 | .detached => { |
| 956 | // restore the original stack pointer so we can free the memory |
| 957 | // without having to worry about freeing the stack |
| 958 | __set_stack_pointer(arg.original_stack_pointer); |
| 959 | // Ensure a copy so we don't free the allocator reference itself |
| 960 | var allocator = arg.thread.allocator; |
| 961 | allocator.free(arg.thread.memory); |
| 962 | }, |
| 963 | } |
| 964 | } |
| 965 | |
| 966 | /// Asks the host to create a new thread for us. |
| 967 | /// Newly created thread will call `wasi_tread_start` with the thread ID as well |
| 968 | /// as the input `arg` that was provided to `spawnWasiThread` |
| 969 | const spawnWasiThread = @"thread-spawn"; |
| 970 | extern "wasi" fn @"thread-spawn"(arg: *Instance) i32; |
| 971 | |
| 972 | /// Initializes the TLS data segment starting at `memory`. |
| 973 | /// This is a synthetic function, generated by the linker. |
| 974 | extern fn __wasm_init_tls(memory: [*]u8) void; |
| 975 | |
| 976 | /// Returns a pointer to the base of the TLS data segment for the current thread |
| 977 | inline fn __tls_base() [*]u8 { |
| 978 | return asm ( |
| 979 | \\ .globaltype __tls_base, i32 |
| 980 | \\ global.get __tls_base |
| 981 | \\ local.set %[ret] |
| 982 | : [ret] "=r" (-> [*]u8), |
| 983 | ); |
| 984 | } |
| 985 | |
| 986 | /// Returns the size of the TLS segment |
| 987 | inline fn __tls_size() u32 { |
| 988 | return asm volatile ( |
| 989 | \\ .globaltype __tls_size, i32, immutable |
| 990 | \\ global.get __tls_size |
| 991 | \\ local.set %[ret] |
| 992 | : [ret] "=r" (-> u32), |
| 993 | ); |
| 994 | } |
| 995 | |
| 996 | /// Returns the alignment of the TLS segment |
| 997 | inline fn __tls_align() u32 { |
| 998 | return asm ( |
| 999 | \\ .globaltype __tls_align, i32, immutable |
| 1000 | \\ global.get __tls_align |
| 1001 | \\ local.set %[ret] |
| 1002 | : [ret] "=r" (-> u32), |
| 1003 | ); |
| 1004 | } |
| 1005 | |
| 1006 | /// Allows for setting the stack pointer in the WebAssembly module. |
| 1007 | inline fn __set_stack_pointer(addr: [*]u8) void { |
| 1008 | asm volatile ( |
| 1009 | \\ local.get %[ptr] |
| 1010 | \\ global.set __stack_pointer |
| 1011 | : |
| 1012 | : [ptr] "r" (addr), |
| 1013 | ); |
| 1014 | } |
| 1015 | |
| 1016 | /// Returns the current value of the stack pointer |
| 1017 | inline fn __get_stack_pointer() [*]u8 { |
| 1018 | return asm ( |
| 1019 | \\ global.get __stack_pointer |
| 1020 | \\ local.set %[stack_ptr] |
| 1021 | : [stack_ptr] "=r" (-> [*]u8), |
| 1022 | ); |
| 1023 | } |
| 1024 | }; |
| 1025 | |
| 736 | 1026 | const LinuxThreadImpl = struct { |
| 737 | 1027 | const linux = os.linux; |
| 738 | 1028 | |