diff --git a/.gitignore b/.gitignore index 38f041b..a3b1811 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ go.work /tmp/ *.orig *_gen.go -/mocks/ \ No newline at end of file +/mocks/ +vendor diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a6e0531 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,31 @@ +run: + skip-files: + - ".*_test.go" + - "mock_.*.go" + timeout: "100s" + +linters: + enable: + - goimports + - revive + - errcheck + - ineffassign + - typecheck + - unconvert + - exportloopref + - dupl + - misspell + - govet + - staticcheck + - unused + - gosimple + disable-all: true + +linters-settings: + goimports: + local-prefixes: github.com/wasify-go + revive: + enableAllRules: true + rules: + - name: receiver-naming + disabled: true diff --git a/build b/build new file mode 100755 index 0000000..3637453 --- /dev/null +++ b/build @@ -0,0 +1,79 @@ +#!/bin/bash + +source "$(dirname "${0}")/resources/tools/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")")" +readonly PROJECT_NAME="$(basename "${PROJECT_PATH}")" + +function help() { + cat <= (1 << 24) { return 0, fmt.Errorf("Size %d exceeds 24 bits precision %d", size, (1 << 24)) @@ -25,20 +23,20 @@ func PackUI64(dataType types.ValueType, offset uint32, size uint32) (uint64, err // Shift the dataType into the highest 8 bits // Shift the offset into the next 32 bits // Use the size as is, but ensure only the lowest 24 bits are used (using bitwise AND) - return (uint64(dataType) << 56) | (uint64(offset) << 24) | uint64(size&0xFFFFFF), nil + return (uint64(typ) << 56) | (uint64(offset) << 24) | uint64(size&0xFFFFFF), nil } // UnpackUI64 reverses the operation done by PackUI64. // Given a packed uint64, it will extract and return the original dataType, offset (ptr), and size. -func UnpackUI64(packedData uint64) (dataType types.ValueType, offset uint32, size uint32) { - // Extract the dataType from the highest 8 bits - dataType = types.ValueType(packedData >> 56) +func UnpackUI64[T ~uint8](data uint64) (T, uint32, uint32) { + // Extract the data type from the highest 8 bits + typ := T(data >> 56) // Extract the offset (ptr) from the next 32 bits using bitwise AND to mask the other bits - offset = uint32((packedData >> 24) & 0xFFFFFFFF) + offset := uint32((data >> 24) & 0xFFFFFFFF) // Extract the size from the lowest 24 bits - size = uint32(packedData & 0xFFFFFF) + size := uint32(data & 0xFFFFFF) - return + return typ, offset, size } diff --git a/internal/utils/pack_test.go b/internal/utils/pack_test.go index 9fc442f..26d98f6 100644 --- a/internal/utils/pack_test.go +++ b/internal/utils/pack_test.go @@ -1,9 +1,10 @@ -package utils +package utils_test import ( "testing" "github.com/wasify-io/wasify-go/internal/types" + . "github.com/wasify-io/wasify-go/internal/utils" ) func TestPackUnpackUI64(t *testing.T) { @@ -16,7 +17,7 @@ func TestPackUnpackUI64(t *testing.T) { t.Fatalf("Failed to pack data: %v", err) } - unpackedDataType, unpackedPtr, unpackedSize := UnpackUI64(packedData) + unpackedDataType, unpackedPtr, unpackedSize := UnpackUI64[types.ValueType](packedData) if unpackedDataType != dataType || unpackedPtr != ptr || unpackedSize != size { t.Errorf("Unpack did not match original data. Expected: %v, %v, %v. Got: %v, %v, %v", diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 88a3e17..abaf24e 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -6,14 +6,49 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "slices" ) +func OneOf[T comparable](value T, values ...T) bool { + return slices.Index(values, value) > -1 +} + +func Ternary[T any](condition bool, first T, second T) T { + if condition { + return first + } + + return second +} + +func Second[T any](_ any, second T, _ ...any) T { + return second +} + +func Must[T any](value T, err error) T { + if err != nil { + panic(err) + } + + return value +} + +func Map[I any, O any](elements []I, transform func(I) O) []O { + result := make([]O, len(elements)) + + for index, element := range elements { + result[index] = transform(element) + } + + return result +} + // calculateHash computes the SHA-256 hash of the input byte slice. // It returns the hash as a hex-encoded string. func CalculateHash(data []byte) (hash string, err error) { hasher := sha256.New() - _, err = hasher.Write(data) - if err != nil { + + if err = Second(hasher.Write(data)); err != nil { return "", err } diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index bd319ac..9946783 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,7 +1,9 @@ -package utils +package utils_test import ( "testing" + + . "github.com/wasify-io/wasify-go/internal/utils" ) func TestCalculateHash(t *testing.T) { diff --git a/logging/log.go b/logging/log.go new file mode 100644 index 0000000..87226ec --- /dev/null +++ b/logging/log.go @@ -0,0 +1,91 @@ +package logging + +import ( + "context" + "log/slog" + "os" +) + +type LogSeverity uint8 + +const ( + LogDebug LogSeverity = iota + 1 + LogInfo + LogWarning + LogError +) + +var logMap = map[LogSeverity]slog.Level{ + LogDebug: slog.LevelDebug, + LogInfo: slog.LevelInfo, + LogWarning: slog.LevelWarn, + LogError: slog.LevelError, +} + +// asSlogLevel gets 'slog' level based on severity specified by user +func asSlogLevel(severity LogSeverity) slog.Level { + level, ok := logMap[severity] + if !ok { + // default logger is Info + return slog.LevelInfo + } + + return level +} + +type Logger interface { + Severity() LogSeverity + ForSeverity(severity LogSeverity) Logger + + Info(message string, arguments ...any) + Warn(message string, arguments ...any) + Error(message string, arguments ...any) + Debug(message string, arguments ...any) + Log(severity LogSeverity, message string, arguments ...any) +} + +// NewSlogLogger returns new slog ref +func NewSlogLogger(severity LogSeverity) Logger { + return &_SlogLogger{ + delegate: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: asSlogLevel(severity), + })), + } +} + +type _SlogLogger struct { + severity LogSeverity + delegate *slog.Logger +} + +func (self *_SlogLogger) Severity() LogSeverity { + return self.severity +} + +func (self *_SlogLogger) ForSeverity(severity LogSeverity) Logger { + return &_SlogLogger{ + delegate: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: asSlogLevel(severity), + })), + } +} + +func (self *_SlogLogger) Info(message string, arguments ...any) { + self.Log(LogInfo, message, arguments...) +} + +func (self *_SlogLogger) Error(message string, arguments ...any) { + self.Log(LogError, message, arguments...) +} + +func (self *_SlogLogger) Debug(message string, arguments ...any) { + self.Log(LogDebug, message, arguments...) +} + +func (self *_SlogLogger) Warn(message string, arguments ...any) { + self.Log(LogWarning, message, arguments...) +} + +func (self *_SlogLogger) Log(severity LogSeverity, message string, arguments ...any) { + self.delegate.Log(context.Background(), asSlogLevel(severity), message, arguments...) +} diff --git a/internal/utils/log_test.go b/logging/log_test.go similarity index 80% rename from internal/utils/log_test.go rename to logging/log_test.go index e9d3136..a4698c5 100644 --- a/internal/utils/log_test.go +++ b/logging/log_test.go @@ -1,4 +1,4 @@ -package utils +package logging import ( "log/slog" @@ -7,9 +7,8 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetLogLevel(t *testing.T) { - - newLogger := NewLogger(LogDebug) +func TestAsSlogLogLevel(t *testing.T) { + newLogger := NewSlogLogger(LogDebug) assert.NotNil(t, newLogger) tests := []struct { @@ -24,7 +23,7 @@ func TestGetLogLevel(t *testing.T) { } for _, test := range tests { - got := GetlogLevel(test.severity) + got := asSlogLevel(test.severity) if got != test.expected { t.Errorf("for severity %d, expected %d but got %d", test.severity, test.expected, got) } diff --git a/mdk/utils.go b/mdk/utils.go index 891fbee..2e76dde 100644 --- a/mdk/utils.go +++ b/mdk/utils.go @@ -42,7 +42,7 @@ func readF32(offset64 uint64) float32 { } func readF64(offset64 uint64) float64 { - return *ptrToData[float64](uint64(offset64)) + return *ptrToData[float64](offset64) } func readString(offset64 uint64, size int) string { diff --git a/models/fs_config.go b/models/fs_config.go new file mode 100644 index 0000000..4960cf1 --- /dev/null +++ b/models/fs_config.go @@ -0,0 +1,23 @@ +package models + +import . "github.com/wasify-io/wasify-go/internal/utils" + +// FSConfig configures a directory to be pre-opened for access by the WASI module if Enabled is set to true. +// If GuestDir is not provided, the default guest directory will be "/". +// Note: If FSConfig is not provided or Enabled is false, the directory will not be attached to WASI. +type FSConfig struct { + // Whether to Enabled the directory for WASI access. + Enabled bool + + // The directory on the host system. + // Default: "/" + HostDir string + + // The directory accessible to the WASI module. + GuestDir string +} + +// GetGuestDir gets the default path for guest module. +func (fs *FSConfig) GetGuestDir() string { + return Ternary(fs.GuestDir == "", "/", fs.GuestDir) +} diff --git a/models/guest_function.go b/models/guest_function.go new file mode 100644 index 0000000..0294c37 --- /dev/null +++ b/models/guest_function.go @@ -0,0 +1,5 @@ +package models + +type GuestFunction interface { + Invoke(args ...any) GuestFunctionResult +} diff --git a/models/guest_function_result.go b/models/guest_function_result.go new file mode 100644 index 0000000..511e76f --- /dev/null +++ b/models/guest_function_result.go @@ -0,0 +1,18 @@ +package models + +import "io" + +type GuestFunctionResult interface { + io.Closer + Error() error + Values() []PackedData + + ReadAnyPack(index int) (any, uint32, uint32, error) + ReadBytesPack(index int) ([]byte, error) + ReadBytePack(index int) (byte, error) + ReadUint32Pack(index int) (uint32, error) + ReadUint64Pack(index int) (uint64, error) + ReadFloat32Pack(index int) (float32, error) + ReadFloat64Pack(index int) (float64, error) + ReadStringPack(index int) (string, error) +} diff --git a/host_function.go b/models/host_function.go similarity index 57% rename from host_function.go rename to models/host_function.go index 93c3b27..eb661c7 100644 --- a/host_function.go +++ b/models/host_function.go @@ -1,30 +1,12 @@ -package wasify +package models import ( "context" "fmt" - "github.com/wasify-io/wasify-go/internal/types" + "github.com/wasify-io/wasify-go/internal/utils" ) -// ValueType represents the type of value used in function parameters and returns. -type ValueType types.ValueType - -// supported value types in params and returns -const ( - ValueTypeBytes ValueType = ValueType(types.ValueTypeBytes) - ValueTypeByte ValueType = ValueType(types.ValueTypeByte) - ValueTypeI32 ValueType = ValueType(types.ValueTypeI32) - ValueTypeI64 ValueType = ValueType(types.ValueTypeI64) - ValueTypeF32 ValueType = ValueType(types.ValueTypeF32) - ValueTypeF64 ValueType = ValueType(types.ValueTypeF64) - ValueTypeString ValueType = ValueType(types.ValueTypeString) -) - -// Param defines the attributes of a function parameter. -type MultiPackedData uint64 -type PackedData uint64 - // HostFunction defines a host function that can be invoked from a guest module. type HostFunction struct { // Callback function to execute when the host function is invoked. @@ -48,21 +30,20 @@ type HostFunction struct { // Allocation map to track parameter and return value allocations for host func. // Configuration of the associated module. - moduleConfig *ModuleConfig + Config *ModuleConfig } // HostFunctionCallback is the function signature for the callback executed by a host function. // // HostFunctionCallback encapsulates the runtime's internal implementation details. // It serves as an intermediary invoked between the processing of function parameters and the final return of the function. -type HostFunctionCallback func(ctx context.Context, moduleProxy *ModuleProxy, multiPackedData []PackedData) MultiPackedData +type HostFunctionCallback func(ctx context.Context, module Module, datas []PackedData) MultiPackedData -// preHostFunctionCallback +// PreHostFunctionCallback // prepares parameters for the host function by converting // packed stack parameters into a slice of PackedData. It validates parameter counts // and leverages ModuleProxy for reading the data. -func (hf *HostFunction) preHostFunctionCallback(ctx context.Context, m *ModuleProxy, stackParams []uint64) ([]PackedData, error) { - +func (hf *HostFunction) PreHostFunctionCallback(stackParams []uint64) ([]PackedData, error) { // If user did not define params, skip the whole process, we still might get stackParams[0] = 0 if len(hf.Params) == 0 { return nil, nil @@ -72,19 +53,14 @@ func (hf *HostFunction) preHostFunctionCallback(ctx context.Context, m *ModulePr return nil, fmt.Errorf("%s: params mismatch expected: %d received: %d ", hf.Name, len(hf.Params), len(stackParams)) } - pds := make([]PackedData, len(hf.Params)) - - for i := range hf.Params { - pds[i] = PackedData(stackParams[i]) - } - - return pds, nil - + return utils.Map(stackParams, func(param uint64) PackedData { + return PackedData(param) + }), nil } -// postHostFunctionCallback +// PostHostFunctionCallback // stores the resulting MultiPackedData into linear memory after the host function execution. -func (hf *HostFunction) postHostFunctionCallback(ctx context.Context, m *ModuleProxy, mpd MultiPackedData, stackParams []uint64) { +func (hf *HostFunction) PostHostFunctionCallback(mpd MultiPackedData, stackParams []uint64) { // Store final MultiPackedData into linear memory stackParams[0] = uint64(mpd) } diff --git a/models/host_function_test.go b/models/host_function_test.go new file mode 100644 index 0000000..e91b801 --- /dev/null +++ b/models/host_function_test.go @@ -0,0 +1,94 @@ +package models_test + +import ( + "context" + _ "embed" + "testing" + + "github.com/stretchr/testify/assert" + test_utils "github.com/wasify-io/wasify-go/internal/test-utils" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" +) + +func TestHostFunctions(t *testing.T) { + t.Run("successful instantiation", func(t *testing.T) { + ctx := context.Background() + + runtime := test_utils.CreateRuntime(t, ctx, &RuntimeConfig{ + Runtime: RuntimeWazero, + Logger: logging.NewSlogLogger(logging.LogInfo), + }) + + module := test_utils.CreateModule(t, runtime, &ModuleConfig{ + Context: context.Background(), + Logger: logging.NewSlogLogger(logging.LogInfo), + Namespace: "host_all_available_types", + Wasm: test_utils.LoadTestWASM(t, "host_all_available_types"), + HostFunctions: []HostFunction{ + { + Name: "hostTest", + Callback: func(ctx context.Context, module Module, params []PackedData) MultiPackedData { + memory := module.Memory() + + _bytes, _ := memory.ReadBytesPack(params[0]) + assert.Equal(t, []byte("Guest: Wello Wasify!"), _bytes) + + _byte, _ := memory.ReadBytePack(params[1]) + assert.Equal(t, byte(1), _byte) + + _uint32, _ := memory.ReadUint32Pack(params[2]) + assert.Equal(t, uint32(11), _uint32) + + _uint64, _ := memory.ReadUint64Pack(params[3]) + assert.Equal(t, uint64(2023), _uint64) + + _float32, _ := memory.ReadFloat32Pack(params[4]) + assert.Equal(t, float32(11.1), _float32) + + _float64, _ := memory.ReadFloat64Pack(params[5]) + assert.Equal(t, float64(11.2023), _float64) + + _string, _ := memory.ReadStringPack(params[6]) + assert.Equal(t, "Guest: Wasify.", _string) + + return memory.WriteMultiPack( + memory.WriteBytesPack([]byte("Some")), + memory.WriteBytePack(1), + memory.WriteUint32Pack(11), + memory.WriteUint64Pack(2023), + memory.WriteFloat32Pack(11.1), + memory.WriteFloat64Pack(11.2023), + memory.WriteStringPack("Host: Wasify."), + ) + + }, + Params: []ValueType{ + ValueTypeBytes, + ValueTypeByte, + ValueTypeI32, + ValueTypeI64, + ValueTypeF32, + ValueTypeF64, + ValueTypeString, + }, + Results: []ValueType{ + ValueTypeBytes, + ValueTypeByte, + ValueTypeI32, + ValueTypeI64, + ValueTypeF32, + ValueTypeF64, + ValueTypeString, + }, + }, + }, + }) + + result := module.GuestFunction(ctx, "guestTest").Invoke() + + assert.NoError(t, result.Error()) + + t.Log("TestHostFunctions RES:", result) + }) +} diff --git a/models/memory.go b/models/memory.go new file mode 100644 index 0000000..4c88b03 --- /dev/null +++ b/models/memory.go @@ -0,0 +1,52 @@ +package models + +type RMemory interface { + ReadBytes(offset uint32, size uint32) ([]byte, error) + ReadByte(offset uint32) (byte, error) + ReadUint32(offset uint32) (uint32, error) + ReadUint64(offset uint32) (uint64, error) + ReadFloat32(offset uint32) (float32, error) + ReadFloat64(offset uint32) (float64, error) + ReadString(offset uint32, size uint32) (string, error) + + ReadAnyPack(pd PackedData) (any, uint32, uint32, error) + ReadBytesPack(pd PackedData) ([]byte, error) + ReadBytePack(pd PackedData) (byte, error) + ReadUint32Pack(pd PackedData) (uint32, error) + ReadUint64Pack(pd PackedData) (uint64, error) + ReadFloat32Pack(pd PackedData) (float32, error) + ReadFloat64Pack(pd PackedData) (float64, error) + ReadStringPack(pd PackedData) (string, error) + + Size() uint32 + Free(...uint32) error + FreePack(...PackedData) error +} + +type WMemory interface { + WriteAny(offset uint32, v any) error + WriteBytes(offset uint32, v []byte) error + WriteByte(offset uint32, v byte) error + WriteUint32(offset uint32, v uint32) error + WriteUint64(offset uint32, v uint64) error + WriteFloat32(offset uint32, v float32) error + WriteFloat64(offset uint32, v float64) error + WriteString(offset uint32, v string) error + + WriteBytesPack(v []byte) PackedData + WriteBytePack(v byte) PackedData + WriteUint32Pack(v uint32) PackedData + WriteUint64Pack(v uint64) PackedData + WriteFloat32Pack(v float32) PackedData + WriteFloat64Pack(v float64) PackedData + WriteStringPack(v string) PackedData + + WriteMultiPack(...PackedData) MultiPackedData + + Malloc(size uint32) (uint32, error) +} + +type Memory interface { + RMemory + WMemory +} diff --git a/models/module.go b/models/module.go new file mode 100644 index 0000000..1e9900b --- /dev/null +++ b/models/module.go @@ -0,0 +1,9 @@ +package models + +import "context" + +type Module interface { + Memory() Memory + Close(ctx context.Context) error + GuestFunction(ctx context.Context, name string) GuestFunction +} diff --git a/models/module_config.go b/models/module_config.go new file mode 100644 index 0000000..78646ad --- /dev/null +++ b/models/module_config.go @@ -0,0 +1,27 @@ +package models + +import ( + "context" + + "github.com/wasify-io/wasify-go/logging" +) + +type ModuleConfig struct { + // Module Namespace. Required. + Namespace string + + // FSConfig configures a directory to be pre-opened for access by the WASI module if Enabled is set to true. + // If GuestDir is not provided, the default guest directory will be "/". + // Note: If FSConfig is not provided or Enabled is false, the directory will not be attached to WASI. + FSConfig FSConfig + + // WASM configuration. Required. + Wasm Wasm + + // List of host functions to be registered. + HostFunctions []HostFunction + + Context context.Context + + Logger logging.Logger +} diff --git a/models/multi_packed_data.go b/models/multi_packed_data.go new file mode 100644 index 0000000..d7f46df --- /dev/null +++ b/models/multi_packed_data.go @@ -0,0 +1,4 @@ +package models + +// MultiPackedData - defines the attributes of a function parameter. +type MultiPackedData uint64 diff --git a/models/packed_data.go b/models/packed_data.go new file mode 100644 index 0000000..25a405b --- /dev/null +++ b/models/packed_data.go @@ -0,0 +1,3 @@ +package models + +type PackedData uint64 diff --git a/models/runtime.go b/models/runtime.go new file mode 100644 index 0000000..160ada2 --- /dev/null +++ b/models/runtime.go @@ -0,0 +1,8 @@ +package models + +import "context" + +type Runtime interface { + Close(ctx context.Context) error + Create(config *ModuleConfig) (Module, error) +} diff --git a/models/runtime_config.go b/models/runtime_config.go new file mode 100644 index 0000000..9877d9a --- /dev/null +++ b/models/runtime_config.go @@ -0,0 +1,11 @@ +package models + +import "github.com/wasify-io/wasify-go/logging" + +// The RuntimeConfig struct holds configuration settings for a runtime. +type RuntimeConfig struct { + // Specifies the type of runtime being used. + Runtime RuntimeType + // Logger to use for the runtime and module + Logger logging.Logger +} diff --git a/models/runtime_type.go b/models/runtime_type.go new file mode 100644 index 0000000..5e7d804 --- /dev/null +++ b/models/runtime_type.go @@ -0,0 +1,23 @@ +package models + +// RuntimeType defines a type of WebAssembly (wasm) runtime. +// +// Currently, the only supported wasm runtime is Wazero. +// However, in the future, more runtimes could be added. +// This means that you'll be able to run modules +// on various wasm runtimes. +type RuntimeType uint8 + +const ( + RuntimeWazero RuntimeType = iota +) + +func (rt RuntimeType) String() (runtimeName string) { + + switch rt { + case RuntimeWazero: + runtimeName = "Wazero" + } + + return +} diff --git a/models/value_type.go b/models/value_type.go new file mode 100644 index 0000000..2238fc6 --- /dev/null +++ b/models/value_type.go @@ -0,0 +1,17 @@ +package models + +import "github.com/wasify-io/wasify-go/internal/types" + +// ValueType represents the type of value used in function parameters and returns. +type ValueType types.ValueType + +// supported value types in params and returns +const ( + ValueTypeBytes = ValueType(types.ValueTypeBytes) + ValueTypeByte = ValueType(types.ValueTypeByte) + ValueTypeI32 = ValueType(types.ValueTypeI32) + ValueTypeI64 = ValueType(types.ValueTypeI64) + ValueTypeF32 = ValueType(types.ValueTypeF32) + ValueTypeF64 = ValueType(types.ValueTypeF64) + ValueTypeString = ValueType(types.ValueTypeString) +) diff --git a/models/wasm.go b/models/wasm.go new file mode 100644 index 0000000..3c8685f --- /dev/null +++ b/models/wasm.go @@ -0,0 +1,9 @@ +package models + +// Wasm configures a new wasm file. +// Binay is required. +// Hash is optional. +type Wasm struct { + Binary []byte + Hash string +} diff --git a/module.go b/module.go deleted file mode 100644 index ba5fe62..0000000 --- a/module.go +++ /dev/null @@ -1,122 +0,0 @@ -package wasify - -import ( - "context" - "log/slog" -) - -type Module interface { - Close(ctx context.Context) error - GuestFunction(ctx context.Context, functionName string) GuestFunction - Memory() Memory -} - -type ModuleProxy struct { - Memory Memory -} - -type GuestFunction interface { - Invoke(args ...any) (*GuestFunctionResult, error) - call(args ...uint64) (uint64, error) -} - -type Memory interface { - ReadBytes(offset uint32, size uint32) ([]byte, error) - ReadByte(offset uint32) (byte, error) - ReadUint32(offset uint32) (uint32, error) - ReadUint64(offset uint32) (uint64, error) - ReadFloat32(offset uint32) (float32, error) - ReadFloat64(offset uint32) (float64, error) - ReadString(offset uint32, size uint32) (string, error) - - ReadAnyPack(pd PackedData) (any, uint32, uint32, error) - ReadBytesPack(pd PackedData) ([]byte, error) - ReadBytePack(pd PackedData) (byte, error) - ReadUint32Pack(pd PackedData) (uint32, error) - ReadUint64Pack(pd PackedData) (uint64, error) - ReadFloat32Pack(pd PackedData) (float32, error) - ReadFloat64Pack(pd PackedData) (float64, error) - ReadStringPack(pd PackedData) (string, error) - - WriteAny(offset uint32, v any) error - WriteBytes(offset uint32, v []byte) error - WriteByte(offset uint32, v byte) error - WriteUint32(offset uint32, v uint32) error - WriteUint64(offset uint32, v uint64) error - WriteFloat32(offset uint32, v float32) error - WriteFloat64(offset uint32, v float64) error - WriteString(offset uint32, v string) error - - WriteBytesPack(v []byte) PackedData - WriteBytePack(v byte) PackedData - WriteUint32Pack(v uint32) PackedData - WriteUint64Pack(v uint64) PackedData - WriteFloat32Pack(v float32) PackedData - WriteFloat64Pack(v float64) PackedData - WriteStringPack(v string) PackedData - - WriteMultiPack(...PackedData) MultiPackedData - - FreePack(...PackedData) error - Free(...uint32) error - - Size() uint32 - Malloc(size uint32) (uint32, error) -} - -type ModuleConfig struct { - // Module Namespace. Required. - Namespace string - - // FSConfig configures a directory to be pre-opened for access by the WASI module if Enabled is set to true. - // If GuestDir is not provided, the default guest directory will be "/". - // Note: If FSConfig is not provided or Enabled is false, the directory will not be attached to WASI. - FSConfig FSConfig - - // WASM configuration. Required. - Wasm Wasm - - // List of host functions to be registered. - HostFunctions []HostFunction - - // Set the severity level for a particular module's logs. - // Note: If LogSeverity isn't specified, the severity is inherited from the parent, like the runtime log severity. - LogSeverity LogSeverity - - // Struct members for internal use. - ctx context.Context - log *slog.Logger -} - -// Wasm configures a new wasm file. -// Binay is required. -// Hash is optional. -type Wasm struct { - Binary []byte - Hash string -} - -// FSConfig configures a directory to be pre-opened for access by the WASI module if Enabled is set to true. -// If GuestDir is not provided, the default guest directory will be "/". -// Note: If FSConfig is not provided or Enabled is false, the directory will not be attached to WASI. -type FSConfig struct { - // Whether to Enabled the directory for WASI access. - Enabled bool - - // The directory on the host system. - // Default: "/" - HostDir string - - // The directory accessible to the WASI module. - GuestDir string -} - -// getGuestDir gets the default path for guest module. -func (fs *FSConfig) getGuestDir() string { - - if fs.GuestDir == "" { - return "/" - } - - return fs.GuestDir -} diff --git a/module_wazero.go b/module_wazero.go deleted file mode 100644 index bb6dd21..0000000 --- a/module_wazero.go +++ /dev/null @@ -1,569 +0,0 @@ -package wasify - -import ( - "context" - "errors" - "fmt" - "reflect" - - "github.com/tetratelabs/wazero/api" - "github.com/wasify-io/wasify-go/internal/types" - "github.com/wasify-io/wasify-go/internal/utils" -) - -// GuestFunction returns a GuestFunction instance associated with the wazeroModule. -// GuestFunction is used to work with exported function from this module. -// -// Example usage: -// -// result, err = module.GuestFunction(ctx, "greet").Invoke("argument1", "argument2", 123) -// if err != nil { -// slog.Error(err.Error()) -// } -func (m *wazeroModule) GuestFunction(ctx context.Context, name string) GuestFunction { - - fn := m.mod.ExportedFunction(name) - if fn == nil { - m.log.Warn("exported function does not exist", "function", name, "namespace", m.Namespace) - } - - return &wazeroGuestFunction{ - ctx, - fn, - name, - m.Memory(), - m.ModuleConfig, - } -} - -// Close closes the resource. -// -// Note: The context parameter is used for value lookup, such as for -// logging. A canceled or otherwise done context will not prevent Close -// from succeeding. -func (m *wazeroModule) Close(ctx context.Context) error { - err := m.mod.Close(ctx) - if err != nil { - err = errors.Join(errors.New("can't close module"), err) - m.log.Error(err.Error()) - return err - } - return nil -} - -// Memory retrieves a Memory instance associated with the wazeroModule. -func (r *wazeroModule) Memory() Memory { - return &wazeroMemory{r} -} - -type wazeroMemory struct { - *wazeroModule -} - -// The wazeroModule struct combines an instantiated wazero modul -// with the generic module configuration. -type wazeroModule struct { - mod api.Module - *ModuleConfig -} - -// ReadAnyPack extracts and reads data from a packed memory location. -// -// Given a packed data representation, this function determines the type, offset, and size of the data to be read. -// It then reads the data from the specified offset and returns it. -// -// Returns: -// - offset: The memory location where the data starts. -// - size: The size or length of the data. -// - data: The actual extracted data of the determined type (i.e., byte slice, uint32, uint64, float32, float64). -// - error: An error if encountered (e.g., unsupported data type, out-of-range error). -func (m *wazeroMemory) ReadAnyPack(pd PackedData) (any, uint32, uint32, error) { - - var err error - var data any - - // Unpack the packedData to extract offset and size values. - valueType, offset, size := utils.UnpackUI64(uint64(pd)) - - switch ValueType(valueType) { - case ValueTypeBytes: - data, err = m.ReadBytes(offset, size) - case ValueTypeByte: - data, err = m.ReadByte(offset) - case ValueTypeI32: - data, err = m.ReadUint32(offset) - case ValueTypeI64: - data, err = m.ReadUint64(offset) - case ValueTypeF32: - data, err = m.ReadFloat32(offset) - case ValueTypeF64: - data, err = m.ReadFloat64(offset) - case ValueTypeString: - data, err = m.ReadString(offset, size) - default: - err = fmt.Errorf("Unsupported read data type %s", valueType) - } - - if err != nil { - m.log.Error(err.Error()) - return nil, 0, 0, err - } - - return data, offset, size, err -} -func (m *wazeroMemory) ReadBytes(offset uint32, size uint32) ([]byte, error) { - buf, ok := m.mod.Memory().Read(offset, size) - if !ok { - err := fmt.Errorf("Memory.ReadBytes(%d, %d) out of range of memory size %d", offset, size, m.Size()) - m.log.Error(err.Error()) - return nil, err - } - - return buf, nil -} -func (m *wazeroMemory) ReadBytesPack(pd PackedData) ([]byte, error) { - _, offset, size := utils.UnpackUI64(uint64(pd)) - return m.ReadBytes(offset, size) -} - -func (m *wazeroMemory) ReadByte(offset uint32) (byte, error) { - buf, ok := m.mod.Memory().ReadByte(offset) - if !ok { - err := fmt.Errorf("Memory.ReadByte(%d, %d) out of range of memory size %d", offset, 1, m.Size()) - m.log.Error(err.Error()) - return 0, err - } - - return buf, nil -} -func (m *wazeroMemory) ReadBytePack(pd PackedData) (byte, error) { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - return m.ReadByte(offset) -} - -func (m *wazeroMemory) ReadUint32(offset uint32) (uint32, error) { - data, ok := m.mod.Memory().ReadUint32Le(offset) - if !ok { - err := fmt.Errorf("Memory.ReadUint32(%d, %d) out of range of memory size %d", offset, 4, m.Size()) - m.log.Error(err.Error()) - return 0, err - } - - return data, nil -} -func (m *wazeroMemory) ReadUint32Pack(pd PackedData) (uint32, error) { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - return m.ReadUint32(offset) -} - -func (m *wazeroMemory) ReadUint64(offset uint32) (uint64, error) { - data, ok := m.mod.Memory().ReadUint64Le(offset) - if !ok { - err := fmt.Errorf("Memory.ReadUint64(%d, %d) out of range of memory size %d", offset, 8, m.Size()) - m.log.Error(err.Error()) - return 0, err - } - - return data, nil -} -func (m *wazeroMemory) ReadUint64Pack(pd PackedData) (uint64, error) { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - return m.ReadUint64(offset) -} - -func (m *wazeroMemory) ReadFloat32(offset uint32) (float32, error) { - data, ok := m.mod.Memory().ReadFloat32Le(offset) - if !ok { - err := fmt.Errorf("Memory.ReadFloat32(%d, %d) out of range of memory size %d", offset, 4, m.Size()) - m.log.Error(err.Error()) - return 0, err - } - - return data, nil -} -func (m *wazeroMemory) ReadFloat32Pack(pd PackedData) (float32, error) { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - return m.ReadFloat32(offset) -} - -func (m *wazeroMemory) ReadFloat64(offset uint32) (float64, error) { - data, ok := m.mod.Memory().ReadFloat64Le(offset) - if !ok { - err := fmt.Errorf("Memory.ReadFloat64(%d, %d) out of range of memory size %d", offset, 8, m.Size()) - m.log.Error(err.Error()) - return 0, err - } - - return data, nil -} -func (m *wazeroMemory) ReadFloat64Pack(pd PackedData) (float64, error) { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - return m.ReadFloat64(offset) -} - -func (m *wazeroMemory) ReadString(offset uint32, size uint32) (string, error) { - buf, err := m.ReadBytes(offset, size) - if err != nil { - return "", err - } - - return string(buf), err -} -func (m *wazeroMemory) ReadStringPack(pd PackedData) (string, error) { - _, offset, size := utils.UnpackUI64(uint64(pd)) - return m.ReadString(offset, size) -} - -// WriteAny writes a value of type interface{} to the memory buffer managed by the wazeroMemory instance, -// starting at the given offset. -// -// The method identifies the type of the value and performs the appropriate write operation. -func (m *wazeroMemory) WriteAny(offset uint32, v any) error { - var err error - - switch vTyped := v.(type) { - case []byte: - err = m.WriteBytes(offset, vTyped) - case byte: - err = m.WriteByte(offset, vTyped) - case uint32: - err = m.WriteUint32(offset, vTyped) - case uint64: - err = m.WriteUint64(offset, vTyped) - case float32: - err = m.WriteFloat32(offset, vTyped) - case float64: - err = m.WriteFloat64(offset, vTyped) - case string: - err = m.WriteString(offset, vTyped) - default: - err := fmt.Errorf("unsupported write data type %s", reflect.TypeOf(v)) - m.log.Error(err.Error()) - return err - } - - return err -} -func (m *wazeroMemory) WriteBytes(offset uint32, v []byte) error { - ok := m.mod.Memory().Write(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteBytes(%d, %d) out of range of memory size %d", offset, len(v), m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} - -func (m *wazeroMemory) WriteBytesPack(v []byte) PackedData { - - size := uint32(len(v)) - - offset, err := m.Malloc(size) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteBytes(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeBytes, offset, size) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteByte(offset uint32, v byte) error { - ok := m.mod.Memory().WriteByte(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteByte(%d, %d) out of range of memory size %d", offset, 1, m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteBytePack(v byte) PackedData { - - offset, err := m.Malloc(1) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteByte(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeByte, offset, 1) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteUint32(offset uint32, v uint32) error { - ok := m.mod.Memory().WriteUint32Le(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteUint32(%d, %d) out of range of memory size %d", offset, 4, m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteUint32Pack(v uint32) PackedData { - - offset, err := m.Malloc(4) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteUint32(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeI32, offset, 4) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteUint64(offset uint32, v uint64) error { - ok := m.mod.Memory().WriteUint64Le(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteUint64(%d, %d) out of range of memory size %d", offset, 8, m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteUint64Pack(v uint64) PackedData { - - offset, err := m.Malloc(8) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteUint64(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeI32, offset, 8) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteFloat32(offset uint32, v float32) error { - ok := m.mod.Memory().WriteFloat32Le(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteFloat32(%d, %d) out of range of memory size %d", offset, 8, m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteFloat32Pack(v float32) PackedData { - - offset, err := m.Malloc(4) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteFloat32(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeF32, offset, 4) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteFloat64(offset uint32, v float64) error { - ok := m.mod.Memory().WriteFloat64Le(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteFloat64(%d, %d) out of range of memory size %d", offset, 8, m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteFloat64Pack(v float64) PackedData { - - offset, err := m.Malloc(8) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteFloat64(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeF64, offset, 8) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteString(offset uint32, v string) error { - - ok := m.mod.Memory().WriteString(offset, v) - if !ok { - err := fmt.Errorf("Memory.WriteString(%d, %d) out of range of memory size %d", offset, len(v), m.Size()) - m.log.Error(err.Error()) - return err - } - - return nil -} -func (m *wazeroMemory) WriteStringPack(v string) PackedData { - - size := uint32(len(v)) - - offset, err := m.Malloc(size) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - err = m.WriteString(offset, v) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeString, offset, size) - if err != nil { - m.log.Error(err.Error()) - return 0 - } - - return PackedData(pd) -} - -func (m *wazeroMemory) WriteMultiPack(pds ...PackedData) MultiPackedData { - - size := uint32(len(pds)) * 8 - if size == 0 { - return 0 - } - - offset, err := m.Malloc(size) - if err != nil { - return 0 - } - - pdsU64 := make([]uint64, size) - for _, pd := range pds { - pdsU64 = append(pdsU64, uint64(pd)) - } - - err = m.WriteBytes(offset, utils.Uint64ArrayToBytes(pdsU64)) - if err != nil { - return 0 - } - - pd, err := utils.PackUI64(types.ValueTypeString, offset, size) - if err != nil { - return 0 - } - - return MultiPackedData(pd) -} - -// Size returns the size in bytes available. e.g. If the underlying memory -// has 1 page: 65536 -func (r *wazeroMemory) Size() uint32 { - return r.mod.Memory().Size() -} - -// Malloc allocates memory in wasm linear memory with the specified size. -// -// It invokes the "malloc" GuestFunction of the associated wazeroModule using the provided size parameter. -// Returns the allocated memory offset and any encountered error. -// -// Malloc allows memory allocation from within a host function or externally, -// returning the allocated memory offset to be used in a guest function. -// This can be helpful, for instance, when passing string data from the host to the guest. -// -// NOTE: Always make sure to free memory after allocation. -func (m *wazeroMemory) Malloc(size uint32) (uint32, error) { - - r, err := m.wazeroModule.GuestFunction(m.wazeroModule.ctx, "malloc").call(uint64(size)) - if err != nil { - err = errors.Join(fmt.Errorf("can't invoke malloc function "), err) - return 0, err - } - - offset := uint32(r) - - return offset, nil -} - -// Free releases the memory block at the specified offset in wazeroMemory. -// It invokes the "free" GuestFunction of the associated wazeroModule using the provided offset parameter. -// Returns any encountered error during the memory deallocation. -func (m *wazeroMemory) Free(offsets ...uint32) error { - - for _, offset := range offsets { - _, err := m.wazeroModule.GuestFunction(m.ModuleConfig.ctx, "free").call(uint64(offset)) - if err != nil { - err = errors.Join(fmt.Errorf("can't invoke free function"), err) - return err - } - } - - return nil -} - -func (m *wazeroMemory) FreePack(pds ...PackedData) error { - - for _, pd := range pds { - _, offset, _ := utils.UnpackUI64(uint64(pd)) - if err := m.Free(offset); err != nil { - return err - } - } - - return nil -} diff --git a/testdata/wasm/empty_host_func/main.go b/resources/testdata/wasm/empty_host_func/main.go similarity index 100% rename from testdata/wasm/empty_host_func/main.go rename to resources/testdata/wasm/empty_host_func/main.go diff --git a/testdata/wasm/empty_host_func/main.wasm b/resources/testdata/wasm/empty_host_func/main.wasm similarity index 100% rename from testdata/wasm/empty_host_func/main.wasm rename to resources/testdata/wasm/empty_host_func/main.wasm diff --git a/testdata/wasm/guest_all_available_types/main.go b/resources/testdata/wasm/guest_all_available_types/main.go similarity index 100% rename from testdata/wasm/guest_all_available_types/main.go rename to resources/testdata/wasm/guest_all_available_types/main.go diff --git a/testdata/wasm/guest_all_available_types/main.wasm b/resources/testdata/wasm/guest_all_available_types/main.wasm similarity index 100% rename from testdata/wasm/guest_all_available_types/main.wasm rename to resources/testdata/wasm/guest_all_available_types/main.wasm diff --git a/testdata/wasm/host_all_available_types/main.go b/resources/testdata/wasm/host_all_available_types/main.go similarity index 100% rename from testdata/wasm/host_all_available_types/main.go rename to resources/testdata/wasm/host_all_available_types/main.go diff --git a/testdata/wasm/host_all_available_types/main.wasm b/resources/testdata/wasm/host_all_available_types/main.wasm similarity index 100% rename from testdata/wasm/host_all_available_types/main.wasm rename to resources/testdata/wasm/host_all_available_types/main.wasm diff --git a/resources/tools/build b/resources/tools/build new file mode 100755 index 0000000..b898bc2 --- /dev/null +++ b/resources/tools/build @@ -0,0 +1,21 @@ +#!/bin/bash + +source "$(dirname "${0}")/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")/../..")" +readonly PROJECT_NAME="$(basename "${PROJECT_PATH}")" + +function main() { + local run_all="$(_read_cmd_flag "run-all" "true" "${@}")" + local build_project="$(_read_cmd_flag "build" "${run_all}" "${@}")" + + if _is_truthy "${build_project}"; then + _info "Building project '${PROJECT_NAME}'..." + + _assert_success "${PROJECT_PATH}" go mod tidy + _assert_success "${PROJECT_PATH}" go mod vendor + _assert_success "${PROJECT_PATH}" go build ./... + fi +} + +main "${@}" diff --git a/resources/tools/commons.lib b/resources/tools/commons.lib new file mode 100644 index 0000000..bffb6e1 --- /dev/null +++ b/resources/tools/commons.lib @@ -0,0 +1,141 @@ +#!/bin/bash + +function _info() { + echo "[INF][$(date '+%F|%T')] ${1}" +} + +function _error() { + local message="[ERR][$(date '+%F|%T')] ${1}" + + echo -e "\033[31m${message}\033[0m" +} + +function _load_environment() { + local file_path="${1}"; shift + + while IFS='=' read -r key value; do + if [[ ! -z "${key}" ]] && [[ -z "$(printenv "${key}")" ]]; then + export "${key}"="${value}" + fi + done < "${file_path}" +} + +function _assert_success() { + local work_dir="${1}"; shift + local command="${1}"; shift + local exit_code + + pushd "${work_dir}" > /dev/null 2>&1 + + "${command}" "${@}"; exit_code="${?}" + + popd > /dev/null 2>&1 + + if [[ "${exit_code}" -ne "0" ]]; then + _error "Execution of command ${command} failed with code ${exit_code}" + _error "Execution callstack (subshell: ${BASH_SUBSHELL}):" + for ((index = 0; index < "${#FUNCNAME[@]}" - 1; index++)); do + _error " ${FUNCNAME[${index}]} (invoked at line: ${BASH_LINENO[${index}]})" + done + + exit "${exit_code}" + fi +} + +function _read_cmd_arg() { + local option="${1}"; shift + local default="${1}"; shift + local previous current + + for current in "${@}"; do + if [[ "${previous}" == "${option}" ]]; then + echo "${current}" + return 0 + fi + + previous="${current}" + done + + echo "${default}" +} + +function _read_cmd_flag() { + local flag="${1}"; shift + local value="${1}"; shift # default + local yes_arg_value="$(_read_cmd_arg "--${flag}" "" "${@}")" + local no_arg_value="$(_read_cmd_arg "--no-${flag}" "" "${@}")" + + if _is_truthy "${no_arg_value}" || _is_falsy "${yes_arg_value}" || _is_raised "--no-${flag}" "${@}"; then + value="false" + elif _is_truthy "${yes_arg_value}" || _is_falsy "${no_arg_value}" || _is_raised "--${flag}" "${@}"; then + value="true" + fi + + echo "${value}" +} + +function _is_truthy() { + local value="${1}"; shift + + [[ "${value}" == "y" ]] \ + || [[ "${value}" == "t" ]] \ + || [[ "${value}" == "on" ]] \ + || [[ "${value}" == "yes" ]] \ + || [[ "${value}" == "true" ]] +} + +function _is_falsy() { + local value="${1}"; shift + + [[ "${value}" == "n" ]] \ + || [[ "${value}" == "f" ]] \ + || [[ "${value}" == "no" ]] \ + || [[ "${value}" == "off" ]] \ + || [[ "${value}" == "false" ]] +} + +function _download_file() { + local download_url="${1}"; shift + local download_path="${1}"; shift + + test -e "${download_path}" || wget -O "${download_path}" "${download_url}" +} + +function _ensure_installed() { + local executable="${1}"; shift + local package_name="$([[ "$(uname)" == "Darwin" ]] && echo "${1}" || echo "${2:-${1}}")" + local installer="$([[ "$(uname)" == "Darwin" ]] && echo brew || echo apt-get)" + + if [[ -z "$(which "${executable}")" ]]; then + sudo "${installer}" install "${package_name}" + fi +} + +function _negate() { + local value="${1}"; shift + + if _is_falsy "${value}"; then + echo "true" + else + echo "false" + fi +} + +function _is_raised() { + local flag="${1}"; shift + local value="$(_read_cmd_arg "${flag}" "" "${@}")" + + ! _is_truthy "${value}" && ! _is_falsy "${value}" && _contains "${flag}" "${@}" +} + +function _contains() { + local target="${1}"; shift + + for value in "${@}"; do + if [[ "${value}" == "${target}" ]]; then + return 0 + fi + done + + return 1 +} \ No newline at end of file diff --git a/resources/tools/lint b/resources/tools/lint new file mode 100755 index 0000000..b36d389 --- /dev/null +++ b/resources/tools/lint @@ -0,0 +1,25 @@ +#!/bin/bash + +source "$(dirname "${0}")/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")/../..")" +readonly PROJECT_NAME="$(basename "${PROJECT_PATH}")" + +readonly GOLANGCI_LINT_VERSION="latest" +readonly GOLANGCI_LINT_REPO="github.com/golangci/golangci-lint/cmd/golangci-lint" + +function main() { + local run_all="$(_read_cmd_flag "run-all" "true" "${@}")" + local lint_project="$(_read_cmd_flag "lint" "${run_all}" "${@}")" + + if _is_truthy "${lint_project}"; then + _info "Running linter for project '${PROJECT_NAME}'..." + + _assert_success "${PROJECT_PATH}" go install -mod=mod "${GOLANGCI_LINT_REPO}@${GOLANGCI_LINT_VERSION}" + _assert_success "${PROJECT_PATH}" go vet ./... + _assert_success "${PROJECT_PATH}" golangci-lint cache clean + _assert_success "${PROJECT_PATH}" golangci-lint run --modules-download-mode=readonly + fi +} + +main "${@}" diff --git a/resources/tools/localdev b/resources/tools/localdev new file mode 100755 index 0000000..99430ea --- /dev/null +++ b/resources/tools/localdev @@ -0,0 +1,12 @@ +#!/bin/bash + +source "$(dirname "${0}")/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")/../..")" + +function main() { + _load_environment "${PROJECT_PATH}/resources/environments/localdev.env" + _assert_success "${PROJECT_PATH}" go run main.go +} + +main "${@}" diff --git a/resources/tools/test b/resources/tools/test new file mode 100755 index 0000000..21b107c --- /dev/null +++ b/resources/tools/test @@ -0,0 +1,20 @@ +#!/bin/bash + +source "$(dirname "${0}")/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")/../..")" +readonly PROJECT_NAME="$(basename "${PROJECT_PATH}")" + +function main() { + local run_all="$(_read_cmd_flag "run-all" "true" "${@}")" + local run_tests="$(_read_cmd_flag "test" "${run_all}" "${@}")" + + if _is_truthy "${run_tests}"; then + _info "Running tests for project '${PROJECT_NAME}'..." + + _assert_success "${path}" go clean -testcache + _assert_success "${path}" go test -mod=readonly -coverprofile cover.out -timeout=20s ./... + fi +} + +main "${@}" diff --git a/resources/tools/updeps b/resources/tools/updeps new file mode 100755 index 0000000..1760eda --- /dev/null +++ b/resources/tools/updeps @@ -0,0 +1,19 @@ +#!/bin/bash + +source "$(dirname "${0}")/commons.lib" + +readonly PROJECT_PATH="$(realpath "$(dirname "${0}")/../..")" +readonly PROJECT_NAME="$(basename "${PROJECT_PATH}")" + +function main() { + local run_all="$(_read_cmd_flag "run-all" "true" "${@}")" + local update_dependencies="$(_read_cmd_flag "update" "${run_all}" "${@}")" + + if _is_truthy "${update_dependencies}"; then + _info "Updating dependencies for project '${PROJECT_NAME}'..." + + _assert_success "${path}" go get -t -u ./... + fi +} + +main "${@}" diff --git a/runtime.go b/runtime.go index 5d820b6..5b0e6a2 100644 --- a/runtime.go +++ b/runtime.go @@ -3,87 +3,33 @@ package wasify import ( "context" "errors" - "log/slog" - "github.com/wasify-io/wasify-go/internal/utils" + "github.com/wasify-io/wasify-go/models" + "github.com/wasify-io/wasify-go/wazero" ) -type LogSeverity utils.LogSeverity - -// The log level is initially set to "Info" for runtimes and "zero" (0) for modules. -// However, modules will adopt the log level from their parent runtime. -// If you want only "Error" level for a runtime but need to debug specific module(s), -// you can set those modules to "Debug". This will replace the inherited log level, -// allowing the module to display debug information. -const ( - LogDebug LogSeverity = LogSeverity(utils.LogDebug) - LogInfo LogSeverity = LogSeverity(utils.LogInfo) - LogWarning LogSeverity = LogSeverity(utils.LogWarning) - LogError LogSeverity = LogSeverity(utils.LogError) -) - -type Runtime interface { - NewModule(context.Context, *ModuleConfig) (Module, error) - Close(ctx context.Context) error -} - -// RuntimeType defines a type of WebAssembly (wasm) runtime. -// -// Currently, the only supported wasm runtime is Wazero. -// However, in the future, more runtimes could be added. -// This means that you'll be able to run modules -// on various wasm runtimes. -type RuntimeType uint8 - -const ( - RuntimeWazero RuntimeType = iota -) - -func (rt RuntimeType) String() (runtimeName string) { - - switch rt { - case RuntimeWazero: - runtimeName = "Wazero" - } - - return -} - -// The RuntimeConfig struct holds configuration settings for a runtime. -type RuntimeConfig struct { - // Specifies the type of runtime being used. - Runtime RuntimeType - // Determines the severity level of logging. - LogSeverity LogSeverity - // Pointer to a logger for recording runtime information. - log *slog.Logger -} - // NewRuntime creates and initializes a new runtime based on the provided configuration. // It returns the initialized runtime and any error that might occur during the process. -func NewRuntime(ctx context.Context, c *RuntimeConfig) (runtime Runtime, err error) { - - c.log = utils.NewLogger(utils.LogSeverity(c.LogSeverity)) - - c.log.Info("runtime has been initialized successfully", "runtime", c.Runtime) +func NewRuntime(ctx context.Context, config *models.RuntimeConfig) (models.Runtime, error) { + config.Logger.Info("runtime has been initialized successfully", "runtime", config.Runtime) // Retrieve the appropriate runtime implementation based on the configured type. - runtime = c.getRuntime(ctx) + runtime := getRuntime(ctx, config) if runtime == nil { - err = errors.New("unsupported runtime") - c.log.Error(err.Error(), "runtime", c.Runtime) - return + err := errors.New("unsupported runtime") + config.Logger.Error(err.Error(), "runtime", config.Runtime) + return nil, err } - return + return runtime, nil } // getRuntime returns an instance of the appropriate runtime implementation // based on the configured runtime type in the RuntimeConfig. -func (c *RuntimeConfig) getRuntime(ctx context.Context) Runtime { - switch c.Runtime { - case RuntimeWazero: - return getWazeroRuntime(ctx, c) +func getRuntime(ctx context.Context, config *models.RuntimeConfig) models.Runtime { + switch config.Runtime { + case models.RuntimeWazero: + return wazero.NewRuntime(ctx, config) default: return nil } diff --git a/runtime_test.go b/runtime_test.go index d6b2d38..77e2f2e 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -5,13 +5,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/wasify-io/wasify-go/logging" + "github.com/wasify-io/wasify-go/models" ) func TestNewRuntime(t *testing.T) { ctx := context.Background() - runtimeConfig := &RuntimeConfig{ - Runtime: RuntimeWazero, + runtimeConfig := &models.RuntimeConfig{ + Runtime: models.RuntimeWazero, + Logger: logging.NewSlogLogger(logging.LogInfo), } runtime, err := NewRuntime(ctx, runtimeConfig) @@ -22,8 +25,9 @@ func TestNewRuntime(t *testing.T) { func TestNewRuntimeUnsupported(t *testing.T) { ctx := context.Background() - runtimeConfig := &RuntimeConfig{ + runtimeConfig := &models.RuntimeConfig{ Runtime: 255, // Assuming this is an unsupported value + Logger: logging.NewSlogLogger(logging.LogInfo), } runtime, err := NewRuntime(ctx, runtimeConfig) @@ -32,5 +36,5 @@ func TestNewRuntimeUnsupported(t *testing.T) { } func TestRuntimeTypeString(t *testing.T) { - assert.Equal(t, "Wazero", RuntimeWazero.String(), "Expected Wazero string representation") + assert.Equal(t, "Wazero", models.RuntimeWazero.String(), "Expected Wazero string representation") } diff --git a/runtime_wazero.go b/runtime_wazero.go deleted file mode 100644 index c884ee1..0000000 --- a/runtime_wazero.go +++ /dev/null @@ -1,254 +0,0 @@ -// This file provides abstractions and implementations for interacting with -// different WebAssembly runtimes, specifically focusing on the Wazero runtime. -package wasify - -import ( - "context" - "errors" - "os" - - "github.com/tetratelabs/wazero" - "github.com/tetratelabs/wazero/api" - "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" - "github.com/wasify-io/wasify-go/internal/utils" -) - -// getWazeroRuntime creates and returns a wazero runtime instance using the provided context and -// RuntimeConfig. It configures the runtime with specific settings and features. -func getWazeroRuntime(ctx context.Context, c *RuntimeConfig) *wazeroRuntime { - // TODO: Allow user to control the following options: - // 1. WithCloseOnContextDone - // 2. Memory - // Create a new wazero runtime instance with specified configuration options. - runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig(). - WithCoreFeatures(api.CoreFeaturesV2). - WithCustomSections(false). - WithCloseOnContextDone(false). - // Enable runtime debug if user sets LogSeverity to debug level in runtime configuration - WithDebugInfoEnabled(c.LogSeverity == LogDebug), - ) - // Instantiate the runtime with the WASI snapshot preview1. - wasi_snapshot_preview1.MustInstantiate(ctx, runtime) - return &wazeroRuntime{runtime, c} -} - -// The wazeroRuntime struct combines a wazero runtime instance with runtime configuration. -type wazeroRuntime struct { - runtime wazero.Runtime - *RuntimeConfig -} - -// NewModule creates a new module instance based on the provided ModuleConfig within -// -// the wazero runtime context. It returns the created module and any potential error. -func (r *wazeroRuntime) NewModule(ctx context.Context, moduleConfig *ModuleConfig) (Module, error) { - - // Set the context, logger and any missing data for the moduleConfig. - moduleConfig.ctx = ctx - moduleConfig.log = r.log - - // Create a new wazeroModule instance and set its ModuleConfig. - // Read more about wazeroModule in module_wazero.go - wazeroModule := new(wazeroModule) - wazeroModule.ModuleConfig = moduleConfig - - // If LogSeverity is set, create a new logger instance for the module. - // - // Module will adopt the log level from their parent runtime. - // If you want only "Error" level for a runtime but need to debug specific module(s), - // you can set those modules to "Debug". This will replace the inherited log level, - // allowing the module to display debug information. - if moduleConfig.LogSeverity != 0 { - moduleConfig.log = utils.NewLogger(utils.LogSeverity(moduleConfig.LogSeverity)) - } - - // Check and compare hashes if provided in the moduleConfig. - if moduleConfig.Wasm.Hash != "" { - actualHash, err := utils.CalculateHash(moduleConfig.Wasm.Binary) - if err != nil { - err = errors.Join(errors.New("can't calculate the hash"), err) - moduleConfig.log.Warn(err.Error(), "namespace", moduleConfig.Namespace, "needed hash", moduleConfig.Wasm.Hash, "actual wasm hash", actualHash) - return nil, err - } - moduleConfig.log.Info("hash calculation", "namespace", moduleConfig.Namespace, "needed hash", moduleConfig.Wasm.Hash, "actual wasm hash", actualHash) - - err = utils.CompareHashes(actualHash, moduleConfig.Wasm.Hash) - if err != nil { - moduleConfig.log.Warn(err.Error(), "namespace", moduleConfig.Namespace, "needed hash", moduleConfig.Wasm.Hash, "actual wasm hash", actualHash) - return nil, err - } - } - - // Instantiate host functions and configure wazeroModule accordingly. - err := r.instantiateHostFunctions(ctx, wazeroModule, moduleConfig) - if err != nil { - moduleConfig.log.Error(err.Error(), "namespace", moduleConfig.Namespace) - r.log.Error(err.Error(), "runtime", r.Runtime, "namespace", moduleConfig.Namespace) - return nil, err - } - - moduleConfig.log.Info("host functions has been instantiated successfully", "namespace", moduleConfig.Namespace) - - // Instantiate the module and set it in wazeroModule. - mod, err := r.instantiateModule(ctx, moduleConfig) - if err != nil { - moduleConfig.log.Error(err.Error(), "namespace", moduleConfig.Namespace) - r.log.Error(err.Error(), "runtime", r.Runtime, "namespace", moduleConfig.Namespace) - return nil, err - } - - moduleConfig.log.Info("module has been instantiated successfully", "namespace", moduleConfig.Namespace) - - wazeroModule.mod = mod - - return wazeroModule, nil -} - -// convertToAPIValueTypes converts an array of ValueType values to their corresponding -// api.ValueType representations used by the Wazero runtime. -// -// ValueType describes a parameter or result type mapped to a WebAssembly -// function signature. -func (r *wazeroRuntime) convertToAPIValueTypes(types []ValueType) []api.ValueType { - valueTypes := make([]api.ValueType, len(types)) - for i, t := range types { - switch t { - case - ValueTypeBytes, - ValueTypeByte, - ValueTypeI32, - ValueTypeI64, - ValueTypeF32, - ValueTypeF64, - ValueTypeString: - valueTypes[i] = api.ValueTypeI64 - } - } - - return valueTypes -} - -// instantiateHostFunctions sets up and exports host functions for the module using the wazero runtime. -// -// It configures host function callbacks, data types, and exports. -func (r *wazeroRuntime) instantiateHostFunctions(ctx context.Context, wazeroModule *wazeroModule, moduleConfig *ModuleConfig) error { - - modBuilder := r.runtime.NewHostModuleBuilder(moduleConfig.Namespace) - - // Iterate over the module's host functions and set up exports. - for _, hostFunc := range moduleConfig.HostFunctions { - - // Create a new local variable inside the loop to ensure that - // each closure captures its own unique variable. This prevents - // the inadvertent capturing of the loop iterator variable, which - // would result in all closures referencing the last element - // in the moduleConfig.HostFunctions slice. - hf := hostFunc - - moduleConfig.log.Debug("build host function", "namespace", moduleConfig.Namespace, "function", hf.Name) - - // Associate the host function with module-related information. - // This configuration ensures that the host function can access ModuleConfig data from various contexts. - // See host_function.go for more details. - hf.moduleConfig = moduleConfig - - // If hsot function has any return values, we pack it as a single uint64 - var resultValuesPackedData = []ValueType{} - if len(hf.Results) > 0 { - resultValuesPackedData = []ValueType{ValueTypeI64} - } - - modBuilder = modBuilder. - NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(wazeroHostFunctionCallback(wazeroModule, moduleConfig, &hf)), - r.convertToAPIValueTypes(hf.Params), - r.convertToAPIValueTypes(resultValuesPackedData), - ). - Export(hf.Name) - - } - - // Instantiate user defined host functions - _, err := modBuilder.Instantiate(ctx) - if err != nil { - err = errors.Join(errors.New("can't instantiate NewHostModuleBuilder [user-defined host funcs]"), err) - return err - } - - // NewHostModuleBuilder for wasify pre-defined host functions - modBuilder = r.runtime.NewHostModuleBuilder(WASIFY_NAMESPACE) - - // initialize pre-defined host functions and pass any necessary configurations - hf := newHostFunctions(moduleConfig) - - // register pre-defined host functions - log := hf.newLog() - - // host logger - modBuilder. - NewFunctionBuilder(). - WithGoModuleFunction(api.GoModuleFunc(wazeroHostFunctionCallback(wazeroModule, moduleConfig, log)), - r.convertToAPIValueTypes(log.Params), - r.convertToAPIValueTypes(log.Results), - ). - Export(log.Name) - - _, err = modBuilder.Instantiate(ctx) - if err != nil { - err = errors.Join(errors.New("can't instantiate wasify NewHostModuleBuilder [pre-defined host funcs]"), err) - return err - } - - return nil - -} - -// instantiateModule compiles and instantiates a WebAssembly module using the wazero runtime. -// -// It compiles the module, creates a module configuration, and then instantiates the module. -// Returns the instantiated module and any potential error. -func (r *wazeroRuntime) instantiateModule(ctx context.Context, moduleConfig *ModuleConfig) (api.Module, error) { - - // Compile the provided WebAssembly binary. - compiled, err := r.runtime.CompileModule(ctx, moduleConfig.Wasm.Binary) - if err != nil { - return nil, errors.Join(errors.New("can't compile module"), err) - } - - // TODO: Add more configurations - cfg := wazero.NewModuleConfig() - cfg = cfg.WithStdin(os.Stdin) - cfg = cfg.WithStdout(os.Stdout) - cfg = cfg.WithStderr(os.Stderr) - - if moduleConfig != nil && moduleConfig.FSConfig.Enabled { - cfg = cfg.WithFSConfig( - wazero.NewFSConfig(). - WithDirMount(moduleConfig.FSConfig.HostDir, moduleConfig.FSConfig.getGuestDir()), - ) - } - - // Instantiate the compiled module with the provided module configuration. - mod, err := r.runtime.InstantiateModule(ctx, compiled, cfg) - if err != nil { - return nil, errors.Join(errors.New("can't instantiate module"), err) - } - - return mod, nil -} - -// Close closes the resource. -// -// Note: The context parameter is used for value lookup, such as for -// logging. A canceled or otherwise done context will not prevent Close -// from succeeding. -func (r *wazeroRuntime) Close(ctx context.Context) error { - err := r.runtime.Close(ctx) - if err != nil { - err = errors.Join(errors.New("can't close runtime"), err) - r.log.Error(err.Error(), "runtime", r.Runtime) - return err - } - - return nil -} diff --git a/wazero/guest_function.go b/wazero/guest_function.go new file mode 100644 index 0000000..4b078f7 --- /dev/null +++ b/wazero/guest_function.go @@ -0,0 +1,113 @@ +package wazero + +import ( + "context" + "errors" + "fmt" + + "github.com/tetratelabs/wazero/api" + "github.com/wasify-io/wasify-go/internal/types" + . "github.com/wasify-io/wasify-go/internal/utils" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" +) + +type _GuestFunction struct { + name string + ctx context.Context + fn api.Function + memory Memory + config *ModuleConfig +} + +// Invoke calls a specified guest function with the provided parameters. It ensures proper memory management, +// data conversion, and compatibility with data types. Each parameter is converted to its packedData format, +// which provides a compact representation of its memory offset, size, and type information. This packedData +// is written into the WebAssembly memory, allowing the guest function to correctly interpret and use the data. +// +// While the method takes care of memory allocation for the parameters and writing them to memory, it does +// not handle freeing the allocated memory. If an error occurs at any step, from data conversion to memory +// allocation, or during the guest function invocation, the error is logged, and the function returns with an error. +// +// Example: +// +// res := guest.GuestFunction(ctx, "guestTest").Invoke([]byte("bytes!"), uint32(32), float32(32.0), "Wasify") +// +// params ...any: A variadic list of parameters of any type that the user wants to pass to the guest function. +// +// Return value: The result of invoking the guest function in the form of a GuestFunctionResult pointer, +// or an error if any step in the process fails. +func (self *_GuestFunction) Invoke(params ...any) GuestFunctionResult { + self.config.Logger.Log( + self.severity(), + "calling guest function", "namespace", self.config.Namespace, "function", self.name, "params", params, + ) + + stack, err := self.process(make([]uint64, len(params)), params...) + if err != nil { + return self.errorResult(Aggregate(fmt.Sprintf("failed to process parameters for guest function %s", self.name), err)) + } + + data, err := self.call(stack...) + if err != nil { + return self.errorResult(Aggregate(fmt.Sprintf("failed to invoke the guest function '%s'", self.name), err)) + } + + return _NewGuestFunctionResult(nil, data, self.memory) +} + +func (self *_GuestFunction) severity() logging.LogSeverity { + return Ternary( + OneOf(self.config.Namespace, "malloc", "free"), + logging.LogDebug, + logging.LogInfo, + ) +} + +func (self *_GuestFunction) errorResult(err error) GuestFunctionResult { + return _NewGuestFunctionResult(Log(self.config.Logger, err), 0, nil) +} + +// Call invokes wazero's CallWithStack method, which returns ome uint64 message, +// in most cases it is used to call built in methods such as "malloc", "free" +// See wazero's CallWithStack for more details. +func (self *_GuestFunction) call(params ...uint64) (MultiPackedData, error) { + // size of params len(params) + one size for return uint64 value + stack := make([]uint64, len(params)+1) + copy(stack, params) + + err := self.fn.CallWithStack(self.ctx, stack[:]) + if err != nil { + err = errors.Join(errors.New("error invoking internal call func"), err) + self.config.Logger.Error(err.Error()) + return 0, err + } + + return MultiPackedData(stack[0]), nil +} + +func (self *_GuestFunction) process(stack []uint64, params ...any) ([]uint64, error) { + for i, p := range params { + valueType, offsetSize, err := types.GetOffsetSizeAndDataTypeByConversion(p) + if err != nil { + return stack, Aggregate(fmt.Sprintf("failed to convert guest function parameter %s", self.name), err) + } + + // allocate memory for each value + offsetI32, err := self.memory.Malloc(offsetSize) + if err != nil { + return stack, Aggregate(fmt.Sprintf("an error occurred while attempting to alloc memory for guest func param in: %s", self.name), err) + } + + if err = self.memory.WriteAny(offsetI32, p); err != nil { + return stack, Aggregate("failed to write arg to memory", err) + } + + stack[i], err = PackUI64(valueType, offsetI32, offsetSize) + if err != nil { + return stack, Aggregate(fmt.Sprintf("failed to pack data for guest func param in: %s", self.name), err) + } + } + + return stack, nil +} diff --git a/wazero/guest_function_result.go b/wazero/guest_function_result.go new file mode 100644 index 0000000..d985b10 --- /dev/null +++ b/wazero/guest_function_result.go @@ -0,0 +1,108 @@ +package wazero + +import ( + "errors" + "fmt" + + "github.com/wasify-io/wasify-go/internal/types" + . "github.com/wasify-io/wasify-go/internal/utils" + . "github.com/wasify-io/wasify-go/models" +) + +func _NewGuestFunctionResult(err error, data MultiPackedData, memory RMemory) GuestFunctionResult { + if err != nil { + return &_GuestFunctionResult{err: err} + } + + if types.ValueType(data>>56) != types.ValueTypePack { + return &_GuestFunctionResult{memory: memory} + } + + values, err := read(data, memory) + if err != nil { + return &_GuestFunctionResult{err: err} + } + + return &_GuestFunctionResult{ + memory: memory, + values: values, + } +} + +type _GuestFunctionResult struct { + err error + memory RMemory + values []PackedData +} + +func (self *_GuestFunctionResult) Close() error { + return self.memory.FreePack(self.values...) +} + +func (self *_GuestFunctionResult) Error() error { + return self.err +} + +func (self *_GuestFunctionResult) Values() []PackedData { + return self.values +} + +func (self *_GuestFunctionResult) ReadAnyPack(index int) (any, uint32, uint32, error) { + return self.memory.ReadAnyPack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadBytesPack(index int) ([]byte, error) { + return self.memory.ReadBytesPack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadBytePack(index int) (byte, error) { + return self.memory.ReadBytePack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadUint32Pack(index int) (uint32, error) { + return self.memory.ReadUint32Pack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadUint64Pack(index int) (uint64, error) { + return self.memory.ReadUint64Pack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadFloat32Pack(index int) (float32, error) { + return self.memory.ReadFloat32Pack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadFloat64Pack(index int) (float64, error) { + return self.memory.ReadFloat64Pack(self.values[index]) +} + +func (self *_GuestFunctionResult) ReadStringPack(index int) (string, error) { + return self.memory.ReadStringPack(self.values[index]) +} + +func read(data MultiPackedData, memory RMemory) ([]PackedData, error) { + if data == 0 { + return nil, errors.New("packedData is empty") + } + + t, offsetU32, size := UnpackUI64[ValueType](uint64(data)) + if t != ValueType(255) { + return nil, fmt.Errorf("invalid data type found, expected %d, got %d", types.ValueTypePack, t) + } + + bytes, err := memory.ReadBytes(offsetU32, size) + if err != nil { + return nil, errors.Join(errors.New("failed to read data"), err) + } + + err = memory.FreePack(PackedData(data)) + if err != nil { + return nil, errors.Join(errors.New("failed to free up pack data"), err) + } + + return Map( + BytesToUint64Array(bytes), + func(data uint64) PackedData { + return PackedData(data) + }, + ), nil +} diff --git a/wazero/guest_function_test.go b/wazero/guest_function_test.go new file mode 100644 index 0000000..f304708 --- /dev/null +++ b/wazero/guest_function_test.go @@ -0,0 +1,44 @@ +package wazero_test + +import ( + "context" + _ "embed" + "testing" + + "github.com/stretchr/testify/assert" + test_utils "github.com/wasify-io/wasify-go/internal/test-utils" + "github.com/wasify-io/wasify-go/logging" + "github.com/wasify-io/wasify-go/models" +) + +func TestGuestFunctions(t *testing.T) { + t.Run("successful instantiation", func(t *testing.T) { + ctx := context.Background() + + runtime := test_utils.CreateRuntime(t, ctx, &models.RuntimeConfig{ + Runtime: models.RuntimeWazero, + Logger: logging.NewSlogLogger(logging.LogError), + }) + + module := test_utils.CreateModule(t, runtime, &models.ModuleConfig{ + Context: context.Background(), + Namespace: "guest_all_available_types", + Logger: logging.NewSlogLogger(logging.LogInfo), + Wasm: test_utils.LoadTestWASM(t, "guest_all_available_types"), + }) + + result := module.GuestFunction(ctx, "guestTest").Invoke( + []byte("bytes!"), + byte(1), + uint32(32), + uint64(64), + float32(32.0), + float64(64.01), + "Wasify", + "any type", + ) + assert.NoError(t, result.Error()) + + t.Log("TestGuestFunctions RES:", result) + }) +} diff --git a/host_function_wazero.go b/wazero/host_function.go similarity index 81% rename from host_function_wazero.go rename to wazero/host_function.go index 0ad805e..7ccdb19 100644 --- a/host_function_wazero.go +++ b/wazero/host_function.go @@ -1,9 +1,11 @@ -package wasify +package wazero import ( "context" "github.com/tetratelabs/wazero/api" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" ) // wazeroHostFunctionCallback returns a callback function that acts as a bridge between @@ -50,23 +52,15 @@ import ( // // Return value: A callback function that takes a context, api.Module, and a stack of parameters, // and handles the integration of the host function within the wazero runtime. -func wazeroHostFunctionCallback(wazeroModule *wazeroModule, moduleConfig *ModuleConfig, hf *HostFunction) func(context.Context, api.Module, []uint64) { - +func wazeroHostFunctionCallback(logger logging.Logger, module *_Module, function *HostFunction) func(context.Context, api.Module, []uint64) { return func(ctx context.Context, mod api.Module, stack []uint64) { - - wazeroModule.mod = mod - moduleProxy := &ModuleProxy{ - Memory: wazeroModule.Memory(), - } - - params, err := hf.preHostFunctionCallback(ctx, moduleProxy, stack) + params, err := function.PreHostFunctionCallback(stack) if err != nil { - moduleConfig.log.Error(err.Error(), "namespace", wazeroModule.Namespace, "func", hf.Name) + logger.Error(err.Error(), "namespace", module.config.Namespace, "func", function.Name) } - results := hf.Callback(ctx, moduleProxy, params) - - hf.postHostFunctionCallback(ctx, moduleProxy, results, stack) + results := function.Callback(ctx, module, params) + function.PostHostFunctionCallback(results, stack) } } diff --git a/wazero/memory.go b/wazero/memory.go new file mode 100644 index 0000000..365f3b4 --- /dev/null +++ b/wazero/memory.go @@ -0,0 +1,446 @@ +package wazero + +import ( + "fmt" + "reflect" + + "github.com/wasify-io/wasify-go/internal/types" + . "github.com/wasify-io/wasify-go/internal/utils" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" +) + +type _Memory struct { + module *_Module + logger logging.Logger +} + +// ReadAnyPack extracts and reads data from a packed memory location. +// +// Given a packed data representation, this function determines the type, offset, and size of the data to be read. +// It then reads the data from the specified offset and returns it. +// +// Returns: +// - offset: The memory location where the data starts. +// - size: The size or length of the data. +// - data: The actual extracted data of the determined type (i.e., byte slice, uint32, uint64, float32, float64). +// - error: An error if encountered (e.g., unsupported data type, out-of-range error). +func (self *_Memory) ReadAnyPack(pd PackedData) (any, uint32, uint32, error) { + var err error + var data any + + // Unpack the packedData to extract offset and size values. + valueType, offset, size := UnpackUI64[ValueType](uint64(pd)) + + switch valueType { + case ValueTypeBytes: + data, err = self.ReadBytes(offset, size) + case ValueTypeByte: + data, err = self.ReadByte(offset) + case ValueTypeI32: + data, err = self.ReadUint32(offset) + case ValueTypeI64: + data, err = self.ReadUint64(offset) + case ValueTypeF32: + data, err = self.ReadFloat32(offset) + case ValueTypeF64: + data, err = self.ReadFloat64(offset) + case ValueTypeString: + data, err = self.ReadString(offset, size) + default: + err = fmt.Errorf("unsupported read data type %v", valueType) + } + + if err != nil { + self.logger.Error(err.Error()) + return nil, 0, 0, err + } + + return data, offset, size, err +} + +func (self *_Memory) ReadBytes(offset uint32, size uint32) ([]byte, error) { + buf, ok := self.module.raw.Memory().Read(offset, size) + if !ok { + err := fmt.Errorf("Memory.ReadBytes(%d, %d) out of range of memory size %d", offset, size, self.Size()) + self.logger.Error(err.Error()) + return nil, err + } + + return buf, nil +} + +func (self *_Memory) ReadBytesPack(pd PackedData) ([]byte, error) { + _, offset, size := UnpackUI64[ValueType](uint64(pd)) + return self.ReadBytes(offset, size) +} + +func (self *_Memory) ReadByte(offset uint32) (byte, error) { + buf, ok := self.module.raw.Memory().ReadByte(offset) + if !ok { + err := fmt.Errorf("Memory.ReadByte(%d, %d) out of range of memory size %d", offset, 1, self.Size()) + self.logger.Error(err.Error()) + return 0, err + } + + return buf, nil +} + +func (self *_Memory) ReadBytePack(pd PackedData) (byte, error) { + return self.ReadByte(Second(UnpackUI64[ValueType](uint64(pd)))) +} + +func (self *_Memory) ReadUint32(offset uint32) (uint32, error) { + data, ok := self.module.raw.Memory().ReadUint32Le(offset) + if !ok { + err := fmt.Errorf("Memory.ReadUint32(%d, %d) out of range of memory size %d", offset, 4, self.Size()) + self.logger.Error(err.Error()) + return 0, err + } + + return data, nil +} + +func (self *_Memory) ReadUint32Pack(pd PackedData) (uint32, error) { + return self.ReadUint32(Second(UnpackUI64[ValueType](uint64(pd)))) +} + +func (self *_Memory) ReadUint64(offset uint32) (uint64, error) { + data, ok := self.module.raw.Memory().ReadUint64Le(offset) + if !ok { + err := fmt.Errorf("Memory.ReadUint64(%d, %d) out of range of memory size %d", offset, 8, self.Size()) + self.logger.Error(err.Error()) + return 0, err + } + + return data, nil +} + +func (self *_Memory) ReadUint64Pack(pd PackedData) (uint64, error) { + return self.ReadUint64(Second(UnpackUI64[ValueType](uint64(pd)))) +} + +func (self *_Memory) ReadFloat32(offset uint32) (float32, error) { + data, ok := self.module.raw.Memory().ReadFloat32Le(offset) + if !ok { + err := fmt.Errorf("Memory.ReadFloat32(%d, %d) out of range of memory size %d", offset, 4, self.Size()) + self.logger.Error(err.Error()) + return 0, err + } + + return data, nil +} + +func (self *_Memory) ReadFloat32Pack(pd PackedData) (float32, error) { + return self.ReadFloat32(Second(UnpackUI64[ValueType](uint64(pd)))) +} + +func (self *_Memory) ReadFloat64(offset uint32) (float64, error) { + data, ok := self.module.raw.Memory().ReadFloat64Le(offset) + if !ok { + err := fmt.Errorf("Memory.ReadFloat64(%d, %d) out of range of memory size %d", offset, 8, self.Size()) + self.logger.Error(err.Error()) + return 0, err + } + + return data, nil +} + +func (self *_Memory) ReadFloat64Pack(pd PackedData) (float64, error) { + return self.ReadFloat64(Second(UnpackUI64[ValueType](uint64(pd)))) +} + +func (self *_Memory) ReadString(offset uint32, size uint32) (string, error) { + buf, err := self.ReadBytes(offset, size) + if err != nil { + return "", err + } + + return string(buf), err +} + +func (self *_Memory) ReadStringPack(pd PackedData) (string, error) { + _, offset, size := UnpackUI64[ValueType](uint64(pd)) + return self.ReadString(offset, size) +} + +// WriteAny writes a value of type interface{} to the memory buffer managed by the wazeroMemory instance, +// starting at the given offset. +// +// The method identifies the type of the value and performs the appropriate write operation. +func (self *_Memory) WriteAny(offset uint32, v any) error { + var err error + + switch vTyped := v.(type) { + case []byte: + err = self.WriteBytes(offset, vTyped) + case byte: + err = self.WriteByte(offset, vTyped) + case uint32: + err = self.WriteUint32(offset, vTyped) + case uint64: + err = self.WriteUint64(offset, vTyped) + case float32: + err = self.WriteFloat32(offset, vTyped) + case float64: + err = self.WriteFloat64(offset, vTyped) + case string: + err = self.WriteString(offset, vTyped) + default: + err := fmt.Errorf("unsupported write data type %s", reflect.TypeOf(v)) + self.logger.Error(err.Error()) + return err + } + + return err +} + +func (self *_Memory) WriteBytes(offset uint32, v []byte) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().Write(offset, v), + nil, + fmt.Errorf("Memory.WriteBytes(%d, %d) out of range of memory size %d", offset, len(v), self.Size()), + ))) +} + +func (self *_Memory) WriteBytesPack(v []byte) PackedData { + size := uint32(len(v)) + + offset, err := self.Malloc(size) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + err = self.WriteBytes(offset, v) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeBytes, offset, size) +} + +func (self *_Memory) WriteByte(offset uint32, v byte) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteByte(offset, v), + nil, + fmt.Errorf("Memory.WriteByte(%d, %d) out of range of memory size %d", offset, 1, self.Size()), + ))) +} + +func (self *_Memory) WriteBytePack(v byte) PackedData { + offset, err := self.Malloc(1) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + err = self.WriteByte(offset, v) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeByte, offset, 1) +} + +func (self *_Memory) WriteUint32(offset uint32, v uint32) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteUint32Le(offset, v), + nil, + fmt.Errorf("Memory.WriteUint32(%d, %d) out of range of memory size %d", offset, 4, self.Size()), + ))) +} + +func (self *_Memory) WriteUint32Pack(v uint32) PackedData { + offset, err := self.Malloc(4) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + err = self.WriteUint32(offset, v) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeI32, offset, 4) +} + +func (self *_Memory) WriteUint64(offset uint32, v uint64) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteUint64Le(offset, v), + nil, + fmt.Errorf("Memory.WriteUint64(%d, %d) out of range of memory size %d", offset, 8, self.Size()), + ))) +} + +func (self *_Memory) WriteUint64Pack(v uint64) PackedData { + offset, err := self.Malloc(8) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + err = self.WriteUint64(offset, v) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeI64, offset, 8) +} + +func (self *_Memory) WriteFloat32(offset uint32, v float32) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteFloat32Le(offset, v), + nil, + fmt.Errorf("Memory.WriteFloat32(%d, %d) out of range of memory size %d", offset, 8, self.Size()), + ))) +} + +func (self *_Memory) WriteFloat32Pack(v float32) PackedData { + offset, err := self.Malloc(4) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + err = self.WriteFloat32(offset, v) + if err != nil { + self.logger.Error(err.Error()) + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeF32, offset, 4) +} + +func (self *_Memory) WriteFloat64(offset uint32, v float64) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteFloat64Le(offset, v), + nil, + fmt.Errorf("Memory.WriteFloat64(%d, %d) out of range of memory size %d", offset, 8, self.Size()), + ))) +} + +func (self *_Memory) WriteFloat64Pack(v float64) PackedData { + offset, err := self.Malloc(8) + if err != nil { + return 0 + } + + err = self.WriteFloat64(offset, v) + if err != nil { + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeF64, offset, 8) +} + +func (self *_Memory) WriteString(offset uint32, v string) error { + return Log(self.logger, Aggregate("failed to write data", Ternary( + self.module.raw.Memory().WriteString(offset, v), + nil, + fmt.Errorf("Memory.WriteString(%d, %d) out of range of memory size %d", offset, len(v), self.Size()), + ))) +} + +func (self *_Memory) WriteStringPack(v string) PackedData { + size := uint32(len(v)) + + offset, err := self.Malloc(size) + if err != nil { + return 0 + } + + err = self.WriteString(offset, v) + if err != nil { + return 0 + } + + return pack[PackedData](self.logger, types.ValueTypeString, offset, size) +} + +func (self *_Memory) WriteMultiPack(datas ...PackedData) MultiPackedData { + size := uint32(len(datas)) * 8 + if size == 0 { + return 0 + } + + offset, err := self.Malloc(size) + if err != nil { + return 0 + } + + pdsU64 := Map(datas, func(data PackedData) uint64 { return uint64(data) }) + err = self.WriteBytes(offset, Uint64ArrayToBytes(pdsU64)) + if err != nil { + return 0 + } + + return pack[MultiPackedData](self.logger, types.ValueTypeString, offset, size) +} + +// Size returns the size in bytes available. e.g. If the underlying memory +// has 1 page: 65536 +func (self *_Memory) Size() uint32 { + return self.module.raw.Memory().Size() +} + +// Malloc allocates memory in wasm linear memory with the specified size. +// +// It invokes the "malloc" GuestFunction of the associated wazeroModule using the provided size parameter. +// Returns the allocated memory offset and any encountered error. +// +// Malloc allows memory allocation from within a host function or externally, +// returning the allocated memory offset to be used in a guest function. +// This can be helpful, for instance, when passing string data from the host to the guest. +// +// NOTE: Always make sure to free memory after allocation. +func (self *_Memory) Malloc(size uint32) (uint32, error) { + offset, err := self.module.GuestFunction(self.module.config.Context, "malloc").(*_GuestFunction).call(uint64(size)) + if err != nil { + return 0, Log(self.logger, Aggregate("can't invoke 'malloc' function", err)) + } + + return uint32(offset), nil +} + +// Free releases the memory block at the specified offset in wazeroMemory. +// It invokes the "free" GuestFunction of the associated wazeroModule using the provided offset parameter. +// Returns any encountered error during the memory deallocation. +func (self *_Memory) Free(offsets ...uint32) error { + return Log( + self.logger, + Aggregate( + "failed to invoke 'free' function", + Map(offsets, func(offset uint32) error { + return Second(self.module.GuestFunction(self.module.config.Context, "free").(*_GuestFunction).call(uint64(offset))) + })..., + ), + ) +} + +func (self *_Memory) FreePack(datas ...PackedData) error { + return Log( + self.logger, + Aggregate( + "failed to free up packed data", + Map(datas, func(data PackedData) error { + return self.Free(Second(UnpackUI64[ValueType](uint64(data)))) + })..., + ), + ) +} + +func pack[T PackedData | MultiPackedData](logger logging.Logger, typ types.ValueType, offset uint32, size uint32) T { + pd, err := PackUI64(typ, offset, size) + if err != nil { + _ = Log(logger, Aggregate("failed to pack data", err)) + return 0 + } + + return T(pd) +} diff --git a/wazero/module.go b/wazero/module.go new file mode 100644 index 0000000..f1600a4 --- /dev/null +++ b/wazero/module.go @@ -0,0 +1,225 @@ +package wazero + +import ( + "context" + "errors" + "os" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + . "github.com/wasify-io/wasify-go/internal/utils" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" +) + +func _NewModule(runtime wazero.Runtime, config *ModuleConfig) (*_Module, error) { + result := &_Module{config: config} + + return result, result.initialize(runtime) +} + +// The wazeroModule struct combines an instantiated wazero modul +// with the generic guest configuration. +type _Module struct { + raw api.Module + config *ModuleConfig + closer func(context.Context) error +} + +// Memory retrieves a Memory instance associated with the wazeroModule. +func (self *_Module) Memory() Memory { + return &_Memory{ + module: self, + logger: self.config.Logger, + } +} + +// Close closes the resource. +// +// Note: The context parameter is used for value lookup, such as for +// logging. A canceled or otherwise done context will not prevent Close +// from succeeding. +func (self *_Module) Close(ctx context.Context) error { + // TODO - invoke the closer too + err := self.raw.Close(ctx) + if err != nil { + err = errors.Join(errors.New("can't close guest"), err) + self.config.Logger.Error(err.Error()) + return err + } + + return nil +} + +// GuestFunction returns a GuestFunction instance associated with the wazeroModule. +// GuestFunction is used to work with exported function from this guest. +// +// Example usage: +// +// result = guest.GuestFunction(ctx, "greet").Invoke("argument1", "argument2", 123) +// if err := result.Error(); err != nil { +// slog.Error(err.Error()) +// } +func (self *_Module) GuestFunction(ctx context.Context, name string) GuestFunction { + fn := self.raw.ExportedFunction(name) + if fn == nil { + self.config.Logger.Warn("exported function does not exist", "function", name, "namespace", self.config.Namespace) + } + + return &_GuestFunction{ + name: name, + ctx: ctx, + fn: fn, + config: self.config, + memory: self.Memory(), + } +} + +func (self *_Module) initialize(runtime wazero.Runtime) error { + defaults, err := self.instantiateDefaultHostFunctions(runtime) + if err != nil { + return Aggregate("failed to instantiate default host functions", err) + } + + custom, err := self.instantiateUserDefinedHostFunctions(runtime) + if err != nil { + return Aggregate("failed to instantiate custom host functions", err) + } + + guest, err := self.instantiateGuestFunctions(runtime) + if err != nil { + return Aggregate("failed to instantiate guest functions", err) + } + + self.raw = guest + self.closer = func(ctx context.Context) error { + return Aggregate("failed to close module", guest.Close(ctx), custom.Close(ctx), defaults.Close(ctx)) + } + + return nil +} + +// instantiateModule compiles and instantiates a WebAssembly guest using the wazero runtime. +// It compiles the guest, creates a guest configuration, and then instantiates the guest. +// Returns the instantiated guest and any potential error. +func (self *_Module) instantiateGuestFunctions(runtime wazero.Runtime) (api.Module, error) { + // TODO: Add more configurations + cfg := wazero.NewModuleConfig() + cfg = cfg.WithStdin(os.Stdin) + cfg = cfg.WithStdout(os.Stdout) + cfg = cfg.WithStderr(os.Stderr) + + if self.config.FSConfig.Enabled { + cfg = cfg.WithFSConfig( + wazero.NewFSConfig(). + WithDirMount(self.config.FSConfig.HostDir, self.config.FSConfig.GetGuestDir()), + ) + } + + // Instantiate the compiled guest with the provided guest configuration. + module, err := runtime.InstantiateWithConfig(self.config.Context, self.config.Wasm.Binary, cfg) + + return module, Aggregate("failed to instantiate module guest functions", err) +} + +// instantiateHostFunctions sets up and exports host functions for the guest using the wazero runtime. +// It configures host function callbacks, data types, and exports. +func (self *_Module) instantiateDefaultHostFunctions(runtime wazero.Runtime) (api.Module, error) { + builder := runtime.NewHostModuleBuilder("wasify") + + for _, function := range self.predefined() { + builder = builder. + NewFunctionBuilder(). + WithGoModuleFunction( + api.GoModuleFunc(wazeroHostFunctionCallback(self.config.Logger, self, function)), + self.convertToAPIValueTypes(function.Params), + self.convertToAPIValueTypes(function.Results), + ). + Export(function.Name) + } + + result, err := builder.Instantiate(self.config.Context) + + return result, Aggregate("failed to instantiate predefined host functions", err) +} + +// instantiateHostFunctions sets up and exports host functions for the guest using the wazero runtime. +// It configures host function callbacks, data types, and exports. +func (self *_Module) instantiateUserDefinedHostFunctions(runtime wazero.Runtime) (api.Module, error) { + config := self.config + logger := self.config.Logger + builder := runtime.NewHostModuleBuilder(config.Namespace) + + for _, function := range config.HostFunctions { + logger.Debug("build host function", "namespace", config.Namespace, "function", function.Name) + + // Associate the host function with guest-related information. + // This configuration ensures that the host function can access ModuleConfig data from various contexts. + // See host_function.go for more details. + function.Config = config + + // If host function has any return values, we pack it as a single uint64 + var resultValuesPackedData = make([]ValueType, 0) + if len(function.Results) > 0 { + resultValuesPackedData = []ValueType{ValueTypeI64} + } + + builder = builder. + NewFunctionBuilder(). + WithGoModuleFunction( + api.GoModuleFunc(wazeroHostFunctionCallback(logger, self, &function)), + self.convertToAPIValueTypes(function.Params), + self.convertToAPIValueTypes(resultValuesPackedData), + ). + Export(function.Name) + } + + result, err := builder.Instantiate(config.Context) + + return result, Aggregate("failed to instantiate user-defined host functions", err) +} + +func (self *_Module) predefined() []*HostFunction { + return []*HostFunction{ + { + Config: self.config, + + Name: "log", + Params: []ValueType{ValueTypeString, ValueTypeBytes}, + Results: nil, + Callback: func(ctx context.Context, module Module, params []PackedData) MultiPackedData { + memory := module.Memory() + self.config.Logger.Log( + logging.LogSeverity(Must(memory.ReadBytePack(params[1]))), + Must(memory.ReadStringPack(params[0])), + ) + + return 0 + }, + }, + } +} + +// convertToAPIValueTypes converts an array of ValueType values to their corresponding +// api.ValueType representations used by the Wazero runtime. +// ValueType describes a parameter or result type mapped to a WebAssembly +// function signature. +func (self *_Module) convertToAPIValueTypes(types []ValueType) []api.ValueType { + valueTypes := make([]api.ValueType, len(types)) + + for i, t := range types { + switch t { + case + ValueTypeBytes, + ValueTypeByte, + ValueTypeI32, + ValueTypeI64, + ValueTypeF32, + ValueTypeF64, + ValueTypeString: + valueTypes[i] = api.ValueTypeI64 + } + } + + return valueTypes +} diff --git a/wazero/runtime.go b/wazero/runtime.go new file mode 100644 index 0000000..293b2bd --- /dev/null +++ b/wazero/runtime.go @@ -0,0 +1,77 @@ +package wazero + +import ( + "context" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + . "github.com/wasify-io/wasify-go/internal/utils" + "github.com/wasify-io/wasify-go/logging" + . "github.com/wasify-io/wasify-go/models" +) + +// NewRuntime creates and returns a wazero runtime instance using the provided context and +// RuntimeConfig. It configures the runtime with specific settings and features. +func NewRuntime(ctx context.Context, config *RuntimeConfig) Runtime { + // TODO: Allow user to control the following options: + // 1. WithCloseOnContextDone + // 2. Memory + // Create a new wazero runtime instance with specified configuration options. + runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig(). + WithCoreFeatures(api.CoreFeaturesV2). + WithCustomSections(false). + WithCloseOnContextDone(false). + // Enable runtime debug if user sets LogSeverity to debug level in runtime configuration + WithDebugInfoEnabled(config.Logger.Severity() == logging.LogDebug), + ) + + // Instantiate the runtime with the WASI snapshot preview1. + wasi_snapshot_preview1.MustInstantiate(ctx, runtime) + + return &_Runtime{ + config: config, + runtime: runtime, + } +} + +// The wazeroRuntime struct combines a wazero runtime instance with runtime configuration. +type _Runtime struct { + runtime wazero.Runtime + config *RuntimeConfig +} + +// Close closes the resource. +// Note: The context parameter is used for value lookup, such as for +// logging. A canceled or otherwise done context will not prevent Close +// from succeeding. +func (self *_Runtime) Close(ctx context.Context) error { + return Log( + self.config.Logger, + Aggregate("failed to close runtime", self.runtime.Close(ctx)), + ) +} + +// Create creates a new guest instance based on the provided ModuleConfig within +// the wazero runtime context. It returns the created guest and any potential error. +func (self *_Runtime) Create(config *ModuleConfig) (Module, error) { + if err := self.verify(config); err != nil { + return nil, Aggregate("failed to create module", err) + } + + return _NewModule(self.runtime, config) +} + +func (self *_Runtime) verify(config *ModuleConfig) error { + // TODO - make sure that all required props are set in the config + if config.Wasm.Hash == "" { + return nil + } + + actual, err := CalculateHash(config.Wasm.Binary) + if err != nil { + return Aggregate("failed to calculate hash for module", err) + } + + return Aggregate("failed to check hash for module", CompareHashes(actual, config.Wasm.Hash)) +} diff --git a/runtime_wazero_test.go b/wazero/runtime_test.go similarity index 86% rename from runtime_wazero_test.go rename to wazero/runtime_test.go index a45e2d9..8787caf 100644 --- a/runtime_wazero_test.go +++ b/wazero/runtime_test.go @@ -1,4 +1,4 @@ -package wasify +package wazero_test // import ( // "context" @@ -57,11 +57,11 @@ package wasify // assert.NoError(t, err, "Expected no error while closing runtime") // }() -// module, err := runtime.NewModule(ctx, &testModuleConfig) -// assert.NoError(t, err, "Expected no error while creating module") -// assert.NotNil(t, module, "Expected a non-nil module") -// err = module.Close(ctx) -// assert.Nil(t, err, "Expected no error while closing module") +// guest, err := runtime.NewModule(ctx, &testModuleConfig) +// assert.NoError(t, err, "Expected no error while creating guest") +// assert.NotNil(t, guest, "Expected a non-nil guest") +// err = guest.Close(ctx) +// assert.Nil(t, err, "Expected no error while closing guest") // }) // t.Run("failure due to invalid runtime", func(t *testing.T) { @@ -85,9 +85,9 @@ package wasify // invalidtestModuleConfig := testModuleConfig // invalidtestModuleConfig.Namespace = "_invalid_namespace_" -// module, err := runtime.NewModule(ctx, &invalidtestModuleConfig) +// guest, err := runtime.NewModule(ctx, &invalidtestModuleConfig) // assert.Error(t, err) -// assert.Nil(t, module) +// assert.Nil(t, guest) // }) @@ -103,9 +103,9 @@ package wasify // invalidtestModuleConfig := testModuleConfig // invalidtestModuleConfig.Wasm.Hash = "invalid_hash" -// module, err := runtime.NewModule(ctx, &invalidtestModuleConfig) +// guest, err := runtime.NewModule(ctx, &invalidtestModuleConfig) // assert.Error(t, err) -// assert.Nil(t, module) +// assert.Nil(t, guest) // }) // t.Run("failure due to invalid wasm", func(t *testing.T) { @@ -121,9 +121,9 @@ package wasify // invalidtestModuleConfig.Wasm.Binary = []byte("invalid wasm data") // invalidtestModuleConfig.Wasm.Hash = "" -// module, err := runtime.NewModule(ctx, &invalidtestModuleConfig) +// guest, err := runtime.NewModule(ctx, &invalidtestModuleConfig) // assert.Error(t, err) -// assert.Nil(t, module) +// assert.Nil(t, guest) // }) // t.Run("test convertToAPIValueTypes", func(t *testing.T) {