Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for wasi-http #56

Merged
merged 13 commits into from
Jul 1, 2023
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ wasirun.src = $(wasi-go.src)
testdata.c.src = $(wildcard testdata/c/*.c)
testdata.c.wasm = $(testdata.c.src:.c=.wasm)

testdata.http.src = $(wildcard testdata/c/http/http*.c)
testdata.http.wasm = $(testdata.http.src:.c=.wasm)

testdata.go.src = $(wildcard testdata/go/*.go)
testdata.go.wasm = $(testdata.go.src:.go=.wasm)

Expand All @@ -18,6 +21,7 @@ testdata.tinygo.wasm = $(testdata.tinygo.src:.go=.wasm)

testdata.files = \
$(testdata.c.wasm) \
$(testdata.http.wasm) \
$(testdata.go.wasm) \
$(testdata.tinygo.wasm)

Expand Down Expand Up @@ -49,7 +53,10 @@ testdata/.sysroot/lib/wasm32-wasi/libc.a: testdata/.wasi-libc

testdata/c/%.c: wasi-libc
testdata/c/%.wasm: testdata/c/%.c
clang $< -o $@ -Wall -Os -target wasm32-unknown-wasi --sysroot testdata/.sysroot
clang $< -o $@ -Wall -Os -target wasm32-unknown-wasi

testdata/c/http/http.wasm: testdata/c/http/http.c
clang $< -o $@ -Wall -Os -target wasm32-unknown-wasi testdata/c/http/proxy.c testdata/c/http/proxy_component_type.o

testdata/go/%.wasm: testdata/go/%.go
GOARCH=wasm GOOS=wasip1 gotip build -o $@ $<
Expand Down
24 changes: 24 additions & 0 deletions cmd/wasirun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/stealthrocket/wasi-go"
"github.com/stealthrocket/wasi-go/imports"
"github.com/stealthrocket/wasi-go/imports/wasi_http"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/sys"
)
Expand Down Expand Up @@ -55,6 +56,10 @@ OPTIONS:
--non-blocking-stdio
Enable non-blocking stdio

--http <MODE>
Optionally enable wasi-http client support and select a
version {none, auto, v1}

-v, --version
Print the version and exit

Expand All @@ -70,6 +75,7 @@ var (
dials stringList
socketExt string
pprofAddr string
wasiHttp string
trace bool
nonBlockingStdio bool
version bool
Expand All @@ -85,6 +91,7 @@ func main() {
flagSet.Var(&dials, "dial", "")
flagSet.StringVar(&socketExt, "sockets", "auto", "")
flagSet.StringVar(&pprofAddr, "pprof-addr", "", "")
flagSet.StringVar(&wasiHttp, "http", "auto", "")
flagSet.BoolVar(&trace, "trace", false, "")
flagSet.BoolVar(&nonBlockingStdio, "non-blocking-stdio", false, "")
flagSet.BoolVar(&version, "version", false, "")
Expand Down Expand Up @@ -158,6 +165,23 @@ func run(wasmFile string, args []string) error {
}
defer system.Close(ctx)

importWasi := false
switch wasiHttp {
case "auto":
importWasi = wasi_http.DetectWasiHttp(wasmModule)
case "v1":
importWasi = true
case "none":
importWasi = false
default:
return fmt.Errorf("invalid value for -http '%v', expected 'auto', 'v1' or 'none'", wasiHttp)
}
if importWasi {
if err := wasi_http.Instantiate(ctx, runtime); err != nil {
return err
}
}

instance, err := runtime.InstantiateModule(ctx, wasmModule, wazero.NewModuleConfig())
if err != nil {
return err
Expand Down
13 changes: 13 additions & 0 deletions imports/wasi_http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# WASI-HTTP
This module implements the [wasi-http](https://github.com/WebAssembly/wasi-http) specification.
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.

You should expect a degree of instability in these interfaces for the foreseeable future.

## Example guest code
There are existing examples of working guest code in the following languages
* [Golang](https://github.com/dev-wasm/dev-wasm-go/tree/main/http)
* [C](https://github.com/dev-wasm/dev-wasm-c/tree/main/http)
* [AssemblyScript](https://github.com/dev-wasm/dev-wasm-ts/tree/main/http)
* [Dotnet](https://github.com/dev-wasm/dev-wasm-ts/tree/main/http)
* [Rust](https://github.com/bytecodealliance/wasmtime/blob/main/crates/test-programs/wasi-http-tests/src/bin/outbound_request.rs)
25 changes: 25 additions & 0 deletions imports/wasi_http/common/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package common

import (
"context"
"log"

"github.com/tetratelabs/wazero/api"
)

func Malloc(ctx context.Context, m api.Module, size uint32) (uint32, error) {
malloc := m.ExportedFunction("cabi_realloc")
result, err := malloc.Call(ctx, 0, 0, 4, uint64(size))
if err != nil {
log.Fatalf(err.Error())
}
return uint32(result[0]), err
}

func ReadString(mod api.Module, ptr, len uint32) (string, bool) {
data, ok := mod.Memory().Read(ptr, len)
if !ok {
return "", false
}
return string(data), true
}
17 changes: 17 additions & 0 deletions imports/wasi_http/default_http/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package default_http

import (
"context"

"github.com/tetratelabs/wazero"
)

const ModuleName = "default-outgoing-HTTP"
achille-roussel marked this conversation as resolved.
Show resolved Hide resolved

func Instantiate(ctx context.Context, r wazero.Runtime) error {
_, err := r.NewHostModuleBuilder(ModuleName).
NewFunctionBuilder().WithFunc(requestFn).Export("request").
NewFunctionBuilder().WithFunc(handleFn).Export("handle").
Instantiate(ctx)
return err
}
30 changes: 30 additions & 0 deletions imports/wasi_http/default_http/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package default_http

import (
"context"
"log"

"github.com/stealthrocket/wasi-go/imports/wasi_http/types"
"github.com/tetratelabs/wazero/api"
)

// Request handles HTTP serving. It's currently unimplemented
func requestFn(_ context.Context, mod api.Module, a, b, c, d, e, f, g, h, j, k, l, m, n, o uint32) int32 {
return 0
}

// Handle handles HTTP client calls.
// The remaining parameters (b..h) are for the HTTP Options, currently unimplemented.
func handleFn(_ context.Context, mod api.Module, request, b, c, d, e, f, g, h uint32) uint32 {
req, ok := types.GetRequest(request)
if !ok {
log.Printf("Failed to get request: %v\n", request)
return 0
}
r, err := req.MakeRequest()
if err != nil {
log.Println(err.Error())
return 0
}
return types.MakeResponse(r)
}
39 changes: 39 additions & 0 deletions imports/wasi_http/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package wasi_http

import (
"context"

"github.com/stealthrocket/wasi-go/imports/wasi_http/default_http"
"github.com/stealthrocket/wasi-go/imports/wasi_http/streams"
"github.com/stealthrocket/wasi-go/imports/wasi_http/types"
"github.com/tetratelabs/wazero"
)

func Instantiate(ctx context.Context, rt wazero.Runtime) error {
if err := types.Instantiate(ctx, rt); err != nil {
return err
}
if err := streams.Instantiate(ctx, rt); err != nil {
return err
}
if err := default_http.Instantiate(ctx, rt); err != nil {
return err
}
return nil
}

func DetectWasiHttp(module wazero.CompiledModule) bool {
functions := module.ImportedFunctions()
hasWasiHttp := false
for _, f := range functions {
moduleName, name, ok := f.Import()
if !ok || moduleName != default_http.ModuleName {
continue
}
switch name {
case "handle":
hasWasiHttp = true
}
}
return hasWasiHttp
}
112 changes: 112 additions & 0 deletions imports/wasi_http/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package wasi_http

import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stealthrocket/wasi-go"
"github.com/stealthrocket/wasi-go/imports"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/sys"
)

type handler struct {
urls []string
}

func (h *handler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(200)
res.Write([]byte("Response"))

h.urls = append(h.urls, req.URL.String())
}

func TestHttp(t *testing.T) {
filePaths, _ := filepath.Glob("../../testdata/c/http/*.wasm")
for _, file := range filePaths {
fmt.Printf("%v\n", file)
}
if len(filePaths) == 0 {
t.Log("nothing to test")
}

h := handler{}
s := &http.Server{
Addr: "127.0.0.1:8080",
Handler: &h,
}
go s.ListenAndServe()
defer s.Shutdown(context.TODO())

expectedPaths := [][]string{
[]string{"/get?some=arg&goes=here"},
}

for testIx, test := range filePaths {
name := test
for strings.HasPrefix(name, "../") {
name = name[3:]
}

t.Run(name, func(t *testing.T) {
bytecode, err := os.ReadFile(test)
if err != nil {
t.Fatal(err)
}

ctx := context.Background()

runtime := wazero.NewRuntime(ctx)
defer runtime.Close(ctx)

builder := imports.NewBuilder().
WithName("http").
WithArgs()
var system wasi.System
ctx, system, err = builder.Instantiate(ctx, runtime)
if err != nil {
t.Error("Failed to build WASI module: ", err)
}
defer system.Close(ctx)

Instantiate(ctx, runtime)

instance, err := runtime.Instantiate(ctx, bytecode)
if err != nil {
switch e := err.(type) {
case *sys.ExitError:
if exitCode := e.ExitCode(); exitCode != 0 {
t.Error("exit code:", exitCode)
}
default:
t.Error("instantiating wasm module instance:", err)
}
}
if instance != nil {
if err := instance.Close(ctx); err != nil {
t.Error("closing wasm module instance:", err)
}
}
ok := true
if len(h.urls) != len(expectedPaths[testIx]) {
ok = false
} else {
for ix := range h.urls {
if h.urls[ix] != expectedPaths[testIx][ix] {
ok = false
break
}
}
}
if !ok {
t.Errorf("Unexpected paths: %v vs %v", h.urls, expectedPaths[testIx])
}
h.urls = []string{}
})
}
}
42 changes: 42 additions & 0 deletions imports/wasi_http/streams/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package streams

import (
"context"
"encoding/binary"
"log"

"github.com/stealthrocket/wasi-go/imports/wasi_http/common"
"github.com/tetratelabs/wazero/api"
)

func streamReadFn(ctx context.Context, mod api.Module, stream_handle uint32, length uint64, out_ptr uint32) {
data := make([]byte, length)
_, _, err := Streams.Read(stream_handle, data)

// data, err := types.ResponseBody()
if err != nil {
log.Fatalf(err.Error())
}

ptr_len := uint32(len(data)) + 1
data = append(data, 0)
ptr, err := common.Malloc(ctx, mod, ptr_len)
if err != nil {
log.Fatalf(err.Error())
}
mod.Memory().Write(ptr, data)

data = []byte{}
// 0 == is_ok, 1 == is_err
le := binary.LittleEndian
data = le.AppendUint32(data, 0)
data = le.AppendUint32(data, ptr)
data = le.AppendUint32(data, ptr_len)
// No more data to read.
data = le.AppendUint32(data, 0)
mod.Memory().Write(out_ptr, data)
}

func dropInputStreamFn(_ context.Context, mod api.Module, stream uint32) {
Streams.DeleteStream(stream)
}
Loading
Loading