-
Notifications
You must be signed in to change notification settings - Fork 100
Add Component function calls and runtime values #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } | ||
|
|
||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| typeInfo := f.Type(store) | ||
| if typeInfo == nil || typeInfo._ptr == nil { | ||
| return nil, fmt.Errorf("component function type unavailable") | ||
| } | ||
|
Comment on lines
+72
to
+74
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to core wasm I think it would be best to return |
||
| } | ||
|
|
||
| // 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) | ||
| } | ||
| 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) | ||
| } |
There was a problem hiding this comment.
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 returningnilif it's not present?