Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions component_feat_component_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import (
// returns the embedded payload type. The slice currently exposed here
// intentionally only surfaces the kinds reachable without those further
// sub-types.
// TODO: ComponentFunc + value marshaling (call exported component functions
// with primitive / composite WIT values).

// Component is a compiled WebAssembly component, the binary representation of
// a component-model artifact. Components are instantiated through a
// [ComponentLinker].
Expand Down
132 changes: 132 additions & 0 deletions component_func_feat_component_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
//go:build wasmtime_component_model

package wasmtime

/*
#include <wasmtime.h>
#include <stdlib.h>

static inline wasmtime_component_val_t *go_component_val_at(void *p, size_t i) {
return &((wasmtime_component_val_t *)p)[i];
}
static inline void go_component_val_shallow_copy(void *p, size_t i, const wasmtime_component_val_t *v) {
((wasmtime_component_val_t *)p)[i] = *v;
}
*/
import "C"

import (
"fmt"
"runtime"
)

// ComponentFunc is a component-model function tied to one Store.
type ComponentFunc struct{ val C.wasmtime_component_func_t }

// ComponentFuncType describes a component function's parameters and result.
type ComponentFuncType struct {
_ptr *C.wasmtime_component_func_type_t
}

func (f *ComponentFunc) Type(store Storelike) *ComponentFuncType {
ptr := C.wasmtime_component_func_type(&f.val, store.Context())
runtime.KeepAlive(f)
runtime.KeepAlive(store)
typeInfo := &ComponentFuncType{_ptr: ptr}
runtime.SetFinalizer(typeInfo, func(typeInfo *ComponentFuncType) { typeInfo.Close() })
return typeInfo
}

func (t *ComponentFuncType) ptr() *C.wasmtime_component_func_type_t {
if t == nil || t._ptr == nil {
panic("component function type has been closed")
}
return t._ptr
}

func (t *ComponentFuncType) ParamCount() int {
return int(C.wasmtime_component_func_type_param_count(t.ptr()))
}

func (t *ComponentFuncType) HasResult() bool {
var result C.wasmtime_component_valtype_t
found := bool(C.wasmtime_component_func_type_result(t.ptr(), &result))
if found {
C.wasmtime_component_valtype_delete(&result)
}
return found
}
Comment on lines +51 to +58

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty expensive operation to get the result type just to delete it and return a bool. Could this instead be modeled as returning the entire result type? For example returning nil if it's not present?


func (t *ComponentFuncType) Close() {
if t != nil && t._ptr != nil {
runtime.SetFinalizer(t, nil)
C.wasmtime_component_func_type_delete(t._ptr)
t._ptr = nil
}
}

// Call invokes the component function synchronously. Returned values are owned
// and must be closed by the caller.
func (f *ComponentFunc) Call(store Storelike, args []*ComponentVal) ([]*ComponentVal, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think it would be best to model a similar interface to core wasm here which takes args ...interface{}. That'll make it more ergonomic to invoke this and additionally doesn't require boxing as a *ComponentVal by the caller. There'd be a conversion internally which would convert interface{} to a *ComponentVal, handling the case it's already a ComponentVal.

typeInfo := f.Type(store)
if typeInfo == nil || typeInfo._ptr == nil {
return nil, fmt.Errorf("component function type unavailable")
}
Comment on lines +72 to +74

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this AI generated? I don't believe that either of these conditions is possible.

defer typeInfo.Close()
resultCount := 0
if typeInfo.HasResult() {
resultCount = 1
}
argsMem := componentValArray(len(args))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do arguments need to be malloc'd in C here as opposed to passing a Go slice?

if argsMem != nil {
defer C.free(argsMem)
}
Comment on lines +81 to +83

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error handling is a bit nonsensical, it looks for allocation failure, deallocates later on success, but then continues to use the failed allocation in the case of failure.

Please be sure to review all AI generated code yourself.

for i, arg := range args {
C.go_component_val_shallow_copy(argsMem, C.size_t(i), arg.ptr())
}
resultsMem := componentValArray(resultCount)
if resultsMem != nil {
defer C.free(resultsMem)
}
err := enterWasm(store, func(_ **C.wasm_trap_t) *C.wasmtime_error_t {
return C.wasmtime_component_func_call(
&f.val, store.Context(),
(*C.wasmtime_component_val_t)(argsMem), C.size_t(len(args)),
(*C.wasmtime_component_val_t)(resultsMem), C.size_t(resultCount),
)
})
runtime.KeepAlive(f)
runtime.KeepAlive(store)
runtime.KeepAlive(args)
if err != nil {
return nil, err
}
results := make([]*ComponentVal, resultCount)
for i := range results {
results[i] = ownComponentVal(*C.go_component_val_at(resultsMem, C.size_t(i)))
}
return results, nil
Comment on lines +104 to +108

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to core wasm I think it would be best to return interface{} from this function which allows unwrapping return values into native Go types as opposed to forcing everything through ComponentVal

}

// GetFuncByIndex resolves a function from a reusable component export index.
func (i *ComponentInstance) GetFuncByIndex(store Storelike, index *ComponentExportIndex) *ComponentFunc {
var value C.wasmtime_component_func_t
found := C.wasmtime_component_instance_get_func(&i.val, store.Context(), index.ptr(), &value)
runtime.KeepAlive(i)
runtime.KeepAlive(store)
runtime.KeepAlive(index)
if !bool(found) {
return nil
}
return &ComponentFunc{val: value}
}

// GetFunc resolves a root or nested exported component function by name.
func (i *ComponentInstance) GetFunc(store Storelike, parent *ComponentExportIndex, name string) *ComponentFunc {
index := i.GetExportIndex(store, parent, name)
if index == nil {
return nil
}
defer index.Close()
return i.GetFuncByIndex(store, index)
}
187 changes: 187 additions & 0 deletions component_func_feat_component_model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//go:build wasmtime_component_model

package wasmtime

import (
"reflect"
"testing"

"github.com/stretchr/testify/require"
)

const byteComponent = `(component
(core module $m
(memory (export "memory") 1)
(global $next (mut i32) (i32.const 1024))
(func (export "realloc")
(param $old i32) (param $old-size i32) (param $align i32) (param $new-size i32)
(result i32)
(local $ptr i32)
(local.set $ptr (global.get $next))
(global.set $next (i32.add (local.get $ptr) (local.get $new-size)))
(local.get $ptr))
(func (export "increment") (param $ptr i32) (param $len i32) (result i32)
(local $i i32)
(loop $loop
(if (i32.lt_u (local.get $i) (local.get $len))
(then
(i32.store8
(i32.add (local.get $ptr) (local.get $i))
(i32.add
(i32.load8_u (i32.add (local.get $ptr) (local.get $i)))
(i32.const 1)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $loop))))
(i32.store (i32.const 0) (local.get $ptr))
(i32.store offset=4 (i32.const 0) (local.get $len))
(i32.const 0)))
(core instance $i (instantiate $m))
(func (export "increment-bytes") (param "bytes" (list u8)) (result (list u8))
(canon lift (core func $i "increment")
(memory $i "memory")
(realloc (func $i "realloc")))))`

func closeComponentVals(values []*ComponentVal) {
for _, value := range values {
value.Close()
}
}

func TestComponentFuncRepeatedListU8Calls(t *testing.T) {
engine := newComponentEngine()
defer engine.Close()
component := newComponent(t, engine, byteComponent)
defer component.Close()
store := NewStore(engine)
linker := NewComponentLinker(engine)
defer linker.Close()
instance, err := linker.Instantiate(store, component)
if err != nil {
t.Fatal(err)
}
increment := instance.GetFunc(store, nil, "increment-bytes")
if increment == nil {
t.Fatal("increment-bytes function not found")
}
typeInfo := increment.Type(store)
require.Equal(t, 1, typeInfo.ParamCount())
require.True(t, typeInfo.HasResult())
typeInfo.Close()
require.Panics(t, func() { typeInfo.ParamCount() })
value := []byte{0, 1, 2, 253}
for iteration := 0; iteration < 100; iteration++ {
elements := make([]*ComponentVal, len(value))
for i, item := range value {
elements[i] = NewComponentU8(item)
}
argument := NewComponentList(elements)
closeComponentVals(elements)
results, err := increment.Call(store, []*ComponentVal{argument})
argument.Close()
if err != nil {
t.Fatal(err)
}
if len(results) != 1 || results[0].Kind() != ComponentValKindList {
t.Fatalf("unexpected results: %#v", results)
}
returned := results[0].Value().([]*ComponentVal)
value = value[:0]
for _, item := range returned {
value = append(value, item.Value().(uint8))
}
closeComponentVals(returned)
closeComponentVals(results)
}
if want := []byte{100, 101, 102, 97}; !reflect.DeepEqual(value, want) {
t.Fatalf("value = %v, want %v", value, want)
}
}

func TestComponentValCompositeOwnership(t *testing.T) {
primitives := []struct {
value *ComponentVal
want any
}{
{NewComponentBool(true), true},
{NewComponentS8(-8), int8(-8)},
{NewComponentU8(8), uint8(8)},
{NewComponentS16(-16), int16(-16)},
{NewComponentU16(16), uint16(16)},
{NewComponentS32(-32), int32(-32)},
{NewComponentU32(32), uint32(32)},
{NewComponentS64(-64), int64(-64)},
{NewComponentU64(64), uint64(64)},
{NewComponentF32(3.25), float32(3.25)},
{NewComponentF64(6.5), float64(6.5)},
{NewComponentChar('F'), rune('F')},
{NewComponentString("Friday"), "Friday"},
}
for _, primitive := range primitives {
if got := primitive.value.Value(); got != primitive.want {
t.Errorf("primitive value = %#v, want %#v", got, primitive.want)
}
primitive.value.Close()
}

left := NewComponentS32(-7)
right := NewComponentU32(49)
record := NewComponentRecord([]ComponentRecordField{{Name: "left", Value: left}, {Name: "right", Value: right}})
left.Close()
right.Close()
recordFields := record.Value().([]ComponentRecordField)
if recordFields[0].Name != "left" || recordFields[0].Value.Value() != int32(-7) || recordFields[1].Name != "right" || recordFields[1].Value.Value() != uint32(49) {
t.Fatalf("unexpected record: %#v", recordFields)
}
closeComponentVals([]*ComponentVal{recordFields[0].Value, recordFields[1].Value})

payload := NewComponentString("payload")
values := []*ComponentVal{
NewComponentTuple([]*ComponentVal{payload}),
NewComponentVariant("case", payload),
NewComponentOption(payload),
NewComponentOption(nil),
NewComponentResult(true, payload),
NewComponentResult(false, nil),
NewComponentEnum("choice"),
NewComponentFlags([]string{"read", "write"}),
}
payload.Close()

tuple := values[0].Value().([]*ComponentVal)
if tuple[0].Value() != "payload" {
t.Fatalf("unexpected tuple: %#v", tuple)
}
closeComponentVals(tuple)
variant := values[1].Value().(ComponentVariantValue)
if variant.Discriminant != "case" || variant.Value.Value() != "payload" {
t.Fatalf("unexpected variant: %#v", variant)
}
variant.Value.Close()
option := values[2].Value().(*ComponentVal)
if option.Value() != "payload" {
t.Fatalf("unexpected option: %#v", option)
}
option.Close()
if values[3].Value().(*ComponentVal) != nil {
t.Fatal("none option returned a payload")
}
ok := values[4].Value().(ComponentResultValue)
if !ok.OK || ok.Value.Value() != "payload" {
t.Fatalf("unexpected ok result: %#v", ok)
}
ok.Value.Close()
errResult := values[5].Value().(ComponentResultValue)
if errResult.OK || errResult.Value != nil {
t.Fatalf("unexpected error result: %#v", errResult)
}
if values[6].Value() != "choice" || !reflect.DeepEqual(values[7].Value(), []string{"read", "write"}) {
t.Fatal("enum or flags did not round trip")
}
clone := record.Clone()
record.Close()
if clone.Kind() != ComponentValKindRecord {
t.Fatal("deep clone did not survive source close")
}
clone.Close()
closeComponentVals(values)
}
Loading
Loading