├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── cmd └── wasirun │ └── main.go ├── error.go ├── error_darwin.go ├── error_linux.go ├── error_test.go ├── error_unix.go ├── exec.go ├── file.go ├── fs.go ├── go.mod ├── go.sum ├── imports ├── builder.go ├── builder_default.go ├── builder_unix.go ├── defaults.go ├── extensions.go ├── wasi_http │ ├── README.md │ ├── common │ │ └── util.go │ ├── default_http │ │ ├── http.go │ │ └── request.go │ ├── http.go │ ├── http_test.go │ ├── server │ │ └── server.go │ ├── streams │ │ ├── read.go │ │ ├── streams.go │ │ └── write.go │ └── types │ │ ├── http.go │ │ ├── request.go │ │ ├── response.go │ │ └── structs.go └── wasi_snapshot_preview1 │ ├── module.go │ └── wasmedge.go ├── internal ├── descriptor │ ├── isatty_darwin.go │ ├── isatty_linux.go │ ├── table.go │ └── table_test.go └── sockets │ ├── dial.go │ ├── listen.go │ └── socket.go ├── poll.go ├── rights.go ├── share └── go_wasip1_wasm_exec ├── socket.go ├── socket_test.go ├── system.go ├── systems └── unix │ ├── file.go │ ├── path_open_sockets.go │ ├── readdir_darwin.go │ ├── readdir_linux.go │ ├── syscall_darwin.go │ ├── syscall_linux.go │ ├── syscall_unix.go │ ├── system.go │ ├── system_test.go │ └── testdata │ ├── empty │ ├── message.txt │ └── tmp │ ├── one │ ├── three │ └── two ├── testdata ├── adapter.py ├── c │ ├── hello_world.c │ ├── hello_world.wasm │ └── http │ │ ├── Makefile │ │ ├── README.md │ │ ├── http.c │ │ ├── http.wasm │ │ ├── proxy.c │ │ ├── proxy.h │ │ ├── proxy_component_type.o │ │ ├── server.c │ │ └── server.wasm ├── go │ ├── hello_world.go │ └── hello_world.wasm └── tinygo │ ├── hello_world.go │ └── hello_world.wasm ├── time.go ├── tracer.go ├── wasi.go ├── wasi_test.go ├── wasitest ├── file.go ├── poll.go ├── proc.go ├── socket.go ├── system.go ├── wasip1.go └── wasitest.go └── wazergo.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | 8 | jobs: 9 | go-test: 10 | name: Go Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version-file: go.mod 21 | check-latest: true 22 | 23 | # We run the tests 20 times because sometimes it helps highlight flaky 24 | # behaviors that do not trigger on a single pass. 25 | - name: Go Tests 26 | run: make test count=20 27 | 28 | golangci-lint: 29 | name: Go Lint 30 | runs-on: ubuntu-latest 31 | timeout-minutes: 30 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions/setup-go@v4 35 | with: 36 | go-version-file: go.mod 37 | check-latest: true 38 | - run: go mod download 39 | - uses: golangci/golangci-lint-action@v3 40 | with: 41 | version: v1.54.0 42 | args: --timeout 5m --issues-exit-code 0 # warn only 43 | 44 | wasi-test: 45 | name: WASI Test Suite 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v3 49 | with: 50 | submodules: recursive 51 | 52 | - name: Set up Go 53 | uses: actions/setup-go@v4 54 | with: 55 | go-version-file: go.mod 56 | check-latest: true 57 | 58 | - name: WASI Tests 59 | run: make wasi-testsuite 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | wasirun 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Emacs 19 | *~ 20 | 21 | # Dependencies 22 | testdata/.sysroot/ 23 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wasi-testsuite"] 2 | path = testdata/.wasi-testsuite 3 | url = https://github.com/WebAssembly/wasi-testsuite 4 | branch = prod/testsuite-base 5 | [submodule "wasi-libc"] 6 | path = testdata/.wasi-libc 7 | url = https://github.com/WebAssembly/wasi-libc 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contibution Guidelines 2 | 3 | ## Troubleshooting 4 | 5 | ### wasm-ld: error: cannot open */libclang_rt.builtins-wasm32.a 6 | 7 | This error may occur is the local clang installation does not have a version of 8 | `libclang_rt` built for WebAssembly, for example: 9 | 10 | ``` 11 | wasm-ld: error: cannot open {...}/lib/wasi/libclang_rt.builtins-wasm32.a: No such file or directory 12 | clang-9: error: linker command failed with exit code 1 (use -v to see invocation) 13 | ``` 14 | 15 | This article describes how to solve the issue https://depth-first.com/articles/2019/10/16/compiling-c-to-webassembly-and-running-it-without-emscripten/ 16 | which instructs to download precompiled versions of the library distributed at https://github.com/jedisct1/libclang_rt.builtins-wasm32.a 17 | and install them at the location where clang expects to find them (the path in the error message). 18 | 19 | Here is a [direct link](https://raw.githubusercontent.com/jedisct1/libclang_rt.builtins-wasm32.a/master/precompiled/libclang_rt.builtins-wasm32.a) 20 | to download the library. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean fmt lint test testdata wasi-libc wasi-testsuite 2 | 3 | GO ?= go 4 | 5 | count ?= 1 6 | 7 | wasi-go.src = \ 8 | $(wildcard *.go) \ 9 | $(wildcard */*.go) \ 10 | $(wildcard */*/*.go) 11 | 12 | wasirun.src = $(wasi-go.src) 13 | 14 | testdata.c.src = $(wildcard testdata/c/*.c) 15 | testdata.c.wasm = $(testdata.c.src:.c=.wasm) 16 | 17 | testdata.http.src = $(wildcard testdata/c/http/http*.c) 18 | testdata.http.wasm = $(testdata.http.src:.c=.wasm) 19 | 20 | testdata.go.src = $(wildcard testdata/go/*.go) 21 | testdata.go.wasm = $(testdata.go.src:.go=.wasm) 22 | 23 | testdata.tinygo.src = $(wildcard testdata/tinygo/*.go) 24 | testdata.tinygo.wasm = $(testdata.tinygo.src:.go=.wasm) 25 | 26 | testdata.files = \ 27 | $(testdata.c.wasm) \ 28 | $(testdata.http.wasm) \ 29 | $(testdata.go.wasm) \ 30 | $(testdata.tinygo.wasm) 31 | 32 | all: test wasi-testsuite 33 | 34 | clean: 35 | rm -f $(testdata.files) 36 | 37 | test: testdata 38 | $(GO) test -count=$(count) ./... 39 | 40 | fmt: 41 | $(GO) fmt ./... 42 | 43 | lint: 44 | which golangci-lint >/dev/null && golangci-lint run 45 | 46 | testdata: $(testdata.files) 47 | 48 | testdata/.sysroot: 49 | mkdir -p testdata/.sysroot 50 | 51 | testdata/.wasi-libc: testdata/.wasi-libc/.git 52 | 53 | testdata/.wasi-libc/.git: .gitmodules 54 | git submodule update --init --recursive -- testdata/.wasi-libc 55 | 56 | testdata/.wasi-testsuite: testdata/.wasi-testsuite/.git 57 | 58 | testdata/.wasi-testsuite/.git: .gitmodules 59 | git submodule update --init --recursive -- testdata/.wasi-testsuite 60 | 61 | testdata/.sysroot/lib/wasm32-wasi/libc.a: testdata/.wasi-libc 62 | make -j4 -C testdata/.wasi-libc install INSTALL_DIR=../.sysroot 63 | 64 | testdata/c/%.c: wasi-libc 65 | testdata/c/%.wasm: testdata/c/%.c 66 | clang $< -o $@ -Wall -Os -target wasm32-unknown-wasi --sysroot testdata/.sysroot 67 | 68 | testdata/c/http/http.wasm: testdata/c/http/http.c 69 | clang $< -o $@ -Wall -Os -target wasm32-unknown-wasi testdata/c/http/proxy.c testdata/c/http/proxy_component_type.o 70 | 71 | testdata/go/%.wasm: testdata/go/%.go 72 | GOARCH=wasm GOOS=wasip1 $(GO) build -o $@ $< 73 | 74 | testdata/tinygo/%.wasm: testdata/tinygo/%.go 75 | tinygo build -target=wasi -o $@ $< 76 | 77 | wasirun: go.mod $(wasirun.src) 78 | $(GO) build -o wasirun ./cmd/wasirun 79 | 80 | wasi-libc: testdata/.sysroot/lib/wasm32-wasi/libc.a 81 | 82 | wasi-testsuite: testdata/.wasi-testsuite wasirun 83 | python3 testdata/.wasi-testsuite/test-runner/wasi_test_runner.py \ 84 | -t testdata/.wasi-testsuite/tests/assemblyscript/testsuite \ 85 | testdata/.wasi-testsuite/tests/c/testsuite \ 86 | testdata/.wasi-testsuite/tests/rust/testsuite \ 87 | -r testdata/adapter.py 88 | @rm -rf testdata/.wasi-testsuite/tests/rust/testsuite/fs-tests.dir/*.cleanup 89 | 90 | .gitmodules: 91 | git submodule add --name wasi-libc -- \ 92 | 'https://github.com/WebAssembly/wasi-libc' testdata/.wasi-libc 93 | git submodule add --name wasi-testsuite -b prod/testsuite-base -- \ 94 | "https://github.com/WebAssembly/wasi-testsuite" testdata/.wasi-testsuite 95 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | wasi-go 2 | Copyright 2023 Stealth Rocket, Inc. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/stealthrocket/wasi-go/actions/workflows/wasi-testuite.yml/badge.svg)](https://github.com/stealthrocket/wasi-go/actions/workflows/go.yml) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/stealthrocket/wasi-go.svg)](https://pkg.go.dev/github.com/stealthrocket/wasi-go) 3 | [![Apache 2 License](https://img.shields.io/badge/license-Apache%202-blue.svg)](LICENSE) 4 | 5 | # WASI 6 | 7 | The [WebAssembly][wasm] System Interface ([WASI][wasi]) is a set of standard 8 | system functions that allow WebAssembly modules to interact with the outside 9 | world (e.g. perform I/O, read clocks). 10 | 11 | The WASI standard is under development. This repository provides a Go 12 | implementation of WASI [preview 1][preview1] for Unix systems, and a command 13 | to run WebAssembly modules that use WASI. 14 | 15 | ## Motivation 16 | 17 | WASI preview 1 was sealed without a complete socket API, and WASI as a standard 18 | is still a moving target. 19 | 20 | Some WebAssembly runtimes have taken the initiative to either extend WASI 21 | preview 1 or provide alternative solutions for capabilities that were missing 22 | from the core specification, enabling a wider range of applications to run as 23 | WebAssembly modules. 24 | 25 | This package intends to bundle WASI preview 1 and these extensions with the 26 | [wazero][wazero] runtime, and more generally be a playground for 27 | experimentation with cutting-edge WASI features. 28 | 29 | :electric_plug: **Sockets** 30 | 31 | This library provides all the socket capabilities specified in WASI preview 1, 32 | as well as a full support for a socket API which is ABI-compatible with the 33 | extensions implemented in the [wasmedge][wasmedge] runtime. 34 | 35 | Applications interested in taking advantage of the socket extensions may be 36 | interested in those libraries: 37 | 38 | | Language | Library | 39 | | -------- | --------------------------------------------------------- | 40 | | Go | [stealthrocket/net][net-go] | 41 | | Python | [stealthrocket/timecraft (Python SDK)][timecraft-python] | 42 | | Rust | [second-state/wasmedge_wasi_socket][wasmedge-wasi-socket] | 43 | 44 | [net-go]: https://github.com/stealthrocket/net 45 | [timecraft-python]: https://github.com/stealthrocket/timecraft/tree/main/python 46 | [wasmedge-wasi-socket]: https://github.com/second-state/wasmedge_wasi_socket 47 | 48 | :zap: **Performance** 49 | 50 | The provided implementation of WASI is a thin zero-allocation layer around OS 51 | system calls. Non-blocking I/O is fully supported, allowing WebAssembly modules 52 | with an embedded scheduler (e.g. the Go runtime, or Rust Tokio scheduler) to 53 | schedule goroutines / green threads while waiting for I/O. 54 | 55 | :battery: **Experimentation** 56 | 57 | The library separates the implementation of WASI from the WebAssembly runtime 58 | host module, so that implementations of the provided WASI interface don't have 59 | to worry about ABI concerns. The design makes it easy to wrap and augment WASI, 60 | and keep up with the evolving WASI specification. 61 | 62 | ## Non-Goals 63 | 64 | `wasi-go` does not aim to be a drop-in replacement for the `wasi_snapshot_preview1` 65 | package that ships with the [wazero][wazero] runtime. For example, the `wasi-go` 66 | package does not build on Windows, nor does it allow customization of the file 67 | systems via a `fs.FS`. 68 | 69 | The following table describes how users should think about capabilities of 70 | wazero and wasi-go: 71 | 72 | | Feature | `wazero/imports/wasi_snapshot_preview1` | `wasi-go/imports/wasi_snapshot_preview1` | 73 | | -------------------------- | --------------------------------------- | ---------------------------------------- | 74 | | WASI preview 1 | ✅ | ✅ | 75 | | Windows Support | ✅ | ❌ | 76 | | WasmEdge Socket Extensions | ❌ | ✅ | 77 | 78 | ## Usage 79 | 80 | ### As a Command 81 | 82 | A `wasirun` command is provided for running WebAssembly modules that use WASI host imports. 83 | It bundles the WASI implementation from this repository with the [wazero][wazero] runtime. 84 | 85 | ```console 86 | $ go install github.com/stealthrocket/wasi-go/cmd/wasirun@latest 87 | ``` 88 | 89 | The `wasirun` command has many options for controlling the capabilities of the WebAssembly 90 | module, and for tracing and profiling execution. See `wasirun --help` for details. 91 | 92 | ### As a Library 93 | 94 | The package layout is as follows: 95 | 96 | - `.` types, constants and an [interface][system] for WASI preview 1 97 | - [`systems/unix`][unix-system] a Unix implementation (tested on Linux and macOS) 98 | - [`imports/wasi_snapshot_preview1`][host-module] a host module for the [wazero][wazero] runtime 99 | - [`cmd/wasirun`][wasirun] a command to run WebAssembly modules 100 | - [`wasitest`][wasitest] a test suite against the WASI interface 101 | 102 | To run a WebAssembly module, it's also necessary to prepare clocks and "preopens" 103 | (files/directories that the WebAssembly module can access). To see how it all fits 104 | together, see the implementation of the [wasirun][wasirun] command. 105 | 106 | ### With Go 107 | 108 | As the providers of a Go implementation of WASI, we're naturally interested in 109 | Go's support for WebAssembly and WASI, and are championing the efforts to make 110 | Go a first class citizen in the ecosystem (along with Rust and Zig). 111 | 112 | [Go v1.21][go-121] has native support for WebAssembly and WASI: 113 | 114 | 115 | ```go 116 | package main 117 | 118 | import "fmt" 119 | 120 | func main() { 121 | fmt.Println("Hello, World!") 122 | } 123 | ``` 124 | 125 | ```console 126 | # go version 127 | go version go1.21.0 darwin/arm64 128 | $ GOOS=wasip1 GOARCH=wasm go build -o hello.wasm hello.go 129 | $ wasirun hello.wasm 130 | Hello, World! 131 | ``` 132 | 133 | This repository bundles [a script][go-script] that can be used to skip the 134 | `go build` step. 135 | 136 | ## Contributing 137 | 138 | Pull requests are welcome! Anything that is not a simple fix would probably 139 | benefit from being discussed in an issue first. 140 | 141 | Remember to be respectful and open minded! 142 | 143 | [wasm]: https://webassembly.org 144 | [wasi]: https://wasi.dev 145 | [system]: https://github.com/stealthrocket/wasi-go/blob/main/system.go 146 | [unix-system]: https://github.com/stealthrocket/wasi-go/blob/main/systems/unix/system.go 147 | [host-module]: https://github.com/stealthrocket/wasi-go/blob/main/imports/wasi_snapshot_preview1/module.go 148 | [preview1]: https://github.com/WebAssembly/WASI/blob/e324ce3/legacy/preview1/docs.md 149 | [wazero]: https://wazero.io 150 | [wasirun]: https://github.com/stealthrocket/wasi-go/blob/main/cmd/wasirun/main.go 151 | [wasitest]: https://github.com/stealthrocket/wasi-go/tree/main/wasitest 152 | [tracer]: https://github.com/stealthrocket/wasi-go/blob/main/tracer.go 153 | [sockets-extension]: https://github.com/stealthrocket/wasi-go/blob/main/sockets_extension.go 154 | [go-121]: https://go.dev/blog/go1.21 155 | [go-script]: https://github.com/stealthrocket/wasi-go/blob/main/share/go_wasip1_wasm_exec 156 | [wasmer]: https://github.com/wasmerio/wasmer 157 | [wasmedge]: https://github.com/WasmEdge/WasmEdge 158 | [lunatic]: https://github.com/lunatic-solutions/lunatic 159 | -------------------------------------------------------------------------------- /cmd/wasirun/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | "path/filepath" 12 | "runtime/debug" 13 | 14 | "github.com/stealthrocket/wasi-go" 15 | "github.com/stealthrocket/wasi-go/imports" 16 | "github.com/stealthrocket/wasi-go/imports/wasi_http" 17 | "github.com/tetratelabs/wazero" 18 | "github.com/tetratelabs/wazero/sys" 19 | ) 20 | 21 | func printUsage() { 22 | fmt.Printf(`wasirun - Run a WebAssembly module 23 | 24 | USAGE: 25 | wasirun [OPTIONS]... [--] [ARGS]... 26 | 27 | ARGS: 28 | 29 | The path of the WebAssembly module to run 30 | 31 | [ARGS]... 32 | Arguments to pass to the module 33 | 34 | OPTIONS: 35 | --dir 36 | Grant access to the specified host directory 37 | 38 | --listen 39 | Grant access to a socket listening on the specified address 40 | 41 | --dial 42 | Grant access to a socket connected to the specified address 43 | 44 | --dns-server 45 | Sets the address of the DNS server to use for name resolution 46 | 47 | --env-inherit 48 | Inherits all environment variables from the calling process 49 | 50 | --env 51 | Pass an environment variable to the module. Overrides 52 | any inherited environment variables from --env-inherit 53 | 54 | --sockets 55 | Enable a sockets extension, either {none, auto, path_open, 56 | wasmedgev1, wasmedgev2} 57 | 58 | --pprof-addr 59 | Start a pprof server listening on the specified address 60 | 61 | --trace 62 | Enable logging of system calls (like strace) 63 | 64 | --non-blocking-stdio 65 | Enable non-blocking stdio 66 | 67 | --max-open-files 68 | Limit the number of files that may be opened by the module 69 | 70 | --max-open-dirs 71 | Limit the number of directories that may be opened by the module 72 | 73 | --http 74 | Optionally enable wasi-http client support and select a 75 | version {none, auto, v1} 76 | 77 | --http-server-addr 78 | If present, assume run this module as an http server which 79 | listens for requests on this address. 80 | 81 | --http-server-path 82 | If present, and --http-server-addr is not empty, serve WebAssembly 83 | on this URL prefix path. Default is '/' 84 | 85 | -v, --version 86 | Print the version and exit 87 | 88 | -h, --help 89 | Show this usage information 90 | `) 91 | } 92 | 93 | var ( 94 | envInherit bool 95 | envs stringList 96 | dirs stringList 97 | listens stringList 98 | dials stringList 99 | dnsServer string 100 | socketExt string 101 | pprofAddr string 102 | wasiHttp string 103 | wasiHttpAddr string 104 | wasiHttpPath string 105 | trace bool 106 | tracerStringSize int 107 | nonBlockingStdio bool 108 | version bool 109 | maxOpenFiles int 110 | maxOpenDirs int 111 | ) 112 | 113 | func main() { 114 | flagSet := flag.NewFlagSet("wasirun", flag.ExitOnError) 115 | flagSet.Usage = printUsage 116 | 117 | flagSet.BoolVar(&envInherit, "env-inherit", false, "") 118 | flagSet.Var(&envs, "env", "") 119 | flagSet.Var(&dirs, "dir", "") 120 | flagSet.Var(&listens, "listen", "") 121 | flagSet.Var(&dials, "dial", "") 122 | flagSet.StringVar(&dnsServer, "dns-server", "", "") 123 | flagSet.StringVar(&socketExt, "sockets", "auto", "") 124 | flagSet.StringVar(&pprofAddr, "pprof-addr", "", "") 125 | flagSet.StringVar(&wasiHttp, "http", "auto", "") 126 | flagSet.StringVar(&wasiHttpAddr, "http-server-addr", "", "") 127 | flagSet.StringVar(&wasiHttpPath, "http-server-path", "/", "") 128 | flagSet.BoolVar(&trace, "trace", false, "") 129 | flagSet.IntVar(&tracerStringSize, "tracer-string-size", 32, "") 130 | flagSet.BoolVar(&nonBlockingStdio, "non-blocking-stdio", false, "") 131 | flagSet.BoolVar(&version, "version", false, "") 132 | flagSet.BoolVar(&version, "v", false, "") 133 | flagSet.IntVar(&maxOpenFiles, "max-open-files", 1024, "") 134 | flagSet.IntVar(&maxOpenDirs, "max-open-dirs", 1024, "") 135 | flagSet.Parse(os.Args[1:]) 136 | 137 | if version { 138 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" { 139 | fmt.Println("wasirun", info.Main.Version) 140 | } else { 141 | fmt.Println("wasirun", "devel") 142 | } 143 | os.Exit(0) 144 | } 145 | 146 | args := flagSet.Args() 147 | if len(args) == 0 { 148 | printUsage() 149 | os.Exit(1) 150 | } 151 | 152 | if envInherit { 153 | envs = append(append([]string{}, os.Environ()...), envs...) 154 | } 155 | 156 | if dnsServer != "" { 157 | _, dnsServerPort, _ := net.SplitHostPort(dnsServer) 158 | net.DefaultResolver.PreferGo = true 159 | net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { 160 | var d net.Dialer 161 | if dnsServerPort != "" { 162 | address = dnsServer 163 | } else { 164 | _, port, err := net.SplitHostPort(address) 165 | if err != nil { 166 | return nil, net.InvalidAddrError(address) 167 | } 168 | address = net.JoinHostPort(dnsServer, port) 169 | } 170 | return d.DialContext(ctx, network, address) 171 | } 172 | } 173 | 174 | if err := run(args[0], args[1:]); err != nil { 175 | if exitErr, ok := err.(*sys.ExitError); ok { 176 | os.Exit(int(exitErr.ExitCode())) 177 | } 178 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 179 | os.Exit(1) 180 | } 181 | } 182 | 183 | func run(wasmFile string, args []string) error { 184 | wasmName := filepath.Base(wasmFile) 185 | wasmCode, err := os.ReadFile(wasmFile) 186 | if err != nil { 187 | return fmt.Errorf("could not read WASM file '%s': %w", wasmFile, err) 188 | } 189 | 190 | if len(args) > 0 && args[0] == "--" { 191 | args = args[1:] 192 | } 193 | 194 | if pprofAddr != "" { 195 | go http.ListenAndServe(pprofAddr, nil) 196 | } 197 | 198 | ctx := context.Background() 199 | runtime := wazero.NewRuntime(ctx) 200 | defer runtime.Close(ctx) 201 | 202 | wasmModule, err := runtime.CompileModule(ctx, wasmCode) 203 | if err != nil { 204 | return err 205 | } 206 | defer wasmModule.Close(ctx) 207 | 208 | builder := imports.NewBuilder(). 209 | WithName(wasmName). 210 | WithArgs(args...). 211 | WithEnv(envs...). 212 | WithDirs(dirs...). 213 | WithListens(listens...). 214 | WithDials(dials...). 215 | WithNonBlockingStdio(nonBlockingStdio). 216 | WithSocketsExtension(socketExt, wasmModule). 217 | WithTracer(trace, os.Stderr, wasi.WithTracerStringSize(tracerStringSize)). 218 | WithMaxOpenFiles(maxOpenFiles). 219 | WithMaxOpenDirs(maxOpenDirs) 220 | 221 | var system wasi.System 222 | ctx, system, err = builder.Instantiate(ctx, runtime) 223 | if err != nil { 224 | return err 225 | } 226 | defer system.Close(ctx) 227 | 228 | importWasi := false 229 | var wasiHTTP *wasi_http.WasiHTTP = nil 230 | switch wasiHttp { 231 | case "auto": 232 | importWasi = wasi_http.DetectWasiHttp(wasmModule) 233 | case "v1": 234 | importWasi = true 235 | case "none": 236 | importWasi = false 237 | default: 238 | return fmt.Errorf("invalid value for -http '%v', expected 'auto', 'v1' or 'none'", wasiHttp) 239 | } 240 | if importWasi { 241 | wasiHTTP = wasi_http.MakeWasiHTTP() 242 | if err := wasiHTTP.Instantiate(ctx, runtime); err != nil { 243 | return err 244 | } 245 | } 246 | 247 | instance, err := runtime.InstantiateModule(ctx, wasmModule, wazero.NewModuleConfig()) 248 | if err != nil { 249 | return err 250 | } 251 | if len(wasiHttpAddr) > 0 { 252 | handler := wasiHTTP.MakeHandler(ctx, instance) 253 | http.Handle(wasiHttpPath, handler) 254 | return http.ListenAndServe(wasiHttpAddr, nil) 255 | } 256 | return instance.Close(ctx) 257 | } 258 | 259 | type stringList []string 260 | 261 | func (s stringList) String() string { 262 | return fmt.Sprintf("%v", []string(s)) 263 | } 264 | 265 | func (s *stringList) Set(value string) error { 266 | *s = append(*s, value) 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /error_darwin.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | func syscallErrnoToWASI(err syscall.Errno) Errno { 8 | switch err { 9 | case syscall.E2BIG: 10 | return E2BIG 11 | case syscall.EACCES: 12 | return EACCES 13 | case syscall.EADDRINUSE: 14 | return EADDRINUSE 15 | case syscall.EADDRNOTAVAIL: 16 | return EADDRNOTAVAIL 17 | case syscall.EAFNOSUPPORT: 18 | return EAFNOSUPPORT 19 | case syscall.EAGAIN: 20 | return EAGAIN 21 | case syscall.EALREADY: 22 | return EALREADY 23 | case syscall.EBADF: 24 | return EBADF 25 | case syscall.EBADMSG: 26 | return EBADMSG 27 | case syscall.EBUSY: 28 | return EBUSY 29 | case syscall.ECANCELED: 30 | return ECANCELED 31 | case syscall.ECHILD: 32 | return ECHILD 33 | case syscall.ECONNABORTED: 34 | return ECONNABORTED 35 | case syscall.ECONNREFUSED: 36 | return ECONNREFUSED 37 | case syscall.ECONNRESET: 38 | return ECONNRESET 39 | case syscall.EDEADLK: 40 | return EDEADLK 41 | case syscall.EDESTADDRREQ: 42 | return EDESTADDRREQ 43 | case syscall.EDOM: 44 | return EDOM 45 | case syscall.EDQUOT: 46 | return EDQUOT 47 | case syscall.EEXIST: 48 | return EEXIST 49 | case syscall.EFAULT: 50 | return EFAULT 51 | case syscall.EFBIG: 52 | return EFBIG 53 | case syscall.EHOSTUNREACH: 54 | return EHOSTUNREACH 55 | case syscall.EIDRM: 56 | return EIDRM 57 | case syscall.EILSEQ: 58 | return EILSEQ 59 | case syscall.EINPROGRESS: 60 | return EINPROGRESS 61 | case syscall.EINTR: 62 | return EINTR 63 | case syscall.EINVAL: 64 | return EINVAL 65 | case syscall.EIO: 66 | return EIO 67 | case syscall.EISCONN: 68 | return EISCONN 69 | case syscall.EISDIR: 70 | return EISDIR 71 | case syscall.ELOOP: 72 | return ELOOP 73 | case syscall.EMFILE: 74 | return EMFILE 75 | case syscall.EMLINK: 76 | return EMLINK 77 | case syscall.EMSGSIZE: 78 | return EMSGSIZE 79 | case syscall.EMULTIHOP: 80 | return EMULTIHOP 81 | case syscall.ENAMETOOLONG: 82 | return ENAMETOOLONG 83 | case syscall.ENETDOWN: 84 | return ENETDOWN 85 | case syscall.ENETRESET: 86 | return ENETRESET 87 | case syscall.ENETUNREACH: 88 | return ENETUNREACH 89 | case syscall.ENFILE: 90 | return ENFILE 91 | case syscall.ENOBUFS: 92 | return ENOBUFS 93 | case syscall.ENODEV: 94 | return ENODEV 95 | case syscall.ENOENT: 96 | return ENOENT 97 | case syscall.ENOEXEC: 98 | return ENOEXEC 99 | case syscall.ENOLCK: 100 | return ENOLCK 101 | case syscall.ENOLINK: 102 | return ENOLINK 103 | case syscall.ENOMEM: 104 | return ENOMEM 105 | case syscall.ENOMSG: 106 | return ENOMSG 107 | case syscall.ENOPROTOOPT: 108 | return ENOPROTOOPT 109 | case syscall.ENOSPC: 110 | return ENOSPC 111 | case syscall.ENOSYS: 112 | return ENOSYS 113 | case syscall.ENOTCONN: 114 | return ENOTCONN 115 | case syscall.ENOTDIR: 116 | return ENOTDIR 117 | case syscall.ENOTEMPTY: 118 | return ENOTEMPTY 119 | case syscall.ENOTRECOVERABLE: 120 | return ENOTRECOVERABLE 121 | case syscall.ENOTSOCK: 122 | return ENOTSOCK 123 | case syscall.ENOTSUP: 124 | return ENOTSUP 125 | case syscall.ENOTTY: 126 | return ENOTTY 127 | case syscall.ENXIO: 128 | return ENXIO 129 | case syscall.EOPNOTSUPP: 130 | // There's no EOPNOTSUPP, but on Linux ENOTSUP==EOPNOTSUPP. 131 | return ENOTSUP 132 | case syscall.EOVERFLOW: 133 | return EOVERFLOW 134 | case syscall.EOWNERDEAD: 135 | return EOWNERDEAD 136 | case syscall.EPERM: 137 | return EPERM 138 | case syscall.EPIPE: 139 | return EPIPE 140 | case syscall.EPROTO: 141 | return EPROTO 142 | case syscall.EPROTONOSUPPORT: 143 | return EPROTONOSUPPORT 144 | case syscall.EPROTOTYPE: 145 | return EPROTOTYPE 146 | case syscall.ERANGE: 147 | return ERANGE 148 | case syscall.EROFS: 149 | return EROFS 150 | case syscall.ESPIPE: 151 | return ESPIPE 152 | case syscall.ESRCH: 153 | return ESRCH 154 | case syscall.ESTALE: 155 | return ESTALE 156 | case syscall.ETIMEDOUT: 157 | return ETIMEDOUT 158 | case syscall.ETXTBSY: 159 | return ETXTBSY 160 | case syscall.EXDEV: 161 | return EXDEV 162 | 163 | // Omitted because they're duplicates: 164 | // case syscall.EWOULDBLOCK: (EAGAIN) 165 | 166 | // Omitted because there's no equivalent Errno: 167 | // case syscall.EAUTH: 168 | // case syscall.EBADARCH: 169 | // case syscall.EBADEXEC: 170 | // case syscall.EBADMACHO: 171 | // case syscall.EBADRPC: 172 | // case syscall.EDEVERR: 173 | // case syscall.EFTYPE: 174 | // case syscall.EHOSTDOWN: 175 | // case syscall.ELAST: 176 | // case syscall.ENEEDAUTH: 177 | // case syscall.ENOATTR: 178 | // case syscall.ENODATA: 179 | // case syscall.ENOPOLICY: 180 | // case syscall.ENOSR: 181 | // case syscall.ENOSTR: 182 | // case syscall.ENOTBLK: 183 | // case syscall.EPFNOSUPPORT: 184 | // case syscall.EPROCLIM: 185 | // case syscall.EPROCUNAVAIL: 186 | // case syscall.EPROGMISMATCH: 187 | // case syscall.EPROGUNAVAIL: 188 | // case syscall.EPWROFF: 189 | // case syscall.EQFULL: 190 | // case syscall.EUSERS: 191 | // case syscall.EREMOTE: 192 | // case syscall.ERPCMISMATCH: 193 | // case syscall.ESHLIBVERS: 194 | // case syscall.ESHUTDOWN: 195 | // case syscall.ESOCKTNOSUPPORT: 196 | // case syscall.ETIME: 197 | // case syscall.ETOOMANYREFS: 198 | 199 | default: 200 | panic("unsupported syscall errno: " + err.Error()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /error_linux.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | func syscallErrnoToWASI(err syscall.Errno) Errno { 8 | switch err { 9 | case syscall.E2BIG: 10 | return E2BIG 11 | case syscall.EACCES: 12 | return EACCES 13 | case syscall.EADDRINUSE: 14 | return EADDRINUSE 15 | case syscall.EADDRNOTAVAIL: 16 | return EADDRNOTAVAIL 17 | case syscall.EAFNOSUPPORT: 18 | return EAFNOSUPPORT 19 | case syscall.EAGAIN: 20 | return EAGAIN 21 | case syscall.EALREADY: 22 | return EALREADY 23 | case syscall.EBADF: 24 | return EBADF 25 | case syscall.EBADMSG: 26 | return EBADMSG 27 | case syscall.EBUSY: 28 | return EBUSY 29 | case syscall.ECANCELED: 30 | return ECANCELED 31 | case syscall.ECHILD: 32 | return ECHILD 33 | case syscall.ECONNABORTED: 34 | return ECONNABORTED 35 | case syscall.ECONNREFUSED: 36 | return ECONNREFUSED 37 | case syscall.ECONNRESET: 38 | return ECONNRESET 39 | case syscall.EDEADLK: 40 | return EDEADLK 41 | case syscall.EDESTADDRREQ: 42 | return EDESTADDRREQ 43 | case syscall.EDOM: 44 | return EDOM 45 | case syscall.EDQUOT: 46 | return EDQUOT 47 | case syscall.EEXIST: 48 | return EEXIST 49 | case syscall.EFAULT: 50 | return EFAULT 51 | case syscall.EFBIG: 52 | return EFBIG 53 | case syscall.EHOSTUNREACH: 54 | return EHOSTUNREACH 55 | case syscall.EIDRM: 56 | return EIDRM 57 | case syscall.EILSEQ: 58 | return EILSEQ 59 | case syscall.EINPROGRESS: 60 | return EINPROGRESS 61 | case syscall.EINTR: 62 | return EINTR 63 | case syscall.EINVAL: 64 | return EINVAL 65 | case syscall.EIO: 66 | return EIO 67 | case syscall.EISCONN: 68 | return EISCONN 69 | case syscall.EISDIR: 70 | return EISDIR 71 | case syscall.ELOOP: 72 | return ELOOP 73 | case syscall.EMFILE: 74 | return EMFILE 75 | case syscall.EMLINK: 76 | return EMLINK 77 | case syscall.EMSGSIZE: 78 | return EMSGSIZE 79 | case syscall.EMULTIHOP: 80 | return EMULTIHOP 81 | case syscall.ENAMETOOLONG: 82 | return ENAMETOOLONG 83 | case syscall.ENETDOWN: 84 | return ENETDOWN 85 | case syscall.ENETRESET: 86 | return ENETRESET 87 | case syscall.ENETUNREACH: 88 | return ENETUNREACH 89 | case syscall.ENFILE: 90 | return ENFILE 91 | case syscall.ENOBUFS: 92 | return ENOBUFS 93 | case syscall.ENODEV: 94 | return ENODEV 95 | case syscall.ENOENT: 96 | return ENOENT 97 | case syscall.ENOEXEC: 98 | return ENOEXEC 99 | case syscall.ENOLCK: 100 | return ENOLCK 101 | case syscall.ENOLINK: 102 | return ENOLINK 103 | case syscall.ENOMEM: 104 | return ENOMEM 105 | case syscall.ENOMSG: 106 | return ENOMSG 107 | case syscall.ENOPROTOOPT: 108 | return ENOPROTOOPT 109 | case syscall.ENOSPC: 110 | return ENOSPC 111 | case syscall.ENOSYS: 112 | return ENOSYS 113 | case syscall.ENOTCONN: 114 | return ENOTCONN 115 | case syscall.ENOTDIR: 116 | return ENOTDIR 117 | case syscall.ENOTEMPTY: 118 | return ENOTEMPTY 119 | case syscall.ENOTRECOVERABLE: 120 | return ENOTRECOVERABLE 121 | case syscall.ENOTSOCK: 122 | return ENOTSOCK 123 | case syscall.ENOTSUP: 124 | return ENOTSUP 125 | case syscall.ENOTTY: 126 | return ENOTTY 127 | case syscall.ENXIO: 128 | return ENXIO 129 | case syscall.EOVERFLOW: 130 | return EOVERFLOW 131 | case syscall.EOWNERDEAD: 132 | return EOWNERDEAD 133 | case syscall.EPERM: 134 | return EPERM 135 | case syscall.EPIPE: 136 | return EPIPE 137 | case syscall.EPROTO: 138 | return EPROTO 139 | case syscall.EPROTONOSUPPORT: 140 | return EPROTONOSUPPORT 141 | case syscall.EPROTOTYPE: 142 | return EPROTOTYPE 143 | case syscall.ERANGE: 144 | return ERANGE 145 | case syscall.EROFS: 146 | return EROFS 147 | case syscall.ESPIPE: 148 | return ESPIPE 149 | case syscall.ESRCH: 150 | return ESRCH 151 | case syscall.ESTALE: 152 | return ESTALE 153 | case syscall.ETIMEDOUT: 154 | return ETIMEDOUT 155 | case syscall.ETXTBSY: 156 | return ETXTBSY 157 | case syscall.EXDEV: 158 | return EXDEV 159 | 160 | // Omitted because they're duplicates: 161 | // case syscall.EWOULDBLOCK: (EAGAIN) 162 | // case syscall.EDEADLOCK: (EDEADLK) 163 | // case syscall.EOPNOTSUPP: (ENOTSUP) 164 | 165 | // Omitted because there's no equivalent Errno: 166 | // case syscall.EADV: 167 | // case syscall.EBADE: 168 | // case syscall.EBADFD: 169 | // case syscall.EBADR: 170 | // case syscall.EBADRQC: 171 | // case syscall.EBADSLT: 172 | // case syscall.EBFONT: 173 | // case syscall.ECHRNG: 174 | // case syscall.ECOMM: 175 | // case syscall.EDOTDOT: 176 | // case syscall.EHOSTDOWN: 177 | // case syscall.EHWPOISON: 178 | // case syscall.EISNAM: 179 | // case syscall.EKEYEXPIRED: 180 | // case syscall.EKEYREJECTED: 181 | // case syscall.EKEYREVOKED: 182 | // case syscall.EL2HLT: 183 | // case syscall.EL2NSYNC: 184 | // case syscall.EL3HLT: 185 | // case syscall.EL3RST: 186 | // case syscall.ELIBACC: 187 | // case syscall.ELIBBAD: 188 | // case syscall.ELIBEXEC: 189 | // case syscall.ELIBMAX: 190 | // case syscall.ELIBSCN: 191 | // case syscall.ELNRNG: 192 | // case syscall.EMEDIUMTYPE: 193 | // case syscall.ENAVAIL: 194 | // case syscall.ENOANO: 195 | // case syscall.ENOCSI: 196 | // case syscall.ENODATA: 197 | // case syscall.ENOKEY: 198 | // case syscall.ENOMEDIUM: 199 | // case syscall.ENONET: 200 | // case syscall.ENOPKG: 201 | // case syscall.ENOSR: 202 | // case syscall.ENOSTR: 203 | // case syscall.ENOTBLK: 204 | // case syscall.ENOTNAM: 205 | // case syscall.ENOTUNIQ: 206 | // case syscall.EPFNOSUPPORT: 207 | // case syscall.EREMCHG: 208 | // case syscall.EREMOTE: 209 | // case syscall.EREMOTEIO: 210 | // case syscall.ERESTART: 211 | // case syscall.ERFKILL: 212 | // case syscall.ESHUTDOWN: 213 | // case syscall.ESOCKTNOSUPPORT: 214 | // case syscall.ESRMNT: 215 | // case syscall.ESTRPIPE: 216 | // case syscall.ETIME: 217 | // case syscall.ETOOMANYREFS: 218 | // case syscall.EUCLEAN: 219 | // case syscall.EUNATCH: 220 | // case syscall.EUSERS: 221 | // case syscall.EXFULL: 222 | 223 | default: 224 | panic("unsupported syscall errno: " + err.Error()) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package wasi_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/fs" 8 | "net" 9 | "os" 10 | "syscall" 11 | "testing" 12 | 13 | "github.com/stealthrocket/wasi-go" 14 | ) 15 | 16 | func TestErrno(t *testing.T) { 17 | for errno := wasi.Errno(0); errno < wasi.ENOTCAPABLE; errno++ { 18 | t.Run(errno.Name(), func(t *testing.T) { 19 | e1 := errno.Syscall() 20 | e2 := wasi.MakeErrno(e1) 21 | if e2 != errno { 22 | t.Errorf("conversion to syscall.Errno did not yield the same error code: want=%d got=%d", errno, e2) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestMakeErrno(t *testing.T) { 29 | tests := []struct { 30 | error error 31 | errno wasi.Errno 32 | }{ 33 | {nil, wasi.ESUCCESS}, 34 | {syscall.EAGAIN, wasi.EAGAIN}, 35 | {context.Canceled, wasi.ECANCELED}, 36 | {context.DeadlineExceeded, wasi.ETIMEDOUT}, 37 | {io.ErrUnexpectedEOF, wasi.EIO}, 38 | {fs.ErrClosed, wasi.EIO}, 39 | {net.ErrClosed, wasi.EIO}, 40 | {syscall.EPERM, wasi.EPERM}, 41 | {wasi.EAGAIN, wasi.EAGAIN}, 42 | {os.ErrDeadlineExceeded, wasi.ETIMEDOUT}, 43 | } 44 | 45 | for _, test := range tests { 46 | t.Run(fmt.Sprint(test.error), func(t *testing.T) { 47 | if errno := wasi.MakeErrno(test.error); errno != test.errno { 48 | t.Errorf("error mismatch: want=%d got=%d (%s)", test.errno, errno, errno) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /error_unix.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "net" 10 | "syscall" 11 | ) 12 | 13 | func makeErrno(err error) Errno { 14 | if err == nil { 15 | return ESUCCESS 16 | } 17 | if err == syscall.EAGAIN { 18 | return EAGAIN 19 | } 20 | return makeErrnoSlow(err) 21 | } 22 | 23 | func makeErrnoSlow(err error) Errno { 24 | switch { 25 | case errors.Is(err, context.Canceled): 26 | return ECANCELED 27 | case errors.Is(err, context.DeadlineExceeded): 28 | return ETIMEDOUT 29 | case errors.Is(err, io.ErrUnexpectedEOF), 30 | errors.Is(err, fs.ErrClosed), 31 | errors.Is(err, net.ErrClosed): 32 | return EIO 33 | } 34 | 35 | var sysErrno syscall.Errno 36 | if errors.As(err, &sysErrno) { 37 | if sysErrno == 0 { 38 | return ESUCCESS 39 | } 40 | return syscallErrnoToWASI(sysErrno) 41 | } 42 | 43 | var wasiErrno Errno 44 | if errors.As(err, &wasiErrno) { 45 | return wasiErrno 46 | } 47 | 48 | var timeout interface{ Timeout() bool } 49 | if errors.As(err, &timeout) { 50 | if timeout.Timeout() { 51 | return ETIMEDOUT 52 | } 53 | } 54 | 55 | panic(fmt.Errorf("unexpected error: %v", err)) 56 | } 57 | 58 | func errnoToSyscall(errno Errno) syscall.Errno { 59 | switch errno { 60 | case ESUCCESS: 61 | return 0 62 | case E2BIG: 63 | return syscall.E2BIG 64 | case EACCES: 65 | return syscall.EACCES 66 | case EADDRINUSE: 67 | return syscall.EADDRINUSE 68 | case EADDRNOTAVAIL: 69 | return syscall.EADDRNOTAVAIL 70 | case EAFNOSUPPORT: 71 | return syscall.EAFNOSUPPORT 72 | case EAGAIN: 73 | return syscall.EAGAIN 74 | case EALREADY: 75 | return syscall.EALREADY 76 | case EBADF: 77 | return syscall.EBADF 78 | case EBADMSG: 79 | return syscall.EBADMSG 80 | case EBUSY: 81 | return syscall.EBUSY 82 | case ECANCELED: 83 | return syscall.ECANCELED 84 | case ECHILD: 85 | return syscall.ECHILD 86 | case ECONNABORTED: 87 | return syscall.ECONNABORTED 88 | case ECONNREFUSED: 89 | return syscall.ECONNREFUSED 90 | case ECONNRESET: 91 | return syscall.ECONNRESET 92 | case EDEADLK: 93 | return syscall.EDEADLK 94 | case EDESTADDRREQ: 95 | return syscall.EDESTADDRREQ 96 | case EDOM: 97 | return syscall.EDOM 98 | case EDQUOT: 99 | return syscall.EDQUOT 100 | case EEXIST: 101 | return syscall.EEXIST 102 | case EFAULT: 103 | return syscall.EFAULT 104 | case EFBIG: 105 | return syscall.EFBIG 106 | case EHOSTUNREACH: 107 | return syscall.EHOSTUNREACH 108 | case EIDRM: 109 | return syscall.EIDRM 110 | case EILSEQ: 111 | return syscall.EILSEQ 112 | case EINPROGRESS: 113 | return syscall.EINPROGRESS 114 | case EINTR: 115 | return syscall.EINTR 116 | case EINVAL: 117 | return syscall.EINVAL 118 | case EIO: 119 | return syscall.EIO 120 | case EISCONN: 121 | return syscall.EISCONN 122 | case EISDIR: 123 | return syscall.EISDIR 124 | case ELOOP: 125 | return syscall.ELOOP 126 | case EMFILE: 127 | return syscall.EMFILE 128 | case EMLINK: 129 | return syscall.EMLINK 130 | case EMSGSIZE: 131 | return syscall.EMSGSIZE 132 | case EMULTIHOP: 133 | return syscall.EMULTIHOP 134 | case ENAMETOOLONG: 135 | return syscall.ENAMETOOLONG 136 | case ENETDOWN: 137 | return syscall.ENETDOWN 138 | case ENETRESET: 139 | return syscall.ENETRESET 140 | case ENETUNREACH: 141 | return syscall.ENETUNREACH 142 | case ENFILE: 143 | return syscall.ENFILE 144 | case ENOBUFS: 145 | return syscall.ENOBUFS 146 | case ENODEV: 147 | return syscall.ENODEV 148 | case ENOENT: 149 | return syscall.ENOENT 150 | case ENOEXEC: 151 | return syscall.ENOEXEC 152 | case ENOLCK: 153 | return syscall.ENOLCK 154 | case ENOLINK: 155 | return syscall.ENOLINK 156 | case ENOMEM: 157 | return syscall.ENOMEM 158 | case ENOMSG: 159 | return syscall.ENOMSG 160 | case ENOPROTOOPT: 161 | return syscall.ENOPROTOOPT 162 | case ENOSPC: 163 | return syscall.ENOSPC 164 | case ENOSYS: 165 | return syscall.ENOSYS 166 | case ENOTCONN: 167 | return syscall.ENOTCONN 168 | case ENOTDIR: 169 | return syscall.ENOTDIR 170 | case ENOTEMPTY: 171 | return syscall.ENOTEMPTY 172 | case ENOTRECOVERABLE: 173 | return syscall.ENOTRECOVERABLE 174 | case ENOTSOCK: 175 | return syscall.ENOTSOCK 176 | case ENOTSUP: 177 | return syscall.ENOTSUP 178 | case ENOTTY: 179 | return syscall.ENOTTY 180 | case ENXIO: 181 | return syscall.ENXIO 182 | case EOVERFLOW: 183 | return syscall.EOVERFLOW 184 | case EOWNERDEAD: 185 | return syscall.EOWNERDEAD 186 | case EPERM: 187 | return syscall.EPERM 188 | case EPIPE: 189 | return syscall.EPIPE 190 | case EPROTO: 191 | return syscall.EPROTO 192 | case EPROTONOSUPPORT: 193 | return syscall.EPROTONOSUPPORT 194 | case EPROTOTYPE: 195 | return syscall.EPROTOTYPE 196 | case ERANGE: 197 | return syscall.ERANGE 198 | case EROFS: 199 | return syscall.EROFS 200 | case ESPIPE: 201 | return syscall.ESPIPE 202 | case ESRCH: 203 | return syscall.ESRCH 204 | case ESTALE: 205 | return syscall.ESTALE 206 | case ETIMEDOUT: 207 | return syscall.ETIMEDOUT 208 | case ETXTBSY: 209 | return syscall.ETXTBSY 210 | case EXDEV: 211 | return syscall.EXDEV 212 | case ENOTCAPABLE: 213 | return syscall.EPERM 214 | default: 215 | panic("unsupport wasi errno: " + errno.Error()) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import "fmt" 4 | 5 | // ExitCode is the exit code generated by a process when exiting. 6 | type ExitCode uint32 7 | 8 | // Signal is a signal condition. 9 | type Signal uint8 10 | 11 | const ( 12 | // SIGNONE means no signal. Note that POSIX has special semantics for 13 | // kill(pid, 0), so this value is reserved. 14 | SIGNONE Signal = iota 15 | 16 | // SIGHUP means hangup. Action: Terminates the process. 17 | SIGHUP 18 | 19 | // SIGINT is the terminate interrupt signal. Action: Terminates the 20 | // process. 21 | SIGINT 22 | 23 | // SIGQUIT is the terminal quit signal. Action: Terminates the process. 24 | SIGQUIT 25 | 26 | // SIGILL means illegal instruction. Action: Terminates the process. 27 | SIGILL 28 | 29 | // SIGTRAP is the trace/breakpoint trap. Action: Terminates the process. 30 | SIGTRAP 31 | 32 | // SIGABRT is the process abort signal. Action: Terminates the process. 33 | SIGABRT 34 | 35 | // SIGBUS indicates access to an undefined portion of a memory object. 36 | // Action: Terminates the process. 37 | SIGBUS 38 | 39 | // SIGFPE indicates an erroneous arithmetic operation. Action: Terminates 40 | // the process. 41 | SIGFPE 42 | 43 | // SIGKILL means kill. Action: Terminates the process. 44 | SIGKILL 45 | 46 | // SIGUSR1 is the user-defined signal 1. Action: Terminates the process. 47 | SIGUSR1 48 | 49 | // SIGSEGV indicates an invalid memory reference. Action: Terminates the 50 | // process. 51 | SIGSEGV 52 | 53 | // SIGUSR2 is the user-defined signal 1. Action: Terminates the process. 54 | SIGUSR2 55 | 56 | // SIGPIPE indicates a write on a pipe with no one to read it. 57 | // Action: Ignored. 58 | SIGPIPE 59 | 60 | // SIGALRM indicates an alarm clock. Action: Terminates the process. 61 | SIGALRM 62 | 63 | // SIGTERM is the termination signal. Action: Terminates the process. 64 | SIGTERM 65 | 66 | // SIGCHLD indicates that a child process terminated, stopped, or 67 | // continued. Action: Ignored. 68 | SIGCHLD 69 | 70 | // SIGCONT indicates that execution should continue, if stopped. 71 | // Action: Continues executing, if stopped. 72 | SIGCONT 73 | 74 | // SIGSTOP means stop executing. Action: Stops executing. 75 | SIGSTOP 76 | 77 | // SIGTSTP is the terminal stop signal. Action: Stops executing. 78 | SIGTSTP 79 | 80 | // SIGTTIN indicates that a background process is attempting read. 81 | // Action: Stops executing. 82 | SIGTTIN 83 | 84 | // SIGTTOU indicates that a background process is attempting write. 85 | // Action: Stops executing. 86 | SIGTTOU 87 | 88 | // SIGURG indicates that high bandwidth data is available at a socket. 89 | // Action: Ignored. 90 | SIGURG 91 | 92 | // SIGXCPU means CPU time limit exceeded. Action: Terminates the process. 93 | SIGXCPU 94 | 95 | // SIGXFSZ means file size limit exceeded. Action: Terminates the process. 96 | SIGXFSZ 97 | 98 | // SIGVTALRM means virtual timer expired. Action: Terminates the process. 99 | SIGVTALRM 100 | 101 | // SIGPROF means profiling timer expired. Action: Terminates the process. 102 | SIGPROF 103 | 104 | // SIGWINCH means window changed. Action: Ignored. 105 | SIGWINCH 106 | 107 | // SIGPOLL means I/O is possible. Action: Terminates the process. 108 | SIGPOLL 109 | 110 | // SIGPWR indicates power failure. Action: Terminates the process. 111 | SIGPWR 112 | 113 | // SIGSYS indicates a bad system call. Action: Terminates the process. 114 | SIGSYS 115 | ) 116 | 117 | func (s Signal) String() string { 118 | if int(s) < len(signalStrings) { 119 | return signalStrings[s] 120 | } 121 | return fmt.Sprintf("Signal(%d)", s) 122 | } 123 | 124 | func (s Signal) Name() string { 125 | if int(s) < len(signalNames) { 126 | return signalNames[s] 127 | } 128 | return fmt.Sprintf("Signal(%d)", s) 129 | } 130 | 131 | var signalStrings = [...]string{ 132 | SIGNONE: "no signal", 133 | SIGHUP: "hangup", 134 | SIGINT: "interrupt", 135 | SIGQUIT: "quit", 136 | SIGILL: "illegal instruction", 137 | SIGTRAP: "trace/breakpoint trap", 138 | SIGABRT: "abort", 139 | SIGBUS: "bus error", 140 | SIGFPE: "floating point exception", 141 | SIGKILL: "killed", 142 | SIGUSR1: "user-defined signal 1", 143 | SIGSEGV: "segmentation fault", 144 | SIGUSR2: "user-defined signal 2", 145 | SIGPIPE: "broken pipe", 146 | SIGALRM: "alarm clock", 147 | SIGTERM: "terminated", 148 | SIGCHLD: "child exited", 149 | SIGCONT: "continued", 150 | SIGSTOP: "stopped (signal)", 151 | SIGTSTP: "stopped", 152 | SIGTTIN: "stopped (tty input)", 153 | SIGTTOU: "stopped (tty output)", 154 | SIGURG: "urgent I/O condition", 155 | SIGXCPU: "CPU time limit exceeded", 156 | SIGXFSZ: "file size limit exceeded", 157 | SIGVTALRM: "virtual timer expired", 158 | SIGPROF: "profiling timer expired", 159 | SIGWINCH: "window changed", 160 | SIGPOLL: "I/O possible", 161 | SIGPWR: "power failure", 162 | SIGSYS: "bad system call", 163 | } 164 | 165 | var signalNames = [...]string{ 166 | SIGNONE: "SIGNONE", 167 | SIGHUP: "SIGHUP", 168 | SIGINT: "SIGINT", 169 | SIGQUIT: "SIGQUIT", 170 | SIGILL: "SIGILL", 171 | SIGTRAP: "SIGTRAP", 172 | SIGABRT: "SIGABRT", 173 | SIGBUS: "SIGBUS", 174 | SIGFPE: "SIGFPE", 175 | SIGKILL: "SIGKILL", 176 | SIGUSR1: "SIGUSR1", 177 | SIGSEGV: "SIGSEGV", 178 | SIGUSR2: "SIGUSR2", 179 | SIGPIPE: "SIGPIPE", 180 | SIGALRM: "SIGALRM", 181 | SIGTERM: "SIGTERM", 182 | SIGCHLD: "SIGCHLD", 183 | SIGCONT: "SIGCONT", 184 | SIGSTOP: "SIGSTOP", 185 | SIGTSTP: "SIGTSTP", 186 | SIGTTIN: "SIGTTIN", 187 | SIGTTOU: "SIGTTOU", 188 | SIGURG: "SIGURG", 189 | SIGXCPU: "SIGXCPU", 190 | SIGXFSZ: "SIGXFSZ", 191 | SIGVTALRM: "SIGVTALRM", 192 | SIGPROF: "SIGPROF", 193 | SIGWINCH: "SIGWINCH", 194 | SIGPOLL: "SIGPOLL", 195 | SIGPWR: "SIGPWR", 196 | SIGSYS: "SIGSYS", 197 | } 198 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "io/fs" 7 | "path" 8 | "time" 9 | ) 10 | 11 | // FS constructs a fs.FS file system backed by a wasi system. 12 | func FS(ctx context.Context, sys System, root FD) fs.FS { 13 | return &fileSystem{ctx, sys, root} 14 | } 15 | 16 | type fileSystem struct { 17 | ctx context.Context 18 | System 19 | root FD 20 | } 21 | 22 | func (fsys *fileSystem) Open(name string) (fs.File, error) { 23 | if !fs.ValidPath(name) { 24 | return nil, &fs.PathError{"open", name, fs.ErrInvalid} 25 | } 26 | const rights = PathOpenRight | 27 | PathFileStatGetRight | 28 | FDReadRight | 29 | FDReadDirRight | 30 | FDSeekRight | 31 | FDTellRight | 32 | FDFileStatGetRight 33 | f, errno := fsys.PathOpen(fsys.ctx, fsys.root, SymlinkFollow, name, 0, rights, rights, 0) 34 | if errno != ESUCCESS { 35 | return nil, &fs.PathError{"open", name, errno} 36 | } 37 | return &file{fsys: fsys, name: name, fd: f}, nil 38 | } 39 | 40 | type file struct { 41 | fsys *fileSystem 42 | name string 43 | fd FD 44 | *dir 45 | } 46 | 47 | type dir struct { 48 | dirent []DirEntry 49 | cookie DirCookie 50 | } 51 | 52 | func (f *file) Close() error { 53 | if f.fd >= 0 { 54 | f.fsys.FDClose(f.fsys.ctx, f.fd) 55 | f.fd = -1 56 | } 57 | return nil 58 | } 59 | 60 | func (f *file) Read(b []byte) (int, error) { 61 | if f.fd < 0 { 62 | return 0, io.EOF 63 | } 64 | if len(b) == 0 { 65 | return 0, nil 66 | } 67 | n, errno := f.fsys.FDRead(f.fsys.ctx, f.fd, []IOVec{b}) 68 | if errno != ESUCCESS { 69 | return int(n), &fs.PathError{"read", f.name, errno} 70 | } 71 | if n == 0 { 72 | return 0, io.EOF 73 | } 74 | return int(n), nil 75 | } 76 | 77 | func (f *file) Stat() (fs.FileInfo, error) { 78 | if f.fd < 0 { 79 | return nil, &fs.PathError{"stat", f.name, fs.ErrClosed} 80 | } 81 | s, errno := f.fsys.FDFileStatGet(f.fsys.ctx, f.fd) 82 | if errno != ESUCCESS { 83 | return nil, &fs.PathError{"stat", f.name, errno} 84 | } 85 | return &fileInfo{stat: s, name: path.Base(f.name)}, nil 86 | } 87 | 88 | func (f *file) Seek(offset int64, whence int) (int64, error) { 89 | if f.fd < 0 { 90 | return 0, &fs.PathError{"seek", f.name, fs.ErrClosed} 91 | } 92 | seek, errno := f.fsys.FDSeek(f.fsys.ctx, f.fd, FileDelta(offset), Whence(whence)) 93 | if errno != ESUCCESS { 94 | return int64(seek), &fs.PathError{"seek", f.name, errno} 95 | } 96 | if f.dir != nil { 97 | f.dir.cookie = DirCookie(seek) 98 | } 99 | return int64(seek), nil 100 | } 101 | 102 | func (f *file) ReadAt(b []byte, off int64) (int, error) { 103 | if f.fd < 0 { 104 | return 0, io.EOF 105 | } 106 | n, errno := f.fsys.FDPread(f.fsys.ctx, f.fd, []IOVec{b}, FileSize(off)) 107 | if errno != ESUCCESS { 108 | return int(n), &fs.PathError{"pread", f.name, errno} 109 | } 110 | if int(n) < len(b) { 111 | return int(n), io.EOF 112 | } 113 | return int(n), nil 114 | } 115 | 116 | func (f *file) ReadDir(n int) ([]fs.DirEntry, error) { 117 | if f.fd < 0 { 118 | return nil, io.EOF 119 | } 120 | if f.dir == nil { 121 | f.dir = new(dir) 122 | } 123 | 124 | capacity := n 125 | if n <= 0 { 126 | if capacity = cap(f.dirent); capacity == 0 { 127 | capacity = 20 128 | } 129 | } 130 | if cap(f.dirent) < capacity { 131 | f.dirent = make([]DirEntry, capacity) 132 | } else { 133 | f.dirent = f.dirent[:capacity] 134 | } 135 | 136 | var dirent []fs.DirEntry 137 | for { 138 | limit := len(f.dirent) 139 | if n > 0 { 140 | limit = n - len(dirent) 141 | } 142 | rn, errno := f.fsys.FDReadDir(f.fsys.ctx, f.fd, f.dirent[:limit], f.cookie, 4096) 143 | if rn > 0 { 144 | for _, e := range f.dirent[:rn] { 145 | switch string(e.Name) { 146 | case ".", "..": 147 | continue 148 | } 149 | dirent = append(dirent, &dirEntry{ 150 | typ: e.Type, 151 | name: string(e.Name), 152 | dir: f, 153 | }) 154 | } 155 | f.cookie = f.dirent[rn-1].Next 156 | } 157 | if errno != ESUCCESS { 158 | return dirent, &fs.PathError{"readdir", f.name, errno} 159 | } 160 | if n > 0 && n == len(dirent) { 161 | return dirent, nil 162 | } 163 | if rn == 0 { 164 | if n <= 0 { 165 | return dirent, nil 166 | } else { 167 | return dirent, io.EOF 168 | } 169 | } 170 | } 171 | } 172 | 173 | var ( 174 | _ fs.ReadDirFile = (*file)(nil) 175 | _ io.ReaderAt = (*file)(nil) 176 | _ io.Seeker = (*file)(nil) 177 | ) 178 | 179 | type fileInfo struct { 180 | stat FileStat 181 | name string 182 | } 183 | 184 | func (info *fileInfo) Name() string { 185 | return info.name 186 | } 187 | 188 | func (info *fileInfo) Size() int64 { 189 | return int64(info.stat.Size) 190 | } 191 | 192 | func (info *fileInfo) Mode() fs.FileMode { 193 | return makeFileMode(info.stat.FileType) 194 | } 195 | 196 | func (info *fileInfo) ModTime() time.Time { 197 | return time.Unix(0, int64(info.stat.ModifyTime)) 198 | } 199 | 200 | func (info *fileInfo) IsDir() bool { 201 | return info.stat.FileType == DirectoryType 202 | } 203 | 204 | func (info *fileInfo) Sys() any { 205 | return &info.stat 206 | } 207 | 208 | type dirEntry struct { 209 | typ FileType 210 | dir *file 211 | name string 212 | } 213 | 214 | func (d *dirEntry) Name() string { 215 | return d.name 216 | } 217 | 218 | func (d *dirEntry) IsDir() bool { 219 | return d.typ == DirectoryType 220 | } 221 | 222 | func (d *dirEntry) Type() fs.FileMode { 223 | return makeFileMode(d.typ) 224 | } 225 | 226 | func (d *dirEntry) Info() (fs.FileInfo, error) { 227 | s, errno := d.dir.fsys.PathFileStatGet(d.dir.fsys.ctx, d.dir.fd, 0, d.name) 228 | if errno != ESUCCESS { 229 | return nil, &fs.PathError{"stat", d.name, errno} 230 | } 231 | return &fileInfo{stat: s, name: d.name}, nil 232 | } 233 | 234 | func makeFileMode(fileType FileType) fs.FileMode { 235 | switch fileType { 236 | case BlockDeviceType: 237 | return fs.ModeDevice 238 | case CharacterDeviceType: 239 | return fs.ModeDevice | fs.ModeCharDevice 240 | case DirectoryType: 241 | return fs.ModeDir 242 | case RegularFileType: 243 | return 0 244 | case SocketDGramType, SocketStreamType: 245 | return fs.ModeSocket 246 | case SymbolicLinkType: 247 | return fs.ModeSymlink 248 | default: 249 | return fs.ModeIrregular 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stealthrocket/wasi-go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/stealthrocket/wazergo v0.19.1 7 | github.com/tetratelabs/wazero v1.2.0 8 | golang.org/x/sys v0.8.0 9 | ) 10 | 11 | require golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/stealthrocket/wazergo v0.19.1 h1:BPrITETPgSFwiytwmToO0MbUC/+RGC39JScz1JmmG6c= 2 | github.com/stealthrocket/wazergo v0.19.1/go.mod h1:riI0hxw4ndZA5e6z7PesHg2BtTftcZaMxRcoiGGipTs= 3 | github.com/tetratelabs/wazero v1.2.0 h1:I/8LMf4YkCZ3r2XaL9whhA0VMyAvF6QE+O7rco0DCeQ= 4 | github.com/tetratelabs/wazero v1.2.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= 5 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= 6 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= 7 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 8 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 9 | -------------------------------------------------------------------------------- /imports/builder.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "time" 9 | 10 | "github.com/stealthrocket/wasi-go" 11 | "github.com/stealthrocket/wasi-go/imports/wasi_snapshot_preview1" 12 | "github.com/tetratelabs/wazero" 13 | ) 14 | 15 | // Builder is used to setup and instantiate the WASI host module. 16 | type Builder struct { 17 | name string 18 | args []string 19 | env []string 20 | mounts []mount 21 | listens []string 22 | dials []string 23 | customStdio bool 24 | stdin int 25 | stdout int 26 | stderr int 27 | realtime func(context.Context) (uint64, error) 28 | realtimePrecision time.Duration 29 | monotonic func(context.Context) (uint64, error) 30 | monotonicPrecision time.Duration 31 | yield func(context.Context) error 32 | exit func(context.Context, int) error 33 | raise func(context.Context, int) error 34 | rand io.Reader 35 | socketsExtension *wasi_snapshot_preview1.Extension 36 | pathOpenSockets bool 37 | nonBlockingStdio bool 38 | tracer io.Writer 39 | tracerOptions []wasi.TracerOption 40 | decorators []wasi_snapshot_preview1.Decorator 41 | wrappers []func(wasi.System) wasi.System 42 | errors []error 43 | maxOpenFiles int 44 | maxOpenDirs int 45 | } 46 | 47 | // NewBuilder creates a Builder. 48 | func NewBuilder() *Builder { 49 | return &Builder{} 50 | } 51 | 52 | type mount struct { 53 | dir string 54 | mode int 55 | } 56 | 57 | // WithName sets the name of the module, which is exposed to the module 58 | // as argv[0]. 59 | func (b *Builder) WithName(name string) *Builder { 60 | b.name = name 61 | return b 62 | } 63 | 64 | // WithArgs sets command line arguments. 65 | func (b *Builder) WithArgs(args ...string) *Builder { 66 | b.args = args 67 | return b 68 | } 69 | 70 | // WithEnv sets environment variables. 71 | func (b *Builder) WithEnv(env ...string) *Builder { 72 | b.env = env 73 | return b 74 | } 75 | 76 | // WithDirs specifies a set of directories to preopen. 77 | // 78 | // The directory can either be a path, or a string of the form "path:path[:ro]" 79 | // for compatibility with wazero's WASI preview 1 host module. Since virtual 80 | // file systems are not supported by this implementation, the two paths must 81 | // be the same when using this syntax. The optional ":ro" prefix means that 82 | // this directory is read-only. 83 | func (b *Builder) WithDirs(dirs ...string) *Builder { 84 | for _, dir := range dirs { 85 | mode := int('r' + 'w') 86 | prefix, readOnly := strings.CutSuffix(dir, ":ro") 87 | if readOnly { 88 | mode = 'r' 89 | } 90 | parts := strings.Split(prefix, ":") 91 | switch { 92 | case len(parts) == 1: 93 | dir = parts[0] 94 | case len(parts) == 2 && parts[0] == parts[1]: 95 | dir = parts[0] 96 | case len(parts) == 2: 97 | b.errors = append(b.errors, fmt.Errorf("virtual filesystems are not supported (cannot mount %q)", dir)) 98 | default: 99 | b.errors = append(b.errors, fmt.Errorf("invalid directory %q", dir)) 100 | } 101 | b.mounts = append(b.mounts, mount{dir: dir, mode: mode}) 102 | } 103 | return b 104 | } 105 | 106 | // WithListens specifies a list of addresses to listen on before starting 107 | // the module. The listener sockets are added to the set of preopens. 108 | func (b *Builder) WithListens(listens ...string) *Builder { 109 | b.listens = listens 110 | return b 111 | } 112 | 113 | // WithDials specifies a list of addresses to dial before starting 114 | // the module. The connection sockets are added to the set of preopens. 115 | func (b *Builder) WithDials(dials ...string) *Builder { 116 | b.dials = dials 117 | return b 118 | } 119 | 120 | // WithStdio sets stdio file descriptors. 121 | // 122 | // Note that the file descriptors will be duplicated before the module takes 123 | // ownership. The caller is responsible for managing the specified 124 | // descriptors. 125 | func (b *Builder) WithStdio(stdin, stdout, stderr int) *Builder { 126 | b.customStdio = true 127 | b.stdin = stdin 128 | b.stdout = stdout 129 | b.stderr = stderr 130 | return b 131 | } 132 | 133 | // WithRealtimeClock sets the realtime clock and precision. 134 | func (b *Builder) WithRealtimeClock(clock func(context.Context) (uint64, error), precision time.Duration) *Builder { 135 | b.realtime = clock 136 | b.realtimePrecision = precision 137 | return b 138 | } 139 | 140 | // WithMonotonicClock sets the monotonic clock and precision. 141 | func (b *Builder) WithMonotonicClock(clock func(context.Context) (uint64, error), precision time.Duration) *Builder { 142 | b.monotonic = clock 143 | b.monotonicPrecision = precision 144 | return b 145 | } 146 | 147 | // WithYield sets the sched_yield function. 148 | func (b *Builder) WithYield(fn func(context.Context) error) *Builder { 149 | b.yield = fn 150 | return b 151 | } 152 | 153 | // WithExit sets the proc_exit function. 154 | func (b *Builder) WithExit(fn func(context.Context, int) error) *Builder { 155 | b.exit = fn 156 | return b 157 | } 158 | 159 | // WithRaise sets the proc_raise function. 160 | func (b *Builder) WithRaise(fn func(context.Context, int) error) *Builder { 161 | b.raise = fn 162 | return b 163 | } 164 | 165 | // WithSocketsExtension enables a sockets extension. 166 | // 167 | // The name can be one of: 168 | // - none: disable sockets extensions (use vanilla WASI preview 1) 169 | // - wasmedgev1: use WasmEdge sockets extension version 1 170 | // - wasmedgev2: use WasmEdge sockets extension version 2 171 | // - path_open: use the extension to the path_open system call (unix.PathOpenSockets) 172 | // - auto: attempt to detect one of the extensions above 173 | func (b *Builder) WithSocketsExtension(name string, module wazero.CompiledModule) *Builder { 174 | switch strings.ToLower(name) { 175 | case "none", "": 176 | // no sockets extension 177 | case "wasmedgev1": 178 | b.socketsExtension = &wasi_snapshot_preview1.WasmEdgeV1 179 | case "wasmedgev2": 180 | b.socketsExtension = &wasi_snapshot_preview1.WasmEdgeV2 181 | case "path_open": 182 | b.socketsExtension = nil 183 | b.pathOpenSockets = true 184 | case "auto": 185 | b.socketsExtension = DetectSocketsExtension(module) 186 | default: 187 | b.errors = append(b.errors, fmt.Errorf("invalid socket extension %q", name)) 188 | } 189 | return b 190 | } 191 | 192 | // WithNonBlockingStdio enables or disables non-blocking stdio. 193 | // When enabled, stdio file descriptors will have the O_NONBLOCK flag set 194 | // before the module is started. 195 | func (b *Builder) WithNonBlockingStdio(enable bool) *Builder { 196 | b.nonBlockingStdio = enable 197 | return b 198 | } 199 | 200 | // WithTracer enables the Tracer, and instructs it to write to the 201 | // specified io.Writer. 202 | func (b *Builder) WithTracer(enable bool, w io.Writer, options ...wasi.TracerOption) *Builder { 203 | if !enable { 204 | w = nil 205 | } 206 | b.tracer = w 207 | b.tracerOptions = options 208 | return b 209 | } 210 | 211 | // WithDecorators sets the host module decorators. 212 | func (b *Builder) WithDecorators(decorators ...wasi_snapshot_preview1.Decorator) *Builder { 213 | b.decorators = decorators 214 | return b 215 | } 216 | 217 | // WithWrappers sets the wasi.System wrappers. 218 | func (b *Builder) WithWrappers(wrappers ...func(wasi.System) wasi.System) *Builder { 219 | b.wrappers = wrappers 220 | return b 221 | } 222 | 223 | // WithMaxOpenFiles sets the limit on the maximum number of files that may be 224 | // opened by the guest module. 225 | func (b *Builder) WithMaxOpenFiles(n int) *Builder { 226 | b.maxOpenFiles = n 227 | return b 228 | } 229 | 230 | // WithMaxOpenFiles sets the limit on the maximum number of directories that may 231 | // be opened by the guest module. 232 | func (b *Builder) WithMaxOpenDirs(n int) *Builder { 233 | b.maxOpenDirs = n 234 | return b 235 | } 236 | -------------------------------------------------------------------------------- /imports/builder_default.go: -------------------------------------------------------------------------------- 1 | //go:build !unix 2 | 3 | package imports 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "runtime" 9 | 10 | "github.com/stealthrocket/wasi-go" 11 | "github.com/tetratelabs/wazero" 12 | ) 13 | 14 | func (b *Builder) Instantiate(ctx context.Context, _ wazero.Runtime) (context.Context, wasi.System, error) { 15 | return ctx, nil, fmt.Errorf("wasi-go is not available on GOOS=%s", runtime.GOOS) 16 | } 17 | -------------------------------------------------------------------------------- /imports/builder_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package imports 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "syscall" 10 | 11 | "github.com/stealthrocket/wasi-go" 12 | "github.com/stealthrocket/wasi-go/imports/wasi_snapshot_preview1" 13 | "github.com/stealthrocket/wasi-go/internal/descriptor" 14 | "github.com/stealthrocket/wasi-go/internal/sockets" 15 | "github.com/stealthrocket/wasi-go/systems/unix" 16 | "github.com/stealthrocket/wazergo" 17 | "github.com/tetratelabs/wazero" 18 | ) 19 | 20 | // Instantiate compiles and instantiates the WASI module and binds it to 21 | // the specified context. 22 | func (b *Builder) Instantiate(ctx context.Context, runtime wazero.Runtime) (ctxret context.Context, sys wasi.System, err error) { 23 | if len(b.errors) > 0 { 24 | return ctx, nil, errors.Join(b.errors...) 25 | } 26 | 27 | name := defaultName 28 | if b.name != "" { 29 | name = b.name 30 | } 31 | 32 | stdin, stdout, stderr := -1, -1, -1 33 | if b.customStdio { 34 | stdin, stdout, stderr = b.stdin, b.stdout, b.stderr 35 | } 36 | 37 | realtime := defaultRealtime 38 | if b.realtime != nil { 39 | realtime = b.realtime 40 | } 41 | realtimePrecision := defaultRealtimePrecision 42 | if b.realtimePrecision > 0 { 43 | realtimePrecision = b.realtimePrecision 44 | } 45 | monotonic := defaultMonotonic 46 | if b.monotonic != nil { 47 | monotonic = b.monotonic 48 | } 49 | monotonicPrecision := defaultMonotonicPrecision 50 | if b.monotonicPrecision > 0 { 51 | monotonicPrecision = b.monotonicPrecision 52 | } 53 | 54 | yield := defaultYield 55 | if b.yield != nil { 56 | yield = b.yield 57 | } 58 | raise := defaultRaise 59 | if b.raise != nil { 60 | raise = b.raise 61 | } 62 | exit := defaultExit 63 | if b.exit != nil { 64 | exit = b.exit 65 | } 66 | rand := defaultRand 67 | if b.rand != nil { 68 | rand = b.rand 69 | } 70 | 71 | unixSystem := &unix.System{ 72 | Args: append([]string{name}, b.args...), 73 | Environ: b.env, 74 | Realtime: realtime, 75 | RealtimePrecision: realtimePrecision, 76 | Monotonic: monotonic, 77 | MonotonicPrecision: monotonicPrecision, 78 | Yield: yield, 79 | Raise: raise, 80 | Rand: rand, 81 | Exit: exit, 82 | } 83 | unixSystem.MaxOpenFiles = b.maxOpenFiles 84 | unixSystem.MaxOpenDirs = b.maxOpenDirs 85 | 86 | system := wasi.System(unixSystem) 87 | defer func() { 88 | if system != nil { 89 | system.Close(context.Background()) 90 | } 91 | }() 92 | 93 | if b.pathOpenSockets { 94 | system = &unix.PathOpenSockets{System: unixSystem} 95 | } 96 | if b.tracer != nil { 97 | system = wasi.Trace(b.tracer, system, b.tracerOptions...) 98 | } 99 | for _, wrap := range b.wrappers { 100 | system = wrap(system) 101 | } 102 | 103 | for fd, stdio := range []struct { 104 | fd int 105 | open int 106 | path string 107 | }{ 108 | {stdin, syscall.O_RDONLY, "/dev/stdin"}, 109 | {stdout, syscall.O_WRONLY, "/dev/stdout"}, 110 | {stderr, syscall.O_WRONLY, "/dev/stderr"}, 111 | } { 112 | var err error 113 | if stdio.fd < 0 { 114 | stdio.fd, err = syscall.Open(stdio.path, stdio.open, 0) 115 | // Some systems may not allow opening stdio files on /dev, fallback 116 | // duplicating the process file descriptors which comes with the 117 | // limitation that setting the file descriptors to non-blocking will 118 | // also impact the behavior of stdio streams on the host. 119 | // 120 | // See: https://github.com/gitpod-io/gitpod/issues/17551 121 | if errors.Is(err, syscall.EACCES) { 122 | stdio.fd, err = dup(fd) 123 | } 124 | } else { 125 | stdio.fd, err = dup(stdio.fd) 126 | } 127 | if err != nil { 128 | return ctx, nil, fmt.Errorf("unable to open %s: %w", stdio.path, err) 129 | } 130 | rights := wasi.FileRights 131 | if descriptor.IsATTY(stdio.fd) { 132 | rights = wasi.TTYRights 133 | } 134 | stat := wasi.FDStat{ 135 | FileType: wasi.CharacterDeviceType, 136 | RightsBase: rights, 137 | } 138 | if b.nonBlockingStdio { 139 | if err := syscall.SetNonblock(stdio.fd, true); err != nil { 140 | return ctx, nil, fmt.Errorf("unable to put %s in non-blocking mode: %w", stdio.path, err) 141 | } 142 | stat.Flags |= wasi.NonBlock 143 | } 144 | unixSystem.Preopen(unix.FD(stdio.fd), stdio.path, stat) 145 | } 146 | 147 | for _, m := range b.mounts { 148 | fd, err := syscall.Open(m.dir, syscall.O_DIRECTORY, 0) 149 | if err != nil { 150 | return ctx, nil, fmt.Errorf("unable to preopen directory %q: %w", m.dir, err) 151 | } 152 | rightsBase := wasi.DirectoryRights 153 | rightsInheriting := wasi.DirectoryRights | wasi.FileRights 154 | if m.mode == 'r' { 155 | rightsBase &^= wasi.WriteRights 156 | rightsInheriting &^= wasi.WriteRights 157 | } 158 | unixSystem.Preopen(unix.FD(fd), m.dir, wasi.FDStat{ 159 | FileType: wasi.DirectoryType, 160 | RightsBase: rightsBase, 161 | RightsInheriting: rightsInheriting, 162 | }) 163 | } 164 | 165 | for _, addr := range b.listens { 166 | fd, err := sockets.Listen(addr) 167 | if err != nil { 168 | return ctx, nil, fmt.Errorf("unable to listen on %q: %w", addr, err) 169 | } 170 | unixSystem.Preopen(unix.FD(fd), addr, wasi.FDStat{ 171 | FileType: wasi.SocketStreamType, 172 | Flags: wasi.NonBlock, 173 | RightsBase: wasi.SockListenRights, 174 | RightsInheriting: wasi.SockConnectionRights, 175 | }) 176 | } 177 | for _, addr := range b.dials { 178 | fd, err := sockets.Dial(addr) 179 | if err != nil && err != sockets.EINPROGRESS { 180 | return ctx, nil, fmt.Errorf("unable to dial %q: %w", addr, err) 181 | } 182 | unixSystem.Preopen(unix.FD(fd), addr, wasi.FDStat{ 183 | FileType: wasi.SocketStreamType, 184 | Flags: wasi.NonBlock, 185 | RightsBase: wasi.SockConnectionRights, 186 | }) 187 | } 188 | 189 | var extensions []wasi_snapshot_preview1.Extension 190 | if b.socketsExtension != nil { 191 | extensions = append(extensions, *b.socketsExtension) 192 | } 193 | 194 | hostModule := wasi_snapshot_preview1.NewHostModule(extensions...) 195 | 196 | instance := wazergo.MustInstantiate(ctx, runtime, 197 | wazergo.Decorate(hostModule, b.decorators...), 198 | wasi_snapshot_preview1.WithWASI(system), 199 | ) 200 | 201 | ctx = wazergo.WithModuleInstance(ctx, instance) 202 | sys = system 203 | system = nil 204 | return ctx, sys, nil 205 | } 206 | 207 | func dup(fd int) (int, error) { 208 | syscall.ForkLock.Lock() 209 | defer syscall.ForkLock.Unlock() 210 | 211 | newfd, err := syscall.Dup(fd) 212 | if err != nil { 213 | return -1, err 214 | } 215 | syscall.CloseOnExec(newfd) 216 | return newfd, nil 217 | } 218 | -------------------------------------------------------------------------------- /imports/defaults.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/tetratelabs/wazero/sys" 10 | ) 11 | 12 | const ( 13 | defaultName = "wasirun-wasm-module" 14 | defaultRealtimePrecision = time.Microsecond 15 | defaultMonotonicPrecision = time.Nanosecond 16 | ) 17 | 18 | var defaultRand = rand.Reader 19 | 20 | var epoch = time.Now() 21 | 22 | func defaultRealtime(ctx context.Context) (uint64, error) { 23 | return uint64(time.Now().UnixNano()), nil 24 | } 25 | 26 | func defaultMonotonic(ctx context.Context) (uint64, error) { 27 | return uint64(time.Since(epoch)), nil 28 | } 29 | 30 | func defaultYield(ctx context.Context) error { 31 | runtime.Gosched() 32 | return nil 33 | } 34 | 35 | var defaultRaise func(ctx context.Context, signal int) error = nil 36 | 37 | func defaultExit(ctx context.Context, exitCode int) error { 38 | panic(sys.NewExitError(uint32(exitCode))) 39 | } 40 | -------------------------------------------------------------------------------- /imports/extensions.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "github.com/stealthrocket/wasi-go/imports/wasi_snapshot_preview1" 5 | "github.com/tetratelabs/wazero" 6 | ) 7 | 8 | // DetectExtensions detects extensions to WASI preview 1. 9 | func DetectExtensions(module wazero.CompiledModule) (ext []wasi_snapshot_preview1.Extension) { 10 | if sockets := DetectSocketsExtension(module); sockets != nil { 11 | ext = append(ext, *sockets) 12 | } 13 | return 14 | } 15 | 16 | // DetectSocketsExtension determines the sockets extension in 17 | // use by inspecting a WASM module's host imports. 18 | // 19 | // This function can detect WasmEdge v1 and WasmEdge v2. 20 | func DetectSocketsExtension(module wazero.CompiledModule) *wasi_snapshot_preview1.Extension { 21 | functions := module.ImportedFunctions() 22 | hasWasmEdgeSockets := false 23 | sockAcceptParamCount := 0 24 | sockLocalAddrParamCount := 0 25 | sockPeerAddrParamCount := 0 26 | for _, f := range functions { 27 | moduleName, name, ok := f.Import() 28 | if !ok || moduleName != wasi_snapshot_preview1.HostModuleName { 29 | continue 30 | } 31 | switch name { 32 | case "sock_open", "sock_bind", "sock_connect", "sock_listen", 33 | "sock_send_to", "sock_recv_from", "sock_getsockopt", "sock_setsockopt", 34 | "sock_getlocaladdr", "sock_getpeeraddr", "sock_getaddrinfo": 35 | hasWasmEdgeSockets = true 36 | } 37 | switch name { 38 | case "sock_accept": 39 | sockAcceptParamCount = len(f.ParamTypes()) 40 | case "sock_getlocaladdr": 41 | sockLocalAddrParamCount = len(f.ParamTypes()) 42 | case "sock_getpeeraddr": 43 | sockPeerAddrParamCount = len(f.ParamTypes()) 44 | } 45 | } 46 | if hasWasmEdgeSockets || sockAcceptParamCount == 2 { 47 | if sockAcceptParamCount == 2 || 48 | sockLocalAddrParamCount == 4 || 49 | sockPeerAddrParamCount == 4 { 50 | return &wasi_snapshot_preview1.WasmEdgeV1 51 | } else { 52 | return &wasi_snapshot_preview1.WasmEdgeV2 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /imports/wasi_http/README.md: -------------------------------------------------------------------------------- 1 | # WASI-HTTP 2 | This module implements the [wasi-http](https://github.com/WebAssembly/wasi-http) specification. 3 | The specification is in active development/flux as is the [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) tool which is used to generate client libraries. 4 | 5 | You should expect a degree of instability in these interfaces for the foreseeable future. 6 | 7 | ## Example guest code 8 | There are existing examples of working guest code in the following languages 9 | * [Golang](https://github.com/dev-wasm/dev-wasm-go/tree/main/http) 10 | * [C](https://github.com/dev-wasm/dev-wasm-c/tree/main/http) 11 | * [AssemblyScript](https://github.com/dev-wasm/dev-wasm-ts/tree/main/http) 12 | * [Dotnet](https://github.com/dev-wasm/dev-wasm-dotnet/tree/main/http) 13 | * [Rust](https://github.com/bytecodealliance/wasmtime/blob/main/crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs) 14 | 15 | ## Example server code 16 | There is an example server in the following languages (more to come): 17 | * [Golang](https://github.com/dev-wasm/dev-wasm-go/blob/main/http/server.go) 18 | * [C](https://github.com/brendandburns/wasi-go/blob/server/testdata/c/http/server.c) 19 | 20 | -------------------------------------------------------------------------------- /imports/wasi_http/common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/tetratelabs/wazero/api" 10 | ) 11 | 12 | func Malloc(ctx context.Context, m api.Module, size uint32) (uint32, error) { 13 | malloc := m.ExportedFunction("cabi_realloc") 14 | result, err := malloc.Call(ctx, 0, 0, 4, uint64(size)) 15 | if err != nil { 16 | log.Fatalf(err.Error()) 17 | } 18 | return uint32(result[0]), err 19 | } 20 | 21 | func ReadString(mod api.Module, ptr, len uint32) (string, bool) { 22 | data, ok := mod.Memory().Read(ptr, len) 23 | if !ok { 24 | return "", false 25 | } 26 | return string(data), true 27 | } 28 | 29 | func WriteString(ctx context.Context, module api.Module, ptr uint32, str string) error { 30 | data := []byte(str) 31 | strPtr, err := Malloc(ctx, module, uint32(len(data))) 32 | if err != nil { 33 | return err 34 | } 35 | if !module.Memory().WriteString(strPtr, str) { 36 | return fmt.Errorf("failed to write string") 37 | } 38 | data = []byte{} 39 | data = binary.LittleEndian.AppendUint32(data, strPtr) 40 | data = binary.LittleEndian.AppendUint32(data, uint32(len(str))) 41 | if !module.Memory().Write(ptr, data) { 42 | return fmt.Errorf("failed to write struct") 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func WriteUint32(ctx context.Context, mod api.Module, val uint32) (uint32, error) { 49 | ptr, err := Malloc(ctx, mod, 4) 50 | if err != nil { 51 | return 0, err 52 | } 53 | data := []byte{} 54 | data = binary.LittleEndian.AppendUint32(data, val) 55 | if !mod.Memory().Write(ptr, data) { 56 | return 0, fmt.Errorf("failed to write uint32") 57 | } 58 | return ptr, nil 59 | } 60 | -------------------------------------------------------------------------------- /imports/wasi_http/default_http/http.go: -------------------------------------------------------------------------------- 1 | package default_http 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stealthrocket/wasi-go/imports/wasi_http/types" 7 | "github.com/tetratelabs/wazero" 8 | ) 9 | 10 | const ModuleName = "default-outgoing-HTTP" 11 | 12 | func Instantiate(ctx context.Context, r wazero.Runtime, req *types.Requests, res *types.Responses, f *types.FieldsCollection) error { 13 | handler := &Handler{req, res, f} 14 | _, err := r.NewHostModuleBuilder(ModuleName). 15 | NewFunctionBuilder().WithFunc(requestFn).Export("request"). 16 | NewFunctionBuilder().WithFunc(handler.handleFn).Export("handle"). 17 | Instantiate(ctx) 18 | return err 19 | } 20 | -------------------------------------------------------------------------------- /imports/wasi_http/default_http/request.go: -------------------------------------------------------------------------------- 1 | package default_http 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/stealthrocket/wasi-go/imports/wasi_http/types" 8 | "github.com/tetratelabs/wazero/api" 9 | ) 10 | 11 | type Handler struct { 12 | req *types.Requests 13 | res *types.Responses 14 | f *types.FieldsCollection 15 | } 16 | 17 | // Request handles HTTP serving. It's currently unimplemented 18 | func requestFn(_ context.Context, mod api.Module, a, b, c, d, e, f, g, h, j, k, l, m, n, o uint32) int32 { 19 | return 0 20 | } 21 | 22 | // Handle handles HTTP client calls. 23 | // The remaining parameters (b..h) are for the HTTP Options, currently unimplemented. 24 | func (handler *Handler) handleFn(_ context.Context, mod api.Module, request, b, c, d, e, f, g, h uint32) uint32 { 25 | req, ok := handler.req.GetRequest(request) 26 | if !ok { 27 | log.Printf("Failed to get request: %v\n", request) 28 | return 0 29 | } 30 | r, err := req.MakeRequest(handler.f) 31 | if err != nil { 32 | log.Println(err.Error()) 33 | return 0 34 | } 35 | return handler.res.MakeResponse(r) 36 | } 37 | -------------------------------------------------------------------------------- /imports/wasi_http/http.go: -------------------------------------------------------------------------------- 1 | package wasi_http 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/stealthrocket/wasi-go/imports/wasi_http/default_http" 8 | "github.com/stealthrocket/wasi-go/imports/wasi_http/server" 9 | "github.com/stealthrocket/wasi-go/imports/wasi_http/streams" 10 | "github.com/stealthrocket/wasi-go/imports/wasi_http/types" 11 | "github.com/tetratelabs/wazero" 12 | "github.com/tetratelabs/wazero/api" 13 | ) 14 | 15 | type WasiHTTP struct { 16 | s *streams.Streams 17 | f *types.FieldsCollection 18 | r *types.Requests 19 | rs *types.Responses 20 | o *types.OutResponses 21 | } 22 | 23 | func MakeWasiHTTP() *WasiHTTP { 24 | s := streams.MakeStreams() 25 | f := types.MakeFields() 26 | r := types.MakeRequests(s, f) 27 | rs := types.MakeResponses(s, f) 28 | o := types.MakeOutresponses() 29 | 30 | return &WasiHTTP{ 31 | s: s, 32 | f: f, 33 | r: r, 34 | rs: rs, 35 | o: o, 36 | } 37 | } 38 | 39 | func (w *WasiHTTP) Instantiate(ctx context.Context, rt wazero.Runtime) error { 40 | if err := types.Instantiate(ctx, rt, w.s, w.r, w.rs, w.f, w.o); err != nil { 41 | return err 42 | } 43 | if err := streams.Instantiate(ctx, rt, w.s); err != nil { 44 | return err 45 | } 46 | if err := default_http.Instantiate(ctx, rt, w.r, w.rs, w.f); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func DetectWasiHttp(module wazero.CompiledModule) bool { 53 | functions := module.ImportedFunctions() 54 | hasWasiHttp := false 55 | for _, f := range functions { 56 | moduleName, name, ok := f.Import() 57 | if !ok || moduleName != default_http.ModuleName { 58 | continue 59 | } 60 | switch name { 61 | case "handle": 62 | hasWasiHttp = true 63 | } 64 | } 65 | return hasWasiHttp 66 | } 67 | 68 | func (w *WasiHTTP) MakeHandler(ctx context.Context, m api.Module) http.Handler { 69 | return server.WasmServer{ 70 | Ctx: ctx, 71 | Module: m, 72 | Requests: w.r, 73 | Responses: w.rs, 74 | Fields: w.f, 75 | OutParams: w.o, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /imports/wasi_http/http_test.go: -------------------------------------------------------------------------------- 1 | package wasi_http 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/stealthrocket/wasi-go" 16 | "github.com/stealthrocket/wasi-go/imports" 17 | "github.com/tetratelabs/wazero" 18 | "github.com/tetratelabs/wazero/sys" 19 | ) 20 | 21 | type handler struct { 22 | urls []string 23 | bodies []string 24 | } 25 | 26 | func (h *handler) reset() { 27 | h.bodies = []string{} 28 | h.urls = []string{} 29 | } 30 | 31 | func (h *handler) ServeHTTP(res http.ResponseWriter, req *http.Request) { 32 | body := "" 33 | 34 | if req.Body != nil { 35 | defer req.Body.Close() 36 | data, err := ioutil.ReadAll(req.Body) 37 | if err != nil { 38 | panic(err.Error()) 39 | } 40 | body = string(data) 41 | } 42 | 43 | res.WriteHeader(200) 44 | res.Write([]byte("Response")) 45 | 46 | h.urls = append(h.urls, req.URL.String()) 47 | h.bodies = append(h.bodies, body) 48 | } 49 | 50 | func TestHttpClient(t *testing.T) { 51 | filePaths, _ := filepath.Glob("../../testdata/c/http/http*.wasm") 52 | for _, file := range filePaths { 53 | fmt.Printf("%v\n", file) 54 | } 55 | if len(filePaths) == 0 { 56 | t.Log("nothing to test") 57 | } 58 | 59 | h := handler{} 60 | s := httptest.NewServer(&h) 61 | defer s.Close() 62 | 63 | expectedPaths := [][]string{ 64 | { 65 | "/get?some=arg&goes=here", 66 | "/post", 67 | }, 68 | } 69 | 70 | expectedBodies := [][]string{ 71 | { 72 | "", 73 | "{\"foo\": \"bar\"}", 74 | }, 75 | } 76 | 77 | for testIx, test := range filePaths { 78 | name := test 79 | for strings.HasPrefix(name, "../") { 80 | name = name[3:] 81 | } 82 | 83 | t.Run(name, func(t *testing.T) { 84 | bytecode, err := os.ReadFile(test) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | ctx := context.Background() 90 | 91 | runtime := wazero.NewRuntime(ctx) 92 | defer runtime.Close(ctx) 93 | 94 | builder := imports.NewBuilder(). 95 | WithName("http"). 96 | WithEnv("SERVER=" + s.URL[7:]). 97 | WithArgs() 98 | var system wasi.System 99 | ctx, system, err = builder.Instantiate(ctx, runtime) 100 | if err != nil { 101 | t.Error("Failed to build WASI module: ", err) 102 | } 103 | defer system.Close(ctx) 104 | 105 | w := MakeWasiHTTP() 106 | w.Instantiate(ctx, runtime) 107 | 108 | instance, err := runtime.Instantiate(ctx, bytecode) 109 | if err != nil { 110 | switch e := err.(type) { 111 | case *sys.ExitError: 112 | if exitCode := e.ExitCode(); exitCode != 0 { 113 | t.Error("exit code:", exitCode) 114 | t.FailNow() 115 | } 116 | default: 117 | t.Error("instantiating wasm module instance:", err) 118 | } 119 | } 120 | if instance != nil { 121 | if err := instance.Close(ctx); err != nil { 122 | t.Error("closing wasm module instance:", err) 123 | } 124 | } 125 | if !reflect.DeepEqual(expectedPaths[testIx], h.urls) { 126 | t.Errorf("Unexpected paths: %v vs %v", h.urls, expectedPaths[testIx]) 127 | } 128 | if !reflect.DeepEqual(expectedBodies[testIx], h.bodies) { 129 | t.Errorf("Unexpected paths: %v vs %v", h.bodies, expectedBodies[testIx]) 130 | } 131 | 132 | h.reset() 133 | }) 134 | } 135 | } 136 | 137 | func TestServer(t *testing.T) { 138 | filePaths, _ := filepath.Glob("../../testdata/c/http/server*.wasm") 139 | for _, file := range filePaths { 140 | fmt.Printf("%v\n", file) 141 | } 142 | if len(filePaths) == 0 { 143 | t.Log("nothing to test") 144 | } 145 | 146 | for _, test := range filePaths { 147 | name := test 148 | for strings.HasPrefix(name, "../") { 149 | name = name[3:] 150 | } 151 | 152 | t.Run(name, func(t *testing.T) { 153 | bytecode, err := os.ReadFile(test) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | ctx := context.Background() 159 | 160 | runtime := wazero.NewRuntime(ctx) 161 | defer runtime.Close(ctx) 162 | 163 | builder := imports.NewBuilder(). 164 | WithName("http"). 165 | WithArgs() 166 | var system wasi.System 167 | ctx, system, err = builder.Instantiate(ctx, runtime) 168 | if err != nil { 169 | t.Error("Failed to build WASI module: ", err) 170 | } 171 | defer system.Close(ctx) 172 | 173 | w := MakeWasiHTTP() 174 | w.Instantiate(ctx, runtime) 175 | 176 | instance, err := runtime.Instantiate(ctx, bytecode) 177 | if err != nil { 178 | switch e := err.(type) { 179 | case *sys.ExitError: 180 | if exitCode := e.ExitCode(); exitCode != 0 { 181 | t.Error("exit code:", exitCode) 182 | } 183 | default: 184 | t.Error("instantiating wasm module instance:", err) 185 | } 186 | } 187 | if instance != nil { 188 | h := w.MakeHandler(ctx, instance) 189 | s := httptest.NewServer(h) 190 | defer s.Close() 191 | 192 | for i := 0; i < 3; i++ { 193 | res, err := http.Get(s.URL) 194 | if err != nil { 195 | t.Error("Failed to read from server.") 196 | continue 197 | } 198 | defer res.Body.Close() 199 | 200 | data, err := ioutil.ReadAll(res.Body) 201 | if err != nil { 202 | t.Error("Failed to read body.") 203 | continue 204 | } 205 | if string(data) != fmt.Sprintf("Hello from WASM! (%d)", i) { 206 | t.Error("Unexpected body: " + string(data)) 207 | } 208 | } 209 | 210 | if err := instance.Close(ctx); err != nil { 211 | t.Error("closing wasm module instance:", err) 212 | } 213 | } 214 | }) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /imports/wasi_http/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/stealthrocket/wasi-go/imports/wasi_http/types" 8 | "github.com/tetratelabs/wazero/api" 9 | ) 10 | 11 | type WasmServer struct { 12 | Ctx context.Context 13 | Module api.Module 14 | Fields *types.FieldsCollection 15 | Requests *types.Requests 16 | Responses *types.Responses 17 | OutParams *types.OutResponses 18 | } 19 | 20 | func (w WasmServer) ServeHTTP(res http.ResponseWriter, req *http.Request) { 21 | fn := w.Module.ExportedFunction("HTTP#handle") 22 | if fn == nil { 23 | res.WriteHeader(500) 24 | res.Write([]byte("Handler not found")) 25 | return 26 | } 27 | id := w.Requests.MakeRequest(req) 28 | out := w.OutParams.MakeOutparameter() 29 | 30 | _, err := fn.Call(w.Ctx, uint64(id), uint64(out)) 31 | if err != nil { 32 | res.WriteHeader(500) 33 | res.Write([]byte(err.Error())) 34 | return 35 | } 36 | responseId, found := w.OutParams.GetResponseByOutparameter(out) 37 | if !found { 38 | res.WriteHeader(500) 39 | res.Write([]byte("Couldn't find outparameter mapping")) 40 | return 41 | } 42 | r, found := w.Responses.GetResponse(responseId) 43 | if !found || r == nil { 44 | res.WriteHeader(500) 45 | res.Write([]byte("Couldn't find response")) 46 | return 47 | } 48 | if headers, found := w.Fields.GetFields(r.HeaderHandle); found { 49 | for key, value := range headers { 50 | for ix := range value { 51 | res.Header().Add(key, value[ix]) 52 | } 53 | } 54 | w.Fields.DeleteFields(r.HeaderHandle) 55 | } 56 | res.WriteHeader(r.StatusCode) 57 | data := r.Buffer.Bytes() 58 | res.Write(data) 59 | 60 | w.Responses.DeleteResponse(responseId) 61 | } 62 | -------------------------------------------------------------------------------- /imports/wasi_http/streams/read.go: -------------------------------------------------------------------------------- 1 | package streams 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "log" 7 | 8 | "github.com/stealthrocket/wasi-go/imports/wasi_http/common" 9 | "github.com/tetratelabs/wazero/api" 10 | ) 11 | 12 | func (s *Streams) streamReadFn(ctx context.Context, mod api.Module, stream_handle uint32, length uint64, out_ptr uint32) { 13 | rawData := make([]byte, length) 14 | n, done, err := s.Read(stream_handle, rawData) 15 | 16 | // data, err := types.ResponseBody() 17 | if err != nil { 18 | log.Fatalf(err.Error()) 19 | } 20 | 21 | data := rawData[0:n] 22 | ptr_len := uint32(len(data)) 23 | ptr, err := common.Malloc(ctx, mod, ptr_len) 24 | if err != nil { 25 | log.Fatalf(err.Error()) 26 | } 27 | mod.Memory().Write(ptr, data) 28 | 29 | data = []byte{} 30 | // 0 == is_ok, 1 == is_err 31 | le := binary.LittleEndian 32 | data = le.AppendUint32(data, 0) 33 | data = le.AppendUint32(data, ptr) 34 | data = le.AppendUint32(data, ptr_len) 35 | if done { 36 | // No more data to read. 37 | data = le.AppendUint32(data, 0) 38 | } else { 39 | data = le.AppendUint32(data, 1) 40 | } 41 | mod.Memory().Write(out_ptr, data) 42 | } 43 | 44 | func (s *Streams) dropInputStreamFn(_ context.Context, mod api.Module, stream uint32) { 45 | s.DeleteStream(stream) 46 | } 47 | -------------------------------------------------------------------------------- /imports/wasi_http/streams/streams.go: -------------------------------------------------------------------------------- 1 | package streams 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/tetratelabs/wazero" 11 | ) 12 | 13 | const ModuleName = "streams" 14 | 15 | type Stream struct { 16 | reader io.Reader 17 | writer io.Writer 18 | } 19 | 20 | type Streams struct { 21 | lock sync.RWMutex 22 | streams map[uint32]Stream 23 | streamHandleBase uint32 24 | } 25 | 26 | func MakeStreams() *Streams { 27 | return &Streams{ 28 | streams: make(map[uint32]Stream), 29 | streamHandleBase: 1, 30 | } 31 | } 32 | 33 | func Instantiate(ctx context.Context, r wazero.Runtime, s *Streams) error { 34 | _, err := r.NewHostModuleBuilder(ModuleName). 35 | NewFunctionBuilder().WithFunc(s.streamReadFn).Export("read"). 36 | NewFunctionBuilder().WithFunc(s.dropInputStreamFn).Export("drop-input-stream"). 37 | NewFunctionBuilder().WithFunc(s.writeStreamFn).Export("write"). 38 | Instantiate(ctx) 39 | return err 40 | } 41 | 42 | func (s *Streams) NewInputStream(reader io.Reader) uint32 { 43 | return s.newStream(reader, nil) 44 | } 45 | 46 | func (s *Streams) NewOutputStream(writer io.Writer) uint32 { 47 | return s.newStream(nil, writer) 48 | } 49 | 50 | func (s *Streams) newStream(reader io.Reader, writer io.Writer) uint32 { 51 | streamHandleBase := atomic.AddUint32(&s.streamHandleBase, 1) 52 | s.lock.Lock() 53 | s.streams[streamHandleBase] = Stream{ 54 | reader: reader, 55 | writer: writer, 56 | } 57 | s.lock.Unlock() 58 | return streamHandleBase 59 | } 60 | 61 | func (s *Streams) DeleteStream(handle uint32) { 62 | s.lock.Lock() 63 | defer s.lock.Unlock() 64 | delete(s.streams, handle) 65 | } 66 | 67 | func (s *Streams) GetStream(handle uint32) (stream Stream, found bool) { 68 | s.lock.RLock() 69 | stream, found = s.streams[handle] 70 | s.lock.RUnlock() 71 | return 72 | } 73 | 74 | func (s *Streams) Read(handle uint32, data []byte) (int, bool, error) { 75 | stream, found := s.GetStream(handle) 76 | if !found { 77 | return 0, false, fmt.Errorf("stream not found: %d", handle) 78 | } 79 | if stream.reader == nil { 80 | return 0, false, fmt.Errorf("not a readable stream: %d", handle) 81 | } 82 | 83 | n, err := stream.reader.Read(data) 84 | if err == io.EOF { 85 | return n, true, nil 86 | } 87 | return n, false, err 88 | } 89 | 90 | func (s *Streams) Write(handle uint32, data []byte) (int, error) { 91 | stream, found := s.GetStream(handle) 92 | if !found { 93 | return 0, fmt.Errorf("stream not found: %d", handle) 94 | } 95 | if stream.writer == nil { 96 | return 0, fmt.Errorf("not a writeable stream: %d", handle) 97 | } 98 | return stream.writer.Write(data) 99 | } 100 | -------------------------------------------------------------------------------- /imports/wasi_http/streams/write.go: -------------------------------------------------------------------------------- 1 | package streams 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "log" 7 | 8 | "github.com/tetratelabs/wazero/api" 9 | ) 10 | 11 | func (s *Streams) writeStreamFn(_ context.Context, mod api.Module, stream, ptr, l, result_ptr uint32) { 12 | data, ok := mod.Memory().Read(ptr, l) 13 | if !ok { 14 | log.Printf("Body read failed!\n") 15 | return 16 | } 17 | n, err := s.Write(stream, data) 18 | if err != nil { 19 | log.Printf("Failed to write: %v\n", err.Error()) 20 | } 21 | 22 | data = []byte{} 23 | // 0 == is_ok, 1 == is_err 24 | le := binary.LittleEndian 25 | data = le.AppendUint32(data, 0) 26 | // write the number of bytes written 27 | data = le.AppendUint32(data, uint32(n)) 28 | mod.Memory().Write(result_ptr, data) 29 | } 30 | -------------------------------------------------------------------------------- /imports/wasi_http/types/http.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/stealthrocket/wasi-go/imports/wasi_http/common" 8 | "github.com/stealthrocket/wasi-go/imports/wasi_http/streams" 9 | "github.com/tetratelabs/wazero" 10 | "github.com/tetratelabs/wazero/api" 11 | ) 12 | 13 | const ModuleName = "types" 14 | 15 | func logFn(ctx context.Context, mod api.Module, ptr, len uint32) { 16 | str, _ := common.ReadString(mod, ptr, len) 17 | fmt.Print(str) 18 | } 19 | 20 | func Instantiate(ctx context.Context, rt wazero.Runtime, s *streams.Streams, r *Requests, rs *Responses, f *FieldsCollection, o *OutResponses) error { 21 | _, err := rt.NewHostModuleBuilder(ModuleName). 22 | NewFunctionBuilder().WithFunc(r.newOutgoingRequestFn).Export("new-outgoing-request"). 23 | NewFunctionBuilder().WithFunc(f.newFieldsFn).Export("new-fields"). 24 | NewFunctionBuilder().WithFunc(f.dropFieldsFn).Export("drop-fields"). 25 | NewFunctionBuilder().WithFunc(f.fieldsEntriesFn).Export("fields-entries"). 26 | NewFunctionBuilder().WithFunc(r.dropOutgoingRequestFn).Export("drop-outgoing-request"). 27 | NewFunctionBuilder().WithFunc(r.outgoingRequestWriteFn).Export("outgoing-request-write"). 28 | NewFunctionBuilder().WithFunc(rs.dropIncomingResponseFn).Export("drop-incoming-response"). 29 | NewFunctionBuilder().WithFunc(rs.incomingResponseStatusFn).Export("incoming-response-status"). 30 | NewFunctionBuilder().WithFunc(rs.incomingResponseHeadersFn).Export("incoming-response-headers"). 31 | NewFunctionBuilder().WithFunc(rs.incomingResponseConsumeFn).Export("incoming-response-consume"). 32 | NewFunctionBuilder().WithFunc(futureResponseGetFn).Export("future-incoming-response-get"). 33 | NewFunctionBuilder().WithFunc(r.incomingRequestMethodFn).Export("incoming-request-method"). 34 | NewFunctionBuilder().WithFunc(r.incomingRequestPathFn).Export("incoming-request-path"). 35 | NewFunctionBuilder().WithFunc(r.incomingRequestAuthorityFn).Export("incoming-request-authority"). 36 | NewFunctionBuilder().WithFunc(r.incomingRequestHeadersFn).Export("incoming-request-headers"). 37 | NewFunctionBuilder().WithFunc(incomingRequestConsumeFn).Export("incoming-request-consume"). 38 | NewFunctionBuilder().WithFunc(r.dropIncomingRequestFn).Export("drop-incoming-request"). 39 | NewFunctionBuilder().WithFunc(o.setResponseOutparamFn).Export("set-response-outparam"). 40 | NewFunctionBuilder().WithFunc(rs.newOutgoingResponseFn).Export("new-outgoing-response"). 41 | NewFunctionBuilder().WithFunc(rs.outgoingResponseWriteFn).Export("outgoing-response-write"). 42 | NewFunctionBuilder().WithFunc(dropOutgoingResponseFn).Export("drop-outgoing-response"). 43 | NewFunctionBuilder().WithFunc(logFn).Export("log-it"). 44 | Instantiate(ctx) 45 | return err 46 | } 47 | -------------------------------------------------------------------------------- /imports/wasi_http/types/request.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "sync" 12 | "sync/atomic" 13 | 14 | "github.com/stealthrocket/wasi-go/imports/wasi_http/common" 15 | "github.com/stealthrocket/wasi-go/imports/wasi_http/streams" 16 | "github.com/tetratelabs/wazero/api" 17 | ) 18 | 19 | type Request struct { 20 | Method string 21 | Path string 22 | Query string 23 | Scheme string 24 | Authority string 25 | Headers uint32 26 | BodyBuffer *bytes.Buffer 27 | } 28 | 29 | func (r Request) Url() string { 30 | return fmt.Sprintf("%s://%s%s%s", r.Scheme, r.Authority, r.Path, r.Query) 31 | } 32 | 33 | type Requests struct { 34 | lock sync.RWMutex 35 | requests map[uint32]*Request 36 | requestIdBase uint32 37 | streams *streams.Streams 38 | fields *FieldsCollection 39 | } 40 | 41 | func MakeRequests(s *streams.Streams, f *FieldsCollection) *Requests { 42 | return &Requests{requests: map[uint32]*Request{}, requestIdBase: 1, streams: s, fields: f} 43 | } 44 | 45 | func (r *Requests) MakeRequest(req *http.Request) uint32 { 46 | request, id := r.newRequest() 47 | request.Method = req.Method 48 | // Fix this if port is missing. 49 | request.Authority = req.Host 50 | request.Path = req.URL.Path 51 | request.Headers = r.fields.MakeFields(Fields(req.Header)) 52 | 53 | return id 54 | } 55 | 56 | func (r *Requests) newRequest() (*Request, uint32) { 57 | request := &Request{} 58 | requestIdBase := atomic.AddUint32(&r.requestIdBase, 1) 59 | r.lock.Lock() 60 | r.requests[requestIdBase] = request 61 | r.lock.Unlock() 62 | return request, requestIdBase 63 | } 64 | 65 | func (r *Requests) deleteRequest(handle uint32) { 66 | r.lock.Lock() 67 | defer r.lock.Unlock() 68 | delete(r.requests, handle) 69 | } 70 | 71 | func (r *Requests) GetRequest(handle uint32) (*Request, bool) { 72 | r.lock.RLock() 73 | req, ok := r.requests[handle] 74 | r.lock.RUnlock() 75 | return req, ok 76 | } 77 | 78 | func (request *Request) MakeRequest(f *FieldsCollection) (*http.Response, error) { 79 | var body io.Reader = nil 80 | if request.BodyBuffer != nil { 81 | body = bytes.NewReader(request.BodyBuffer.Bytes()) 82 | } 83 | r, err := http.NewRequest(request.Method, request.Url(), body) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if fields, found := f.GetFields(request.Headers); found { 89 | r.Header = http.Header(fields) 90 | } 91 | 92 | return http.DefaultClient.Do(r) 93 | } 94 | 95 | func incomingRequestConsumeFn(ctx context.Context, mod api.Module, request, ptr uint32) { 96 | data := []byte{} 97 | // Unsupported for now. 98 | // error 99 | data = binary.LittleEndian.AppendUint32(data, 1) 100 | data = binary.LittleEndian.AppendUint32(data, 0) 101 | 102 | mod.Memory().Write(ptr, data) 103 | } 104 | 105 | func (r *Requests) incomingRequestHeadersFn(ctx context.Context, mod api.Module, request uint32) uint32 { 106 | req, ok := r.GetRequest(request) 107 | if !ok { 108 | return 0 109 | } 110 | return req.Headers 111 | } 112 | 113 | func (r *Requests) incomingRequestPathFn(ctx context.Context, mod api.Module, request, ptr uint32) { 114 | req, ok := r.GetRequest(request) 115 | if !ok { 116 | return 117 | } 118 | if err := common.WriteString(ctx, mod, ptr, req.Path); err != nil { 119 | panic(err.Error()) 120 | } 121 | } 122 | 123 | func (r *Requests) incomingRequestAuthorityFn(ctx context.Context, mod api.Module, request, ptr uint32) { 124 | req, ok := r.GetRequest(request) 125 | if !ok { 126 | return 127 | } 128 | if err := common.WriteString(ctx, mod, ptr, req.Authority); err != nil { 129 | panic(err.Error()) 130 | } 131 | } 132 | 133 | func (r *Requests) incomingRequestMethodFn(_ context.Context, mod api.Module, request, ptr uint32) { 134 | req, ok := r.GetRequest(request) 135 | if !ok { 136 | return 137 | } 138 | 139 | method := 0 140 | switch req.Method { 141 | case "GET": 142 | method = 0 143 | case "HEAD": 144 | method = 1 145 | case "POST": 146 | method = 2 147 | case "PUT": 148 | method = 3 149 | case "DELETE": 150 | method = 4 151 | case "CONNECT": 152 | method = 5 153 | case "OPTIONS": 154 | method = 6 155 | case "TRACE": 156 | method = 7 157 | case "PATCH": 158 | method = 8 159 | default: 160 | log.Fatalf("Unknown method: %s", req.Method) 161 | } 162 | 163 | data := []byte{} 164 | data = binary.LittleEndian.AppendUint32(data, uint32(method)) 165 | mod.Memory().Write(ptr, data) 166 | } 167 | 168 | func (r *Requests) newOutgoingRequestFn(_ context.Context, mod api.Module, 169 | method, method_ptr, method_len, 170 | path_ptr, path_len, 171 | query_ptr, query_len, 172 | scheme_is_some, scheme, scheme_ptr, scheme_len, 173 | authority_ptr, authority_len, header_handle uint32) uint32 { 174 | 175 | request, id := r.newRequest() 176 | 177 | switch method { 178 | case 0: 179 | request.Method = "GET" 180 | case 1: 181 | request.Method = "HEAD" 182 | case 2: 183 | request.Method = "POST" 184 | case 3: 185 | request.Method = "PUT" 186 | case 4: 187 | request.Method = "DELETE" 188 | case 5: 189 | request.Method = "CONNECT" 190 | case 6: 191 | request.Method = "OPTIONS" 192 | case 7: 193 | request.Method = "TRACE" 194 | case 8: 195 | request.Method = "PATCH" 196 | default: 197 | log.Fatalf("Unknown method: %d", method) 198 | } 199 | 200 | path, ok := mod.Memory().Read(uint32(path_ptr), uint32(path_len)) 201 | if !ok { 202 | return 0 203 | } 204 | request.Path = string(path) 205 | 206 | query, ok := mod.Memory().Read(uint32(query_ptr), uint32(query_len)) 207 | if !ok { 208 | return 0 209 | } 210 | request.Query = string(query) 211 | 212 | request.Scheme = "https" 213 | if scheme_is_some == 1 { 214 | if scheme == 0 { 215 | request.Scheme = "http" 216 | } 217 | if scheme == 2 { 218 | d, ok := mod.Memory().Read(uint32(scheme_ptr), uint32(scheme_len)) 219 | if !ok { 220 | return 0 221 | } 222 | request.Scheme = string(d) 223 | } 224 | } 225 | 226 | authority, ok := mod.Memory().Read(uint32(authority_ptr), uint32(authority_len)) 227 | if !ok { 228 | return 0 229 | } 230 | request.Authority = string(authority) 231 | 232 | request.Headers = header_handle 233 | 234 | return id 235 | } 236 | 237 | func (r *Requests) dropOutgoingRequestFn(_ context.Context, mod api.Module, handle uint32) { 238 | r.deleteRequest(handle) 239 | } 240 | 241 | func (r *Requests) dropIncomingRequestFn(_ context.Context, mod api.Module, handle uint32) { 242 | req, found := r.GetRequest(handle) 243 | if !found { 244 | return 245 | } 246 | r.fields.DeleteFields(req.Headers) 247 | // Delete body stream here 248 | r.deleteRequest(handle) 249 | } 250 | 251 | func (r *Requests) outgoingRequestWriteFn(_ context.Context, mod api.Module, handle, ptr uint32) { 252 | request, found := r.GetRequest(handle) 253 | if !found { 254 | fmt.Printf("Failed to find request: %d\n", handle) 255 | return 256 | } 257 | request.BodyBuffer = &bytes.Buffer{} 258 | stream := r.streams.NewOutputStream(request.BodyBuffer) 259 | 260 | data := []byte{} 261 | data = binary.LittleEndian.AppendUint32(data, 0) 262 | data = binary.LittleEndian.AppendUint32(data, stream) 263 | mod.Memory().Write(ptr, data) 264 | } 265 | -------------------------------------------------------------------------------- /imports/wasi_http/types/response.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "log" 8 | "net/http" 9 | "sync" 10 | "sync/atomic" 11 | 12 | "github.com/stealthrocket/wasi-go/imports/wasi_http/streams" 13 | "github.com/tetratelabs/wazero/api" 14 | ) 15 | 16 | type Response struct { 17 | *http.Response 18 | HeaderHandle uint32 19 | streamHandle uint32 20 | Buffer *bytes.Buffer 21 | } 22 | 23 | type Responses struct { 24 | lock sync.RWMutex 25 | responses map[uint32]*Response 26 | baseResponseId uint32 27 | streams *streams.Streams 28 | fields *FieldsCollection 29 | } 30 | 31 | type OutResponses struct { 32 | lock sync.RWMutex 33 | responses map[uint32]uint32 34 | baseResponseId uint32 35 | } 36 | 37 | func MakeOutresponses() *OutResponses { 38 | return &OutResponses{ 39 | responses: make(map[uint32]uint32), 40 | baseResponseId: 1, 41 | } 42 | } 43 | 44 | func (o *OutResponses) MakeOutparameter() uint32 { 45 | baseResponseId := atomic.AddUint32(&o.baseResponseId, 1) 46 | return baseResponseId 47 | } 48 | 49 | func (o *OutResponses) GetResponseByOutparameter(out uint32) (uint32, bool) { 50 | o.lock.RLock() 51 | defer o.lock.RUnlock() 52 | r, ok := o.responses[out] 53 | return r, ok 54 | } 55 | 56 | func (r *Responses) GetResponse(handle uint32) (*Response, bool) { 57 | r.lock.RLock() 58 | defer r.lock.RUnlock() 59 | res, ok := r.responses[handle] 60 | return res, ok 61 | } 62 | 63 | func (r *Responses) DeleteResponse(handle uint32) { 64 | r.lock.Lock() 65 | defer r.lock.Unlock() 66 | delete(r.responses, handle) 67 | } 68 | 69 | func (r *Responses) dropIncomingResponseFn(_ context.Context, mod api.Module, handle uint32) { 70 | r.DeleteResponse(handle) 71 | } 72 | 73 | func dropOutgoingResponseFn(_ context.Context, mod api.Module, handle uint32) { 74 | // pass 75 | } 76 | 77 | func (r *Responses) outgoingResponseWriteFn(ctx context.Context, mod api.Module, res, ptr uint32) { 78 | response, found := r.GetResponse(res) 79 | data := []byte{} 80 | if !found { 81 | // Error 82 | data = binary.LittleEndian.AppendUint32(data, 1) 83 | data = binary.LittleEndian.AppendUint32(data, 0) 84 | } else { 85 | writer := &bytes.Buffer{} 86 | stream := r.streams.NewOutputStream(writer) 87 | 88 | response.streamHandle = stream 89 | response.Buffer = writer 90 | // 0 == no error 91 | data = binary.LittleEndian.AppendUint32(data, 0) 92 | data = binary.LittleEndian.AppendUint32(data, stream) 93 | } 94 | if !mod.Memory().Write(ptr, data) { 95 | panic("Failed to write data!") 96 | } 97 | } 98 | 99 | func (r *Responses) newOutgoingResponseFn(_ context.Context, status, headers uint32) uint32 { 100 | res := &Response{&http.Response{}, headers, 0, nil} 101 | res.StatusCode = int(status) 102 | baseResponseId := atomic.AddUint32(&r.baseResponseId, 1) 103 | r.lock.Lock() 104 | defer r.lock.Unlock() 105 | r.responses[baseResponseId] = res 106 | return baseResponseId 107 | } 108 | 109 | func (o *OutResponses) setResponseOutparamFn(_ context.Context, mod api.Module, res, err, resOut, _msg_ptr, _msg_str uint32) uint32 { 110 | if err == 1 { 111 | // TODO: details here. 112 | return 1 113 | } 114 | o.lock.Lock() 115 | defer o.lock.Unlock() 116 | o.responses[res] = resOut 117 | return 0 118 | } 119 | 120 | func (r *Responses) incomingResponseStatusFn(_ context.Context, mod api.Module, handle uint32) int32 { 121 | response, found := r.GetResponse(handle) 122 | if !found { 123 | log.Printf("Unknown handle: %v", handle) 124 | return 0 125 | } 126 | return int32(response.StatusCode) 127 | } 128 | 129 | func MakeResponses(s *streams.Streams, f *FieldsCollection) *Responses { 130 | return &Responses{responses: map[uint32]*Response{}, baseResponseId: 1, streams: s, fields: f} 131 | } 132 | 133 | func (r *Responses) MakeResponse(res *http.Response) uint32 { 134 | baseResponseId := atomic.AddUint32(&r.baseResponseId, 1) 135 | r.lock.Lock() 136 | defer r.lock.Unlock() 137 | r.responses[baseResponseId] = &Response{res, 0, 0, nil} 138 | return baseResponseId 139 | } 140 | 141 | func (r *Responses) incomingResponseHeadersFn(_ context.Context, mod api.Module, handle uint32) uint32 { 142 | res, found := r.GetResponse(handle) 143 | if !found { 144 | log.Printf("Unknown handle: %v", handle) 145 | return 0 146 | } 147 | if res.HeaderHandle == 0 { 148 | res.HeaderHandle = r.fields.MakeFields(Fields(res.Header)) 149 | } 150 | return res.HeaderHandle 151 | } 152 | 153 | func (r *Responses) incomingResponseConsumeFn(_ context.Context, mod api.Module, handle, ptr uint32) { 154 | response, found := r.GetResponse(handle) 155 | le := binary.LittleEndian 156 | data := []byte{} 157 | if !found { 158 | // 0 == ok, 1 == is_err 159 | data = le.AppendUint32(data, 1) 160 | } else { 161 | // 0 == ok, 1 == is_err 162 | data = le.AppendUint32(data, 0) 163 | stream := r.streams.NewInputStream(response.Body) 164 | // This is the stream number 165 | data = le.AppendUint32(data, stream) 166 | } 167 | mod.Memory().Write(ptr, data) 168 | } 169 | 170 | func futureResponseGetFn(_ context.Context, mod api.Module, handle, ptr uint32) { 171 | le := binary.LittleEndian 172 | data := []byte{} 173 | // 1 == is_some, 0 == none 174 | data = le.AppendUint32(data, 1) 175 | // 0 == ok, 1 == is_err, consistency ftw! 176 | data = le.AppendUint32(data, 0) 177 | // Copy the future into the actual 178 | data = le.AppendUint32(data, handle) 179 | mod.Memory().Write(ptr, data) 180 | } 181 | -------------------------------------------------------------------------------- /imports/wasi_http/types/structs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "sync" 9 | "sync/atomic" 10 | 11 | "github.com/stealthrocket/wasi-go/imports/wasi_http/common" 12 | "github.com/tetratelabs/wazero/api" 13 | ) 14 | 15 | type Fields map[string][]string 16 | type FieldsCollection struct { 17 | lock sync.RWMutex 18 | fields map[uint32]Fields 19 | baseFieldsId uint32 20 | } 21 | 22 | func MakeFields() *FieldsCollection { 23 | return &FieldsCollection{fields: map[uint32]Fields{}, baseFieldsId: 1} 24 | } 25 | 26 | func (f *FieldsCollection) MakeFields(fields Fields) uint32 { 27 | baseFieldsId := atomic.AddUint32(&f.baseFieldsId, 1) 28 | f.lock.Lock() 29 | f.fields[baseFieldsId] = fields 30 | f.lock.Unlock() 31 | return baseFieldsId 32 | } 33 | 34 | func (f *FieldsCollection) GetFields(handle uint32) (Fields, bool) { 35 | f.lock.RLock() 36 | fields, found := f.fields[handle] 37 | f.lock.RUnlock() 38 | return fields, found 39 | } 40 | 41 | func (f *FieldsCollection) DeleteFields(handle uint32) { 42 | f.lock.Lock() 43 | defer f.lock.Unlock() 44 | delete(f.fields, handle) 45 | } 46 | 47 | func (f *FieldsCollection) dropFieldsFn(_ context.Context, handle uint32) { 48 | f.DeleteFields(handle) 49 | } 50 | 51 | func (f *FieldsCollection) newFieldsFn(_ context.Context, mod api.Module, ptr, len uint32) uint32 { 52 | data, ok := mod.Memory().Read(ptr, len*16) 53 | if !ok { 54 | fmt.Println("Error reading fields.") 55 | return 0 56 | } 57 | fields := make(Fields) 58 | for i := uint32(0); i < len; i++ { 59 | key_ptr := binary.LittleEndian.Uint32(data[i*16 : i*16+4]) 60 | key_len := binary.LittleEndian.Uint32(data[i*16+4 : i*16+8]) 61 | key, ok := common.ReadString(mod, key_ptr, key_len) 62 | if !ok { 63 | fmt.Println("Error reading key") 64 | return 0 65 | } 66 | val_ptr := binary.LittleEndian.Uint32(data[i*16+8 : i*16+12]) 67 | val_len := binary.LittleEndian.Uint32(data[i*16+12 : i*16+16]) 68 | val, ok := common.ReadString(mod, val_ptr, val_len) 69 | if !ok { 70 | fmt.Println("Error reading value") 71 | return 0 72 | } 73 | if _, found := fields[key]; !found { 74 | fields[key] = []string{} 75 | } 76 | fields[key] = append(fields[key], val) 77 | } 78 | return f.MakeFields(fields) 79 | } 80 | 81 | func allocateWriteString(ctx context.Context, m api.Module, s string) uint32 { 82 | ptr, err := common.Malloc(ctx, m, uint32(len(s))) 83 | if err != nil { 84 | log.Fatalf(err.Error()) 85 | } 86 | m.Memory().Write(ptr, []byte(s)) 87 | return ptr 88 | } 89 | 90 | func (f *FieldsCollection) fieldsEntriesFn(ctx context.Context, mod api.Module, handle, out_ptr uint32) { 91 | headers, found := f.GetFields(handle) 92 | if !found { 93 | return 94 | } 95 | l := uint32(len(headers)) 96 | // 8 bytes per string/string 97 | ptr, err := common.Malloc(ctx, mod, l*16) 98 | if err != nil { 99 | log.Fatalf(err.Error()) 100 | } 101 | 102 | le := binary.LittleEndian 103 | data := []byte{} 104 | data = le.AppendUint32(data, ptr) 105 | data = le.AppendUint32(data, l) 106 | // write result 107 | mod.Memory().Write(out_ptr, data) 108 | 109 | // ok now allocate and write the strings. 110 | data = []byte{} 111 | for k, v := range headers { 112 | data = le.AppendUint32(data, allocateWriteString(ctx, mod, k)) 113 | data = le.AppendUint32(data, uint32(len(k))) 114 | data = le.AppendUint32(data, allocateWriteString(ctx, mod, v[0])) 115 | data = le.AppendUint32(data, uint32(len(v[0]))) 116 | } 117 | mod.Memory().Write(ptr, data) 118 | } 119 | -------------------------------------------------------------------------------- /internal/descriptor/isatty_darwin.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | // IsATTY is true if the file descriptor fd refers to a valid terminal type device. 6 | func IsATTY(fd int) bool { 7 | _, err := unix.IoctlGetTermios(fd, unix.TIOCGETA) 8 | return err == nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/descriptor/isatty_linux.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import "golang.org/x/sys/unix" 4 | 5 | // IsATTY is true if the file descriptor fd refers to a valid terminal type device. 6 | func IsATTY(fd int) bool { 7 | _, err := unix.IoctlGetTermios(fd, unix.TCGETS) 8 | return err == nil 9 | } 10 | -------------------------------------------------------------------------------- /internal/descriptor/table.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import "math/bits" 4 | 5 | // Table is a data structure mapping 32 bit descriptor to objects. 6 | // 7 | // The data structure optimizes for memory density and lookup performance, 8 | // trading off compute at insertion time. This is a useful compromise for the 9 | // use cases we employ it with: objects are usually accessed a lot more 10 | // often than they are inserted, each operation requires a table lookup so we are 11 | // better off spending extra compute to insert objects in the table in order to 12 | // get cheaper lookups. Memory efficiency is also crucial to support scaling 13 | // with programs that open thousands of objects: having a high or non-linear 14 | // memory-to-item ratio could otherwise be used as an attack vector by malicous 15 | // applications attempting to damage performance of the host. 16 | type Table[Descriptor ~int32 | ~uint32, Object any] struct { 17 | masks []uint64 18 | table []Object 19 | } 20 | 21 | // Len returns the number of objects stored in the table. 22 | func (t *Table[Descriptor, Object]) Len() (n int) { 23 | // We could make this a O(1) operation if we cached the number of objects in 24 | // the table. More state usually means more problems, so until we have a 25 | // clear need for this, the simple implementation may be a better trade off. 26 | for _, mask := range t.masks { 27 | n += bits.OnesCount64(mask) 28 | } 29 | return n 30 | } 31 | 32 | // Grow ensures that t has enough room for n objects, potentially reallocating the 33 | // internal buffers if their capacity was too small to hold this many objects. 34 | func (t *Table[Descriptor, Object]) Grow(n int) { 35 | // Round up to a multiple of 64 since this is the smallest increment due to 36 | // using 64 bits masks. 37 | n = (n*64 + 63) / 64 38 | 39 | if n > len(t.masks) { 40 | masks := make([]uint64, n) 41 | copy(masks, t.masks) 42 | 43 | table := make([]Object, n*64) 44 | copy(table, t.table) 45 | 46 | t.masks = masks 47 | t.table = table 48 | } 49 | } 50 | 51 | // Insert inserts the given object to the table, returning the descriptor that 52 | // it is mapped to. 53 | // 54 | // The method does not perform deduplication, it is possible for the same object 55 | // to be inserted multiple times, each insertion will return a different 56 | // descriptor. 57 | func (t *Table[Descriptor, Object]) Insert(object Object) (desc Descriptor) { 58 | offset := 0 59 | for { 60 | // Note: this loop could be made a lot more efficient using vectorized 61 | // operations: 256 bits vector registers would yield a theoretical 4x 62 | // speed up (e.g. using AVX2). 63 | for index, mask := range t.masks[offset:] { 64 | if ^mask != 0 { // not full? 65 | shift := bits.TrailingZeros64(^mask) 66 | index += offset 67 | desc = Descriptor(index)*64 + Descriptor(shift) 68 | t.table[desc] = object 69 | t.masks[index] = mask | uint64(1<= len(t.table) { 90 | t.Grow(int(desc) + 1) 91 | } 92 | index := uint(desc) / 64 93 | shift := uint(desc) % 64 94 | if (t.masks[index] & (1 << shift)) != 0 { 95 | prev, replaced = t.table[desc], true 96 | } 97 | t.masks[index] |= 1 << shift 98 | t.table[desc] = object 99 | return 100 | } 101 | 102 | // Access returns a pointer to the object associated with the given 103 | // descriptor, which may be nil if it was not found in the table. 104 | func (t *Table[Descriptor, Object]) Access(desc Descriptor) *Object { 105 | if i := int(desc); i >= 0 && i < len(t.table) { 106 | index := uint(desc) / 64 107 | shift := uint(desc) % 64 108 | if (t.masks[index] & (1 << shift)) != 0 { 109 | return &t.table[i] 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | // Lookup returns the object associated with the given descriptor. 116 | func (t *Table[Descriptor, Object]) Lookup(desc Descriptor) (object Object, found bool) { 117 | ptr := t.Access(desc) 118 | if ptr != nil { 119 | object, found = *ptr, true 120 | } 121 | return 122 | } 123 | 124 | // Delete deletes the object stored at the given descriptor from the table. 125 | func (t *Table[Descriptor, Object]) Delete(desc Descriptor) { 126 | if index, shift := desc/64, desc%64; int(index) < len(t.masks) { 127 | mask := t.masks[index] 128 | if (mask & (1 << shift)) != 0 { 129 | var zero Object 130 | t.table[desc] = zero 131 | t.masks[index] = mask & ^uint64(1< EBADF") 52 | debug.PrintStack() 53 | } 54 | } 55 | return err 56 | } 57 | 58 | func socketAddress(network, addr string) (int, syscall.Sockaddr, error) { 59 | switch network { 60 | case "tcp", "tcp4", "tcp6": 61 | default: 62 | return -1, nil, fmt.Errorf("unsupported --Listen network: %v", network) 63 | } 64 | host, portstr, err := net.SplitHostPort(addr) 65 | if err != nil { 66 | return 0, nil, err 67 | } 68 | port, err := net.LookupPort(network, portstr) 69 | if err != nil { 70 | return 0, nil, err 71 | } 72 | var ips []net.IP 73 | if host == "" && network == "tcp6" { 74 | ips = []net.IP{net.IPv6zero} 75 | } else if host == "" { 76 | ips = []net.IP{net.IPv4zero} 77 | } else { 78 | ips, err = net.LookupIP(host) 79 | if err != nil { 80 | return 0, nil, err 81 | } 82 | } 83 | if network == "tcp" || network == "tcp4" { 84 | for _, ip := range ips { 85 | if ipv4 := ip.To4(); ipv4 != nil { 86 | return syscall.AF_INET, &syscall.SockaddrInet4{ 87 | Port: port, 88 | Addr: ([4]byte)(ipv4), 89 | }, nil 90 | } 91 | } 92 | } else if network == "tcp" || network == "tcp6" { 93 | for _, ip := range ips { 94 | if ipv6 := ip.To16(); ipv6 != nil { 95 | return syscall.AF_INET6, &syscall.SockaddrInet6{ 96 | Port: port, 97 | Addr: ([16]byte)(ipv6), 98 | }, nil 99 | } 100 | } 101 | } 102 | return 0, nil, fmt.Errorf("no IPs for network %s and host: %s", network, addr) 103 | } 104 | 105 | func intopt(q url.Values, key string, defaultValue int) int { 106 | values, ok := q[key] 107 | if !ok || len(values) == 0 { 108 | return defaultValue 109 | } 110 | n, err := strconv.Atoi(values[0]) 111 | if err != nil { 112 | return defaultValue 113 | } 114 | return n 115 | } 116 | 117 | func boolopt(q url.Values, key string, defaultValue bool) bool { 118 | values, ok := q[key] 119 | if !ok || len(values) == 0 { 120 | return defaultValue 121 | } 122 | switch values[0] { 123 | case "true", "t", "1", "yes": 124 | return true 125 | case "false", "f", "0", "no": 126 | return false 127 | default: 128 | return defaultValue 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /poll.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | // Subscription is a subscription to an event. 9 | type Subscription struct { 10 | // UserData is a user-provided value that is attached to the subscription 11 | // in the implementation and returned through Event.UserData. 12 | UserData UserData 13 | 14 | // EventType is the type of the event to subscribe to. 15 | EventType EventType 16 | _ [7]byte 17 | 18 | // Variant is the contents of the subscription. 19 | // 20 | // It's a union field; either SubscriptionFDReadWrite or SubscriptionClock. 21 | // Use the Set and Get functions to access and mutate the variant. 22 | variant [32]byte 23 | } 24 | 25 | // UserData is a user-provided value that may be attached to objects that is 26 | // retained when extracted from the implementation. 27 | type UserData uint64 28 | 29 | // MakeSubscriptionFDReadWrite makes a Subscription for FDReadEvent or 30 | // FDWriteEvent events. 31 | func MakeSubscriptionFDReadWrite(userData UserData, eventType EventType, fdrw SubscriptionFDReadWrite) Subscription { 32 | s := Subscription{UserData: userData, EventType: eventType} 33 | s.SetFDReadWrite(fdrw) 34 | return s 35 | } 36 | 37 | // MakeSubscriptionClock makes a Subscription for ClockEvent events. 38 | func MakeSubscriptionClock(userData UserData, c SubscriptionClock) Subscription { 39 | s := Subscription{UserData: userData, EventType: ClockEvent} 40 | s.SetClock(c) 41 | return s 42 | } 43 | 44 | // SetFDReadWrite sets the subscription variant to a SubscriptionFDReadWrite. 45 | func (s *Subscription) SetFDReadWrite(fdrw SubscriptionFDReadWrite) { 46 | variant := (*SubscriptionFDReadWrite)(unsafe.Pointer(&s.variant)) 47 | *variant = fdrw 48 | } 49 | 50 | // GetFDReadWrite gets the embedded SubscriptionFDReadWrite. 51 | func (s *Subscription) GetFDReadWrite() SubscriptionFDReadWrite { 52 | return *(*SubscriptionFDReadWrite)(unsafe.Pointer(&s.variant)) 53 | } 54 | 55 | // SetClock sets the subscription variant to a SubscriptionClock. 56 | func (s *Subscription) SetClock(c SubscriptionClock) { 57 | variant := (*SubscriptionClock)(unsafe.Pointer(&s.variant)) 58 | *variant = c 59 | } 60 | 61 | // GetClock gets the embedded SubscriptionClock. 62 | func (s *Subscription) GetClock() SubscriptionClock { 63 | return *(*SubscriptionClock)(unsafe.Pointer(&s.variant)) 64 | } 65 | 66 | // SubscriptionFDReadWrite is the contents of a subscription when event type 67 | // is FDReadEvent or FDWriteEvent. 68 | type SubscriptionFDReadWrite struct { 69 | // FD is the file descriptor to wait on. 70 | FD FD 71 | } 72 | 73 | // SubscriptionClock is the contents of a subscription when event type is 74 | // ClockEvent. 75 | type SubscriptionClock struct { 76 | // ID is the clock against which to compare the timestamp. 77 | ID ClockID 78 | 79 | // Timeout is the absolute or relative timestamp. 80 | Timeout Timestamp 81 | 82 | // Precision is the amount of time that the implementation may wait 83 | // additionally to coalesce with other events. 84 | Precision Timestamp 85 | 86 | // Flags specify whether the timeout is absolute or relative. 87 | Flags SubscriptionClockFlags 88 | } 89 | 90 | // SubscriptionClockFlags are flags determining how to interpret the timestamp 91 | // provided in SubscriptionClock.Timeout. 92 | type SubscriptionClockFlags uint16 93 | 94 | const ( 95 | // Abstime is a flag indicating that the timestamp provided in 96 | // SubscriptionClock.Timeout is an absolute timestamp of clock 97 | // SubscriptionClock.ID. If unset, treat the timestamp provided in 98 | // SubscriptionClock.Timeout as relative to the current time value of clock 99 | // SubscriptionClock.ID. 100 | Abstime SubscriptionClockFlags = 1 << iota 101 | ) 102 | 103 | // Has is true if the flag is set. 104 | func (flags SubscriptionClockFlags) Has(f SubscriptionClockFlags) bool { 105 | return (flags & f) == f 106 | } 107 | 108 | func (flags SubscriptionClockFlags) String() string { 109 | switch flags { 110 | case Abstime: 111 | return "Abstime" 112 | default: 113 | return fmt.Sprintf("SubscriptionClockFlags(%d)", flags) 114 | } 115 | } 116 | 117 | // Event is an event that occurred. 118 | type Event struct { 119 | // UserData is the user-provided value that got attached to 120 | // Subscription.UserData. 121 | UserData UserData 122 | 123 | // Errno is an error that occurred while processing the subscription 124 | // request. 125 | Errno Errno 126 | 127 | // EventType is the type of event that occurred. 128 | EventType EventType 129 | 130 | // FDReadWrite is the contents of the event, if it is a FDReadEvent or 131 | // FDWriteEvent. ClockEvent events ignore this field. 132 | FDReadWrite EventFDReadWrite 133 | } 134 | 135 | // EventFDReadWrite is the contents of an event when event type is FDReadEvent 136 | // or FDWriteEvent. 137 | type EventFDReadWrite struct { 138 | // NBytes is the number of bytes available for reading or writing. 139 | NBytes FileSize 140 | 141 | // Flags is the state of the file descriptor. 142 | Flags EventFDReadWriteFlags 143 | } 144 | 145 | // EventType is a type of a subscription to an event, or its occurrence. 146 | type EventType uint8 147 | 148 | const ( 149 | // ClockEvent is an event type that indicates that the time value of clock 150 | // SubscriptionClock.ID has reached timestamp SubscriptionClock.Timeout. 151 | ClockEvent EventType = iota 152 | 153 | // FDReadEvent is an event type that indicates that the file descriptor 154 | // SubscriptionFDReadWrite.FD has data available for reading. 155 | FDReadEvent 156 | 157 | // FDWriteEvent is an event type that indicates that the file descriptor 158 | // SubscriptionFDReadWrite.FD has data available for writing. 159 | FDWriteEvent 160 | ) 161 | 162 | func (e EventType) String() string { 163 | switch e { 164 | case ClockEvent: 165 | return "ClockEvent" 166 | case FDReadEvent: 167 | return "FDReadEvent" 168 | case FDWriteEvent: 169 | return "FDWriteEvent" 170 | default: 171 | return fmt.Sprintf("EventType(%d)", e) 172 | } 173 | } 174 | 175 | // EventFDReadWriteFlags is the state of the file descriptor subscribed to with 176 | // FDReadEvent or FDWriteEvent. 177 | type EventFDReadWriteFlags uint16 178 | 179 | const ( 180 | // Hangup is a flag that indicates that the peer of this socket 181 | // has closed or disconnected. 182 | Hangup EventFDReadWriteFlags = 1 << iota 183 | ) 184 | 185 | // Has is true if the flag is set. 186 | func (flags EventFDReadWriteFlags) Has(f EventFDReadWriteFlags) bool { 187 | return (flags & f) == f 188 | } 189 | 190 | func (flags EventFDReadWriteFlags) String() string { 191 | switch flags { 192 | case Hangup: 193 | return "Hangup" 194 | default: 195 | return fmt.Sprintf("EventFDReadWriteFlags(%d)", flags) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /rights.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import "fmt" 4 | 5 | // Rights are file descriptor rights, determining which actions may be performed. 6 | type Rights uint64 7 | 8 | const ( 9 | // FDDataSyncRight is the right to invoke FDDataSync. 10 | // 11 | // If PathOpenRight is set, it includes the right to invoke PathOpen with 12 | // the DSync flag. 13 | FDDataSyncRight Rights = 1 << iota 14 | 15 | // FDReadRight is the right to invoke FDRead and SockRecv. 16 | // 17 | // If FDSeekRight is set, it includes the right to invoke FDPread. 18 | FDReadRight 19 | 20 | // FDSeekRight is the right to invoke FDSeek. This flag implies FDTellRight. 21 | FDSeekRight 22 | 23 | // FDStatSetFlagsRight is the right to invoke FDStatSetFlags. 24 | FDStatSetFlagsRight 25 | 26 | // FDSyncRight is the right to invoke FDSync. 27 | // 28 | // If PathOpenRight is set, it includes the right to invoke PathOpen with 29 | // flags RSync and DSync. 30 | FDSyncRight 31 | 32 | // FDTellRight is the right to invoke FDTell, and the right to invoke 33 | // FDSeek in such a way that the file offset remains unaltered (i.e. 34 | // WhenceCurrent with offset zero). 35 | FDTellRight 36 | 37 | // FDWriteRight is the right to invoke FDWrite and SockSend. 38 | // 39 | // If FDSeekRight is set, it includes the right to invoke FDPwrite. 40 | FDWriteRight 41 | 42 | // FDAdviseRight is the right to invoke FDAdvise. 43 | FDAdviseRight 44 | 45 | // FDAllocateRight is the right to invoke FDAllocate. 46 | FDAllocateRight 47 | 48 | // PathCreateDirectoryRight is the right to invoke PathCreateDirectory. 49 | PathCreateDirectoryRight 50 | 51 | // PathCreateFileRight is (along with PathOpenRight) the right to invoke 52 | // PathOpen with the OpenCreate flag. 53 | PathCreateFileRight 54 | 55 | // PathLinkSourceRight is the right to invoke PathLink with the file 56 | // descriptor as the source directory. 57 | PathLinkSourceRight 58 | 59 | // PathLinkTargetRight is the right to invoke PathLink with the file 60 | // descriptor as the target directory. 61 | PathLinkTargetRight 62 | 63 | // PathOpenRight is the right to invoke PathOpen. 64 | PathOpenRight 65 | 66 | // FDReadDirRight is the right to invoke FDReadDir. 67 | FDReadDirRight 68 | 69 | // PathReadLinkRight is the right to invoke PathReadLink. 70 | PathReadLinkRight 71 | 72 | // PathRenameSourceRight is the right to invoke PathRename with the file 73 | // descriptor as the source directory. 74 | PathRenameSourceRight 75 | 76 | // PathRenameTargetRight is the right to invoke PathRename with the file 77 | // descriptor as the target directory. 78 | PathRenameTargetRight 79 | 80 | // PathFileStatGetRight is the right to invoke PathFileStatGet. 81 | PathFileStatGetRight 82 | 83 | // PathFileStatSetSizeRight is the right to change a file's size. 84 | // 85 | // If PathOpenRight is set, it includes the right to invoke PathOpen with 86 | // the OpenTruncate flag. 87 | // 88 | // Note: there is no function named PathFileStatSetSize. This follows POSIX 89 | // design, which only has ftruncate and does not provide ftruncateat. While 90 | // such function would be desirable from the API design perspective, there 91 | // are virtually no use cases for it since no code written for POSIX 92 | // systems would use it. Moreover, implementing it would require multiple 93 | // syscalls, leading to inferior performance. 94 | PathFileStatSetSizeRight 95 | 96 | // PathFileStatSetTimesRight is the right to invoke PathFileStatSetTimes. 97 | PathFileStatSetTimesRight 98 | 99 | // FDFileStatGetRight is the right to invoke FDFileStatGet. 100 | FDFileStatGetRight 101 | 102 | // FDFileStatSetSizeRight is the right to invoke FDFileStatSetSize. 103 | FDFileStatSetSizeRight 104 | 105 | // FDFileStatSetTimesRight is the right to invoke FDFileStatSetTimes. 106 | FDFileStatSetTimesRight 107 | 108 | // PathSymlinkRight is the right to invoke PathSymlink. 109 | PathSymlinkRight 110 | 111 | // PathRemoveDirectoryRight is the right to invoke PathRemoveDirectory. 112 | PathRemoveDirectoryRight 113 | 114 | // PathUnlinkFileRight is the right to invoke PathUnlinkFile. 115 | PathUnlinkFileRight 116 | 117 | // PollFDReadWriteRight is the right to invoke PollOneOff. 118 | // 119 | // If FDReadWrite is set, it includes the right to invoke PollOneOff with a 120 | // FDReadEvent subscription. If FDWriteWrite is set, it includes the right 121 | // to invoke PollOneOff with a FDWriteEvent subscription. 122 | PollFDReadWriteRight 123 | 124 | // SockShutdownRight is the right to invoke SockShutdown 125 | SockShutdownRight 126 | 127 | // SockAccessRight is the right to invoke SockAccept 128 | SockAcceptRight 129 | 130 | // AllRights is the set of all available rights 131 | AllRights Rights = (1 << 30) - 1 132 | 133 | // ReadRights are rights related to reads. 134 | ReadRights Rights = FDReadRight | FDReadDirRight 135 | 136 | // WriteRights are rights related to writes. 137 | WriteRights Rights = FDWriteRight | FDAllocateRight | PathFileStatSetSizeRight | FDDataSyncRight 138 | 139 | syncRights Rights = FDSyncRight | FDDataSyncRight 140 | seekRights Rights = FDSeekRight | FDTellRight 141 | fileStatRights Rights = FDFileStatGetRight | FDFileStatSetSizeRight | FDFileStatSetTimesRight 142 | pathRights Rights = PathCreateDirectoryRight | PathCreateFileRight | PathLinkSourceRight | PathLinkTargetRight | PathOpenRight | PathReadLinkRight | PathRenameSourceRight | PathRenameTargetRight | PathFileStatGetRight | PathFileStatSetSizeRight | PathFileStatSetTimesRight | PathSymlinkRight | PathRemoveDirectoryRight | PathUnlinkFileRight 143 | 144 | // FileRights are rights related to files. 145 | FileRights Rights = syncRights | seekRights | fileStatRights | FDReadRight | FDStatSetFlagsRight | FDWriteRight | FDAdviseRight | FDAllocateRight | PollFDReadWriteRight 146 | 147 | // DirectoryRights are rights related to directories. 148 | // See https://github.com/WebAssembly/wasi-testsuite/blob/1b1d4a5/tests/rust/src/bin/directory_seek.rs 149 | DirectoryRights Rights = pathRights | syncRights | fileStatRights | FDStatSetFlagsRight | FDReadDirRight 150 | 151 | // TTYRights are rights related to terminals. 152 | // See https://github.com/WebAssembly/wasi-libc/blob/a6f871343/libc-bottom-half/sources/isatty.c 153 | TTYRights = FileRights &^ seekRights 154 | 155 | // SockListenRights are rights for listener sockets. 156 | SockListenRights = SockAcceptRight | PollFDReadWriteRight | FDFileStatGetRight | FDStatSetFlagsRight 157 | 158 | // SockConnectionRights are rights for connection sockets. 159 | SockConnectionRights = FDReadRight | FDWriteRight | PollFDReadWriteRight | SockShutdownRight | FDFileStatGetRight | FDStatSetFlagsRight 160 | ) 161 | 162 | // Has is true if the flag is set. If multiple flags are specified, Has returns 163 | // true if all flags are set. 164 | func (flags Rights) Has(f Rights) bool { 165 | return (flags & f) == f 166 | } 167 | 168 | // HasAny is true if any flag in a set of flags is set. 169 | func (flags Rights) HasAny(f Rights) bool { 170 | return (flags & f) != 0 171 | } 172 | 173 | var rightsStrings = [...]string{ 174 | "FDDataSyncRight", 175 | "FDReadRight", 176 | "FDSeekRight", 177 | "FDStatSetFlagsRight", 178 | "FDSyncRight", 179 | "FDTellRight", 180 | "FDWriteRight", 181 | "FDAdviseRight", 182 | "FDAllocateRight", 183 | "PathCreateDirectoryRight", 184 | "PathCreateFileRight", 185 | "PathLinkSourceRight", 186 | "PathLinkTargetRight", 187 | "PathOpenRight", 188 | "FDReadDirRight", 189 | "PathReadLinkRight", 190 | "PathRenameSourceRight", 191 | "PathRenameTargetRight", 192 | "PathFileStatGetRight", 193 | "PathFileStatSetSizeRight", 194 | "PathFileStatSetTimesRight", 195 | "FDFileStatGetRight", 196 | "FDFileStatSetSizeRight", 197 | "FDFileStatSetTimesRight", 198 | "PathSymlinkRight", 199 | "PathRemoveDirectoryRight", 200 | "PathUnlinkFileRight", 201 | "PollFDReadWriteRight", 202 | "SockShutdownRight", 203 | "SockAcceptRight", 204 | } 205 | 206 | func (flags Rights) String() (s string) { 207 | switch { 208 | case flags == 0: 209 | return "Rights(0)" 210 | case flags.Has(AllRights): 211 | return "AllRights" 212 | case flags == FileRights: 213 | return "FileRights" 214 | case flags == DirectoryRights: 215 | return "DirectoryRights" 216 | case flags == DirectoryRights|FileRights: 217 | return "DirectoryRights|FileRights" 218 | case flags == TTYRights: 219 | return "TTYRights" 220 | case flags == SockListenRights: 221 | return "SockListenRights" 222 | case flags == SockConnectionRights: 223 | return "SockConnectionRights" 224 | case flags == SockConnectionRights|SockListenRights: 225 | return "SockConnectionRights|SockListenRights" 226 | } 227 | for i, name := range rightsStrings { 228 | if !flags.Has(1 << i) { 229 | continue 230 | } 231 | if len(s) > 0 { 232 | s += "|" 233 | } 234 | s += name 235 | } 236 | if len(s) == 0 { 237 | return fmt.Sprintf("Rights(%d)", flags) 238 | } 239 | return 240 | } 241 | -------------------------------------------------------------------------------- /share/go_wasip1_wasm_exec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # go_wasip1_wasm_exec is a script that Go uses to run WASM modules 4 | # compiled with GOOS=wasip1 GOARCH=wasm. 5 | # 6 | # From `go help run`: 7 | # 8 | # If [...] GOOS or GOARCH is different from the system default, and a 9 | # program named go_$GOOS_$GOARCH_exec can be found on the current search 10 | # path, 'go run' invokes the binary using that program. This allows 11 | # execution of cross-compiled programs when a simulator or other execution 12 | # method is available. 13 | # 14 | # To use the script, first ensure wasirun is installed and available in $PATH: 15 | # 16 | # $ go install github.com/stealthrocket/wasi-go/cmd/wasirun@latest 17 | # 18 | # Then, add the directory this script resides in to your $PATH: 19 | # 20 | # $ export PATH="$PATH:/path/to/stealthrocket/wasi-go/share" 21 | # 22 | # This will grant you the ability to run WASM modules using wasirun: 23 | # 24 | # $ GOOS-wasip1 GOARCH=wasm go run ... 25 | # 26 | # Note that there is a similar script in the Go source repository under 27 | # ./misc/wasm, but it only supports the wasmtime and wazero runtimes (and 28 | # their WASI host modules) at this time. 29 | 30 | exec wasirun --dir / --env PWD="$PWD" ${GOWASIRUNTIMEARGS:-} "$1" -- "${@:2}" 31 | -------------------------------------------------------------------------------- /socket_test.go: -------------------------------------------------------------------------------- 1 | package wasi_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stealthrocket/wasi-go" 8 | ) 9 | 10 | func TestInet4AddressMarshalJSON(t *testing.T) { 11 | testMarshalJSON(t, 12 | &wasi.Inet4Address{ 13 | Port: 4242, 14 | Addr: [4]byte{192, 168, 0, 2}, 15 | }, 16 | `"192.168.0.2:4242"`, 17 | ) 18 | } 19 | 20 | func TestInet4AddressMarshalYAML(t *testing.T) { 21 | testMarshalYAML(t, 22 | &wasi.Inet4Address{ 23 | Port: 4242, 24 | Addr: [4]byte{192, 168, 0, 2}, 25 | }, 26 | `192.168.0.2:4242`, 27 | ) 28 | } 29 | 30 | func TestInet6AddressMarshalJSON(t *testing.T) { 31 | testMarshalJSON(t, 32 | &wasi.Inet6Address{ 33 | Port: 4242, 34 | Addr: [16]byte{ 35 | 0x20, 0x01, 36 | 0x0d, 0xb8, 37 | 0x85, 0xa3, 38 | 0x08, 0xd3, 39 | 0x13, 0x19, 40 | 0x8a, 0x2e, 41 | 0x03, 0x70, 42 | 0x73, 0x48, 43 | }, 44 | }, 45 | `"[2001:db8:85a3:8d3:1319:8a2e:370:7348]:4242"`, 46 | ) 47 | } 48 | 49 | func TestInet6AddressMarshalYAML(t *testing.T) { 50 | testMarshalYAML(t, 51 | &wasi.Inet6Address{ 52 | Port: 4242, 53 | Addr: [16]byte{ 54 | 0x20, 0x01, 55 | 0x0d, 0xb8, 56 | 0x85, 0xa3, 57 | 0x08, 0xd3, 58 | 0x13, 0x19, 59 | 0x8a, 0x2e, 60 | 0x03, 0x70, 61 | 0x73, 0x48, 62 | }, 63 | }, 64 | `[2001:db8:85a3:8d3:1319:8a2e:370:7348]:4242`, 65 | ) 66 | } 67 | 68 | func testMarshalJSON(t *testing.T, addr wasi.SocketAddress, want string) { 69 | b, err := addr.(interface{ MarshalJSON() ([]byte, error) }).MarshalJSON() 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | if string(b) != want { 74 | t.Errorf("%q != %q", b, want) 75 | } 76 | } 77 | 78 | func testMarshalYAML(t *testing.T, addr wasi.SocketAddress, want any) { 79 | v, err := addr.(interface{ MarshalYAML() (any, error) }).MarshalYAML() 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | if !reflect.DeepEqual(v, want) { 84 | t.Errorf("%#v != %#v", v, want) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /systems/unix/file.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/stealthrocket/wasi-go" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | type FD int 11 | 12 | func (fd FD) FDAdvise(ctx context.Context, offset, length wasi.FileSize, advice wasi.Advice) wasi.Errno { 13 | err := ignoreEINTR(func() error { return fdadvise(int(fd), int64(offset), int64(length), advice) }) 14 | return makeErrno(err) 15 | } 16 | 17 | func (fd FD) FDAllocate(ctx context.Context, offset, length wasi.FileSize) wasi.Errno { 18 | err := ignoreEINTR(func() error { return fallocate(int(fd), int64(offset), int64(length)) }) 19 | return makeErrno(err) 20 | } 21 | 22 | func (fd FD) FDClose(ctx context.Context) wasi.Errno { 23 | // It's unclear what to do for EINTR on Linux, so do nothing and assume the 24 | // file descriptor has been closed. 25 | // 26 | // See: 27 | // - https://man7.org/linux/man-pages/man2/close.2.html 28 | // - https://lwn.net/Articles/576478/ 29 | err := closeTraceEBADF(int(fd)) 30 | return makeErrno(err) 31 | } 32 | 33 | func (fd FD) FDDataSync(ctx context.Context) wasi.Errno { 34 | err := ignoreEINTR(func() error { return fdatasync(int(fd)) }) 35 | return makeErrno(err) 36 | } 37 | 38 | func (fd FD) FDStatSetFlags(ctx context.Context, flags wasi.FDFlags) wasi.Errno { 39 | fl, err := ignoreEINTR2(func() (int, error) { 40 | return unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0) 41 | }) 42 | if err != nil { 43 | return makeErrno(err) 44 | } 45 | if flags.Has(wasi.Append) { 46 | fl |= unix.O_APPEND 47 | } else { 48 | fl &^= unix.O_APPEND 49 | } 50 | if flags.Has(wasi.NonBlock) { 51 | fl |= unix.O_NONBLOCK 52 | } else { 53 | fl &^= unix.O_NONBLOCK 54 | } 55 | _, err = ignoreEINTR2(func() (int, error) { 56 | return unix.FcntlInt(uintptr(fd), unix.F_SETFL, fl) 57 | }) 58 | return makeErrno(err) 59 | } 60 | 61 | func (fd FD) FDFileStatGet(ctx context.Context) (wasi.FileStat, wasi.Errno) { 62 | var sysStat unix.Stat_t 63 | if err := ignoreEINTR(func() error { return unix.Fstat(int(fd), &sysStat) }); err != nil { 64 | return wasi.FileStat{}, makeErrno(err) 65 | } 66 | stat := makeFileStat(&sysStat) 67 | return stat, wasi.ESUCCESS 68 | } 69 | 70 | func (fd FD) FDFileStatSetSize(ctx context.Context, size wasi.FileSize) wasi.Errno { 71 | err := ignoreEINTR(func() error { return unix.Ftruncate(int(fd), int64(size)) }) 72 | return makeErrno(err) 73 | } 74 | 75 | func (fd FD) FDFileStatSetTimes(ctx context.Context, accessTime, modifyTime wasi.Timestamp, flags wasi.FSTFlags) wasi.Errno { 76 | ts := [2]unix.Timespec{ 77 | {Nsec: __UTIME_OMIT}, 78 | {Nsec: __UTIME_OMIT}, 79 | } 80 | if flags.Has(wasi.AccessTime) { 81 | if flags.Has(wasi.AccessTimeNow) { 82 | ts[0] = unix.Timespec{Nsec: __UTIME_NOW} 83 | } else { 84 | ts[0] = unix.NsecToTimespec(int64(accessTime)) 85 | } 86 | } 87 | if flags.Has(wasi.ModifyTime) { 88 | if flags.Has(wasi.ModifyTimeNow) { 89 | ts[1] = unix.Timespec{Nsec: __UTIME_NOW} 90 | } else { 91 | ts[1] = unix.NsecToTimespec(int64(modifyTime)) 92 | } 93 | } 94 | err := ignoreEINTR(func() error { return futimens(int(fd), &ts) }) 95 | return makeErrno(err) 96 | } 97 | 98 | func (fd FD) FDPread(ctx context.Context, iovecs []wasi.IOVec, offset wasi.FileSize) (wasi.Size, wasi.Errno) { 99 | n, err := handleEINTR(func() (int, error) { return preadv(int(fd), makeIOVecs(iovecs), int64(offset)) }) 100 | return wasi.Size(n), makeErrno(err) 101 | } 102 | 103 | func (fd FD) FDPwrite(ctx context.Context, iovecs []wasi.IOVec, offset wasi.FileSize) (wasi.Size, wasi.Errno) { 104 | n, err := handleEINTR(func() (int, error) { return pwritev(int(fd), makeIOVecs(iovecs), int64(offset)) }) 105 | return wasi.Size(n), makeErrno(err) 106 | } 107 | 108 | func (fd FD) FDRead(ctx context.Context, iovecs []wasi.IOVec) (wasi.Size, wasi.Errno) { 109 | n, err := handleEINTR(func() (int, error) { return readv(int(fd), makeIOVecs(iovecs)) }) 110 | return wasi.Size(n), makeErrno(err) 111 | } 112 | 113 | func (fd FD) FDWrite(ctx context.Context, iovecs []wasi.IOVec) (wasi.Size, wasi.Errno) { 114 | n, err := handleEINTR(func() (int, error) { return writev(int(fd), makeIOVecs(iovecs)) }) 115 | return wasi.Size(n), makeErrno(err) 116 | } 117 | 118 | func (fd FD) FDOpenDir(ctx context.Context) (wasi.Dir, wasi.Errno) { 119 | if _, err := ignoreEINTR2(func() (int64, error) { 120 | return lseek(int(fd), 0, 0) 121 | }); err != nil { 122 | return nil, makeErrno(err) 123 | } 124 | return &dirbuf{fd: int(fd)}, wasi.ESUCCESS 125 | } 126 | 127 | func (fd FD) FDSync(ctx context.Context) wasi.Errno { 128 | err := ignoreEINTR(func() error { return fsync(int(fd)) }) 129 | return makeErrno(err) 130 | } 131 | 132 | func (fd FD) FDSeek(ctx context.Context, delta wasi.FileDelta, whence wasi.Whence) (wasi.FileSize, wasi.Errno) { 133 | var sysWhence int 134 | switch whence { 135 | case wasi.SeekStart: 136 | sysWhence = unix.SEEK_SET 137 | case wasi.SeekCurrent: 138 | sysWhence = unix.SEEK_CUR 139 | case wasi.SeekEnd: 140 | sysWhence = unix.SEEK_END 141 | default: 142 | return 0, wasi.EINVAL 143 | } 144 | off, err := ignoreEINTR2(func() (int64, error) { return lseek(int(fd), int64(delta), sysWhence) }) 145 | return wasi.FileSize(off), makeErrno(err) 146 | } 147 | 148 | func (fd FD) PathCreateDirectory(ctx context.Context, path string) wasi.Errno { 149 | err := ignoreEINTR(func() error { return unix.Mkdirat(int(fd), path, 0755) }) 150 | return makeErrno(err) 151 | } 152 | 153 | func (fd FD) PathFileStatGet(ctx context.Context, flags wasi.LookupFlags, path string) (wasi.FileStat, wasi.Errno) { 154 | var sysStat unix.Stat_t 155 | var sysFlags int 156 | if !flags.Has(wasi.SymlinkFollow) { 157 | sysFlags |= unix.AT_SYMLINK_NOFOLLOW 158 | } 159 | err := ignoreEINTR(func() error { return unix.Fstatat(int(fd), path, &sysStat, sysFlags) }) 160 | return makeFileStat(&sysStat), makeErrno(err) 161 | } 162 | 163 | func (fd FD) PathFileStatSetTimes(ctx context.Context, lookupFlags wasi.LookupFlags, path string, accessTime, modifyTime wasi.Timestamp, fstFlags wasi.FSTFlags) wasi.Errno { 164 | var sysFlags int 165 | if !lookupFlags.Has(wasi.SymlinkFollow) { 166 | sysFlags |= unix.AT_SYMLINK_NOFOLLOW 167 | } 168 | ts := [2]unix.Timespec{ 169 | {Nsec: __UTIME_OMIT}, 170 | {Nsec: __UTIME_OMIT}, 171 | } 172 | if fstFlags.Has(wasi.AccessTime) { 173 | if fstFlags.Has(wasi.AccessTimeNow) { 174 | ts[0] = unix.Timespec{Nsec: __UTIME_NOW} 175 | } else { 176 | ts[0] = unix.NsecToTimespec(int64(accessTime)) 177 | } 178 | } 179 | if fstFlags.Has(wasi.ModifyTime) { 180 | if fstFlags.Has(wasi.ModifyTimeNow) { 181 | ts[1] = unix.Timespec{Nsec: __UTIME_NOW} 182 | } else { 183 | ts[1] = unix.NsecToTimespec(int64(modifyTime)) 184 | } 185 | } 186 | err := ignoreEINTR(func() error { return unix.UtimesNanoAt(int(fd), path, ts[:], sysFlags) }) 187 | return makeErrno(err) 188 | } 189 | 190 | func (fd FD) PathLink(ctx context.Context, flags wasi.LookupFlags, oldPath string, newDir FD, newPath string) wasi.Errno { 191 | var sysFlags int 192 | if flags.Has(wasi.SymlinkFollow) { 193 | sysFlags |= unix.AT_SYMLINK_FOLLOW 194 | } 195 | err := ignoreEINTR(func() error { return unix.Linkat(int(fd), oldPath, int(newDir), newPath, sysFlags) }) 196 | return makeErrno(err) 197 | } 198 | 199 | func (fd FD) PathOpen(ctx context.Context, lookupFlags wasi.LookupFlags, path string, openFlags wasi.OpenFlags, rightsBase, rightsInheriting wasi.Rights, fdFlags wasi.FDFlags) (FD, wasi.Errno) { 200 | oflags := unix.O_CLOEXEC 201 | if openFlags.Has(wasi.OpenDirectory) { 202 | oflags |= unix.O_DIRECTORY 203 | rightsBase &= wasi.DirectoryRights 204 | } 205 | if openFlags.Has(wasi.OpenCreate) { 206 | oflags |= unix.O_CREAT 207 | } 208 | if openFlags.Has(wasi.OpenExclusive) { 209 | oflags |= unix.O_EXCL 210 | } 211 | if openFlags.Has(wasi.OpenTruncate) { 212 | oflags |= unix.O_TRUNC 213 | } 214 | if fdFlags.Has(wasi.Append) { 215 | oflags |= unix.O_APPEND 216 | } 217 | if fdFlags.Has(wasi.DSync) { 218 | oflags |= unix.O_DSYNC 219 | } 220 | if fdFlags.Has(wasi.Sync) { 221 | oflags |= unix.O_SYNC 222 | } 223 | if fdFlags.Has(wasi.RSync) { 224 | // O_RSYNC is not widely supported, and in many cases is an 225 | // alias for O_SYNC. 226 | oflags |= unix.O_SYNC 227 | } 228 | if fdFlags.Has(wasi.NonBlock) { 229 | oflags |= unix.O_NONBLOCK 230 | } 231 | if !lookupFlags.Has(wasi.SymlinkFollow) { 232 | oflags |= unix.O_NOFOLLOW 233 | } 234 | switch { 235 | case openFlags.Has(wasi.OpenDirectory): 236 | oflags |= unix.O_RDONLY 237 | case rightsBase.Has(wasi.FDReadRight) && rightsBase.Has(wasi.FDWriteRight): 238 | oflags |= unix.O_RDWR 239 | case rightsBase.Has(wasi.FDReadRight): 240 | oflags |= unix.O_RDONLY 241 | case rightsBase.Has(wasi.FDWriteRight): 242 | oflags |= unix.O_WRONLY 243 | default: 244 | oflags |= unix.O_RDONLY 245 | } 246 | 247 | mode := uint32(0644) 248 | if (oflags & unix.O_DIRECTORY) != 0 { 249 | mode = 0 250 | } 251 | hostfd, err := ignoreEINTR2(func() (int, error) { 252 | return unix.Openat(int(fd), path, oflags, mode) 253 | }) 254 | return FD(hostfd), makeErrno(err) 255 | } 256 | 257 | func (fd FD) PathReadLink(ctx context.Context, path string, buffer []byte) (int, wasi.Errno) { 258 | n, err := ignoreEINTR2(func() (int, error) { 259 | return unix.Readlinkat(int(fd), path, buffer) 260 | }) 261 | if err != nil { 262 | return n, makeErrno(err) 263 | } else if n == len(buffer) { 264 | return n, wasi.ERANGE 265 | } else { 266 | return n, wasi.ESUCCESS 267 | } 268 | } 269 | 270 | func (fd FD) PathRemoveDirectory(ctx context.Context, path string) wasi.Errno { 271 | err := ignoreEINTR(func() error { return unix.Unlinkat(int(fd), path, unix.AT_REMOVEDIR) }) 272 | return makeErrno(err) 273 | } 274 | 275 | func (fd FD) PathRename(ctx context.Context, oldPath string, newDir FD, newPath string) wasi.Errno { 276 | err := ignoreEINTR(func() error { return unix.Renameat(int(fd), oldPath, int(newDir), newPath) }) 277 | return makeErrno(err) 278 | } 279 | 280 | func (fd FD) PathSymlink(ctx context.Context, oldPath string, newPath string) wasi.Errno { 281 | err := ignoreEINTR(func() error { return unix.Symlinkat(oldPath, int(fd), newPath) }) 282 | return makeErrno(err) 283 | } 284 | 285 | func (fd FD) PathUnlinkFile(ctx context.Context, path string) wasi.Errno { 286 | err := ignoreEINTR(func() error { return unix.Unlinkat(int(fd), path, 0) }) 287 | return makeErrno(err) 288 | } 289 | 290 | func (d *dirbuf) FDReadDir(ctx context.Context, entries []wasi.DirEntry, cookie wasi.DirCookie, bufferSizeBytes int) (int, wasi.Errno) { 291 | n, err := d.readDirEntries(entries, cookie, bufferSizeBytes) 292 | return n, makeErrno(err) 293 | } 294 | 295 | func (d *dirbuf) FDCloseDir(ctx context.Context) wasi.Errno { 296 | return wasi.ESUCCESS 297 | } 298 | -------------------------------------------------------------------------------- /systems/unix/path_open_sockets.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/stealthrocket/wasi-go" 9 | "github.com/stealthrocket/wasi-go/internal/sockets" 10 | ) 11 | 12 | // PathOpenSockets is an extension to WASI preview 1 that adds the ability to 13 | // create TCP sockets. It works by proxying calls to path_open. If fd<0 14 | // and the path is of the form: 15 | // 16 | // :://:[?options=value[&option=value]* 17 | // 18 | // where network is one of "tcp", "tcp4" or "tcp6", and operation is either 19 | // "listen" or "dial", the extension will open a socket that either listens 20 | // on, or connects to, the specified host:port address. Otherwise, the 21 | // extension passes the arguments to the underlying WASI implementation to open 22 | // a file or directory as normal. 23 | // 24 | // The following options are available 25 | // - nonblock=<0|1>: Open the socket in non-blocking mode. Default is 1. 26 | // - nodelay=<0|1>: Set TCP_NODELAY. Default is 1. 27 | // - reuseaddr=<0|1>: Set SO_REUSEADDR. Default is 1. 28 | // - backlog=: Set the listen(2) backlog. Default is 128. 29 | type PathOpenSockets struct{ *System } 30 | 31 | func (p *PathOpenSockets) PathOpen(ctx context.Context, fd wasi.FD, lookupFlags wasi.LookupFlags, path string, openFlags wasi.OpenFlags, rightsBase, rightsInheriting wasi.Rights, fdFlags wasi.FDFlags) (wasi.FD, wasi.Errno) { 32 | addr, op, ok := parseURI(path) 33 | if !ok || fd >= 0 { 34 | return p.System.PathOpen(ctx, fd, lookupFlags, path, openFlags, rightsBase, rightsInheriting, fdFlags) 35 | } 36 | var sockfd int 37 | var err error 38 | switch op { 39 | case "listen": 40 | sockfd, err = sockets.Listen(addr) 41 | case "dial": 42 | sockfd, err = sockets.Dial(addr) 43 | } 44 | errno := wasi.ESUCCESS 45 | if err != nil { 46 | errno = makeErrno(err) 47 | if errno != wasi.EINPROGRESS { 48 | return -1, errno 49 | } 50 | } 51 | return p.Register(FD(sockfd), wasi.FDStat{ 52 | FileType: wasi.SocketStreamType, 53 | Flags: fdFlags, 54 | RightsBase: rightsBase, 55 | RightsInheriting: rightsInheriting, 56 | }), errno 57 | } 58 | 59 | func parseURI(path string) (network string, op string, ok bool) { 60 | u, err := url.Parse(path) 61 | if err != nil { 62 | return 63 | } 64 | network, op, ok = strings.Cut(u.Scheme, "+") 65 | if !ok || (op != "listen" && op != "dial") { 66 | return 67 | } 68 | u.Scheme = network 69 | return u.String(), op, true 70 | } 71 | -------------------------------------------------------------------------------- /systems/unix/readdir_darwin.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "github.com/stealthrocket/wasi-go" 8 | ) 9 | 10 | const sizeOfDirent = 21 11 | 12 | type dirent struct { 13 | ino uint64 14 | seekoff uint64 15 | reclen uint16 16 | namlen uint16 17 | typ uint8 18 | } 19 | 20 | const maxNameLen = 1024 21 | const bufferSize = 4 * maxNameLen // must be greater than sizeOfDirent 22 | 23 | type dirbuf struct { 24 | buffer *[bufferSize]byte 25 | offset int 26 | length int 27 | fd int 28 | cookie wasi.DirCookie 29 | basep uintptr 30 | } 31 | 32 | func (d *dirbuf) readDirEntries(entries []wasi.DirEntry, cookie wasi.DirCookie, bufferSizeBytes int) (int, error) { 33 | if d.buffer == nil { 34 | d.buffer = new([bufferSize]byte) 35 | } 36 | 37 | if cookie < d.cookie { 38 | if _, err := ignoreEINTR2(func() (int64, error) { 39 | return syscall.Seek(d.fd, 0, 0) 40 | }); err != nil { 41 | return 0, err 42 | } 43 | d.offset = 0 44 | d.length = 0 45 | d.cookie = 0 46 | d.basep = 0 47 | } 48 | 49 | numEntries := 0 50 | for { 51 | if numEntries == len(entries) { 52 | return numEntries, nil 53 | } 54 | 55 | if (d.length - d.offset) < sizeOfDirent { 56 | if numEntries > 0 { 57 | return numEntries, nil 58 | } 59 | n, err := ignoreEINTR2(func() (int, error) { 60 | return syscall.Getdirentries(d.fd, d.buffer[:], &d.basep) 61 | }) 62 | if err != nil { 63 | return numEntries, err 64 | } 65 | if n == 0 { 66 | return numEntries, nil 67 | } 68 | d.offset = 0 69 | d.length = n 70 | } 71 | 72 | dirent := (*dirent)(unsafe.Pointer(&d.buffer[d.offset])) 73 | 74 | if (d.offset + int(dirent.reclen)) > d.length { 75 | d.offset = d.length 76 | continue 77 | } 78 | 79 | if dirent.ino == 0 { 80 | d.offset += int(dirent.reclen) 81 | continue 82 | } 83 | 84 | if d.cookie >= cookie { 85 | dirEntry := wasi.DirEntry{ 86 | Next: d.cookie + 1, 87 | INode: wasi.INode(dirent.ino), 88 | } 89 | 90 | switch dirent.typ { 91 | case syscall.DT_BLK: 92 | dirEntry.Type = wasi.BlockDeviceType 93 | case syscall.DT_CHR: 94 | dirEntry.Type = wasi.CharacterDeviceType 95 | case syscall.DT_DIR: 96 | dirEntry.Type = wasi.DirectoryType 97 | case syscall.DT_LNK: 98 | dirEntry.Type = wasi.SymbolicLinkType 99 | case syscall.DT_REG: 100 | dirEntry.Type = wasi.RegularFileType 101 | case syscall.DT_SOCK: 102 | dirEntry.Type = wasi.SocketStreamType 103 | default: // DT_FIFO, DT_WHT, DT_UNKNOWN 104 | dirEntry.Type = wasi.UnknownType 105 | } 106 | 107 | i := d.offset + sizeOfDirent 108 | j := d.offset + sizeOfDirent + int(dirent.namlen) 109 | dirEntry.Name = d.buffer[i:j:j] 110 | 111 | entries[numEntries] = dirEntry 112 | numEntries++ 113 | 114 | bufferSizeBytes -= wasi.SizeOfDirent 115 | bufferSizeBytes -= int(dirent.namlen) 116 | 117 | if bufferSizeBytes <= 0 { 118 | return numEntries, nil 119 | } 120 | } 121 | 122 | d.offset += int(dirent.reclen) 123 | d.cookie += 1 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /systems/unix/readdir_linux.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "bytes" 5 | "unsafe" 6 | 7 | "github.com/stealthrocket/wasi-go" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | const sizeOfDirent = 19 12 | 13 | type dirent struct { 14 | ino uint64 15 | off int64 16 | reclen uint16 17 | typ uint8 18 | } 19 | 20 | const maxNameLen = 1024 21 | const bufferSize = 4 * maxNameLen // must be greater than sizeOfDirent 22 | 23 | type dirbuf struct { 24 | buffer *[bufferSize]byte 25 | offset int 26 | length int 27 | fd int 28 | cookie wasi.DirCookie 29 | } 30 | 31 | func (d *dirbuf) readDirEntries(entries []wasi.DirEntry, cookie wasi.DirCookie, bufferSizeBytes int) (int, error) { 32 | if d.buffer == nil { 33 | d.buffer = new([bufferSize]byte) 34 | } 35 | 36 | if cookie < d.cookie { 37 | if _, err := ignoreEINTR2(func() (int64, error) { 38 | return unix.Seek(d.fd, 0, unix.SEEK_SET) 39 | }); err != nil { 40 | return 0, err 41 | } 42 | d.offset = 0 43 | d.length = 0 44 | d.cookie = 0 45 | } 46 | 47 | numEntries := 0 48 | for { 49 | if numEntries == len(entries) { 50 | return numEntries, nil 51 | } 52 | 53 | if (d.length - d.offset) < sizeOfDirent { 54 | if numEntries > 0 { 55 | return numEntries, nil 56 | } 57 | n, err := ignoreEINTR2(func() (int, error) { 58 | return unix.Getdents(d.fd, d.buffer[:]) 59 | }) 60 | if err != nil { 61 | return numEntries, err 62 | } 63 | if n == 0 { 64 | return numEntries, nil 65 | } 66 | d.offset = 0 67 | d.length = n 68 | } 69 | 70 | dirent := (*dirent)(unsafe.Pointer(&d.buffer[d.offset])) 71 | 72 | if (d.offset + int(dirent.reclen)) > d.length { 73 | d.offset = d.length 74 | continue 75 | } 76 | 77 | if dirent.ino == 0 { 78 | d.offset += int(dirent.reclen) 79 | continue 80 | } 81 | 82 | if d.cookie >= cookie { 83 | dirEntry := wasi.DirEntry{ 84 | Next: d.cookie + 1, 85 | INode: wasi.INode(dirent.ino), 86 | } 87 | 88 | switch dirent.typ { 89 | case unix.DT_BLK: 90 | dirEntry.Type = wasi.BlockDeviceType 91 | case unix.DT_CHR: 92 | dirEntry.Type = wasi.CharacterDeviceType 93 | case unix.DT_DIR: 94 | dirEntry.Type = wasi.DirectoryType 95 | case unix.DT_LNK: 96 | dirEntry.Type = wasi.SymbolicLinkType 97 | case unix.DT_REG: 98 | dirEntry.Type = wasi.RegularFileType 99 | case unix.DT_SOCK: 100 | dirEntry.Type = wasi.SocketStreamType 101 | default: // DT_FIFO, DT_UNKNOWN 102 | dirEntry.Type = wasi.UnknownType 103 | } 104 | 105 | i := d.offset + sizeOfDirent 106 | j := d.offset + int(dirent.reclen) 107 | dirEntry.Name = d.buffer[i:j:j] 108 | 109 | n := bytes.IndexByte(dirEntry.Name, 0) 110 | if n >= 0 { 111 | dirEntry.Name = dirEntry.Name[:n:n] 112 | } 113 | 114 | entries[numEntries] = dirEntry 115 | numEntries++ 116 | 117 | bufferSizeBytes -= wasi.SizeOfDirent 118 | bufferSizeBytes -= len(dirEntry.Name) 119 | 120 | if bufferSizeBytes <= 0 { 121 | return numEntries, nil 122 | } 123 | } 124 | 125 | d.offset += int(dirent.reclen) 126 | d.cookie += 1 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /systems/unix/syscall_darwin.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | 7 | "github.com/stealthrocket/wasi-go" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func accept(socket, flags int) (int, unix.Sockaddr, error) { 12 | conn, addr, err := acceptCloseOnExec(socket) 13 | if err != nil { 14 | return -1, addr, err 15 | } 16 | if (flags & unix.O_NONBLOCK) != 0 { 17 | if err := unix.SetNonblock(conn, true); err != nil { 18 | closeTraceEBADF(conn) 19 | return -1, addr, err 20 | } 21 | } 22 | return conn, addr, nil 23 | } 24 | 25 | func acceptCloseOnExec(socket int) (int, unix.Sockaddr, error) { 26 | syscall.ForkLock.Lock() 27 | defer syscall.ForkLock.Unlock() 28 | // This must only be called on non-blocking sockets or we may prevent 29 | // other goroutines from spawning processes. 30 | conn, addr, err := unix.Accept(socket) 31 | if err != nil { 32 | return -1, addr, err 33 | } 34 | unix.CloseOnExec(conn) 35 | return conn, addr, nil 36 | } 37 | 38 | func pipe(fds []int, flags int) error { 39 | if err := pipeCloseOnExec(fds); err != nil { 40 | return err 41 | } 42 | if (flags & unix.O_NONBLOCK) != 0 { 43 | if err := unix.SetNonblock(fds[1], true); err != nil { 44 | closePipe(fds) 45 | return err 46 | } 47 | if err := unix.SetNonblock(fds[0], true); err != nil { 48 | closePipe(fds) 49 | return err 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | func pipeCloseOnExec(fds []int) error { 56 | syscall.ForkLock.Lock() 57 | defer syscall.ForkLock.Unlock() 58 | 59 | if err := unix.Pipe(fds); err != nil { 60 | return err 61 | } 62 | unix.CloseOnExec(fds[0]) 63 | unix.CloseOnExec(fds[1]) 64 | return nil 65 | } 66 | 67 | func closePipe(fds []int) { 68 | closeTraceEBADF(fds[1]) 69 | closeTraceEBADF(fds[0]) 70 | fds[0] = -1 71 | fds[1] = -1 72 | } 73 | 74 | const ( 75 | __UTIME_NOW = -1 76 | __UTIME_OMIT = -2 77 | ) 78 | 79 | func prepareTimesAndAttrs(ts *[2]unix.Timespec) (attrs, size int, times [2]unix.Timespec) { 80 | const sizeOfTimespec = int(unsafe.Sizeof(times[0])) 81 | i := 0 82 | if ts[1].Nsec != __UTIME_OMIT { 83 | attrs |= unix.ATTR_CMN_MODTIME 84 | times[i] = ts[1] 85 | i++ 86 | } 87 | if ts[0].Nsec != __UTIME_OMIT { 88 | attrs |= unix.ATTR_CMN_ACCTIME 89 | times[i] = ts[0] 90 | i++ 91 | } 92 | return attrs, i * sizeOfTimespec, times 93 | } 94 | 95 | func futimens(fd int, ts *[2]unix.Timespec) error { 96 | attrs, size, times := prepareTimesAndAttrs(ts) 97 | attrlist := unix.Attrlist{ 98 | Bitmapcount: unix.ATTR_BIT_MAP_COUNT, 99 | Commonattr: uint32(attrs), 100 | } 101 | return fsetattrlist(fd, &attrlist, unsafe.Pointer(×), size, 0) 102 | } 103 | 104 | func fsetattrlist(fd int, attrlist *unix.Attrlist, attrbuf unsafe.Pointer, attrbufsize int, options uint32) error { 105 | _, _, e := unix.Syscall6( 106 | uintptr(unix.SYS_FSETATTRLIST), 107 | uintptr(fd), 108 | uintptr(unsafe.Pointer(attrlist)), 109 | uintptr(attrbuf), 110 | uintptr(attrbufsize), 111 | uintptr(options), 112 | uintptr(0), 113 | ) 114 | if e != 0 { 115 | return e 116 | } 117 | return nil 118 | } 119 | 120 | const minIovec = 8 121 | 122 | func appendBytes(vecs []unix.Iovec, bs [][]byte) []unix.Iovec { 123 | for _, b := range bs { 124 | vecs = append(vecs, unix.Iovec{ 125 | Base: unsafe.SliceData(b), 126 | Len: uint64(len(b)), 127 | }) 128 | } 129 | return vecs 130 | } 131 | 132 | func fdadvise(fd int, offset, length int64, advice wasi.Advice) error { 133 | // Since posix_fadvise is not available, just ignore the hint. 134 | return nil 135 | } 136 | 137 | func fallocate(fd int, offset, length int64) error { 138 | var sysStat unix.Stat_t 139 | if err := unix.Fstat(fd, &sysStat); err != nil { 140 | return err 141 | } 142 | if offset != sysStat.Size { 143 | return wasi.ENOSYS 144 | } 145 | err := unix.FcntlFstore(uintptr(fd), unix.F_PREALLOCATE, &unix.Fstore_t{ 146 | Flags: unix.F_ALLOCATEALL | unix.F_ALLOCATECONTIG, 147 | Posmode: unix.F_PEOFPOSMODE, 148 | Offset: 0, 149 | Length: length, 150 | }) 151 | if err != nil { 152 | return err 153 | } 154 | return unix.Ftruncate(fd, sysStat.Size+length) 155 | } 156 | 157 | func fdatasync(fd int) error { 158 | _, _, err := unix.Syscall(unix.SYS_FDATASYNC, uintptr(fd), 0, 0) 159 | if err != 0 { 160 | return err 161 | } 162 | return nil 163 | } 164 | 165 | func fsync(fd int) error { 166 | // See https://twitter.com/TigerBeetleDB/status/1422854887113732097 167 | _, err := unix.FcntlInt(uintptr(fd), unix.F_FULLFSYNC, 0) 168 | return err 169 | } 170 | 171 | func lseek(fd int, offset int64, whence int) (int64, error) { 172 | // Note: there is an issue with unix.Seek where it returns random error 173 | // values for delta >= 2^32-1; syscall.Seek does not appear to suffer from 174 | // this problem, nor does using unix.Syscall directly. 175 | // 176 | // The standard syscall package uses a special syscallX function to call 177 | // lseek, which x/sys/unix does not, here is the reason (copied from 178 | // src/runtime/sys_darwin.go): 179 | // 180 | // The X versions of syscall expect the libc call to return a 64-bit result. 181 | // Otherwise (the non-X version) expects a 32-bit result. 182 | // This distinction is required because an error is indicated by returning -1, 183 | // and we need to know whether to check 32 or 64 bits of the result. 184 | // (Some libc functions that return 32 bits put junk in the upper 32 bits of AX.) 185 | // 186 | // return unix.Seek(f.FD, int64(delta), sysWhence) 187 | return syscall.Seek(fd, offset, whence) 188 | } 189 | 190 | func readv(fd int, iovs [][]byte) (int, error) { 191 | iovecs := make([]unix.Iovec, 0, minIovec) 192 | iovecs = appendBytes(iovecs, iovs) 193 | n, _, err := unix.Syscall( 194 | uintptr(unix.SYS_READV), 195 | uintptr(fd), 196 | uintptr(unsafe.Pointer(unsafe.SliceData(iovecs))), 197 | uintptr(len(iovecs)), 198 | ) 199 | if err != 0 { 200 | return int(n), err 201 | } 202 | return int(n), nil 203 | } 204 | 205 | func writev(fd int, iovs [][]byte) (int, error) { 206 | iovecs := make([]unix.Iovec, 0, minIovec) 207 | iovecs = appendBytes(iovecs, iovs) 208 | n, _, err := unix.Syscall( 209 | uintptr(unix.SYS_WRITEV), 210 | uintptr(fd), 211 | uintptr(unsafe.Pointer(unsafe.SliceData(iovecs))), 212 | uintptr(len(iovecs)), 213 | ) 214 | if err != 0 { 215 | return int(n), err 216 | } 217 | return int(n), nil 218 | } 219 | 220 | func preadv(fd int, iovs [][]byte, offset int64) (int, error) { 221 | read := 0 222 | for _, iov := range iovs { 223 | n, err := unix.Pread(fd, iov, offset) 224 | offset += int64(n) 225 | read += n 226 | if err != nil { 227 | return read, err 228 | } 229 | } 230 | return read, nil 231 | } 232 | 233 | func pwritev(fd int, iovs [][]byte, offset int64) (int, error) { 234 | written := 0 235 | for _, iov := range iovs { 236 | n, err := unix.Pwrite(fd, iov, offset) 237 | offset += int64(n) 238 | written += n 239 | if err != nil { 240 | return written, err 241 | } 242 | } 243 | return written, nil 244 | } 245 | 246 | func getsocketdomain(fd int) (int, error) { 247 | return 0, unix.ENOSYS 248 | } 249 | -------------------------------------------------------------------------------- /systems/unix/syscall_linux.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "unsafe" 5 | 6 | "github.com/stealthrocket/wasi-go" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | const ( 11 | __UTIME_NOW = unix.UTIME_NOW 12 | __UTIME_OMIT = unix.UTIME_OMIT 13 | ) 14 | 15 | func accept(socket, flags int) (int, unix.Sockaddr, error) { 16 | return unix.Accept4(socket, flags|unix.O_CLOEXEC) 17 | } 18 | 19 | func pipe(fds []int, flags int) error { 20 | return unix.Pipe2(fds, flags|unix.O_CLOEXEC) 21 | } 22 | 23 | func futimens(fd int, ts *[2]unix.Timespec) error { 24 | // https://github.com/bminor/glibc/blob/master/sysdeps/unix/sysv/linux/futimens.c 25 | _, _, err := unix.Syscall6( 26 | uintptr(unix.SYS_UTIMENSAT), 27 | uintptr(fd), 28 | uintptr(0), // path=NULL 29 | uintptr(unsafe.Pointer(ts)), 30 | uintptr(0), 31 | uintptr(0), 32 | uintptr(0), 33 | ) 34 | if err != 0 { 35 | return err 36 | } 37 | return nil 38 | } 39 | 40 | func fdadvise(fd int, offset, length int64, advice wasi.Advice) error { 41 | var sysAdvice int 42 | switch advice { 43 | case wasi.Normal: 44 | sysAdvice = unix.FADV_NORMAL 45 | case wasi.Sequential: 46 | sysAdvice = unix.FADV_SEQUENTIAL 47 | case wasi.Random: 48 | sysAdvice = unix.FADV_RANDOM 49 | case wasi.WillNeed: 50 | sysAdvice = unix.FADV_WILLNEED 51 | case wasi.DontNeed: 52 | sysAdvice = unix.FADV_DONTNEED 53 | case wasi.NoReuse: 54 | sysAdvice = unix.FADV_NOREUSE 55 | default: 56 | return wasi.EINVAL 57 | } 58 | return unix.Fadvise(fd, offset, length, sysAdvice) 59 | } 60 | 61 | func fallocate(fd int, offset, length int64) error { 62 | return unix.Fallocate(fd, 0, offset, length) 63 | } 64 | 65 | func fdatasync(fd int) error { 66 | return unix.Fdatasync(fd) 67 | } 68 | 69 | func fsync(fd int) error { 70 | return unix.Fsync(fd) 71 | } 72 | 73 | func lseek(fd int, offset int64, whence int) (int64, error) { 74 | return unix.Seek(fd, offset, whence) 75 | } 76 | 77 | func readv(fd int, iovs [][]byte) (int, error) { 78 | return unix.Readv(fd, iovs) 79 | } 80 | 81 | func writev(fd int, iovs [][]byte) (int, error) { 82 | return unix.Writev(fd, iovs) 83 | } 84 | 85 | func preadv(fd int, iovs [][]byte, offset int64) (int, error) { 86 | return unix.Preadv(fd, iovs, offset) 87 | } 88 | 89 | func pwritev(fd int, iovs [][]byte, offset int64) (int, error) { 90 | return unix.Pwritev(fd, iovs, offset) 91 | } 92 | 93 | func getsocketdomain(fd int) (int, error) { 94 | return unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_DOMAIN) 95 | } 96 | -------------------------------------------------------------------------------- /systems/unix/syscall_unix.go: -------------------------------------------------------------------------------- 1 | package unix 2 | 3 | import ( 4 | "runtime/debug" 5 | "unsafe" 6 | 7 | "github.com/stealthrocket/wasi-go" 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | // This function is used to automtically retry syscalls when they return EINTR 12 | // due to having handled a signal instead of executing. Despite defininig a 13 | // EINTR constant and having proc_raise to trigger signals from the guest, WASI 14 | // does not provide any mechanism for handling signals so masking those errors 15 | // seems like a safer approach to ensure that guest applications will work the 16 | // same regardless of the compiler being used. 17 | func ignoreEINTR(f func() error) error { 18 | for { 19 | if err := f(); err != unix.EINTR { 20 | return err 21 | } 22 | } 23 | } 24 | 25 | func ignoreEINTR2[F func() (R, error), R any](f F) (R, error) { 26 | for { 27 | v, err := f() 28 | if err != unix.EINTR { 29 | return v, err 30 | } 31 | } 32 | } 33 | 34 | // This function is used to handle EINTR returned by syscalls like read(2) 35 | // or write(2). Those syscalls are allowed to transfer partial payloads, 36 | // in which case we silence EINTR and let the caller handle the continuation. 37 | // The syscall is only retried if no data has been transfered at all. 38 | func handleEINTR(f func() (int, error)) (int, error) { 39 | for { 40 | n, err := f() 41 | if err != unix.EINTR { 42 | return n, err 43 | } 44 | if n > 0 { 45 | return n, nil 46 | } 47 | } 48 | } 49 | 50 | func closeTraceEBADF(fd int) error { 51 | if fd < 0 { 52 | return unix.EBADF 53 | } 54 | err := unix.Close(fd) 55 | if err != nil { 56 | if err == unix.EBADF { 57 | println("DEBUG: close", fd, "=> EBADF") 58 | debug.PrintStack() 59 | } 60 | } 61 | return err 62 | } 63 | 64 | func makeErrno(err error) wasi.Errno { 65 | return wasi.MakeErrno(err) 66 | } 67 | 68 | func makeFileStat(s *unix.Stat_t) wasi.FileStat { 69 | return wasi.FileStat{ 70 | FileType: makeFileType(uint32(s.Mode)), 71 | Device: wasi.Device(s.Dev), 72 | INode: wasi.INode(s.Ino), 73 | NLink: wasi.LinkCount(s.Nlink), 74 | Size: wasi.FileSize(s.Size), 75 | AccessTime: wasi.Timestamp(s.Atim.Nano()), 76 | ModifyTime: wasi.Timestamp(s.Mtim.Nano()), 77 | ChangeTime: wasi.Timestamp(s.Ctim.Nano()), 78 | } 79 | } 80 | 81 | func makeFileType(mode uint32) wasi.FileType { 82 | switch mode & unix.S_IFMT { // see stat(2) 83 | case unix.S_IFCHR: // character special 84 | return wasi.CharacterDeviceType 85 | case unix.S_IFDIR: // directory 86 | return wasi.DirectoryType 87 | case unix.S_IFBLK: // block special 88 | return wasi.BlockDeviceType 89 | case unix.S_IFREG: // regular 90 | return wasi.RegularFileType 91 | case unix.S_IFLNK: // symbolic link 92 | return wasi.SymbolicLinkType 93 | case unix.S_IFSOCK: // socket 94 | return wasi.SocketStreamType // or wasi.SocketDGramType? 95 | default: 96 | // e.g. S_IFIFO, S_IFWHT 97 | return wasi.UnknownType 98 | } 99 | } 100 | 101 | var _ []byte = (wasi.IOVec)(nil) 102 | 103 | func makeIOVecs(iovecs []wasi.IOVec) [][]byte { 104 | return *(*[][]byte)(unsafe.Pointer(&iovecs)) 105 | } 106 | -------------------------------------------------------------------------------- /systems/unix/testdata/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/systems/unix/testdata/empty -------------------------------------------------------------------------------- /systems/unix/testdata/message.txt: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | -------------------------------------------------------------------------------- /systems/unix/testdata/tmp/one: -------------------------------------------------------------------------------- 1 | one 2 | -------------------------------------------------------------------------------- /systems/unix/testdata/tmp/three: -------------------------------------------------------------------------------- 1 | three 2 | -------------------------------------------------------------------------------- /systems/unix/testdata/tmp/two: -------------------------------------------------------------------------------- 1 | two 2 | -------------------------------------------------------------------------------- /testdata/adapter.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | import sys 4 | import os 5 | 6 | dir_path = os.path.dirname(os.path.realpath(__file__)) 7 | WASIRUN = os.path.join(dir_path, "..", "wasirun") 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("--version", action="store_true") 11 | parser.add_argument("--test-file", action="store") 12 | parser.add_argument("--arg", action="append", default=[]) 13 | parser.add_argument("--env", action="append", default=[]) 14 | parser.add_argument("--dir", action="append", default=[]) 15 | 16 | args = parser.parse_args() 17 | 18 | if args.version: 19 | subprocess.run([WASIRUN] + ["--version"]) 20 | sys.exit(0) 21 | 22 | TEST_FILE = args.test_file 23 | PROG_ARGS = args.arg 24 | ENV_ARGS = [j for i in args.env for j in ["--env", i]] 25 | DIR_ARGS = [j for i in args.dir for j in ["--dir", i]] 26 | 27 | r = subprocess.run([WASIRUN] + ENV_ARGS + DIR_ARGS + [TEST_FILE] + PROG_ARGS) 28 | sys.exit(r.returncode) 29 | -------------------------------------------------------------------------------- /testdata/c/hello_world.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | puts("Hello World!"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /testdata/c/hello_world.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/c/hello_world.wasm -------------------------------------------------------------------------------- /testdata/c/http/Makefile: -------------------------------------------------------------------------------- 1 | cc := /usr/local/lib/wasi-sdk-20.0/bin/clang 2 | 3 | default: http.wasm 4 | 5 | http.wasm: http.c 6 | ${cc} proxy.c proxy_component_type.o http.c -o http.wasm 7 | -------------------------------------------------------------------------------- /testdata/c/http/README.md: -------------------------------------------------------------------------------- 1 | # HTTP WASI tests 2 | The proxy* files are created using the [`wit-bindgen`](https://github.com/bytecodealliance/wit-bindgen) tool. 3 | 4 | See [here](https://github.com/dev-wasm/dev-wasm-c/blob/main/http/Makefile#L9) for details about how to regenerate. -------------------------------------------------------------------------------- /testdata/c/http/http.c: -------------------------------------------------------------------------------- 1 | #include "proxy.h" 2 | #include 3 | 4 | void http_handle(uint32_t req, uint32_t resp_out) { 5 | } 6 | 7 | int request(uint8_t method_tag, uint8_t scheme_tag, const char * authority_str, const char* path_str, const char* query_str, const char* body) { 8 | types_tuple2_string_string_t content_type[] = {{ 9 | .f0 = { .ptr = "User-agent", .len = 10 }, 10 | .f1 = { .ptr = "WASI-HTTP/0.0.1", .len = 15}, 11 | }, 12 | { 13 | .f0 = { .ptr = "Content-type", .len = 12 }, 14 | .f1 = { .ptr = "application/json", .len = 16}, 15 | }}; 16 | types_list_tuple2_string_string_t headers_list = { 17 | .ptr = &content_type[0], 18 | .len = 2, 19 | }; 20 | types_fields_t headers = types_new_fields(&headers_list); 21 | types_method_t method = { .tag = method_tag }; 22 | types_scheme_t scheme = { .tag = scheme_tag }; 23 | proxy_string_t path, authority, query; 24 | proxy_string_set(&path, path_str); 25 | proxy_string_set(&authority, authority_str); 26 | proxy_string_set(&query, query_str); 27 | 28 | default_outgoing_http_outgoing_request_t req = types_new_outgoing_request(&method, &path, &query, &scheme, &authority, headers); 29 | default_outgoing_http_future_incoming_response_t res; 30 | 31 | if (req == 0) { 32 | printf("Error creating request\n"); 33 | return 4; 34 | } 35 | if (body != NULL) { 36 | types_outgoing_stream_t ret; 37 | if (!types_outgoing_request_write(req, &ret)) { 38 | printf("Error getting output stream\n"); 39 | return 7; 40 | } 41 | streams_list_u8_t buf = { 42 | .ptr = (uint8_t *) body, 43 | .len = strlen(body), 44 | }; 45 | uint64_t ret_val; 46 | if (!streams_write(ret, &buf, &ret_val, NULL)) { 47 | printf("Error writing stream\n"); 48 | return 8; 49 | } 50 | } 51 | 52 | res = default_outgoing_http_handle(req, NULL); 53 | if (res == 0) { 54 | printf("Error sending request\n"); 55 | return 5; 56 | } 57 | 58 | types_result_incoming_response_error_t result; 59 | if (!types_future_incoming_response_get(res, &result)) { 60 | printf("failed to get value for incoming request\n"); 61 | return 1; 62 | } 63 | 64 | if (result.is_err) { 65 | printf("response is error!\n"); 66 | return 2; 67 | } 68 | // poll_drop_pollable(res); 69 | 70 | types_status_code_t code = types_incoming_response_status(result.val.ok); 71 | printf("STATUS: %d\n", code); 72 | 73 | types_headers_t header_handle = types_incoming_response_headers(result.val.ok); 74 | types_list_tuple2_string_string_t header_list; 75 | types_fields_entries(header_handle, &header_list); 76 | 77 | for (int i = 0; i < header_list.len; i++) { 78 | char name[128]; 79 | char value[128]; 80 | strncpy(name, header_list.ptr[i].f0.ptr, header_list.ptr[i].f0.len); 81 | name[header_list.ptr[i].f0.len] = 0; 82 | strncpy(value, header_list.ptr[i].f1.ptr, header_list.ptr[i].f1.len); 83 | value[header_list.ptr[i].f1.len] = 0; 84 | printf("%s: %s\n", name, value); 85 | } 86 | 87 | 88 | types_incoming_stream_t stream; 89 | if (!types_incoming_response_consume(result.val.ok, &stream)) { 90 | printf("stream is error!\n"); 91 | return 3; 92 | } 93 | 94 | printf("Stream is %d\n", stream); 95 | 96 | int32_t len = 64 * 1024; 97 | streams_tuple2_list_u8_bool_t body_res; 98 | streams_stream_error_t err; 99 | if (!streams_read(stream, len, &body_res, &err)) { 100 | printf("BODY read is error!\n"); 101 | return 6; 102 | } 103 | if (body_res.f0.len != strlen("Response")) { 104 | printf("Unexpected body length: %zu\n", body_res.f0.len); 105 | return 9; 106 | } 107 | printf("data from read: %.*s\n", (int) body_res.f0.ptr, body_res.f0.ptr); 108 | streams_tuple2_list_u8_bool_free(&body_res); 109 | 110 | 111 | types_drop_outgoing_request(req); 112 | streams_drop_input_stream(stream); 113 | types_drop_incoming_response(result.val.ok); 114 | 115 | return 0; 116 | } 117 | 118 | int main() { 119 | const char *authority = getenv("SERVER"); 120 | int r = request(TYPES_METHOD_GET, TYPES_SCHEME_HTTP, authority, "/get", "?some=arg&goes=here", NULL); 121 | if (r != 0) { 122 | return r; 123 | } 124 | r = request(TYPES_METHOD_POST, TYPES_SCHEME_HTTP, authority, "/post", "", "{\"foo\": \"bar\"}"); 125 | if (r != 0) { 126 | return r; 127 | } 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /testdata/c/http/http.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/c/http/http.wasm -------------------------------------------------------------------------------- /testdata/c/http/proxy_component_type.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/c/http/proxy_component_type.o -------------------------------------------------------------------------------- /testdata/c/http/server.c: -------------------------------------------------------------------------------- 1 | #include "proxy.h" 2 | #include 3 | #include 4 | 5 | char buffer[1024]; 6 | __attribute__((import_module("types"), import_name("log-it"))) 7 | void _wasm_log(int32_t, int32_t); 8 | 9 | void send_log() { 10 | _wasm_log((int32_t) buffer, strlen(buffer)); 11 | } 12 | 13 | void print_headers(types_headers_t header_handle) { 14 | types_list_tuple2_string_string_t header_list; 15 | types_fields_entries(header_handle, &header_list); 16 | 17 | for (int i = 0; i < header_list.len; i++) { 18 | char name[128]; 19 | char value[128]; 20 | strncpy(name, header_list.ptr[i].f0.ptr, header_list.ptr[i].f0.len); 21 | name[header_list.ptr[i].f0.len] = 0; 22 | strncpy(value, (const char*)header_list.ptr[i].f1.ptr, header_list.ptr[i].f1.len); 23 | value[header_list.ptr[i].f1.len] = 0; 24 | sprintf(buffer, "%s: %s\n", name, value); 25 | send_log(); 26 | } 27 | } 28 | 29 | const char* str_for_method(types_method_t method) { 30 | switch (method.tag) { 31 | case TYPES_METHOD_GET: return "GET"; 32 | case TYPES_METHOD_POST: return "POST"; 33 | case TYPES_METHOD_PUT: return "PUT"; 34 | case TYPES_METHOD_DELETE: return "DELETE"; 35 | case TYPES_METHOD_PATCH: return "PATCH"; 36 | case TYPES_METHOD_HEAD: return "HEAD"; 37 | case TYPES_METHOD_OPTIONS: return "OPTIONS"; 38 | case TYPES_METHOD_CONNECT: return "CONNECT"; 39 | case TYPES_METHOD_TRACE: return "TRACE"; 40 | } 41 | return "unknown"; 42 | } 43 | 44 | static int count = 0; 45 | 46 | void http_handle(uint32_t req, uint32_t res) { 47 | sprintf(buffer, "request: %d\n", req); 48 | send_log(); 49 | proxy_string_t ret; 50 | types_incoming_request_authority(req, &ret); 51 | sprintf(buffer, "authority: %.*s\n", (int) ret.len, ret.ptr); 52 | send_log(); 53 | 54 | proxy_string_t path; 55 | types_incoming_request_path(req, &path); 56 | sprintf(buffer, "path: %.*s\n", (int) path.len, path.ptr); 57 | send_log(); 58 | 59 | types_method_t method; 60 | types_incoming_request_method(req, &method); 61 | sprintf(buffer, "method: %s\n", str_for_method(method)); 62 | send_log(); 63 | 64 | types_headers_t headers = types_incoming_request_headers(req); 65 | print_headers(headers); 66 | 67 | types_tuple2_string_string_t content_type[] = {{ 68 | .f0 = { .ptr = "Server", .len = strlen("Server") }, 69 | .f1 = { .ptr = "WASI-HTTP/0.0.1", .len = 15}, 70 | }, 71 | { 72 | .f0 = { .ptr = "Content-type", .len = 12 }, 73 | .f1 = { .ptr = "text/plain", .len = strlen("text/plain")}, 74 | }}; 75 | types_list_tuple2_string_string_t headers_list = { 76 | .ptr = &content_type[0], 77 | .len = 2, 78 | }; 79 | types_fields_t out_headers = types_new_fields(&headers_list); 80 | sprintf(buffer, "Headers are : %d\n", out_headers); 81 | send_log(); 82 | 83 | types_outgoing_response_t response = types_new_outgoing_response(404, out_headers); 84 | 85 | sprintf(buffer, "Response is : %d\n", response); 86 | send_log(); 87 | 88 | types_result_outgoing_response_error_t res_err = { 89 | .is_err = false, 90 | .val = { 91 | .ok = response, 92 | }, 93 | }; 94 | if (!types_set_response_outparam(res, &res_err)) { 95 | sprintf(buffer, "Failed to set response outparam: %d -> %d\n", res, response); 96 | send_log(); 97 | } 98 | 99 | types_outgoing_stream_t stream; 100 | if (!types_outgoing_response_write(response, &stream)) { 101 | sprintf(buffer, "Failed to get response\n"); 102 | send_log(); 103 | } 104 | 105 | sprintf(buffer, "got response %d\n", stream); 106 | send_log(); 107 | 108 | char buffer[64]; 109 | snprintf(buffer, 64, "Hello from WASM! (%d)", count); 110 | count = count + 1; 111 | const char* body = buffer; 112 | streams_list_u8_t buf = { 113 | .ptr = (uint8_t *) body, 114 | .len = strlen(body), 115 | }; 116 | uint64_t ret_val; 117 | streams_write(stream, &buf, &ret_val, NULL); 118 | 119 | types_drop_outgoing_response(res); 120 | types_drop_fields(out_headers); 121 | } 122 | 123 | int main() { 124 | return 0; 125 | } -------------------------------------------------------------------------------- /testdata/c/http/server.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/c/http/server.wasm -------------------------------------------------------------------------------- /testdata/go/hello_world.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello World!") 7 | } 8 | -------------------------------------------------------------------------------- /testdata/go/hello_world.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/go/hello_world.wasm -------------------------------------------------------------------------------- /testdata/tinygo/hello_world.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("Hello World!") 7 | } 8 | -------------------------------------------------------------------------------- /testdata/tinygo/hello_world.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dispatchrun/wasi-go/038d5104aacbb966c25af43797473f03c5da3e4f/testdata/tinygo/hello_world.wasm -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // Timestamp is a timestamp in nanoseconds. 9 | type Timestamp uint64 10 | 11 | func (t Timestamp) Duration() time.Duration { 12 | return time.Duration(t) 13 | } 14 | 15 | func (t Timestamp) Time() time.Time { 16 | return time.Unix(0, int64(t)).UTC() 17 | } 18 | 19 | func (t Timestamp) String() string { 20 | return t.Time().Format(time.RFC3339Nano) 21 | } 22 | 23 | // ClockID is an identifier for clocks. 24 | type ClockID uint32 25 | 26 | const ( 27 | // Realtime is the clock measuring real time. Time value zero corresponds 28 | // with 1970-01-01T00:00:00Z. 29 | Realtime ClockID = iota 30 | 31 | // Monotonic is the store-wide monotonic clock, which is defined as a clock 32 | // measuring real time, whose value cannot be adjusted and which cannot 33 | // have negative clock jumps. The epoch of this clock is undefined. The 34 | // absolute time value of this clock therefore has no meaning. 35 | Monotonic 36 | 37 | // ProcessCPUTimeID is the CPU-time clock associated with the current 38 | // process. 39 | ProcessCPUTimeID 40 | 41 | // ThreadCPUTimeID is the CPU-time clock associated with the current 42 | // thread. 43 | ThreadCPUTimeID 44 | ) 45 | 46 | func (c ClockID) String() string { 47 | switch c { 48 | case Realtime: 49 | return "Realtime" 50 | case Monotonic: 51 | return "Monotonic" 52 | case ProcessCPUTimeID: 53 | return "ProcessCPUTimeID" 54 | case ThreadCPUTimeID: 55 | return "ThreadCPUTimeID" 56 | default: 57 | return fmt.Sprintf("ClockID(%d)", c) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /wasitest/file.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stealthrocket/wasi-go" 10 | ) 11 | 12 | var file = testSuite{ 13 | "exceeding the limit of open files": testMaxOpenFiles, 14 | "exceeding the limit of open directories": testMaxOpenDirs, 15 | } 16 | 17 | func testMaxOpenFiles(t *testing.T, ctx context.Context, newSystem newSystem) { 18 | tmp := t.TempDir() 19 | sys := newSystem(TestConfig{ 20 | RootFS: tmp, 21 | MaxOpenFiles: 10, 22 | }) 23 | 24 | const rights = wasi.DirectoryRights 25 | 26 | for i := 0; i < 10; i++ { 27 | _, errno := sys.PathOpen(ctx, 3, 0, ".", wasi.OpenDirectory, rights, rights, 0) 28 | if errno == wasi.ENFILE { 29 | break 30 | } 31 | assertEqual(t, errno, wasi.ESUCCESS) 32 | } 33 | 34 | for i := 0; i < 10; i++ { 35 | _, errno := sys.PathOpen(ctx, 3, 0, ".", wasi.OpenDirectory, rights, rights, 0) 36 | assertEqual(t, errno, wasi.ENFILE) 37 | } 38 | } 39 | 40 | func testMaxOpenDirs(t *testing.T, ctx context.Context, newSystem newSystem) { 41 | tmp := t.TempDir() 42 | sys := newSystem(TestConfig{ 43 | RootFS: tmp, 44 | MaxOpenDirs: 10, 45 | }) 46 | 47 | assertOK(t, os.WriteFile(filepath.Join(tmp, "file-1"), []byte("1"), 0666)) 48 | assertOK(t, os.WriteFile(filepath.Join(tmp, "file-2"), []byte("2"), 0666)) 49 | assertOK(t, os.WriteFile(filepath.Join(tmp, "file-3"), []byte("3"), 0666)) 50 | 51 | const rights = wasi.DirectoryRights 52 | 53 | for i := 0; i < 10; i++ { 54 | d, errno := sys.PathOpen(ctx, 3, 0, ".", wasi.OpenDirectory, rights, rights, 0) 55 | assertEqual(t, errno, wasi.ESUCCESS) 56 | 57 | dirEntry := [1]wasi.DirEntry{} 58 | n, errno := sys.FDReadDir(ctx, d, dirEntry[:], 0, 1024) 59 | assertEqual(t, n, 1) 60 | assertEqual(t, errno, wasi.ESUCCESS) 61 | } 62 | 63 | for i := 0; i < 10; i++ { 64 | d, errno := sys.PathOpen(ctx, 3, 0, ".", wasi.OpenDirectory, rights, rights, 0) 65 | assertEqual(t, errno, wasi.ESUCCESS) 66 | 67 | dirEntry := [1]wasi.DirEntry{} 68 | n, errno := sys.FDReadDir(ctx, d, dirEntry[:], 0, 1024) 69 | assertEqual(t, n, 0) 70 | assertEqual(t, errno, wasi.ENFILE) 71 | assertEqual(t, sys.FDClose(ctx, d), wasi.ESUCCESS) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /wasitest/poll.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stealthrocket/wasi-go" 10 | ) 11 | 12 | var poll = testSuite{ 13 | "with no subscriptions returns EINVAL": func(t *testing.T, ctx context.Context, newSystem newSystem) { 14 | sys := newSystem(TestConfig{}) 15 | numEvents, errno := sys.PollOneOff(ctx, nil, nil) 16 | assertEqual(t, errno, wasi.EINVAL) 17 | assertEqual(t, numEvents, 0) 18 | }, 19 | 20 | "an unknown file number sets the event to EBADF": func(t *testing.T, ctx context.Context, newSystem newSystem) { 21 | sys := newSystem(TestConfig{}) 22 | 23 | subs := []wasi.Subscription{ 24 | wasi.MakeSubscriptionFDReadWrite(42, wasi.FDReadEvent, wasi.SubscriptionFDReadWrite{FD: 1234}), 25 | } 26 | evs := make([]wasi.Event, len(subs)) 27 | 28 | numEvents, errno := sys.PollOneOff(ctx, subs, evs) 29 | assertEqual(t, errno, wasi.ESUCCESS) 30 | assertEqual(t, numEvents, 1) 31 | assertEqual(t, evs[0], wasi.Event{ 32 | UserData: 42, 33 | Errno: wasi.EBADF, 34 | EventType: wasi.FDReadEvent, 35 | }) 36 | }, 37 | 38 | "read from stdin": func(t *testing.T, ctx context.Context, newSystem newSystem) { 39 | stdinR, stdinW := io.Pipe() 40 | defer stdinW.Close() 41 | defer stdinR.Close() 42 | 43 | sys := newSystem(TestConfig{ 44 | Stdin: stdinR, 45 | }) 46 | 47 | errno := sys.FDStatSetFlags(ctx, 0, wasi.NonBlock) 48 | assertEqual(t, errno, wasi.ESUCCESS) 49 | 50 | buffer := make([]byte, 32) 51 | n, errno := sys.FDRead(ctx, 0, []wasi.IOVec{buffer}) 52 | assertEqual(t, n, ^wasi.Size(0)) 53 | assertEqual(t, errno, wasi.EAGAIN) 54 | 55 | go func() { 56 | n, err := io.WriteString(stdinW, "Hello, World!") 57 | assertOK(t, err) 58 | assertEqual(t, n, 13) 59 | assertEqual(t, stdinW.Close(), nil) 60 | }() 61 | 62 | subs := []wasi.Subscription{ 63 | wasi.MakeSubscriptionFDReadWrite(42, wasi.FDReadEvent, wasi.SubscriptionFDReadWrite{FD: 0}), 64 | } 65 | evs := make([]wasi.Event, len(subs)) 66 | 67 | numEvents, errno := sys.PollOneOff(ctx, subs, evs) 68 | assertEqual(t, numEvents, 1) 69 | assertEqual(t, errno, wasi.ESUCCESS) 70 | assertEqual(t, evs[0], wasi.Event{ 71 | UserData: 42, 72 | EventType: wasi.FDReadEvent, 73 | }) 74 | 75 | n, errno = sys.FDRead(ctx, 0, []wasi.IOVec{buffer}) 76 | assertEqual(t, errno, wasi.ESUCCESS) 77 | assertEqual(t, n, 13) 78 | assertEqual(t, string(buffer[:n]), "Hello, World!") 79 | }, 80 | 81 | "write to stdout": func(t *testing.T, ctx context.Context, newSystem newSystem) { 82 | stdoutR, stdoutW := io.Pipe() 83 | defer stdoutR.Close() 84 | defer stdoutW.Close() 85 | 86 | ch := make(chan []byte) 87 | defer func() { 88 | for range ch { 89 | } 90 | }() 91 | 92 | go func() { 93 | b, err := io.ReadAll(stdoutR) 94 | assertOK(t, err) 95 | ch <- b 96 | close(ch) 97 | }() 98 | 99 | sys := newSystem(TestConfig{ 100 | Stdout: stdoutW, 101 | }) 102 | 103 | errno := sys.FDStatSetFlags(ctx, 1, wasi.NonBlock) 104 | assertEqual(t, errno, wasi.ESUCCESS) 105 | 106 | n, errno := sys.FDWrite(ctx, 1, []wasi.IOVec{[]byte("Hello, World!")}) 107 | if errno == wasi.ESUCCESS { 108 | assertEqual(t, errno, wasi.ESUCCESS) 109 | assertEqual(t, n, 13) 110 | } else { 111 | assertEqual(t, n, ^wasi.Size(0)) 112 | assertEqual(t, errno, wasi.EAGAIN) 113 | 114 | subs := []wasi.Subscription{ 115 | wasi.MakeSubscriptionFDReadWrite(42, wasi.FDWriteEvent, wasi.SubscriptionFDReadWrite{FD: 1}), 116 | } 117 | evs := make([]wasi.Event, len(subs)) 118 | 119 | numEvents, errno := sys.PollOneOff(ctx, subs, evs) 120 | assertEqual(t, numEvents, 1) 121 | assertEqual(t, errno, wasi.ESUCCESS) 122 | assertEqual(t, evs[0], wasi.Event{ 123 | UserData: 42, 124 | EventType: wasi.FDWriteEvent, 125 | }) 126 | 127 | n, errno = sys.FDWrite(ctx, 1, []wasi.IOVec{[]byte("Hello, World!")}) 128 | assertEqual(t, errno, wasi.ESUCCESS) 129 | assertEqual(t, n, 13) 130 | } 131 | 132 | assertEqual(t, sys.FDClose(ctx, 1), wasi.ESUCCESS) 133 | assertEqual(t, string(<-ch), "Hello, World!") 134 | }, 135 | 136 | "monotonic clock with timeout in the future": testPollTimeout(wasi.Monotonic, futureTimeout), 137 | "realtime clock with timeout in the future": testPollTimeout(wasi.Realtime, futureTimeout), 138 | "process CPU clock with timeout in the future": testPollTimeout(wasi.ProcessCPUTimeID, futureTimeout), 139 | "thread CPU clock with timeout in the future": testPollTimeout(wasi.ThreadCPUTimeID, futureTimeout), 140 | 141 | "monotonic clock with timeout in the past": testPollTimeout(wasi.Monotonic, pastTimeout), 142 | "realtime clock with timeout in the past": testPollTimeout(wasi.Realtime, pastTimeout), 143 | "process CPU clock with timeout in the past": testPollTimeout(wasi.ProcessCPUTimeID, pastTimeout), 144 | "thread CPU clock with timeout in the past": testPollTimeout(wasi.ThreadCPUTimeID, pastTimeout), 145 | 146 | "monotonic clock with deadline in the future": testPollDeadline(wasi.Monotonic, futureTimeout), 147 | "realtime clock with deadline in the future": testPollDeadline(wasi.Realtime, futureTimeout), 148 | "process CPU clock with deadline in the future": testPollDeadline(wasi.ProcessCPUTimeID, futureTimeout), 149 | "thread CPU clock with deadline in the future": testPollDeadline(wasi.ThreadCPUTimeID, futureTimeout), 150 | 151 | "monotonic clock with deadline in the past": testPollDeadline(wasi.Monotonic, pastTimeout), 152 | "realtime clock with deadline in the past": testPollDeadline(wasi.Realtime, pastTimeout), 153 | "process CPU clock with deadline in the past": testPollDeadline(wasi.ProcessCPUTimeID, pastTimeout), 154 | "thread CPU clock with deadline in the past": testPollDeadline(wasi.ThreadCPUTimeID, pastTimeout), 155 | } 156 | 157 | const ( 158 | futureTimeout = 10 * time.Millisecond 159 | pastTimeout = -1 * time.Second // longer absolute value to notice if poll_oneoff waits or blocks 160 | ) 161 | 162 | func testPollTimeout(clock wasi.ClockID, timeout time.Duration) testFunc { 163 | return func(t *testing.T, ctx context.Context, newSystem newSystem) { 164 | sys := newSystem(TestConfig{ 165 | Now: time.Now, 166 | }) 167 | 168 | subs := []wasi.Subscription{ 169 | wasi.MakeSubscriptionClock(42, wasi.SubscriptionClock{ 170 | ID: clock, 171 | Timeout: wasi.Timestamp(timeout), 172 | Precision: wasi.Timestamp(time.Millisecond), 173 | }), 174 | } 175 | evs := make([]wasi.Event, len(subs)) 176 | now := time.Now() 177 | 178 | numEvents, errno := sys.PollOneOff(ctx, subs, evs) 179 | assertEqual(t, errno, wasi.ESUCCESS) 180 | assertEqual(t, numEvents, 1) 181 | if evs[0].Errno == wasi.ENOTSUP { 182 | t.Skip("clock not supported on this system") 183 | } 184 | if elapsed := time.Since(now); elapsed < timeout { 185 | t.Errorf("returned too early: %s < 10ms", elapsed) 186 | } 187 | assertEqual(t, evs[0], wasi.Event{ 188 | UserData: 42, 189 | EventType: wasi.ClockEvent, 190 | }) 191 | } 192 | } 193 | 194 | func testPollDeadline(clock wasi.ClockID, timeout time.Duration) testFunc { 195 | return func(t *testing.T, ctx context.Context, newSystem newSystem) { 196 | sys := newSystem(TestConfig{ 197 | Now: time.Now, 198 | }) 199 | 200 | timestamp, errno := sys.ClockTimeGet(ctx, clock, 1) 201 | switch errno { 202 | case wasi.ESUCCESS: 203 | case wasi.ENOTSUP: 204 | t.Skip("clock not supported on this system") 205 | default: 206 | t.Fatal("ClockTimeGet:", errno) 207 | } 208 | 209 | subs := []wasi.Subscription{ 210 | wasi.MakeSubscriptionClock(42, wasi.SubscriptionClock{ 211 | ID: clock, 212 | Timeout: timestamp + wasi.Timestamp(timeout), 213 | Precision: wasi.Timestamp(time.Millisecond), 214 | Flags: wasi.Abstime, 215 | }), 216 | } 217 | evs := make([]wasi.Event, len(subs)) 218 | now := time.Now() 219 | 220 | numEvents, errno := sys.PollOneOff(ctx, subs, evs) 221 | assertEqual(t, errno, wasi.ESUCCESS) 222 | assertEqual(t, numEvents, 1) 223 | if elapsed := time.Since(now); elapsed < timeout { 224 | t.Errorf("PollOneOff returned too early: %s < 10ms", elapsed) 225 | } 226 | assertEqual(t, evs[0], wasi.Event{ 227 | UserData: 42, 228 | EventType: wasi.ClockEvent, 229 | }) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /wasitest/proc.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stealthrocket/wasi-go" 9 | "github.com/tetratelabs/wazero/sys" 10 | ) 11 | 12 | var proc = testSuite{ 13 | "ProcExit panics with a value of type sys.ExitError": func(t *testing.T, ctx context.Context, newSystem newSystem) { 14 | s := newSystem(TestConfig{}) 15 | 16 | defer func() { 17 | switch v := recover().(type) { 18 | case nil: 19 | t.Error("proc_exit must not return") 20 | case *sys.ExitError: 21 | if exitCode := v.ExitCode(); exitCode != 42 { 22 | t.Errorf("exit error contains the wrong exit code: %d", exitCode) 23 | } 24 | default: 25 | t.Errorf("proc_exit panicked with a value of the wrong type: %T", v) 26 | } 27 | }() 28 | 29 | s.ProcExit(ctx, 42) 30 | }, 31 | 32 | "ProcRaise panics with a value of type sys.ExitError": func(t *testing.T, ctx context.Context, newSystem newSystem) { 33 | s := newSystem(TestConfig{}) 34 | 35 | defer func() { 36 | switch v := recover().(type) { 37 | case nil: 38 | t.Error("proc_raise must not return") 39 | case *sys.ExitError: 40 | if exitCode := v.ExitCode(); exitCode != 127+42 { 41 | t.Errorf("exit error contains the wrong exit code: %d", exitCode) 42 | } 43 | default: 44 | t.Errorf("proc_raise panicked with a value of the wrong type: %T", v) 45 | } 46 | }() 47 | 48 | s.ProcRaise(ctx, 42) 49 | }, 50 | 51 | "SchedYield does nothing": func(t *testing.T, ctx context.Context, newSystem newSystem) { 52 | s := newSystem(TestConfig{}) 53 | assertEqual(t, s.SchedYield(ctx), wasi.ESUCCESS) 54 | }, 55 | 56 | "ArgsSizesGet returns zero when there are no arguments": func(t *testing.T, ctx context.Context, newSystem newSystem) { 57 | s := newSystem(TestConfig{}) 58 | count, bytes, errno := s.ArgsSizesGet(ctx) 59 | assertEqual(t, errno, wasi.ESUCCESS) 60 | assertEqual(t, count, 0) 61 | assertEqual(t, bytes, 0) 62 | }, 63 | 64 | "ArgsSizesGet returns the number of arguments and their size in bytes": func(t *testing.T, ctx context.Context, newSystem newSystem) { 65 | args := []string{ 66 | "hello", 67 | "world", 68 | } 69 | s := newSystem(TestConfig{ 70 | Args: args, 71 | }) 72 | gotCount, gotBytes, errno := s.ArgsSizesGet(ctx) 73 | wantCount, wantBytes := wasi.SizesGet(args) 74 | assertEqual(t, errno, wasi.ESUCCESS) 75 | assertEqual(t, gotCount, wantCount) 76 | assertEqual(t, gotBytes, wantBytes) 77 | }, 78 | 79 | "EnvironSizesGet returns zero when there are no environment variables": func(t *testing.T, ctx context.Context, newSystem newSystem) { 80 | s := newSystem(TestConfig{}) 81 | count, bytes, errno := s.EnvironSizesGet(ctx) 82 | assertEqual(t, errno, wasi.ESUCCESS) 83 | assertEqual(t, count, 0) 84 | assertEqual(t, bytes, 0) 85 | }, 86 | 87 | "EnvironSizesGet returns the number of environment variables and their size in bytes": func(t *testing.T, ctx context.Context, newSystem newSystem) { 88 | environ := []string{ 89 | "hello", 90 | "world", 91 | } 92 | s := newSystem(TestConfig{ 93 | Environ: environ, 94 | }) 95 | gotCount, gotBytes, errno := s.EnvironSizesGet(ctx) 96 | wantCount, wantBytes := wasi.SizesGet(environ) 97 | assertEqual(t, errno, wasi.ESUCCESS) 98 | assertEqual(t, gotCount, wantCount) 99 | assertEqual(t, gotBytes, wantBytes) 100 | }, 101 | 102 | "ClockResGet with an invalid clock id returns EINVAL": func(t *testing.T, ctx context.Context, newSystem newSystem) { 103 | s := newSystem(TestConfig{ 104 | Now: time.Now, 105 | }) 106 | _, errno := s.ClockResGet(ctx, 42) 107 | assertEqual(t, errno, wasi.EINVAL) 108 | }, 109 | 110 | "ClockTimeGet with an invalid clock id returns EINVAL": func(t *testing.T, ctx context.Context, newSystem newSystem) { 111 | s := newSystem(TestConfig{ 112 | Now: time.Now, 113 | }) 114 | _, errno := s.ClockTimeGet(ctx, 42, 0) 115 | assertEqual(t, errno, wasi.EINVAL) 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /wasitest/system.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "slices" 6 | "testing" 7 | 8 | "github.com/stealthrocket/wasi-go" 9 | "golang.org/x/exp/maps" 10 | ) 11 | 12 | // TestSystem is a test suite which validates the behavior of wasi.System 13 | // implementations. 14 | func TestSystem(t *testing.T, makeSystem MakeSystem) { 15 | t.Run("file", file.runFunc(makeSystem)) 16 | t.Run("proc", proc.runFunc(makeSystem)) 17 | t.Run("poll", poll.runFunc(makeSystem)) 18 | t.Run("socket", socket.runFunc(makeSystem)) 19 | } 20 | 21 | type skip string 22 | 23 | func (err skip) Error() string { return string(err) } 24 | 25 | type newSystem func(TestConfig) wasi.System 26 | 27 | type testFunc func(*testing.T, context.Context, newSystem) 28 | 29 | type testSuite map[string]testFunc 30 | 31 | func (tests testSuite) names() []string { 32 | names := maps.Keys(tests) 33 | slices.Sort(names) 34 | return names 35 | } 36 | 37 | func (tests testSuite) runFunc(makeSystem MakeSystem) func(*testing.T) { 38 | return func(t *testing.T) { tests.run(t, makeSystem) } 39 | } 40 | 41 | func (tests testSuite) run(t *testing.T, makeSystem MakeSystem) { 42 | for _, name := range tests.names() { 43 | t.Run(name, func(t *testing.T) { 44 | ctx, cancel := testContext(t) 45 | defer cancel() 46 | 47 | tests[name](t, ctx, func(c TestConfig) wasi.System { 48 | s, err := makeSystem(c) 49 | if err != nil { 50 | t.Fatalf("system initialization failed: %s", err) 51 | } 52 | t.Cleanup(func() { 53 | if err := s.Close(ctx); err != nil { 54 | t.Errorf("system closure failed: %s", err) 55 | } 56 | }) 57 | return s 58 | }) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /wasitest/wasip1.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stealthrocket/wasi-go" 14 | "github.com/stealthrocket/wasi-go/imports/wasi_snapshot_preview1" 15 | "github.com/stealthrocket/wazergo" 16 | "github.com/tetratelabs/wazero" 17 | "github.com/tetratelabs/wazero/sys" 18 | ) 19 | 20 | // TestConfig carries the configuration used to create systems to run the test 21 | // suites against. 22 | type TestConfig struct { 23 | Args []string 24 | Environ []string 25 | Stdin io.ReadCloser 26 | Stdout io.WriteCloser 27 | Stderr io.WriteCloser 28 | Rand io.Reader 29 | RootFS string 30 | Now func() time.Time 31 | // Limits, zero means none. 32 | MaxOpenFiles int 33 | MaxOpenDirs int 34 | } 35 | 36 | // MakeSystem is a function used to create a system to run the test suites 37 | // against. 38 | // 39 | // The test guarantees that the system will be closed when it isn't needed 40 | // anymore. 41 | type MakeSystem func(TestConfig) (wasi.System, error) 42 | 43 | // TestWASIP1 is a generic test suite which runs the list of WebAssembly 44 | // programs passed as file paths, creating a system and runtime to execute 45 | // each of the test programs. 46 | // 47 | // Tests pass if the execution completed without trapping nor calling proc_exit 48 | // with a non-zero exit code. 49 | func TestWASIP1(t *testing.T, filePaths []string, makeSystem MakeSystem) { 50 | if len(filePaths) == 0 { 51 | t.Log("nothing to test") 52 | } 53 | 54 | for _, test := range filePaths { 55 | name := test 56 | for strings.HasPrefix(name, "../") { 57 | name = name[3:] 58 | } 59 | 60 | t.Run(name, func(t *testing.T) { 61 | bytecode, err := os.ReadFile(test) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | stdinR, stdinW, err := os.Pipe() 67 | if err != nil { 68 | t.Fatal("stdin:", err) 69 | } 70 | defer stdinR.Close() 71 | defer stdinW.Close() 72 | 73 | stdoutR, stdoutW, err := os.Pipe() 74 | if err != nil { 75 | t.Fatal("stdout:", err) 76 | } 77 | defer stdoutR.Close() 78 | defer stdoutW.Close() 79 | 80 | stderrR, stderrW, err := os.Pipe() 81 | if err != nil { 82 | t.Fatal("stderr:", err) 83 | } 84 | defer stderrR.Close() 85 | defer stderrW.Close() 86 | 87 | stdinW.Close() // nothing to read on stdin 88 | go io.Copy(os.Stdout, stdoutR) 89 | go io.Copy(os.Stderr, stderrR) 90 | 91 | system, err := makeSystem(TestConfig{ 92 | Args: []string{ 93 | filepath.Base(test), 94 | }, 95 | Environ: []string{ 96 | "PWD=" + filepath.Dir(test), 97 | }, 98 | Stdin: stdinR, 99 | Stdout: stdoutW, 100 | Stderr: stderrW, 101 | Rand: rand.Reader, 102 | Now: time.Now, 103 | RootFS: "/", 104 | }) 105 | if err != nil { 106 | t.Fatal("system:", err) 107 | } 108 | ctx := context.Background() 109 | defer system.Close(ctx) 110 | 111 | runtime := wazero.NewRuntime(ctx) 112 | defer runtime.Close(ctx) 113 | 114 | ctx = wazergo.WithModuleInstance(ctx, 115 | wazergo.MustInstantiate(ctx, runtime, 116 | wasi_snapshot_preview1.NewHostModule(), 117 | wasi_snapshot_preview1.WithWASI(system), 118 | ), 119 | ) 120 | 121 | instance, err := runtime.Instantiate(ctx, bytecode) 122 | if err != nil { 123 | switch e := err.(type) { 124 | case *sys.ExitError: 125 | if exitCode := e.ExitCode(); exitCode != 0 { 126 | t.Error("exit code:", exitCode) 127 | } 128 | default: 129 | t.Error("instantiating wasm module instance:", err) 130 | } 131 | } 132 | if instance != nil { 133 | if err := instance.Close(ctx); err != nil { 134 | t.Error("closing wasm module instance:", err) 135 | } 136 | } 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /wasitest/wasitest.go: -------------------------------------------------------------------------------- 1 | package wasitest 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stealthrocket/wasi-go" 9 | ) 10 | 11 | func testContext(t *testing.T) (context.Context, context.CancelFunc) { 12 | ctx, cancel := context.Background(), func() {} 13 | if deadline, ok := t.Deadline(); ok { 14 | ctx, cancel = context.WithDeadline(ctx, deadline) 15 | } 16 | return ctx, cancel 17 | } 18 | 19 | func assertOK(t *testing.T, err error) { 20 | if err != nil { 21 | t.Helper() 22 | t.Fatalf("unexpected error: %v", err) 23 | } 24 | } 25 | 26 | func assertEqual[T comparable](t *testing.T, got, want T) { 27 | if got != want { 28 | t.Helper() 29 | t.Fatalf("%T values must be equal\nwant = %+v\ngot = %+v", want, want, got) 30 | } 31 | } 32 | 33 | func assertNotEqual[T comparable](t *testing.T, got, want T) { 34 | if got == want { 35 | t.Helper() 36 | t.Fatalf("%T values must not be equal\ndo not want = %+v\ngot = %+v", want, want, got) 37 | } 38 | } 39 | 40 | func assertDeepEqual(t *testing.T, got, want any) { 41 | if !reflect.DeepEqual(got, want) { 42 | t.Helper() 43 | t.Fatalf("%T values must be deep equal\nwant = %+v\ngot = %+v", want, want, got) 44 | } 45 | } 46 | 47 | func skipIfNotImplemented(t *testing.T, errno wasi.Errno) { 48 | if errno == wasi.ENOSYS { 49 | t.Helper() 50 | t.Skip("operation not implemented on this system") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /wazergo.go: -------------------------------------------------------------------------------- 1 | package wasi 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "unsafe" 8 | 9 | "github.com/stealthrocket/wazergo/types" 10 | "github.com/stealthrocket/wazergo/wasm" 11 | "github.com/tetratelabs/wazero/api" 12 | ) 13 | 14 | func (f FDStat) ObjectSize() int { return int(unsafe.Sizeof(FDStat{})) } 15 | func (f FDStat) LoadObject(_ api.Memory, b []byte) FDStat { return unsafeLoad[FDStat](b) } 16 | func (f FDStat) StoreObject(_ api.Memory, b []byte) { unsafeStore(b, f) } 17 | func (f FDStat) FormatObject(w io.Writer, _ api.Memory, b []byte) { formatObject(w, b, f) } 18 | 19 | func (f FileStat) ObjectSize() int { return int(unsafe.Sizeof(FileStat{})) } 20 | func (f FileStat) LoadObject(_ api.Memory, b []byte) FileStat { return unsafeLoad[FileStat](b) } 21 | func (f FileStat) StoreObject(_ api.Memory, b []byte) { unsafeStore(b, f) } 22 | func (f FileStat) FormatObject(w io.Writer, _ api.Memory, b []byte) { formatObject(w, b, f) } 23 | 24 | func (p PreStat) ObjectSize() int { return int(unsafe.Sizeof(PreStat{})) } 25 | func (p PreStat) LoadObject(_ api.Memory, b []byte) PreStat { return unsafeLoad[PreStat](b) } 26 | func (p PreStat) StoreObject(_ api.Memory, b []byte) { unsafeStore(b, p) } 27 | func (p PreStat) FormatObject(w io.Writer, _ api.Memory, b []byte) { formatObject(w, b, p) } 28 | 29 | func (e Event) ObjectSize() int { return int(unsafe.Sizeof(Event{})) } 30 | func (e Event) LoadObject(_ api.Memory, b []byte) Event { return unsafeLoad[Event](b) } 31 | func (e Event) StoreObject(_ api.Memory, b []byte) { unsafeStore(b, e) } 32 | func (e Event) FormatObject(w io.Writer, _ api.Memory, b []byte) { formatObject(w, b, e) } 33 | 34 | func (s Subscription) ObjectSize() int { 35 | return int(unsafe.Sizeof(Subscription{})) 36 | } 37 | 38 | func (s Subscription) LoadObject(_ api.Memory, b []byte) Subscription { 39 | return unsafeLoad[Subscription](b) 40 | } 41 | 42 | func (s Subscription) StoreObject(_ api.Memory, b []byte) { 43 | unsafeStore(b, s) 44 | } 45 | 46 | func (s Subscription) FormatObject(w io.Writer, m api.Memory, b []byte) { 47 | s = s.LoadObject(m, b) 48 | fmt.Fprintf(w, `{UserData:%#016x,EventType:%s,`, s.UserData, s.EventType) 49 | 50 | switch s.EventType { 51 | case ClockEvent: 52 | io.WriteString(w, `SubscriptionClock:`) 53 | s.GetClock().Format(w) 54 | 55 | case FDReadEvent, FDWriteEvent: 56 | io.WriteString(w, `SubscriptionFDReadWrite:`) 57 | s.GetFDReadWrite().Format(w) 58 | 59 | default: 60 | fmt.Fprintf(w, `SubscriptionU:%x`, s.variant) 61 | } 62 | 63 | fmt.Fprintf(w, `}`) 64 | } 65 | 66 | func (arg IOVec) ObjectSize() int { 67 | return 8 68 | } 69 | 70 | func (arg IOVec) LoadObject(memory api.Memory, object []byte) IOVec { 71 | offset := binary.LittleEndian.Uint32(object[:4]) 72 | length := binary.LittleEndian.Uint32(object[4:]) 73 | return wasm.Read(memory, offset, length) 74 | } 75 | 76 | func (arg IOVec) StoreObject(memory api.Memory, object []byte) { 77 | panic("BUG: i/o vectors cannot be stored back to wasm memory") 78 | } 79 | 80 | func (arg IOVec) FormatObject(w io.Writer, memory api.Memory, object []byte) { 81 | types.Bytes(arg.LoadObject(memory, object)).Format(w) 82 | } 83 | 84 | func formatObject[T types.Object[T]](w io.Writer, object []byte, typ T) { 85 | types.Format(w, typ.LoadObject(nil, object)) 86 | } 87 | 88 | func unsafeStore[T types.Object[T]](b []byte, t T) { 89 | types.UnsafeStoreObject(b, t) 90 | } 91 | 92 | func unsafeLoad[T types.Object[T]](b []byte) T { 93 | return types.UnsafeLoadObject[T](b) 94 | } 95 | 96 | func (t Timestamp) Format(w io.Writer) { 97 | io.WriteString(w, t.String()) 98 | } 99 | 100 | func (c SubscriptionFDReadWrite) Format(w io.Writer) { 101 | fmt.Fprintf(w, `{FD:%d}`, c.FD) 102 | } 103 | 104 | func (c SubscriptionClock) Format(w io.Writer) { 105 | var formatTimeout func(Timestamp) string 106 | 107 | switch c.Flags { 108 | case Abstime: 109 | formatTimeout = Timestamp.String 110 | default: 111 | formatTimeout = func(t Timestamp) string { 112 | return t.Duration().String() 113 | } 114 | } 115 | 116 | fmt.Fprintf(w, `{ID:%s,Timeout:%s,Precision:%s}`, 117 | c.ID, 118 | formatTimeout(c.Timeout), 119 | c.Precision.Duration().String(), 120 | ) 121 | } 122 | 123 | var ( 124 | _ types.Formatter = Timestamp(0) 125 | _ types.Formatter = SubscriptionClock{} 126 | ) 127 | --------------------------------------------------------------------------------