├── .github └── workflows │ └── wasix-text.yml ├── .gitignore ├── README.md ├── build.zig ├── build.zig.zon └── src ├── root.zig └── test.zig /.github/workflows/wasix-text.yml: -------------------------------------------------------------------------------- 1 | name: wasix-test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | runs-on: ${{ matrix.os }} 12 | name: Build and Test 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Zig 18 | uses: mlugg/setup-zig@v1 19 | with: 20 | version: 0.14.0-dev.2571+01081cc8e 21 | 22 | - name: Setup Wasmer 23 | uses: wasmerio/setup-wasmer@v3.1 24 | 25 | - name: Build for WASM 26 | run: zig build -Dtarget=wasm32-wasi 27 | 28 | - name: Run tests 29 | run: wasmer run --enable-all --net --enable-async-threads --http-client --verbose zig-out/bin/wasix-test.wasm 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | run.sh 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zig-WASIX 2 | WASIX extensions for Zig ⚡. This module let's you to create programs with Wasmer & WASIX that can run on edge or be sandboxed. 3 | 4 | ## Installation 5 | Install via Zig package manager (Copy the full SHA of latest commit hash from GitHub): 6 | ```sh 7 | zig fetch --save https://github.com/nikneym/zig-wasix/archive/.tar.gz 8 | ``` 9 | In your `build` function at `build.zig`, make sure your build step and source files are aware of the module: 10 | ```zig 11 | const dep_opts = .{ .target = target, .optimize = optimize }; 12 | 13 | const wasix_dep = b.dependency("wasix", dep_opts); 14 | const wasix_module = wasix_dep.module("wasix"); 15 | 16 | exe_mod.addImport("wasix", wasix_module); 17 | ``` 18 | 19 | ## Building Projects 20 | Now you can build your projects by targeting WASM & WASI and run on Wasmer, as the following: 21 | ```sh 22 | zig build -Dtarget=wasm32-wasi 23 | wasmer run zig-out/bin/.wasm 24 | 25 | # You may want to have networking and other stuff be enabled 26 | wasmer run --enable-all --net zig-out/bin/.wasm 27 | ``` 28 | 29 | ## Documentation 30 | You can refer to [wasix.org](https://wasix.org/) for documentation and further API reference. 31 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) !void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | // Expose zig-wasix as a module. 19 | _ = b.addModule("wasix", .{ 20 | .root_source_file = b.path("src/root.zig"), 21 | .imports = &.{}, 22 | }); 23 | 24 | // In order to test, we output a .wasm file that can be run with wasmer. 25 | // zig build -Dtarget=wasm32-wasi 26 | const main_tests = b.addExecutable(.{ 27 | .name = "wasix-test", 28 | .root_source_file = b.path("src/test.zig"), 29 | .target = target, 30 | .optimize = optimize, 31 | }); 32 | 33 | b.installArtifact(main_tests); 34 | } 35 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .paths = .{""}, 3 | .name = "wasix", 4 | .version = "0.2.0", 5 | .dependencies = .{}, 6 | } 7 | -------------------------------------------------------------------------------- /src/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const os = std.os; 3 | const w = os.wasi; 4 | 5 | pub const bool_t = u8; 6 | 7 | pub const pointersize_t = u32; 8 | 9 | pub const address_family_t = u8; 10 | 11 | pub const ip_port_t = u16; 12 | 13 | pub const sock_type_t = u8; 14 | 15 | pub const sock_proto_t = u16; 16 | 17 | pub const sock_option_t = u8; 18 | 19 | pub const epoll_ctl_t = u32; 20 | 21 | pub const CTL = struct { 22 | pub const ADD = 1; 23 | pub const MOD = 2; 24 | pub const DEL = 3; 25 | }; 26 | 27 | pub const epoll_type_t = u32; 28 | 29 | pub const EPOLL = struct { 30 | pub const IN = 0x001; 31 | pub const OUT = 0x004; 32 | pub const RDHUP = 1 << 2; 33 | pub const PRI = 1 << 3; 34 | pub const ERR = 0x008; 35 | pub const HUP = 0x010; 36 | pub const ET = (1 << 31); 37 | pub const ONESHOT = 1 << 7; 38 | }; 39 | 40 | // this is actually defined as union on Linux 41 | pub const epoll_data_t = extern struct { 42 | ptr: ?*anyopaque, 43 | fd: w.fd_t, 44 | data1: u32, 45 | data2: u64, 46 | }; 47 | 48 | pub const epoll_event_t = extern struct { 49 | events: epoll_type_t, 50 | data: epoll_data_t, 51 | }; 52 | 53 | pub const http_handles_t = extern struct { 54 | request: w.fd_t, 55 | response: w.fd_t, 56 | headers: w.fd_t, 57 | }; 58 | 59 | const option_timestamp_u_t = extern union { 60 | none: u8, 61 | some: w.timestamp_t, 62 | }; 63 | 64 | const option_timestamp_t = extern struct { 65 | tag: enum(u8) { 66 | none = 0, 67 | some = 1, 68 | }, 69 | u: option_timestamp_u_t, 70 | }; 71 | 72 | // An IPv4 address is a 32-bit number that uniquely identifies a network interface on a machine. 73 | const addr_ip4_t = extern struct { 74 | n0: u8, 75 | n1: u8, 76 | h0: u8, 77 | h1: u8, 78 | }; 79 | 80 | // An IPv6 address is a 128-bit number that uniquely identifies a network interface on a machine. 81 | const addr_ip6_t = extern struct { 82 | n0: u16, 83 | n1: u16, 84 | n2: u16, 85 | n3: u16, 86 | h0: u16, 87 | h1: u16, 88 | h2: u16, 89 | h3: u16, 90 | }; 91 | 92 | pub const addr_unix_t = extern struct { 93 | b0: u8, 94 | b1: u8, 95 | b2: u8, 96 | b3: u8, 97 | b4: u8, 98 | b5: u8, 99 | b6: u8, 100 | b7: u8, 101 | b8: u8, 102 | b9: u8, 103 | b10: u8, 104 | b11: u8, 105 | b12: u8, 106 | b13: u8, 107 | b14: u8, 108 | b15: u8, 109 | }; 110 | 111 | pub const addr_unspec_t = extern struct { 112 | n0: u8, 113 | }; 114 | 115 | pub const addr_unspec_port_t = extern struct { 116 | port: ip_port_t, 117 | addr: addr_unspec_t, 118 | }; 119 | 120 | pub const addr_ip4_port_t = extern struct { 121 | port: ip_port_t, 122 | addr: addr_ip4_t, 123 | }; 124 | 125 | pub const addr_ip6_port_t = extern struct { 126 | port: ip_port_t, 127 | addr: addr_ip6_t, 128 | }; 129 | 130 | pub const addr_unix_port_t = extern struct { 131 | port: ip_port_t, 132 | addr: addr_unix_t, 133 | }; 134 | 135 | pub const addr_port_u_t = extern union { 136 | unspec: addr_unspec_port_t, 137 | inet4: addr_ip4_port_t, 138 | inet6: addr_ip6_port_t, 139 | unix: addr_unix_port_t, 140 | }; 141 | 142 | pub const addr_tag_t = enum(u8) { 143 | unspec, 144 | inet4, 145 | inet6, 146 | unix, 147 | 148 | pub fn name(tag: addr_tag_t) []const u8 { 149 | return switch (tag) { 150 | .unspec => "unspec", 151 | .inet4 => "inet4", 152 | .inet6 => "inet6", 153 | .unix => "unix", 154 | }; 155 | } 156 | }; 157 | 158 | pub const addr_port_t = extern struct { 159 | tag: addr_tag_t, 160 | u: addr_port_u_t, 161 | }; 162 | 163 | pub const addr_u_t = extern union { 164 | unspec: addr_unspec_port_t, 165 | inet4: addr_ip4_port_t, 166 | inet6: addr_ip6_port_t, 167 | unix: addr_unix_port_t, 168 | }; 169 | 170 | pub const addr_t = extern struct { 171 | tag: addr_tag_t, 172 | u: addr_u_t, 173 | }; 174 | 175 | pub const AddressFamily = enum(address_family_t) { 176 | unspec, 177 | inet4, 178 | inet6, 179 | unix, 180 | }; 181 | 182 | pub const SockType = enum(sock_type_t) { 183 | unused, 184 | stream, 185 | datagram, 186 | raw, 187 | seqpacket, 188 | }; 189 | 190 | pub const SockProto = enum(sock_proto_t) { 191 | ip = 0, 192 | icmp = 1, 193 | igmp = 2, 194 | ipip = 4, 195 | tcp = 6, 196 | egp = 8, 197 | pup = 12, 198 | udp = 17, 199 | idp = 22, 200 | dccp = 33, 201 | ipv6 = 41, 202 | routing = 43, 203 | fragment = 44, 204 | rsvp = 46, 205 | gre = 47, 206 | esp = 50, 207 | ah = 51, 208 | icmpv6 = 58, 209 | none = 59, 210 | dstopts = 60, 211 | mtp = 92, 212 | beetph = 94, 213 | encap = 98, 214 | pim = 103, 215 | comp = 108, 216 | sctp = 132, 217 | mh = 135, 218 | udplite = 136, 219 | mpls = 137, 220 | ethernet = 143, 221 | raw = 255, 222 | mptcp = 262, 223 | max = 263, 224 | _, 225 | }; 226 | 227 | pub const tty_t = extern struct { 228 | height: u32, 229 | stdin_tty: bool_t, 230 | stdout_tty: bool_t, 231 | stderr_tty: bool_t, 232 | echo: bool_t, 233 | line_buffered: bool_t, 234 | line_feeds: bool_t, 235 | }; 236 | 237 | pub const tid_t = i32; 238 | 239 | pub const thread_start_t = extern struct { 240 | // address where the stack starts 241 | stack: pointersize_t, 242 | 243 | // Address where the TLS starts 244 | tls_base: pointersize_t, 245 | 246 | // Function that will be invoked when the thread starts 247 | start_funct: pointersize_t, 248 | 249 | // Arguments to pass the callback function 250 | start_args: pointersize_t, 251 | 252 | // Reserved for future WASI usage 253 | reserved0: pointersize_t, 254 | reserved1: pointersize_t, 255 | reserved2: pointersize_t, 256 | reserved3: pointersize_t, 257 | reserved4: pointersize_t, 258 | reserved5: pointersize_t, 259 | reserved6: pointersize_t, 260 | reserved7: pointersize_t, 261 | reserved8: pointersize_t, 262 | reserved9: pointersize_t, 263 | 264 | // The size of the stack 265 | stack_size: pointersize_t, 266 | 267 | // The size of the guards at the end of the stack 268 | guard_size: pointersize_t, 269 | }; 270 | 271 | pub const hash_t = extern struct { 272 | b0: u64, 273 | b1: u64, 274 | }; 275 | 276 | pub const stack_snapshot_t = extern struct { 277 | user: u64, 278 | hash: hash_t, 279 | }; 280 | 281 | pub const longsize_t = u64; 282 | 283 | // clock 284 | pub extern "wasix_32v1" fn clock_time_set(id: w.clockid_t, timestamp: w.timestamp_t) w.errno_t; 285 | 286 | // file descriptor 287 | pub extern "wasix_32v1" fn fd_dup(id: w.fd_t, ret_fd: *w.fd_t) w.errno_t; 288 | pub extern "wasix_32v1" fn fd_event(initial_val: u64, flags: w.eventrwflags_t, ret_fd: *w.fd_t) w.errno_t; 289 | pub extern "wasix_32v1" fn fd_pipe(fd1: w.fd_t, fd2: w.fd_t) w.errno_t; 290 | 291 | // TTY 292 | pub extern "wasix_32v1" fn tty_get(state: *tty_t) w.errno_t; 293 | pub extern "wasix_32v1" fn tty_set(state: *tty_t) w.errno_t; 294 | 295 | // directory 296 | pub extern "wasix_32v1" fn getcwd(buf: [*]u8, path_len: *pointersize_t) w.errno_t; 297 | pub extern "wasix_32v1" fn chdir(path: [*:0]const u8, path_len: usize) w.errno_t; 298 | 299 | // signal handling 300 | pub extern "wasix_32v1" fn callback_signal(callback: [*:0]const u8, callback_len: usize) void; 301 | 302 | // threading 303 | pub extern "wasix_32v1" fn thread_spawn_v2(start_ptr: *thread_start_t, ret_tid: *tid_t) w.errno_t; 304 | pub extern "wasix_32v1" fn thread_sleep(duration: w.timestamp_t) w.errno_t; 305 | pub extern "wasix_32v1" fn thread_id(ret_tid: *tid_t) w.errno_t; 306 | pub extern "wasix_32v1" fn thread_join(tid: tid_t) w.errno_t; 307 | pub extern "wasix_32v1" fn thread_parallelism(ret_parallelism: *usize) w.errno_t; 308 | pub extern "wasix_32v1" fn thread_signal(tid: tid_t, signal: w.signal_t) w.errno_t; 309 | // TODO: add futex functions here 310 | pub extern "wasix_32v1" fn thread_exit(rval: w.exitcode_t) noreturn; 311 | 312 | // NOTE: has bugs, disabled 313 | //pub extern "wasix_32v1" fn stack_checkpoint(snapshot: *stack_snapshot_t, retptr: *longsize_t) w.errno_t; 314 | 315 | // socket 316 | pub extern "wasix_32v1" fn sock_open(af: AddressFamily, ty: SockType, pt: SockProto, ro_sock: *w.fd_t) w.errno_t; 317 | pub extern "wasix_32v1" fn sock_set_opt_flag(sock: w.fd_t, opt: sock_option_t, flag: bool_t) w.errno_t; 318 | pub extern "wasix_32v1" fn sock_get_opt_flag(sock: w.fd_t, opt: sock_option_t, ret_flag: *bool_t) w.errno_t; 319 | pub extern "wasix_32v1" fn sock_set_opt_time(sock: w.fd_t, opt: sock_option_t, time: *const option_timestamp_t) w.errno_t; 320 | pub extern "wasix_32v1" fn sock_get_opt_time(sock: w.fd_t, opt: sock_option_t, ret_time: *option_timestamp_t) w.errno_t; 321 | pub extern "wasix_32v1" fn sock_set_opt_size(sock: w.fd_t, opt: sock_option_t, size: w.filesize_t) w.errno_t; 322 | pub extern "wasix_32v1" fn sock_get_opt_size(sock: w.fd_t, opt: sock_option_t, ret_size: w.filesize_t) w.errno_t; 323 | pub extern "wasix_32v1" fn sock_join_multicast_v4(sock: w.fd_t, multiaddr: *const addr_ip4_t, iface: *const addr_ip4_t) w.errno_t; 324 | pub extern "wasix_32v1" fn sock_leave_multicast_v4(sock: w.fd_t, multiaddr: *const addr_ip4_t, iface: *const addr_ip4_t) w.errno_t; 325 | pub extern "wasix_32v1" fn sock_join_multicast_v6(sock: w.fd_t, multiaddr: *const addr_ip6_t, iface: u32) w.errno_t; 326 | pub extern "wasix_32v1" fn sock_leave_multicast_v6(sock: w.fd_t, multiaddr: *const addr_ip6_t, iface: u32) w.errno_t; 327 | pub extern "wasix_32v1" fn sock_bind(sock: w.fd_t, addr: *const addr_port_t) w.errno_t; 328 | pub extern "wasix_32v1" fn sock_listen(sock: w.fd_t, backlog: usize) w.errno_t; 329 | pub extern "wasix_32v1" fn sock_accept_v2(sock: w.fd_t, fd_flags: w.fdflags_t, ro_fd: *w.fd_t, ro_addr: *addr_port_t) w.errno_t; 330 | pub extern "wasix_32v1" fn sock_connect(sock: w.fd_t, addr: *const addr_port_t) w.errno_t; 331 | // TODO: implement more sock functions 332 | 333 | // DNS resolution 334 | pub extern "wasix_32v1" fn resolve(host: [*]const u8, host_len: usize, port: u16, addrs: [*]addr_t, naddrs: usize, ret_naddrs: *usize) w.errno_t; 335 | 336 | // epoll 337 | pub extern "wasix_32v1" fn epoll_create(ret_fd: *w.fd_t) w.errno_t; 338 | pub extern "wasix_32v1" fn epoll_ctl(epfd: w.fd_t, op: epoll_ctl_t, fd: w.fd_t, event: *const epoll_event_t) w.errno_t; 339 | pub extern "wasix_32v1" fn epoll_wait(epfd: w.fd_t, events: [*]epoll_event_t, maxevents: usize, timeout: w.timestamp_t, ret_size: *usize) w.errno_t; 340 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | // we can't use test blocks since the tests are going to be run by wasmer executable 2 | const std = @import("std"); 3 | const testing = std.testing; 4 | const assert = std.debug.assert; 5 | const os = std.os; 6 | const w = std.os.wasi; 7 | const wx = @import("root.zig"); 8 | 9 | // TODO: add more tests 10 | pub fn main() !void { 11 | try testSocketsAndEpoll(); 12 | try testDnsResolver(); 13 | } 14 | 15 | fn testSocketsAndEpoll() !void { 16 | // Create an epoll instance 17 | var epfd: w.fd_t = -1; 18 | var err = wx.epoll_create(&epfd); 19 | assert(err == .SUCCESS); 20 | defer { 21 | err = w.fd_close(epfd); 22 | assert(err == .SUCCESS); 23 | } 24 | 25 | // Create a listener socket 26 | var server_fd: w.fd_t = -1; 27 | err = wx.sock_open(.inet4, .stream, .tcp, &server_fd); 28 | assert(err == .SUCCESS); 29 | defer { 30 | err = w.fd_close(server_fd); 31 | assert(err == .SUCCESS); 32 | } 33 | 34 | // Bind the server socket 35 | const server_addr_port = &wx.addr_port_t{ 36 | .tag = .inet4, 37 | .u = .{ 38 | .inet4 = .{ 39 | .port = 6666, 40 | .addr = .{ 41 | .n0 = 127, 42 | .n1 = 0, 43 | .h0 = 0, 44 | .h1 = 1, 45 | }, 46 | }, 47 | }, 48 | }; 49 | 50 | err = wx.sock_bind(server_fd, server_addr_port); 51 | assert(err == .SUCCESS); 52 | 53 | // Listen for incoming connections 54 | err = wx.sock_listen(server_fd, 128); 55 | assert(err == .SUCCESS); 56 | 57 | // Register the server fd to epoll 58 | err = wx.epoll_ctl(epfd, wx.CTL.ADD, server_fd, &.{ 59 | .data = .{ .ptr = null, .fd = server_fd, .data1 = 0, .data2 = 0 }, 60 | // Only watch for read events 61 | .events = wx.EPOLL.IN, 62 | }); 63 | assert(err == .SUCCESS); 64 | 65 | // Create a client socket 66 | var client_fd: w.fd_t = -1; 67 | err = wx.sock_open(.inet4, .stream, .tcp, &client_fd); 68 | assert(err == .SUCCESS); 69 | defer { 70 | err = w.fd_close(client_fd); 71 | assert(err == .SUCCESS); 72 | } 73 | 74 | // Connect to server 75 | const client_addr_port = &wx.addr_port_t{ 76 | .tag = .inet4, 77 | .u = .{ 78 | .inet4 = .{ 79 | .port = 6666, 80 | .addr = .{ 81 | .n0 = 127, 82 | .n1 = 0, 83 | .h0 = 0, 84 | .h1 = 1, 85 | }, 86 | }, 87 | }, 88 | }; 89 | 90 | err = wx.sock_connect(client_fd, client_addr_port); 91 | assert(err == .SUCCESS); 92 | 93 | // We should get a single notification from epoll right now 94 | { 95 | var events: [1]wx.epoll_event_t = undefined; 96 | var ret_events: usize = 0; 97 | 98 | err = wx.epoll_wait(epfd, @ptrCast(&events), events.len, 0, &ret_events); 99 | assert(err == .SUCCESS); 100 | try testing.expectEqual(1, ret_events); 101 | 102 | const event = events[0]; 103 | 104 | // We should only get an EPOLLIN event 105 | try testing.expectEqual(true, event.events & wx.EPOLL.IN == wx.EPOLL.IN); 106 | 107 | // Accept the incoming connection, set the SOCK_NONBLOCK flag 108 | var incoming_fd: w.fd_t = -1; 109 | err = w.sock_accept(server_fd, .{ .NONBLOCK = true }, &incoming_fd); 110 | assert(err == .SUCCESS); 111 | 112 | try testing.expect(incoming_fd != -1); 113 | 114 | // Add the accepted socket to epoll watch list 115 | err = wx.epoll_ctl(epfd, wx.CTL.ADD, incoming_fd, &.{ 116 | .data = .{ .ptr = null, .fd = incoming_fd, .data1 = 0, .data2 = 0 }, 117 | // Watch for read and write events 118 | .events = wx.EPOLL.IN, 119 | }); 120 | assert(err == .SUCCESS); 121 | } 122 | 123 | // Send a message from client to server 124 | { 125 | const buf = "hello"; 126 | const iovec = &w.iovec_t{ .base = @ptrCast(@constCast(buf.ptr)), .len = buf.len }; 127 | var sent_len: usize = 0; 128 | 129 | err = w.sock_send(client_fd, @ptrCast(iovec), 1, 0, &sent_len); 130 | assert(err == .SUCCESS); 131 | 132 | try testing.expectEqual(buf.len, sent_len); 133 | } 134 | } 135 | 136 | fn testDnsResolver() !void { 137 | // Do a DNS resolution 138 | const host = "www.google.com"; 139 | var results: [1]wx.addr_t = undefined; 140 | var results_len: usize = 0; 141 | 142 | const err = wx.resolve(host.ptr, host.len, 80, @ptrCast(&results), 1, &results_len); 143 | assert(err == .SUCCESS); 144 | } 145 | --------------------------------------------------------------------------------