diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index abb648f908..812f054711 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -21,7 +21,10 @@ import ( "github.com/tetratelabs/wazero/experimental/gojs" "github.com/tetratelabs/wazero/experimental/logging" "github.com/tetratelabs/wazero/experimental/sock" + "github.com/tetratelabs/wazero/imports/default_http" + "github.com/tetratelabs/wazero/imports/wasi_http" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "github.com/tetratelabs/wazero/imports/wasi_streams" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/version" "github.com/tetratelabs/wazero/sys" @@ -324,6 +327,9 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { switch detectImports(code.ImportedFunctions()) { case modeWasi: wasi_snapshot_preview1.MustInstantiate(ctx, rt) + wasi_streams.MustInstantiate(ctx, rt) + wasi_http.MustInstantiate(ctx, rt) + default_http.MustInstantiate(ctx, rt) _, err = rt.InstantiateModule(ctx, code, conf) case modeWasiUnstable: // Instantiate the current WASI functions under the wasi_unstable diff --git a/imports/default_http/http.go b/imports/default_http/http.go new file mode 100644 index 0000000000..cd08096e31 --- /dev/null +++ b/imports/default_http/http.go @@ -0,0 +1,181 @@ +package default_http + +import ( + "context" + "encoding/binary" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasip1" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// ModuleName is the module name WASI functions are exported into. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md +const ModuleName = "default-outgoing-HTTP" + +const i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 + +var le = binary.LittleEndian + +// MustInstantiate calls Instantiate or panics on error. +// +// This is a simpler function for those who know the module ModuleName is not +// already instantiated, and don't need to unload it. +func MustInstantiate(ctx context.Context, r wazero.Runtime) { + if _, err := Instantiate(ctx, r); err != nil { + panic(err) + } +} + +// Instantiate instantiates the ModuleName module into the runtime. +// +// # Notes +// +// - Failure cases are documented on wazero.Runtime InstantiateModule. +// - Closing the wazero.Runtime has the same effect as closing the result. +func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { + return NewBuilder(r).Instantiate(ctx) +} + +// Builder configures the ModuleName module for later use via Compile or Instantiate. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type Builder interface { + // Compile compiles the ModuleName module. Call this before Instantiate. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Compile(context.Context) (wazero.CompiledModule, error) + + // Instantiate instantiates the ModuleName module and returns a function to close it. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Instantiate(context.Context) (api.Closer, error) +} + +// NewBuilder returns a new Builder. +func NewBuilder(r wazero.Runtime) Builder { + return &builder{r} +} + +type builder struct{ r wazero.Runtime } + +// hostModuleBuilder returns a new wazero.HostModuleBuilder for ModuleName +func (b *builder) hostModuleBuilder() wazero.HostModuleBuilder { + ret := b.r.NewHostModuleBuilder(ModuleName) + exportFunctions(ret) + return ret +} + +// Compile implements Builder.Compile +func (b *builder) Compile(ctx context.Context) (wazero.CompiledModule, error) { + return b.hostModuleBuilder().Compile(ctx) +} + +// Instantiate implements Builder.Instantiate +func (b *builder) Instantiate(ctx context.Context) (api.Closer, error) { + return b.hostModuleBuilder().Instantiate(ctx) +} + +// FunctionExporter exports functions into a wazero.HostModuleBuilder. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type FunctionExporter interface { + ExportFunctions(wazero.HostModuleBuilder) +} + +func NewFunctionExporter() FunctionExporter { + return &functionExporter{} +} + +type functionExporter struct{} + +// ExportFunctions implements FunctionExporter.ExportFunctions +func (functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) { + exportFunctions(builder) +} + +func exportFunctions(builder wazero.HostModuleBuilder) { + exporter := builder.(wasm.HostFuncExporter) + + exporter.ExportHostFunc(request) + exporter.ExportHostFunc(handle) +} + +func newHostFunc( + name string, + goFunc wasiFunc, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +func newHostMethod( + name string, + goFunc wasiMethod, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{}, + ResultNames: []string{}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +// wasiFunc special cases that all WASI functions return a single Errno +// result. The returned value will be written back to the stack at index zero. +type wasiFunc func(ctx context.Context, mod api.Module, params []uint64) int32 + +// Call implements the same method as documented on api.GoModuleFunction. +func (f wasiFunc) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + errno := f(ctx, mod, stack) + if errno != 0 { + stack[0] = uint64(errno) + } else { // special case ass ErrnoSuccess is zero + stack[0] = 0 + } +} + +type wasiMethod func(ctx context.Context, mod api.Module, params []uint64) + +func (f wasiMethod) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + f(ctx, mod, stack) +} + +// stubFunction stubs for GrainLang per #271. +func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{ + GoFunc: api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = uint64(wasip1.ErrnoNosys) }), + }, + } +} diff --git a/imports/default_http/request.go b/imports/default_http/request.go new file mode 100644 index 0000000000..3fb3e93cc2 --- /dev/null +++ b/imports/default_http/request.go @@ -0,0 +1,31 @@ +package default_http + +import ( + "context" + "log" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_http" +) + +var request = newHostFunc("request", requestFn, + []api.ValueType{i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32}, + "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "l", "m", "n", "o") + +func requestFn(_ context.Context, mod api.Module, params []uint64) int32 { + return 0 +} + +var handle = newHostFunc("handle", handleFn, + []api.ValueType{i32, i32, i32, i32, i32, i32, i32, i32}, + "a", "b", "c", "d", "e", "f", "g", "h") + +func handleFn(_ context.Context, mod api.Module, params []uint64) int32 { + r, err := wasi_http.MakeRequest() + if err != nil { + log.Printf(err.Error()) + return 0 + } + wasi_http.SetResponse(r) + return 1 +} diff --git a/imports/wasi_http/http.go b/imports/wasi_http/http.go new file mode 100644 index 0000000000..c1883da760 --- /dev/null +++ b/imports/wasi_http/http.go @@ -0,0 +1,240 @@ +package wasi_http + +import ( + "context" + "encoding/binary" + "log" + "syscall" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasip1" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// ModuleName is the module name WASI functions are exported into. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md +const ModuleName = "types" + +const i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 + +var le = binary.LittleEndian + +// MustInstantiate calls Instantiate or panics on error. +// +// This is a simpler function for those who know the module ModuleName is not +// already instantiated, and don't need to unload it. +func MustInstantiate(ctx context.Context, r wazero.Runtime) { + if _, err := Instantiate(ctx, r); err != nil { + panic(err) + } +} + +// Instantiate instantiates the ModuleName module into the runtime. +// +// # Notes +// +// - Failure cases are documented on wazero.Runtime InstantiateModule. +// - Closing the wazero.Runtime has the same effect as closing the result. +func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { + return NewBuilder(r).Instantiate(ctx) +} + +// Builder configures the ModuleName module for later use via Compile or Instantiate. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type Builder interface { + // Compile compiles the ModuleName module. Call this before Instantiate. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Compile(context.Context) (wazero.CompiledModule, error) + + // Instantiate instantiates the ModuleName module and returns a function to close it. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Instantiate(context.Context) (api.Closer, error) +} + +// NewBuilder returns a new Builder. +func NewBuilder(r wazero.Runtime) Builder { + return &builder{r} +} + +type builder struct{ r wazero.Runtime } + +// hostModuleBuilder returns a new wazero.HostModuleBuilder for ModuleName +func (b *builder) hostModuleBuilder() wazero.HostModuleBuilder { + ret := b.r.NewHostModuleBuilder(ModuleName) + exportFunctions(ret) + return ret +} + +// Compile implements Builder.Compile +func (b *builder) Compile(ctx context.Context) (wazero.CompiledModule, error) { + return b.hostModuleBuilder().Compile(ctx) +} + +// Instantiate implements Builder.Instantiate +func (b *builder) Instantiate(ctx context.Context) (api.Closer, error) { + return b.hostModuleBuilder().Instantiate(ctx) +} + +// FunctionExporter exports functions into a wazero.HostModuleBuilder. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type FunctionExporter interface { + ExportFunctions(wazero.HostModuleBuilder) +} + +func NewFunctionExporter() FunctionExporter { + return &functionExporter{} +} + +type functionExporter struct{} + +// ExportFunctions implements FunctionExporter.ExportFunctions +func (functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) { + exportFunctions(builder) +} + +func exportFunctions(builder wazero.HostModuleBuilder) { + exporter := builder.(wasm.HostFuncExporter) + + exporter.ExportHostFunc(newOutgoingRequest) + exporter.ExportHostFunc(newFields) + exporter.ExportHostFunc(fieldsEntries) + exporter.ExportHostFunc(dropOutgoingRequest) + exporter.ExportHostFunc(outgoingRequestWrite) + exporter.ExportHostFunc(dropIncomingResponse) + exporter.ExportHostFunc(incomingResponseStatus) + exporter.ExportHostFunc(incomingResponseHeaders) + exporter.ExportHostFunc(incomingResponseConsume) + exporter.ExportHostFunc(futureResponseGet) +} + +// writeOffsetsAndNullTerminatedValues is used to write NUL-terminated values +// for args or environ, given a pre-defined bytesLen (which includes NUL +// terminators). +func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offsets, bytes, bytesLen uint32) syscall.Errno { + // The caller may not place bytes directly after offsets, so we have to + // read them independently. + valuesLen := len(values) + offsetsLen := uint32(valuesLen * 4) // uint32Le + offsetsBuf, ok := mem.Read(offsets, offsetsLen) + if !ok { + return syscall.EFAULT + } + bytesBuf, ok := mem.Read(bytes, bytesLen) + if !ok { + return syscall.EFAULT + } + + // Loop through the values, first writing the location of its data to + // offsetsBuf[oI], then its NUL-terminated data at bytesBuf[bI] + var oI, bI uint32 + for _, value := range values { + // Go can't guarantee inlining as there's not //go:inline directive. + // This inlines uint32 little-endian encoding instead. + bytesOffset := bytes + bI + offsetsBuf[oI] = byte(bytesOffset) + offsetsBuf[oI+1] = byte(bytesOffset >> 8) + offsetsBuf[oI+2] = byte(bytesOffset >> 16) + offsetsBuf[oI+3] = byte(bytesOffset >> 24) + oI += 4 // size of uint32 we just wrote + + // Write the next value to memory with a NUL terminator + copy(bytesBuf[bI:], value) + bI += uint32(len(value)) + bytesBuf[bI] = 0 // NUL terminator + bI++ + } + + return 0 +} + +func newHostFunc( + name string, + goFunc wasiFunc, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +func newHostMethod( + name string, + goFunc wasiMethod, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{}, + ResultNames: []string{}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +// wasiFunc special cases that all WASI functions return a single Errno +// result. The returned value will be written back to the stack at index zero. +type wasiFunc func(ctx context.Context, mod api.Module, params []uint64) int32 + +// Call implements the same method as documented on api.GoModuleFunction. +func (f wasiFunc) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + errno := f(ctx, mod, stack) + if errno != 0 { + stack[0] = uint64(errno) + } else { // special case ass ErrnoSuccess is zero + stack[0] = 0 + } +} + +type wasiMethod func(ctx context.Context, mod api.Module, params []uint64) + +func (f wasiMethod) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + f(ctx, mod, stack) +} + +// stubFunction stubs for GrainLang per #271. +func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{ + GoFunc: api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = uint64(wasip1.ErrnoNosys) }), + }, + } +} + +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 +} diff --git a/imports/wasi_http/request.go b/imports/wasi_http/request.go new file mode 100644 index 0000000000..84fccdf1ef --- /dev/null +++ b/imports/wasi_http/request.go @@ -0,0 +1,115 @@ +package wasi_http + +import ( + "bytes" + "context" + "fmt" + "io" + "log" + "net/http" + + "github.com/tetratelabs/wazero/api" +) + +var newOutgoingRequest = newHostFunc("new-outgoing-request", newOutgoingRequestFn, + []api.ValueType{i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32}, + "a", "b", "c", "d", "e", "f", "g", "h", "j", "k", "l", "m", "n", "o") + +type Request struct { + Method string + Path string + Query string + Scheme string + Authority string + Body io.Reader +} + +func (r Request) Url() string { + return fmt.Sprintf("%s://%s%s%s", r.Scheme, r.Authority, r.Path, r.Query) +} + +var request = Request{} + +func MakeRequest() (*http.Response, error) { + r, err := http.NewRequest(request.Method, request.Url(), request.Body) + if err != nil { + return nil, err + } + + return http.DefaultClient.Do(r) +} + +func RequestBody(body []byte) { + request.Body = bytes.NewBuffer(body) +} + +func newOutgoingRequestFn(_ context.Context, mod api.Module, params []uint64) int32 { + switch params[0] { + case 0: + request.Method = "GET" + case 1: + request.Method = "HEAD" + case 2: + request.Method = "POST" + case 3: + request.Method = "PUT" + case 4: + request.Method = "DELETE" + case 5: + request.Method = "CONNECT" + case 6: + request.Method = "OPTIONS" + case 7: + request.Method = "TRACE" + case 8: + request.Method = "PATCH" + default: + log.Fatalf("Unknown method: %d", params[0]) + } + + path_ptr := params[3] + path_len := params[4] + path, ok := mod.Memory().Read(uint32(path_ptr), uint32(path_len)) + if !ok { + return 0 + } + request.Path = string(path) + + query_ptr := params[5] + query_len := params[6] + query, ok := mod.Memory().Read(uint32(query_ptr), uint32(query_len)) + if !ok { + return 0 + } + request.Query = string(query) + + scheme_is_some := params[7] + request.Scheme = "https" + if scheme_is_some == 1 { + if params[8] == 0 { + request.Scheme = "http" + } + } + + authority_ptr := params[11] + authority_len := params[12] + authority, ok := mod.Memory().Read(uint32(authority_ptr), uint32(authority_len)) + if !ok { + return 0 + } + request.Authority = string(authority) + log.Printf("%s %s\n", request.Method, request.Url()) + return 2 +} + +var dropOutgoingRequest = newHostMethod("drop-outgoing-request", dropOutgoingRequestFn, []api.ValueType{i32}, "a") + +func dropOutgoingRequestFn(_ context.Context, mod api.Module, params []uint64) { + // pass +} + +var outgoingRequestWrite = newHostMethod("outgoing-request-write", outgoingRequestWriteFn, []api.ValueType{i32, i32}, "a", "b") + +func outgoingRequestWriteFn(_ context.Context, mod api.Module, params []uint64) { + +} diff --git a/imports/wasi_http/response.go b/imports/wasi_http/response.go new file mode 100644 index 0000000000..51f610d5b9 --- /dev/null +++ b/imports/wasi_http/response.go @@ -0,0 +1,65 @@ +package wasi_http + +import ( + "context" + "io/ioutil" + "net/http" + + "github.com/tetratelabs/wazero/api" +) + +var response *http.Response + +func SetResponse(r *http.Response) { + response = r +} + +func ResponseBody() ([]byte, error) { + return ioutil.ReadAll(response.Body) +} + +func ResponseHeaders() http.Header { + return response.Header +} + +var dropIncomingResponse = newHostMethod("drop-incoming-response", dropIncomingResponseFn, []api.ValueType{i32}, "a") + +func dropIncomingResponseFn(_ context.Context, mod api.Module, params []uint64) { + // pass +} + +var incomingResponseStatus = newHostFunc("incoming-response-status", incomingResponseStatusFn, []api.ValueType{i32}, "a") + +func incomingResponseStatusFn(_ context.Context, mod api.Module, params []uint64) int32 { + return int32(response.StatusCode) +} + +var incomingResponseHeaders = newHostFunc("incoming-response-headers", incomingResponseHeadersFn, []api.ValueType{i32}, "a") + +func incomingResponseHeadersFn(_ context.Context, mod api.Module, params []uint64) int32 { + return 1 +} + +var incomingResponseConsume = newHostMethod("incoming-response-consume", incomingResponseConsumeFn, []api.ValueType{i32, i32}, "a", "b") + +func incomingResponseConsumeFn(_ context.Context, mod api.Module, params []uint64) { + data := []byte{} + // 0 == ok, 1 == is_err + data = le.AppendUint32(data, 0) + // This is the stream number + data = le.AppendUint32(data, 1) + mod.Memory().Write(uint32(params[1]), data) +} + +var futureResponseGet = newHostMethod("future-incoming-response-get", futureResponseGetFn, []api.ValueType{i32, i32}, "a", "b") + +func futureResponseGetFn(_ context.Context, mod api.Module, params []uint64) { + data := []byte{} + // 1 == is_some, 0 == none + data = le.AppendUint32(data, 1) + // 0 == ok, 1 == is_err, consistency ftw! + data = le.AppendUint32(data, 0) + // Copy the future into the actual + data = le.AppendUint32(data, uint32(params[0])) + mod.Memory().Write(uint32(params[1]), data) +} diff --git a/imports/wasi_http/structs.go b/imports/wasi_http/structs.go new file mode 100644 index 0000000000..65f6a97a65 --- /dev/null +++ b/imports/wasi_http/structs.go @@ -0,0 +1,50 @@ +package wasi_http + +import ( + "context" + "log" + + "github.com/tetratelabs/wazero/api" +) + +var newFields = newHostFunc("new-fields", newFieldsFn, []api.ValueType{i32, i32}, "a", "b") + +func newFieldsFn(_ context.Context, mod api.Module, params []uint64) int32 { + return 0 +} + +func allocateWriteString(ctx context.Context, m api.Module, s string) uint32 { + ptr, err := Malloc(ctx, m, uint32(len(s))) + if err != nil { + log.Fatalf(err.Error()) + } + m.Memory().Write(ptr, []byte(s)) + return ptr +} + +var fieldsEntries = newHostMethod("fields-entries", fieldsEntriesFn, []api.ValueType{i32, i32}, "a", "b") + +func fieldsEntriesFn(ctx context.Context, mod api.Module, params []uint64) { + headers := ResponseHeaders() + l := uint32(len(headers)) + // 8 bytes per string/string + ptr, err := Malloc(ctx, mod, l*16) + if err != nil { + log.Fatalf(err.Error()) + } + data := []byte{} + data = le.AppendUint32(data, ptr) + data = le.AppendUint32(data, l) + // write result + mod.Memory().Write(uint32(params[1]), data) + + // ok now allocate and write the strings. + data = []byte{} + for k, v := range headers { + data = le.AppendUint32(data, allocateWriteString(ctx, mod, k)) + data = le.AppendUint32(data, uint32(len(k))) + data = le.AppendUint32(data, allocateWriteString(ctx, mod, v[0])) + data = le.AppendUint32(data, uint32(len(v[0]))) + } + mod.Memory().Write(ptr, data) +} diff --git a/imports/wasi_streams/read.go b/imports/wasi_streams/read.go new file mode 100644 index 0000000000..dba8fdc292 --- /dev/null +++ b/imports/wasi_streams/read.go @@ -0,0 +1,40 @@ +package wasi_streams + +import ( + "context" + "log" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_http" +) + +var streamsRead = newHostMethod("read", streamReadFn, []api.ValueType{i32, i64, i32}, "a", "b", "c") + +func streamReadFn(ctx context.Context, mod api.Module, params []uint64) { + data, err := wasi_http.ResponseBody() + if err != nil { + log.Fatalf(err.Error()) + } + ptr_len := uint32(len(data)) + data = append(data, 0) + ptr, err := wasi_http.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 + 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(uint32(params[2]), data) +} + +var dropInputStream = newHostMethod("drop-input-stream", dropInputStreamFn, []api.ValueType{i32}, "a") + +func dropInputStreamFn(_ context.Context, mod api.Module, params []uint64) { + // pass +} diff --git a/imports/wasi_streams/streams.go b/imports/wasi_streams/streams.go new file mode 100644 index 0000000000..932970ab39 --- /dev/null +++ b/imports/wasi_streams/streams.go @@ -0,0 +1,223 @@ +package wasi_streams + +import ( + "context" + "encoding/binary" + "syscall" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasip1" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// ModuleName is the module name WASI functions are exported into. +// +// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md +const ModuleName = "streams" + +const i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 + +var le = binary.LittleEndian + +// MustInstantiate calls Instantiate or panics on error. +// +// This is a simpler function for those who know the module ModuleName is not +// already instantiated, and don't need to unload it. +func MustInstantiate(ctx context.Context, r wazero.Runtime) { + if _, err := Instantiate(ctx, r); err != nil { + panic(err) + } +} + +// Instantiate instantiates the ModuleName module into the runtime. +// +// # Notes +// +// - Failure cases are documented on wazero.Runtime InstantiateModule. +// - Closing the wazero.Runtime has the same effect as closing the result. +func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) { + return NewBuilder(r).Instantiate(ctx) +} + +// Builder configures the ModuleName module for later use via Compile or Instantiate. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type Builder interface { + // Compile compiles the ModuleName module. Call this before Instantiate. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Compile(context.Context) (wazero.CompiledModule, error) + + // Instantiate instantiates the ModuleName module and returns a function to close it. + // + // Note: This has the same effect as the same function on wazero.HostModuleBuilder. + Instantiate(context.Context) (api.Closer, error) +} + +// NewBuilder returns a new Builder. +func NewBuilder(r wazero.Runtime) Builder { + return &builder{r} +} + +type builder struct{ r wazero.Runtime } + +// hostModuleBuilder returns a new wazero.HostModuleBuilder for ModuleName +func (b *builder) hostModuleBuilder() wazero.HostModuleBuilder { + ret := b.r.NewHostModuleBuilder(ModuleName) + exportFunctions(ret) + return ret +} + +// Compile implements Builder.Compile +func (b *builder) Compile(ctx context.Context) (wazero.CompiledModule, error) { + return b.hostModuleBuilder().Compile(ctx) +} + +// Instantiate implements Builder.Instantiate +func (b *builder) Instantiate(ctx context.Context) (api.Closer, error) { + return b.hostModuleBuilder().Instantiate(ctx) +} + +// FunctionExporter exports functions into a wazero.HostModuleBuilder. +// +// # Notes +// +// - This is an interface for decoupling, not third-party implementations. +// All implementations are in wazero. +type FunctionExporter interface { + ExportFunctions(wazero.HostModuleBuilder) +} + +func NewFunctionExporter() FunctionExporter { + return &functionExporter{} +} + +type functionExporter struct{} + +// ExportFunctions implements FunctionExporter.ExportFunctions +func (functionExporter) ExportFunctions(builder wazero.HostModuleBuilder) { + exportFunctions(builder) +} + +func exportFunctions(builder wazero.HostModuleBuilder) { + exporter := builder.(wasm.HostFuncExporter) + + exporter.ExportHostFunc(streamsRead) + exporter.ExportHostFunc(dropInputStream) + exporter.ExportHostFunc(writeStream) +} + +// writeOffsetsAndNullTerminatedValues is used to write NUL-terminated values +// for args or environ, given a pre-defined bytesLen (which includes NUL +// terminators). +func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offsets, bytes, bytesLen uint32) syscall.Errno { + // The caller may not place bytes directly after offsets, so we have to + // read them independently. + valuesLen := len(values) + offsetsLen := uint32(valuesLen * 4) // uint32Le + offsetsBuf, ok := mem.Read(offsets, offsetsLen) + if !ok { + return syscall.EFAULT + } + bytesBuf, ok := mem.Read(bytes, bytesLen) + if !ok { + return syscall.EFAULT + } + + // Loop through the values, first writing the location of its data to + // offsetsBuf[oI], then its NUL-terminated data at bytesBuf[bI] + var oI, bI uint32 + for _, value := range values { + // Go can't guarantee inlining as there's not //go:inline directive. + // This inlines uint32 little-endian encoding instead. + bytesOffset := bytes + bI + offsetsBuf[oI] = byte(bytesOffset) + offsetsBuf[oI+1] = byte(bytesOffset >> 8) + offsetsBuf[oI+2] = byte(bytesOffset >> 16) + offsetsBuf[oI+3] = byte(bytesOffset >> 24) + oI += 4 // size of uint32 we just wrote + + // Write the next value to memory with a NUL terminator + copy(bytesBuf[bI:], value) + bI += uint32(len(value)) + bytesBuf[bI] = 0 // NUL terminator + bI++ + } + + return 0 +} + +func newHostFunc( + name string, + goFunc wasiFunc, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +func newHostMethod( + name string, + goFunc wasiMethod, + paramTypes []api.ValueType, + paramNames ...string, +) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{}, + ResultNames: []string{}, + Code: wasm.Code{GoFunc: goFunc}, + } +} + +// wasiFunc special cases that all WASI functions return a single Errno +// result. The returned value will be written back to the stack at index zero. +type wasiFunc func(ctx context.Context, mod api.Module, params []uint64) syscall.Errno + +// Call implements the same method as documented on api.GoModuleFunction. +func (f wasiFunc) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + errno := f(ctx, mod, stack) + if errno != 0 { + stack[0] = uint64(wasip1.ToErrno(errno)) + } else { // special case ass ErrnoSuccess is zero + stack[0] = 0 + } +} + +type wasiMethod func(ctx context.Context, mod api.Module, params []uint64) + +func (f wasiMethod) Call(ctx context.Context, mod api.Module, stack []uint64) { + // Write the result back onto the stack + f(ctx, mod, stack) +} + +// stubFunction stubs for GrainLang per #271. +func stubFunction(name string, paramTypes []wasm.ValueType, paramNames ...string) *wasm.HostFunc { + return &wasm.HostFunc{ + ExportName: name, + Name: name, + ParamTypes: paramTypes, + ParamNames: paramNames, + ResultTypes: []api.ValueType{i32}, + ResultNames: []string{"errno"}, + Code: wasm.Code{ + GoFunc: api.GoModuleFunc(func(_ context.Context, _ api.Module, stack []uint64) { stack[0] = uint64(wasip1.ErrnoNosys) }), + }, + } +}