diff --git a/cue/interpreter/wasm/abi_c.go b/cue/interpreter/wasm/call.go similarity index 77% rename from cue/interpreter/wasm/abi_c.go rename to cue/interpreter/wasm/call.go index 3750584b25f..6b4d6e1cf62 100644 --- a/cue/interpreter/wasm/abi_c.go +++ b/cue/interpreter/wasm/call.go @@ -119,6 +119,12 @@ func decNumber(typ cue.Value, val uint64) (r any) { panic(fmt.Sprintf("unsupported argument type %v (kind %v)", typ, typ.IncompleteKind())) } +func encBytes(i *instance, b []byte) *memory { + m, _ := i.Alloc(uint32(len(b))) + m.WriteAt(b, 0) + return m +} + // cABIFunc implements the Wasm/System V ABI translation. The named // function, which must be loadable by the instance, and must be of // the specified sig type, will be called by the runtime after its @@ -126,20 +132,52 @@ func decNumber(typ cue.Value, val uint64) (r any) { // call will be then also be converted back into a Go value and handed // to the runtime. func cABIFunc(i *instance, name string, sig []cue.Value) func(*pkg.CallCtxt) { + // Compute the layout of all encountered structs (arguments + // and result) such that we will have it available at the time + // of an actual call. + argsTyp, resTyp := splitLast(sig) + argLayouts := make([]*structLayout, 0, len(argsTyp)) + var retLayout *structLayout + for _, typ := range argsTyp { + switch typ.IncompleteKind() { + case cue.StructKind: + argLayouts = append(argLayouts, structLayoutVal(typ)) + default: + argLayouts = append(argLayouts, nil) + } + } + if resTyp.IncompleteKind() == cue.StructKind { + retLayout = structLayoutVal(resTyp) + } + fn, _ := i.load(name) return func(c *pkg.CallCtxt) { - var args []uint64 argsTyp, resTyp := splitLast(sig) + args := make([]uint64, 0, len(argsTyp)) for k, typ := range argsTyp { switch typ.IncompleteKind() { case cue.BoolKind: args = append(args, encBool(c.Bool(k))) case cue.IntKind, cue.FloatKind, cue.NumberKind: args = append(args, encNumber(typ, c.Value(k))) + case cue.StructKind: + ms := encodeStruct(i, c.Value(k), argLayouts[k]) + defer i.FreeAll(ms) + + args = append(args, uint64(ms[0].ptr)) default: panic(fmt.Sprintf("unsupported argument type %v (kind %v)", typ, typ.IncompleteKind())) } } + + var retMem *memory + if resTyp.IncompleteKind() == cue.StructKind { + retMem, _ = i.Alloc(uint32(retLayout.size)) + // TODO: add support for structs containing pointers. + defer i.Free(retMem) + args = append(args, uint64(retMem.ptr)) + } + if c.Do() { res, err := fn.Call(i.ctx, args...) if err != nil { @@ -151,6 +189,8 @@ func cABIFunc(i *instance, name string, sig []cue.Value) func(*pkg.CallCtxt) { c.Ret = decBool(res[0]) case cue.IntKind, cue.FloatKind, cue.NumberKind: c.Ret = decNumber(resTyp, res[0]) + case cue.StructKind: + c.Ret = decodeStruct(retMem.Bytes(), retLayout) default: panic(fmt.Sprintf("unsupported result type %v (kind %v)", resTyp, resTyp.IncompleteKind())) } diff --git a/cue/interpreter/wasm/doc.go b/cue/interpreter/wasm/doc.go index 8262c89965a..26ff0018de1 100644 --- a/cue/interpreter/wasm/doc.go +++ b/cue/interpreter/wasm/doc.go @@ -43,10 +43,10 @@ // used by the function (see below) while sig indicates the type // signature of the function. The grammar for sig is: // -// list := ident [ { "," ident } ] -// func := "func" "(" [ list ] ")" ":" ident +// list := expr [ { "," expr } ] +// func := "func" "(" [ list ] ")" ":" expr // -// Where ident are all valid CUE identifiers. +// Where each expr is a valid CUE identifier or selector. // // The specific ABI used may restrict the allowable signatures further. // @@ -88,11 +88,22 @@ // // # ABI requirements for Wasm modules // -// Currently only the [C ABI] is supported. Furthermore, only scalar -// data types can be exchanged between CUE and Wasm. That means booleans, -// sized integers, and sized floats. The sig field in the attribute -// refers to these data types by their CUE names, such as bool, uint16, -// float64. +// Currently only the [System V ABI] (also known as the C ABI) is +// supported. Furthermore, only scalar data types and structs containing +// either scalar types or other structs can be exchanged between CUE +// and Wasm. Scalar means booleans, sized integers, and sized floats. +// The sig field in the attribute refers to these data types by their +// CUE names, such as bool, uint16, float64. +// +// Additionally the Wasm module must export two functions with the +// following C type signature: +// +// void* allocate(int n); +// void deallocate(void *ptr, int n); +// +// Allocate returns a Wasm pointer to a buffer of size n. Deallocate +// takes a Wasm pointer and the size of the buffer it points to and +// frees it. // // # How to compile Rust for use in CUE // @@ -116,7 +127,32 @@ // a * b // } // -// [C ABI]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md +// The following Rust functions can be used to implement allocate and +// deallocate described above: +// +// #[cfg_attr(all(target_arch = "wasm32"), export_name = "allocate")] +// #[no_mangle] +// pub extern "C" fn _allocate(size: u32) -> *mut u8 { +// allocate(size as usize) +// } +// +// fn allocate(size: usize) -> *mut u8 { +// let vec: Vec> = Vec::with_capacity(size); +// +// Box::into_raw(vec.into_boxed_slice()) as *mut u8 +// } +// +// #[cfg_attr(all(target_arch = "wasm32"), export_name = "deallocate")] +// #[no_mangle] +// pub unsafe extern "C" fn _deallocate(ptr: u32, size: u32) { +// deallocate(ptr as *mut u8, size as usize); +// } +// +// unsafe fn deallocate(ptr: *mut u8, size: usize) { +// let _ = Vec::from_raw_parts(ptr, 0, size); +// } +// +// [System V ABI]: https://github.com/WebAssembly/tool-conventions/blob/main/BasicCABI.md // [no_std]: https://docs.rust-embedded.org/book/intro/no-std.html // [WASI]: https://wasi.dev // [cargo target]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html diff --git a/cue/interpreter/wasm/layout.go b/cue/interpreter/wasm/layout.go new file mode 100644 index 00000000000..7ad3f4d5a30 --- /dev/null +++ b/cue/interpreter/wasm/layout.go @@ -0,0 +1,343 @@ +// Copyright 2023 CUE Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package wasm + +import ( + "encoding/binary" + "fmt" + "math" + + "cuelang.org/go/cue" + "cuelang.org/go/internal/core/adt" +) + +// typ is the type (or kind) of an external type. +type typ int8 + +const ( + typErr typ = iota + typBool + typUint8 + typUint16 + typUint32 + typUint64 + typInt8 + typInt16 + typInt32 + typInt64 + typFloat32 + typFloat64 + typStruct +) + +// field represents a name struct field. +type field struct { + typ + from string // the field name +} + +// positionedField represents a struct field with a known location. +type positionedField struct { + field + offset int // memory offset in the parent struct. + + inner *structLayout // IFF typ==typStruct +} + +// structLayout describes the memory layout of a struct. +type structLayout struct { + fields []positionedField + size int + align int +} + +func sizeof(t typ) int { + switch t { + case typBool, typUint8, typInt8: + return 1 + case typUint16, typInt16: + return 2 + case typUint32, typInt32, typFloat32: + return 4 + case typUint64, typInt64, typFloat64: + return 8 + } + panic("unreachable") +} + +func encodeStruct(i *instance, v cue.Value, l *structLayout) []*memory { + buf := make([]byte, l.size) + ms := make([]*memory, 1, 2) // cap is 2 for strings and bytes. + + buf, ms = encode(i, v, l, buf, ms) + ms[0] = encBytes(i, buf) + return ms +} + +// encodeStruct serializes v into buf according to the layout. +func encode(i *instance, v cue.Value, l *structLayout, buf []byte, ms []*memory) ([]byte, []*memory) { + for _, f := range l.fields { + arg := v.LookupPath(cue.ParsePath(f.from)) + + switch f.typ { + case typBool: + b, _ := arg.Bool() + if b { + buf[f.offset] = 1 + } else { + buf[f.offset] = 0 + } + + case typUint8: + u, _ := arg.Uint64() + buf[f.offset] = byte(u) + case typUint16: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint16(buf[f.offset:], uint16(u)) + case typUint32: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint32(buf[f.offset:], uint32(u)) + case typUint64: + u, _ := arg.Uint64() + binary.LittleEndian.PutUint64(buf[f.offset:], u) + + case typInt8: + u, _ := arg.Int64() + buf[f.offset] = byte(u) + case typInt16: + u, _ := arg.Int64() + binary.LittleEndian.PutUint16(buf[f.offset:], uint16(u)) + case typInt32: + u, _ := arg.Int64() + binary.LittleEndian.PutUint32(buf[f.offset:], uint32(u)) + case typInt64: + u, _ := arg.Int64() + binary.LittleEndian.PutUint64(buf[f.offset:], uint64(u)) + + case typFloat32: + x, _ := arg.Float64() + binary.LittleEndian.PutUint32(buf[f.offset:], math.Float32bits(float32(x))) + case typFloat64: + x, _ := arg.Float64() + binary.LittleEndian.PutUint64(buf[f.offset:], math.Float64bits(x)) + + case typStruct: + encode(i, arg, f.inner, buf[f.offset:], ms) + + default: + panic(fmt.Sprintf("unsupported argument %v (kind %v)", v, v.IncompleteKind())) + } + } + return buf, ms +} + +// decodeStruct takes the binary representation of a struct described +// by the layout and returns its Go representation as a map. +func decodeStruct(buf []byte, l *structLayout) map[string]any { + m := make(map[string]any) + + for _, f := range l.fields { + switch f.typ { + case typBool: + u := buf[f.offset] + if u == 1 { + m[f.from] = true + } else { + m[f.from] = false + } + + case typUint8: + u := buf[f.offset] + m[f.from] = u + case typUint16: + u := binary.LittleEndian.Uint16(buf[f.offset:]) + m[f.from] = u + case typUint32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = u + case typUint64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = u + + case typInt8: + u := buf[f.offset] + m[f.from] = int8(u) + case typInt16: + u := binary.LittleEndian.Uint16(buf[f.offset:]) + m[f.from] = int16(u) + case typInt32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = int32(u) + case typInt64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = int64(u) + + case typFloat32: + u := binary.LittleEndian.Uint32(buf[f.offset:]) + m[f.from] = math.Float32frombits(u) + case typFloat64: + u := binary.LittleEndian.Uint64(buf[f.offset:]) + m[f.from] = math.Float64frombits(u) + + case typStruct: + to := f.offset + f.inner.size + m[f.from] = decodeStruct(buf[f.offset:to], f.inner) + + default: + panic(fmt.Sprintf("unsupported argument type: %v", f.typ)) + } + } + return m +} + +func align(x, n int) int { + return (x + n - 1) & ^(n - 1) +} + +// structLayoutVal returns the System V (C ABI) memory layout of the +// struct expressed by t. +func structLayoutVal(t cue.Value) *structLayout { + if t.IncompleteKind() != adt.StructKind { + panic("expected CUE struct") + } + + var sl structLayout + off, size := 0, 0 + for i, _ := t.Fields(cue.Attributes(true)); i.Next(); { + f := i.Value() + path := i.Selector().String() + + switch f.IncompleteKind() { + case adt.StructKind: + inner := structLayoutVal(f) + off = align(off, inner.align) + + lval := positionedField{ + field: field{ + typ: typStruct, + from: path, + }, + offset: off, + inner: inner, + } + sl.fields = append(sl.fields, lval) + + off += inner.size + case cue.BoolKind, cue.IntKind, cue.FloatKind, cue.NumberKind: + typ := typVal(f) + size = sizeof(typ) + off = align(off, size) + + lval := positionedField{ + field: field{ + typ: typ, + from: path, + }, + offset: off, + } + sl.fields = append(sl.fields, lval) + + off += size + default: + panic(fmt.Sprintf("unsupported argument type %v (kind %v)", f, f.IncompleteKind())) + } + } + + // The alignment of a struct is the maximum alignment of its + // constituent fields. + maxalign := 0 + for _, f := range sl.fields { + if f.typ == typStruct { + if f.inner.align > maxalign { + maxalign = f.inner.align + } + continue + } + if sizeof(f.typ) > maxalign { + maxalign = sizeof(f.typ) + } + } + sl.size = align(off, maxalign) + sl.align = maxalign + + return &sl +} + +func typVal(v cue.Value) typ { + switch v.IncompleteKind() { + case cue.BoolKind: + return typBool + case cue.IntKind, cue.FloatKind, cue.NumberKind: + return typNum(v) + default: + panic(fmt.Sprintf("unsupported argument type %v (kind %v)", v, v.IncompleteKind())) + } +} + +func typNum(t cue.Value) typ { + ctx := t.Context() + + _int8 := ctx.CompileString("int8") + if _int8.Subsume(t) == nil { + return typInt8 + } + + _uint8 := ctx.CompileString("uint8") + if _uint8.Subsume(t) == nil { + return typUint8 + } + + _int16 := ctx.CompileString("int16") + if _int16.Subsume(t) == nil { + return typInt16 + } + + _uint16 := ctx.CompileString("uint16") + if _uint16.Subsume(t) == nil { + return typUint16 + } + + _int32 := ctx.CompileString("int32") + if _int32.Subsume(t) == nil { + return typInt32 + } + + _uint32 := ctx.CompileString("uint32") + if _uint32.Subsume(t) == nil { + return typUint32 + } + + _int64 := ctx.CompileString("int64") + if _int64.Subsume(t) == nil { + return typInt64 + } + + _uint64 := ctx.CompileString("uint64") + if _uint64.Subsume(t) == nil { + return typUint64 + } + + _float32 := ctx.CompileString("float32") + if _float32.Subsume(t) == nil { + return typFloat32 + } + + _float64 := ctx.CompileString("float64") + if _float64.Subsume(t) == nil { + return typFloat64 + } + + panic("unreachable") +} diff --git a/cue/interpreter/wasm/runtime.go b/cue/interpreter/wasm/runtime.go index 6c0422b77f4..44957012d2d 100644 --- a/cue/interpreter/wasm/runtime.go +++ b/cue/interpreter/wasm/runtime.go @@ -157,6 +157,13 @@ func (i *instance) Free(m *memory) { i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len)) } +// Free frees several previously allocated guest memories. +func (i *instance) FreeAll(ms []*memory) { + for _, m := range ms { + i.free.Call(i.ctx, uint64(m.ptr), uint64(m.len)) + } +} + // memory is a read and write reference to guest memory that the host // requested. type memory struct { @@ -171,15 +178,25 @@ func (m *memory) Bytes() []byte { if !ok { panic(fmt.Sprintf("can't read %d bytes from Wasm address %#x", m.len, m.ptr)) } - return bytes + return append([]byte{}, bytes...) } -// Write writes into p guest memory referenced by n. -// p must fit into m. -func (m *memory) Write(p []byte) (int, error) { - ok := m.i.instance.Memory().Write(m.ptr, p) +// WriteAt writes p at the given relative offset within m. +// It panics if buf doesn't fit into m, or if off is out of bounds. +func (m *memory) WriteAt(p []byte, off int64) (int, error) { + if (off < 0) || (off >= 1<<32-1) { + panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr)) + } + + ok := m.i.instance.Memory().Write(m.ptr+uint32(off), p) if !ok { panic(fmt.Sprintf("can't write %d bytes to Wasm address %#x", len(p), m.ptr)) } return len(p), nil } + +// Args returns a memory in the form of pair of arguments directly +// passable to Wasm. +func (m *memory) Args() []uint64 { + return []uint64{uint64(m.ptr), uint64(m.len)} +} diff --git a/cue/interpreter/wasm/testdata/def.txtar b/cue/interpreter/wasm/testdata/def.txtar index bc8a405956a..f63783e6189 100644 --- a/cue/interpreter/wasm/testdata/def.txtar +++ b/cue/interpreter/wasm/testdata/def.txtar @@ -4,7 +4,7 @@ # Since this is `cue def` we don't run the functions, so we see the # call to this higher-order construct. -exec cue def ./basic ./morewasm ./noload ./unusedwasm +exec cue def ./basic ./morewasm ./noload ./struct ./unusedwasm cmp stdout eval.out -- basic/foo.cue -- @@ -73,6 +73,61 @@ add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) x2: add(100, 1) +-- struct/struct.cue -- +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8, + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +-- struct/struct.wasm -- -- unusedwasm/foo.wasm -- -- unusedwasm/empty.wasm -- -- eval.out -- @@ -116,6 +171,90 @@ x: 42 // --- package p +import "math" + +#vector2: { + x: float64 + y: float64 +} +magnitude2: magnitude2() @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: magnitude3() @extern("struct.wasm", abi=c, sig="func(#vector3): float64") +_v0: { + x: 1 + y: 1 +} +_v1: { + x: math.Sqrt2 + y: math.Sqrt2 +} +_v2: { + x: 123.456 + y: 789.012 +} +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) +#vector3: { + x: float64 + y: float64 + z: float64 +} +_v3: { + x: 1 + y: 1 + z: 1 +} +_v4: { + x: 0 + y: 2 + z: 2 +} +_v5: { + x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 +} +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) +cornucopia: cornucopia() @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} +_c0: { + b: true + n0: 10 + n1: 20 + n2: 30 +} +_c1: { + b: false + n0: 1 + n1: 2 + n2: 3 +} +_c2: { + b: false + n0: -1 + n1: 0 + n2: 100 +} +_c3: { + b: false + n0: -15000 + n1: 10 + n2: -10000000 +} +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +// --- +package p + add: add() @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) diff --git a/cue/interpreter/wasm/testdata/eval.txtar b/cue/interpreter/wasm/testdata/eval.txtar index 5b75c437c67..8719f47ce4b 100644 --- a/cue/interpreter/wasm/testdata/eval.txtar +++ b/cue/interpreter/wasm/testdata/eval.txtar @@ -1,4 +1,4 @@ -exec cue eval -a ./basic ./morewasm ./noload ./unusedwasm +exec cue eval -a ./basic ./morewasm ./noload ./struct ./unusedwasm cmp stdout eval.out -- basic/foo.cue -- @@ -67,6 +67,61 @@ add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64") x0: add(1, 2) x1: add(-1, 2) x2: add(100, 1) +-- struct/struct.cue -- +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8, + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) +-- struct/struct.wasm -- -- unusedwasm/foo.wasm -- -- unusedwasm/empty.wasm -- -- eval.out -- @@ -102,6 +157,86 @@ z: false // --- x: 42 // --- +#vector2: { + x: float64 + y: float64 +} +magnitude2: magnitude2() +magnitude3: magnitude3() +_v0: { + x: 1 + y: 1 +} +_v1: { + x: 1.41421356237309504880168872420969807856967187537694807317667974 + y: 1.41421356237309504880168872420969807856967187537694807317667974 +} +_v2: { + x: 123.456 + y: 789.012 +} +m0: 1.4142135623730951 +m1: 2.0 +m2: 798.6121211702211 +#vector3: { + x: float64 + y: float64 + z: float64 +} +_v3: { + x: 1 + y: 1 + z: 1 +} +_v4: { + x: 0 + y: 2 + z: 2 +} +_v5: { + x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 + z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144 +} +m3: 1.7320508075688772 +m4: 2.8284271247461903 +m5: 6.666666666666667 +cornucopia: cornucopia() +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} +_c0: { + b: true + n0: 10 + n1: 20 + n2: 30 +} +_c1: { + b: false + n0: 1 + n1: 2 + n2: 3 +} +_c2: { + b: false + n0: -1 + n1: 0 + n2: 100 +} +_c3: { + b: false + n0: -15000 + n1: 10 + n2: -10000000 +} +c0: 42 +c1: 6 +c2: 99 +c3: -10014990 +// --- add: add x0: 3 x1: 1 diff --git a/cue/interpreter/wasm/testdata/struct.golden b/cue/interpreter/wasm/testdata/struct.golden new file mode 100644 index 00000000000..7c8bcc87670 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct.golden @@ -0,0 +1,32 @@ +{ + magnitude2: magnitude2() @extern("struct.wasm", abi=c, sig="func(#vector2): float64") + magnitude3: magnitude3() @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + m0: 1.4142135623730951 + m1: 2.0 + m2: 798.6121211702211 + normalize2: normalize2 @extern("struct.wasm", abi=c, sig="func(#vector2): #vector2") + n2: { + x: 0.7071067811865476 + y: 0.7071067811865476 + } + n2m: 1.0 + m3: 1.7320508075688772 + m4: 2.8284271247461903 + m5: 6.666666666666667 + double3: double3 @extern("struct.wasm", abi=c, sig="func(#vector3): #vector3") + d4: { + x: 0.0 + y: 4.0 + z: 4.0 + } + cornucopia: cornucopia() @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + c0: 42 + c1: 6 + c2: 99 + c3: -10014990 + mag: magnitude_foo() @extern("struct.wasm", abi=c, name=magnitude_foo, sig="func(#foo): float64") + mb0: 1.4142135623730951 + mb1: 5.0 + mb2: 37.0 + mb3: 6.472356603278283 +} \ No newline at end of file diff --git a/cue/interpreter/wasm/testdata/struct/Cargo.lock b/cue/interpreter/wasm/testdata/struct/Cargo.lock new file mode 100644 index 00000000000..1d975707b19 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/Cargo.lock @@ -0,0 +1,62 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "greet" +version = "0.1.0" +dependencies = [ + "wee_alloc", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/cue/interpreter/wasm/testdata/struct/Cargo.toml b/cue/interpreter/wasm/testdata/struct/Cargo.toml new file mode 100644 index 00000000000..9d5785f6f78 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "greet" +version = "0.1.0" +edition = "2021" + +[lib] +# cdylib builds a a %.wasm file with `cargo build --release --target wasm32-unknown-unknown` +crate-type = ["cdylib"] +name = "struct" +path = "struct.rs" + +[dependencies] +# wee_aloc is a WebAssembly optimized allocator, which is needed to use non-numeric types like strings. +# See https://docs.rs/wee_alloc/latest/wee_alloc/ +wee_alloc = "0.4.5" + +# Below settings dramatically reduce wasm output size +# See https://rustwasm.github.io/book/reference/code-size.html#optimizing-builds-for-code-sizewasm-opt -Oz -o +# See https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 \ No newline at end of file diff --git a/cue/interpreter/wasm/testdata/struct/struct.cue b/cue/interpreter/wasm/testdata/struct/struct.cue new file mode 100644 index 00000000000..9e539c38107 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/struct.cue @@ -0,0 +1,82 @@ +@extern("wasm") +package p + +import "math" + +#vector2: { + x: float64 + y: float64 +} + +magnitude2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): float64") +magnitude3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): float64") + +_v0: {x: 1, y: 1} +_v1: {x: math.Sqrt2, y: math.Sqrt2} +_v2: {x: 123.456, y: 789.012} + +m0: magnitude2(_v0) +m1: magnitude2(_v1) +m2: magnitude2(_v2) + +normalize2: _ @extern("struct.wasm", abi=c, sig="func(#vector2): #vector2") +n2: normalize2(_v1) +n2m: magnitude2(n2) + +#vector3: { + x: float64 + y: float64 + z: float64 +} + +_v3: {x: 1, y: 1, z: 1} +_v4: {x: 0, y: 2, z: 2} +_v5: {x: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, y: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144, z: 3.84900179459750509672765853667971637098401167513417917345734884322651781535288897129144} + +m3: magnitude3(_v3) +m4: magnitude3(_v4) +m5: magnitude3(_v5) + +double3: _ @extern("struct.wasm", abi=c, sig="func(#vector3): #vector3") +d4: double3(_v4) + +cornucopia: _ @extern("struct.wasm", abi=c, sig="func(#cornucopia): int64") + +#cornucopia: { + b: bool + n0: int16 + n1: uint8 + n2: int64 +} + +_c0: {b: true, n0: 10, n1: 20, n2: 30} +_c1: {b: false, n0: 1, n1: 2, n2: 3} +_c2: {b: false, n0: -1, n1: 0, n2: 100} +_c3: {b: false, n0: -15000, n1: 10, n2: -10000000} + +c0: cornucopia(_c0) +c1: cornucopia(_c1) +c2: cornucopia(_c2) +c3: cornucopia(_c3) + +#foo: { + b: bool + bar: #bar +} + +#bar: { + b: bool + baz: #baz + n: uint16 +} + +#baz: { + vec: #vector2 +} + +mag: _ @extern("struct.wasm", abi=c, name=magnitude_foo, sig="func(#foo): float64") + +mb0: mag({b: false, bar: {b: true, baz: {vec: {x: 1, y: 1}}, n: 0}}) +mb1: mag({b: true, bar: {b: false, baz: {vec: {x: 3, y: 4}}, n: 1}}) +mb2: mag({b: false, bar: {b: true, baz: {vec: {x: 12, y: 35}}, n: 5}}) +mb3: mag({b: false, bar: {b: false, baz: {vec: {x: 3.33, y: 5.55}}, n: 110}}) diff --git a/cue/interpreter/wasm/testdata/struct/struct.rs b/cue/interpreter/wasm/testdata/struct/struct.rs new file mode 100644 index 00000000000..4f1a7061824 --- /dev/null +++ b/cue/interpreter/wasm/testdata/struct/struct.rs @@ -0,0 +1,115 @@ +/* + cargo build --release --target wasm32-unknown-unknown && cp target/wasm32-unknown-unknown/release/struct.wasm . +*/ + +extern crate alloc; +extern crate core; +extern crate wee_alloc; + +#[repr(C)] +pub struct Vector2 { + x: f64, + y: f64, +} + +#[repr(C)] +pub struct Vector3 { + x: f64, + y: f64, + z: f64, +} + +#[no_mangle] +pub extern "C" fn magnitude2(v: &Vector2) -> f64 { + (v.x.powi(2) + v.y.powi(2)).sqrt() +} + +#[no_mangle] +pub extern "C" fn magnitude3(v: &Vector3) -> f64 { + (v.x.powi(2) + v.y.powi(2) + v.z.powi(2)).sqrt() +} + +#[no_mangle] +pub extern "C" fn normalize2(v: &Vector2) -> Vector2 { + let l = magnitude2(v); + Vector2 { + x: v.x / l, + y: v.y / l, + } +} + +#[no_mangle] +pub extern "C" fn double3(v: &Vector3) -> Vector3 { + Vector3 { + x: v.x * 2.0, + y: v.y * 2.0, + z: v.z * 2.0, + } +} + +#[repr(C)] +pub struct Cornucopia { + b: bool, + n0: i16, + n1: u8, + n2: i64, +} + +#[no_mangle] +pub extern "C" fn cornucopia(x: &Cornucopia) -> i64 { + if x.b { + return 42; + } + return x.n0 as i64 + x.n1 as i64 + x.n2; +} + +#[repr(C)] +pub struct Foo { + b: bool, + bar: Bar, +} + +#[repr(C)] +pub struct Bar { + b: bool, + baz: Baz, + n: u16, +} + +#[repr(C)] +pub struct Baz { + vec: Vector2, +} + +#[no_mangle] +pub extern "C" fn magnitude_foo(x: &Foo) -> f64 { + magnitude2(&x.bar.baz.vec) +} + +use alloc::vec::Vec; +use std::mem::MaybeUninit; + +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[cfg_attr(all(target_arch = "wasm32"), export_name = "allocate")] +#[no_mangle] +pub extern "C" fn _allocate(size: u32) -> *mut u8 { + allocate(size as usize) +} + +fn allocate(size: usize) -> *mut u8 { + let vec: Vec> = Vec::with_capacity(size); + + Box::into_raw(vec.into_boxed_slice()) as *mut u8 +} + +#[cfg_attr(all(target_arch = "wasm32"), export_name = "deallocate")] +#[no_mangle] +pub unsafe extern "C" fn _deallocate(ptr: u32, size: u32) { + deallocate(ptr as *mut u8, size as usize); +} + +unsafe fn deallocate(ptr: *mut u8, size: usize) { + let _ = Vec::from_raw_parts(ptr, 0, size); +} diff --git a/cue/interpreter/wasm/testdata/struct/struct.wasm b/cue/interpreter/wasm/testdata/struct/struct.wasm new file mode 100755 index 00000000000..96bc67387f9 Binary files /dev/null and b/cue/interpreter/wasm/testdata/struct/struct.wasm differ