-
Notifications
You must be signed in to change notification settings - Fork 307
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cue/interpreter/wasm: add Wasm support for abi=c
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
Showing
42 changed files
with
1,296 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Oops, something went wrong.