193 lines
5.7 KiB
Go
193 lines
5.7 KiB
Go
/*
|
|
Copyright 2023 The Dapr Authors
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implieout.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package wasm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/stealthrocket/wasi-go/imports/wasi_http"
|
|
"github.com/stealthrocket/wasi-go/imports/wasi_http/default_http"
|
|
"github.com/tetratelabs/wazero"
|
|
"github.com/tetratelabs/wazero/api"
|
|
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
|
|
|
|
"github.com/dapr/components-contrib/bindings"
|
|
"github.com/dapr/components-contrib/internal/wasm"
|
|
"github.com/dapr/components-contrib/metadata"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
// ExecuteOperation is defined here as it isn't in the bindings package.
|
|
const ExecuteOperation bindings.OperationKind = "execute"
|
|
|
|
type outputBinding struct {
|
|
logger logger.Logger
|
|
runtimeConfig wazero.RuntimeConfig
|
|
|
|
meta *wasm.InitMetadata
|
|
runtime wazero.Runtime
|
|
module wazero.CompiledModule
|
|
|
|
instanceCounter atomic.Uint64
|
|
}
|
|
|
|
var (
|
|
_ bindings.OutputBinding = (*outputBinding)(nil)
|
|
_ io.Closer = (*outputBinding)(nil)
|
|
)
|
|
|
|
func NewWasmOutput(logger logger.Logger) bindings.OutputBinding {
|
|
return &outputBinding{
|
|
logger: logger,
|
|
|
|
// The below ensures context cancels in-flight wasm functions.
|
|
runtimeConfig: wazero.NewRuntimeConfig().
|
|
WithCloseOnContextDone(true),
|
|
}
|
|
}
|
|
|
|
func (out *outputBinding) Init(ctx context.Context, metadata bindings.Metadata) (err error) {
|
|
if out.meta, err = wasm.GetInitMetadata(ctx, metadata.Base); err != nil {
|
|
return fmt.Errorf("wasm: failed to parse metadata: %w", err)
|
|
}
|
|
|
|
// Create the runtime, which when closed releases any resources associated with it.
|
|
out.runtime = wazero.NewRuntimeWithConfig(ctx, out.runtimeConfig)
|
|
|
|
// Compile the module, which reduces execution time of Invoke
|
|
out.module, err = out.runtime.CompileModule(ctx, out.meta.Guest)
|
|
if err != nil {
|
|
_ = out.runtime.Close(context.Background())
|
|
return fmt.Errorf("wasm: error compiling binary: %w", err)
|
|
}
|
|
|
|
imports := detectImports(out.module.ImportedFunctions())
|
|
|
|
if _, found := imports[modeWasiP1]; found {
|
|
_, err = wasi_snapshot_preview1.Instantiate(ctx, out.runtime)
|
|
}
|
|
if err != nil {
|
|
_ = out.runtime.Close(context.Background())
|
|
return fmt.Errorf("wasm: error instantiating host wasi functions: %w", err)
|
|
}
|
|
if _, found := imports[modeWasiHTTP]; found {
|
|
if out.meta.StrictSandbox {
|
|
_ = out.runtime.Close(context.Background())
|
|
return fmt.Errorf("can not instantiate wasi-http with strict sandbox")
|
|
}
|
|
err = wasi_http.Instantiate(ctx, out.runtime)
|
|
}
|
|
if err != nil {
|
|
_ = out.runtime.Close(context.Background())
|
|
return fmt.Errorf("wasm: error instantiating host wasi-http functions: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (out *outputBinding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
guestName := out.meta.GuestName
|
|
if guestName == "" {
|
|
guestName = out.module.Name()
|
|
}
|
|
|
|
// Currently, concurrent modules can conflict on name. Make sure we have
|
|
// a unique one.
|
|
instanceNum := out.instanceCounter.Add(1)
|
|
instanceName := guestName + "-" + strconv.FormatUint(instanceNum, 10)
|
|
moduleConfig := wasm.NewModuleConfig(out.meta).WithName(instanceName)
|
|
|
|
// Only assign STDIN if it is present in the request.
|
|
if len(req.Data) > 0 {
|
|
moduleConfig = moduleConfig.WithStdin(bytes.NewReader(req.Data))
|
|
}
|
|
|
|
// Any STDOUT is returned as a result: capture it into a buffer.
|
|
var stdout bytes.Buffer
|
|
moduleConfig = moduleConfig.WithStdout(&stdout)
|
|
|
|
// Set the program name to the binary name
|
|
argsSlice := []string{guestName}
|
|
|
|
// Get any remaining args from configuration
|
|
if args := req.Metadata["args"]; args != "" {
|
|
argsSlice = append(argsSlice, strings.Split(args, ",")...)
|
|
}
|
|
moduleConfig = moduleConfig.WithArgs(argsSlice...)
|
|
|
|
// Instantiating executes the guest's main function (exported as _start).
|
|
mod, err := out.runtime.InstantiateModule(ctx, out.module, moduleConfig)
|
|
|
|
// WASI typically calls proc_exit which exits the module, but just in case
|
|
// it doesn't, close the module manually.
|
|
_ = mod.Close(ctx)
|
|
|
|
// Return STDOUT if there was no error.
|
|
if err == nil {
|
|
return &bindings.InvokeResponse{Data: stdout.Bytes()}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func (out *outputBinding) Operations() []bindings.OperationKind {
|
|
return []bindings.OperationKind{
|
|
ExecuteOperation,
|
|
}
|
|
}
|
|
|
|
// Close implements io.Closer
|
|
func (out *outputBinding) Close() error {
|
|
// wazero's runtime closes everything.
|
|
if rt := out.runtime; rt != nil {
|
|
return rt.Close(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// In the future
|
|
const (
|
|
modeDefault importMode = iota
|
|
modeWasiP1
|
|
modeWasiHTTP
|
|
)
|
|
|
|
type importMode uint
|
|
|
|
func detectImports(imports []api.FunctionDefinition) map[importMode]bool {
|
|
result := make(map[importMode]bool)
|
|
for _, f := range imports {
|
|
moduleName, _, _ := f.Import()
|
|
switch moduleName {
|
|
case wasi_snapshot_preview1.ModuleName:
|
|
result[modeWasiP1] = true
|
|
case default_http.ModuleName:
|
|
result[modeWasiHTTP] = true
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetComponentMetadata returns the metadata of the component.
|
|
func (out *outputBinding) GetComponentMetadata() (metadataInfo metadata.MetadataMap) {
|
|
metadataStruct := wasm.InitMetadata{}
|
|
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType)
|
|
return
|
|
}
|