Skip to content

Commit

Permalink
cue/interpreter/wasm: add support for Wasm functions that take and re…
Browse files Browse the repository at this point in the history
…turn structs

Add support for structs in Wasm functions. A function declares that
is uses or returns a struct by referring to a CUE struct (defined
elsewhere) in its type signature. The CUE struct is a straightforward
translation of the native struct type with native scalars replaced
by their CUE counterpats (e.g. u32 in Rust becomes uint32 in CUE,
int16_t in C becomes int16 in CUE, etc). The order of fields in the
CUE struct must match that of the target language.

Only structs that contain scalars or other supported structs are
currently supported. Struct fields that are pointers in the target
language are not currently supported. This may change.

In order to support structs CUE needs access to the memory allocator
of the guest program. For this, the guest must export two Wasm
functions with the following C type signatures:

	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.

Updates #2035.
Updates #2281.

Change-Id: Ic509448226b22d2a54b70d565db6c702b801a839
Signed-off-by: Aram Hăvărneanu <aram@mgk.ro>
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1173964
TryBot-Result: CUEcueckoo <cueckoo@gmail.com>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Roger Peppe <rogpeppe@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
  • Loading branch information
4ad committed Feb 12, 2024
1 parent c896078 commit 2ce1036
Show file tree
Hide file tree
Showing 12 changed files with 1,041 additions and 17 deletions.
42 changes: 41 additions & 1 deletion cue/interpreter/wasm/abi_c.go → cue/interpreter/wasm/call.go
Expand Up @@ -119,27 +119,65 @@ 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
// arguments will be converted according to the ABI. The result of the
// 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 {
Expand All @@ -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()))
}
Expand Down
54 changes: 45 additions & 9 deletions cue/interpreter/wasm/doc.go
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
//
Expand All @@ -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<MaybeUninit<u8>> = 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
Expand Down

0 comments on commit 2ce1036

Please sign in to comment.