Skip to content

Commit

Permalink
cue/interpreter/wasm: add Wasm support for abi=c
Browse files Browse the repository at this point in the history
Add a new package, cuelang.org/go/cue/interpreter/wasm that enables
Wasm support in CUE as an external interpreter. Code wishing to use
Wasm must use an `@extern("wasm")` package attribute. Individual
functions are imported from Wasm modules like so:

	add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64")
	mul: _ @extern("foo.wasm", abi=c, sig="func(float64, float64): float64")
	not: _ @extern("foo.wasm", abi=c, sig="func(bool): bool")

Where "foo.wasm" is a compiled Wasm module, and sig is the type
signature of the imported function.

So far, only the C ABI is supported, and only relatively few data
types can be exchanged with the Wasm module. Basically only fixed-sized
integers of any signness, fixed-sized floats, and booleans.

The Wasm module is instantiated in a sandbox, with no access to the
outside world. Users must ensure the functions exposed by the Wasm
modules are pure, that is, they always returns the same answer for
the same arguments. Functions may make use of global state for
memoization and other optimizations as long as purity is preserved
as viewed from the outside. Be aware that functions from the standard
libraries of many languages are often not pure.

Wasm is only enabled if the user explicitly imports
cuelang.org/go/cue/wasm, otherwise it is not available. The Go cue
package does not impart upon the user a dependency on the Wasm
runtime if Wasm is not explicitly requested.

Wasm is enabled and available in the command line tool.

Updates #2035.
Updates #2281.
Updates #2282.
Updates #2007.

Change-Id: I844ec1229ea465dfeca45bc54006e87ed8ef0460
Signed-off-by: Aram Hăvărneanu <aram@mgk.ro>
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/551738
Unity-Result: CUEcueckoo <cueckoo+gerrithub@cuelang.org>
TryBot-Result: CUEcueckoo <cueckoo+gerrithub@cuelang.org>
Reviewed-by: Marcel van Lohuizen <mpvl@gmail.com>
Reviewed-by: Roger Peppe <rogpeppe@gmail.com>
  • Loading branch information
4ad committed May 17, 2023
1 parent 88922d1 commit 0520a3f
Show file tree
Hide file tree
Showing 42 changed files with 1,296 additions and 4 deletions.
3 changes: 2 additions & 1 deletion cmd/cue/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/interpreter/wasm"
"cuelang.org/go/internal/core/adt"
"cuelang.org/go/internal/encoding"
"cuelang.org/go/internal/filetypes"
Expand Down Expand Up @@ -125,7 +126,7 @@ For more information on writing CUE configuration files see cuelang.org.`,
c := &Command{
Command: cmd,
root: cmd,
ctx: cuecontext.New(),
ctx: cuecontext.New(cuecontext.Interpreter(wasm.New())),
}

cmdCmd := newCmdCmd(c)
Expand Down
78 changes: 78 additions & 0 deletions cue/interpreter/wasm/abi_c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 (
"cuelang.org/go/internal/pkg"
"github.com/tetratelabs/wazero/api"
)

func decodeRet(r uint64, t typ) any {
switch t {
case _bool:
u := api.DecodeU32(r)
if u == 1 {
return true
}
return false
case _int8, _int16, _int32:
return api.DecodeI32(r)
case _uint8, _uint16, _uint32:
return api.DecodeU32(r)
case _int64, _uint64:
return r
case _float32:
return api.DecodeF32(r)
case _float64:
return api.DecodeF64(r)
}
panic("unsupported return type")
}

// loadArg load the i'th argument (which must be of type t)
// passed to a function call represented by the call context.
// It returns the argument as an uint64, so it can be passed
// directly to Wasm functions.
func loadArg(c *pkg.CallCtxt, i int, t typ) uint64 {
switch t {
case _bool:
b := c.Bool(i)
if b {
return api.EncodeU32(1)
}
return api.EncodeU32(0)
case _int8:
return api.EncodeI32(int32(c.Int8(i)))
case _int16:
return api.EncodeI32(int32(c.Int16(i)))
case _int32:
return api.EncodeI32(c.Int32(i))
case _int64:
return api.EncodeI64(c.Int64(i))
case _uint8:
return api.EncodeU32(uint32(c.Uint8(i)))
case _uint16:
return api.EncodeU32(uint32(c.Uint16(i)))
case _uint32:
return api.EncodeU32(c.Uint32(i))
case _uint64:
return c.Uint64(i)
case _float32:
return api.EncodeF32(float32(c.Float64(i)))
case _float64:
return api.EncodeF64(c.Float64(i))
}
panic("unsupported argument type")
}
48 changes: 48 additions & 0 deletions cue/interpreter/wasm/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 (
"cuelang.org/go/internal/core/adt"
"cuelang.org/go/internal/pkg"
)

// builtin attempts to load the named function of type typ from the
// instance, returning it as an *adt.Builtin if successful, otherwise
// returning any encountered errors.
func builtin(name string, typ fnTyp, i *instance) (*adt.Builtin, error) {
b, err := loadBuiltin(name, typ, i)
if err != nil {
return nil, err
}
return pkg.ToBuiltin(b), nil
}

// loadBuiltin attempts to load the named function of type typ from
// the instance, returning it as an *pkg.Builtin if successful, otherwise
// returning any encountered errors.
func loadBuiltin(name string, typ fnTyp, i *instance) (*pkg.Builtin, error) {
fn, err := i.load(name)
if err != nil {
return nil, err
}
b := &pkg.Builtin{
Name: name,
Params: params(typ),
Result: typ.ret.kind(),
Func: i.callCtxFunc(fn, typ),
}
return b, nil
}
157 changes: 157 additions & 0 deletions cue/interpreter/wasm/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// 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 (
"context"
"fmt"
"os"

"cuelang.org/go/internal/pkg"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

// defaultRuntime is a global runtime for all Wasm modules used in a
// CUE process. It acts as a compilation cache, however, every module
// instance is independent. The same module loaded by two different
// CUE packages will not share memory, although it will share the
// excutable code produced by the runtime.
var defaultRuntime runtime

func init() {
ctx := context.Background()
defaultRuntime = runtime{
ctx: ctx,
Runtime: newRuntime(ctx),
}
}

// A runtime is a Wasm runtime that can compile, load, and execute
// Wasm code.
type runtime struct {
// ctx exists so that we have something to pass to Wazero
// functions, but it's unused otherwise.
ctx context.Context

wazero.Runtime
}

func newRuntime(ctx context.Context) wazero.Runtime {
r := wazero.NewRuntime(ctx)
wasi_snapshot_preview1.MustInstantiate(ctx, r)
return r
}

// compile takes the name of a Wasm module, and returns its compiled
// form, or an error.
func (r *runtime) compile(name string) (*module, error) {
buf, err := os.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("can't compile Wasm module: %w", err)
}

mod, err := r.Runtime.CompileModule(r.ctx, buf)
if err != nil {
return nil, fmt.Errorf("can't compile Wasm module: %w", err)
}
return &module{
runtime: r,
name: name,
CompiledModule: mod,
}, nil
}

// compileAndLoad is a convenience function that compile a module then
// loads it into memory returning the loaded instance, or an error.
func compileAndLoad(name string) (*instance, error) {
m, err := defaultRuntime.compile(name)
if err != nil {
return nil, err
}
i, err := m.load()
if err != nil {
return nil, err
}
return i, nil
}

// A module is a compiled Wasm module.
type module struct {
*runtime
name string
wazero.CompiledModule
}

// load loads the compiled module into memory, returning a new instance
// that can be called into, or an error. Different instances of the
// same module do not share memory.
func (m *module) load() (*instance, error) {
cfg := wazero.NewModuleConfig().WithName(m.name)
wInst, err := m.Runtime.InstantiateModule(m.ctx, m.CompiledModule, cfg)
if err != nil {
return nil, fmt.Errorf("can't instantiate Wasm module: %w", err)
}

inst := instance{
module: m,
instance: wInst,
}
return &inst, nil
}

// An instance is a Wasm module loaded into memory.
type instance struct {
*module
instance api.Module
}

// load attempts to load the named function from the instance, returning
// it if found, or an error.
func (i *instance) load(funcName string) (api.Function, error) {
f := i.instance.ExportedFunction(funcName)
if f == nil {
return nil, fmt.Errorf("can't find function %q in Wasm module %v", funcName, i.module.Name())
}
return f, nil
}

// callCtxFunc returns a function that wraps fn, which is assumed to
// be of type typ, into a function that knows how to load its arguments
// from CUE, call fn with the arguments, then pass its result
// back to CUE.
func (i *instance) callCtxFunc(fn api.Function, typ fnTyp) func(*pkg.CallCtxt) {
return func(c *pkg.CallCtxt) {
var args []uint64
for k, t := range typ.args {
//
// TODO: support more than abi=c here.
//
args = append(args, loadArg(c, k, t))
}
if c.Do() {
results, err := fn.Call(i.ctx, args...)
if err != nil {
c.Err = err
return
}
//
// TODO: support more than abi=c here.
//
c.Ret = decodeRet(results[0], typ.ret)
}
}
}
8 changes: 8 additions & 0 deletions cue/interpreter/wasm/testdata/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
The .wasm files are not generated by `go generate`, because we don't
want to burden CUE developers with a Rust dependency. Rather, each
.rs file contains instructions on how to compile it to Wasm in its
header.

A better option might be to have `go generate` compile the rust
code conditional on some environment variable, but we don't have
that yet.
12 changes: 12 additions & 0 deletions cue/interpreter/wasm/testdata/basic.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
add: add @extern("foo.wasm", abi=c, sig="func(int64, int64): int64")
mul: mul @extern("foo.wasm", abi=c, sig="func(float64, float64): float64")
not: not() @extern("foo.wasm", abi=c, sig="func(bool): bool")
x0: 3
x1: 1
x2: 101
y0: 15.0
y1: -8.425
y2: 7.006652
z: false
}
18 changes: 18 additions & 0 deletions cue/interpreter/wasm/testdata/basic/foo.cue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// This file checks basic Wasm functionality.

@extern("wasm")
package p

add: _ @extern("foo.wasm", abi=c, sig="func(int64, int64): int64")
mul: _ @extern("foo.wasm", abi=c, sig="func(float64, float64): float64")
not: _ @extern("foo.wasm", abi=c, sig="func(bool): bool")
x0: add(1, 2)
x1: add(-1, 2)
x2: add(100, 1)
y0: mul(3.0, 5.0)
y1: mul(-2.5, 3.37)
y2: mul(1.234, 5.678)
z: not(true)
27 changes: 27 additions & 0 deletions cue/interpreter/wasm/testdata/basic/foo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
rustc -O --target wasm32-wasi --crate-type cdylib -C link-arg=--strip-debug -Cpanic=abort $%
*/

#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}

#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
a + b
}

#[no_mangle]
pub extern "C" fn mul(a: f64, b: f64) -> f64 {
a * b
}

#[no_mangle]
pub extern "C" fn not(x: bool) -> bool {
!x
}
Binary file added cue/interpreter/wasm/testdata/basic/foo.wasm
Binary file not shown.
Loading

0 comments on commit 0520a3f

Please sign in to comment.