Removes api.ImportRenamer for a different approach to "env" (#680)

Before, we introduced a type `api.ImportRenamer` to resolve conflicts
where the "env" module was shared between AssemblyScript and
user-defined functions. This API was never used in GitHub, and is
complicated.

AssemblyScript also isn't the only ABI to share the "env" module, as
other web APIs like Emscripten do also. The less complicated approach is
to have packages that need to share "env" use
`ModuleBuilder.ExportFunctions` instead, and use namespaces as needed if
there is overlap.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-07-11 14:57:26 +08:00
committed by GitHub
parent 09a8aa6030
commit 14d892d310
14 changed files with 322 additions and 493 deletions

View File

@@ -409,33 +409,6 @@ func DecodeF64(input uint64) float64 {
return math.Float64frombits(input)
}
// ImportRenamer applies during compilation after a module has been decoded from wasm, but before it is instantiated.
//
// For example, you may have a module like below, but the exported functions are in two different modules:
// (import "js" "increment" (func $increment (result i32)))
// (import "js" "decrement" (func $decrement (result i32)))
// (import "js" "wasm_increment" (func $wasm_increment (result i32)))
// (import "js" "wasm_decrement" (func $wasm_decrement (result i32)))
//
// The below breaks up the imports: "increment" and "decrement" from the module "go" and other functions from "wasm":
// renamer := func(externType api.ExternType, oldModule, oldName string) (string, string) {
// if externType != api.ExternTypeFunc {
// return oldModule, oldName
// }
// switch oldName {
// case "increment", "decrement": return "go", oldName
// default: return "wasm", oldName
// }
// }
//
// The resulting CompiledModule imports will look identical to this:
// (import "go" "increment" (func $increment (result i32)))
// (import "go" "decrement" (func $decrement (result i32)))
// (import "wasm" "wasm_increment" (func $wasm_increment (result i32)))
// (import "wasm" "wasm_decrement" (func $wasm_decrement (result i32)))
//
type ImportRenamer func(externType ExternType, oldModule, oldName string) (newModule, newName string)
// MemorySizer applies during compilation after a module has been decoded from wasm, but before it is instantiated.
// This determines the amount of memory pages (65536 bytes per page) to use when a memory is instantiated as a []byte.
//

View File

@@ -1,19 +1,24 @@
// Package assemblyscript contains Go-defined special functions imported by AssemblyScript under the module name "env".
// Package assemblyscript contains Go-defined special functions imported by
// AssemblyScript under the module name "env".
//
// Special Functions
//
// AssemblyScript code import the below special functions when not using WASI. Sometimes only "abort"
// AssemblyScript code import the below special functions when not using WASI.
// Note: Sometimes only "abort" is imported.
//
// * "abort" - exits with 255 with an abort message written to wazero.ModuleConfig WithStderr.
// * "abort" - exits with 255 with an abort message written to
// wazero.ModuleConfig WithStderr.
// * "trace" - no output unless.
// * "seed" - uses wazero.ModuleConfig WithRandSource as the source of seed values.
// * "seed" - uses wazero.ModuleConfig WithRandSource as the source of seed
// values.
//
// Relationship to WASI
//
// A program compiled to use WASI, via "import wasi" in any file, won't import these functions.
// See wasi_snapshot_preview1.InstantiateSnapshotPreview1
// A program compiled to use WASI, via "import wasi" in any file, won't import
// these functions.
//
// See https://www.assemblyscript.org/concepts.html#special-imports
// See wasi_snapshot_preview1.Instantiate and
// https://www.assemblyscript.org/concepts.html#special-imports
package assemblyscript
import (
@@ -31,44 +36,46 @@ import (
"github.com/tetratelabs/wazero/sys"
)
// Instantiate instantiates the "env" module used by AssemblyScript into the runtime default namespace.
// Instantiate instantiates the "env" module used by AssemblyScript into the
// runtime default namespace.
//
// Notes
//
// * Closing the wazero.Runtime has the same effect as closing the result.
// * To instantiate into another wazero.Namespace, use NewBuilder instead.
// * To add more functions to the "env" module, use FunctionExporter.
// * To instantiate into another wazero.Namespace, use FunctionExporter.
func Instantiate(ctx context.Context, r wazero.Runtime) (api.Closer, error) {
return NewBuilder(r).Instantiate(ctx, r)
return r.NewModuleBuilder("env").
ExportFunctions(NewFunctionExporter().ExportFunctions()).
Instantiate(ctx, r)
}
// Builder configures the "env" module used by AssemblyScript for later use via Compile or Instantiate.
type Builder interface {
// WithAbortMessageDisabled configures the AssemblyScript abort function to discard any message.
WithAbortMessageDisabled() Builder
// FunctionExporter configures the functions in the "env" module used by
// AssemblyScript.
type FunctionExporter interface {
// WithAbortMessageDisabled configures the AssemblyScript abort function to
// discard any message.
WithAbortMessageDisabled() FunctionExporter
// WithTraceToStdout configures the AssemblyScript trace function to output messages to Stdout, as configured by
// wazero.ModuleConfig WithStdout.
WithTraceToStdout() Builder
// WithTraceToStdout configures the AssemblyScript trace function to output
// messages to Stdout, as configured by wazero.ModuleConfig WithStdout.
WithTraceToStdout() FunctionExporter
// WithTraceToStderr configures the AssemblyScript trace function to output messages to Stderr, as configured by
// wazero.ModuleConfig WithStderr. Because of the potential volume of trace messages, it is often more appropriate
// to use WithTraceToStdout instead.
WithTraceToStderr() Builder
// Compile compiles the "env" module that can instantiated in any namespace (wazero.Namespace).
// WithTraceToStderr configures the AssemblyScript trace function to output
// messages to Stderr, as configured by wazero.ModuleConfig WithStderr.
//
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error)
// Because of the potential volume of trace messages, it is often more
// appropriate to use WithTraceToStdout instead.
WithTraceToStderr() FunctionExporter
// Instantiate instantiates the "env" module into the provided namespace.
//
// Note: This has the same effect as the same function on wazero.ModuleBuilder.
Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
// ExportFunctions builds functions to export with a wazero.ModuleBuilder
// named "env".
ExportFunctions() (nameToGoFunc map[string]interface{})
}
// NewBuilder returns a new Builder with trace disabled.
func NewBuilder(r wazero.Runtime) Builder {
return &builder{r: r, traceMode: traceDisabled}
// NewFunctionExporter returns a FunctionExporter object with trace disabled.
func NewFunctionExporter() FunctionExporter {
return &functionExporter{traceMode: traceDisabled}
}
type traceMode int
@@ -79,54 +86,44 @@ const (
traceStderr traceMode = 2
)
type builder struct {
r wazero.Runtime
type functionExporter struct {
abortMessageDisabled bool
traceMode traceMode
}
// WithAbortMessageDisabled implements Builder.WithAbortMessageDisabled
func (b *builder) WithAbortMessageDisabled() Builder {
ret := *b // copy
// WithAbortMessageDisabled implements FunctionExporter.WithAbortMessageDisabled
func (e *functionExporter) WithAbortMessageDisabled() FunctionExporter {
ret := *e // copy
ret.abortMessageDisabled = true
return &ret
}
// WithTraceToStdout implements Builder.WithTraceToStdout
func (b *builder) WithTraceToStdout() Builder {
ret := *b // copy
// WithTraceToStdout implements FunctionExporter.WithTraceToStdout
func (e *functionExporter) WithTraceToStdout() FunctionExporter {
ret := *e // copy
ret.traceMode = traceStdout
return &ret
}
// WithTraceToStderr implements Builder.WithTraceToStderr
func (b *builder) WithTraceToStderr() Builder {
ret := *b // copy
// WithTraceToStderr implements FunctionExporter.WithTraceToStderr
func (e *functionExporter) WithTraceToStderr() FunctionExporter {
ret := *e // copy
ret.traceMode = traceStderr
return &ret
}
// moduleBuilder returns a new wazero.ModuleBuilder
func (b *builder) moduleBuilder() wazero.ModuleBuilder {
env := &assemblyscript{abortMessageDisabled: b.abortMessageDisabled, traceMode: b.traceMode}
return b.r.NewModuleBuilder("env").
ExportFunction("abort", env.abort).
ExportFunction("trace", env.trace).
ExportFunction("seed", env.seed)
// ExportFunctions implements FunctionExporter.ExportFunctions
func (e *functionExporter) ExportFunctions() (nameToGoFunc map[string]interface{}) {
env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode}
return map[string]interface{}{
"abort": env.abort,
"trace": env.trace,
"seed": env.seed,
}
}
// Compile implements Builder.Compile
func (b *builder) Compile(ctx context.Context, config wazero.CompileConfig) (wazero.CompiledModule, error) {
return b.moduleBuilder().Compile(ctx, config)
}
// Instantiate implements Builder.Instantiate
func (b *builder) Instantiate(ctx context.Context, ns wazero.Namespace) (api.Closer, error) {
return b.moduleBuilder().Instantiate(ctx, ns)
}
// assemblyScript includes "Special imports" only used In AssemblyScript when a user didn't add `import "wasi"` to their
// entry file.
// assemblyScript includes "Special imports" only used In AssemblyScript when a
// user didn't add `import "wasi"` to their entry file.
//
// See https://www.assemblyscript.org/concepts.html#special-imports
// See https://www.assemblyscript.org/concepts.html#targeting-wasi
@@ -137,13 +134,15 @@ type assemblyscript struct {
traceMode traceMode
}
// abort is called on unrecoverable errors. This is typically present in Wasm compiled from AssemblyScript, if
// assertions are enabled or errors are thrown.
// abort is called on unrecoverable errors. This is typically present in Wasm
// compiled from AssemblyScript, if assertions are enabled or errors are
// thrown.
//
// The implementation writes the message to stderr, unless abortMessageDisabled, and closes the module with exit code
// 255.
// The implementation writes the message to stderr, unless
// abortMessageDisabled, and closes the module with exit code 255.
//
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
// Here's the import in a user's module that ends up using this, in WebAssembly
// 1.0 (MVP) Text Format:
// (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L18
@@ -179,9 +178,11 @@ func (a *assemblyscript) abort(
panic(sys.NewExitError(mod.Name(), exitCode))
}
// trace implements the same named function in AssemblyScript (ex. trace('Hello World!'))
// trace implements the same named function in AssemblyScript. Ex.
// trace('Hello World!')
//
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
// Here's the import in a user's module that ends up using this, in WebAssembly
// 1.0 (MVP) Text Format:
// (import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L61
@@ -236,9 +237,11 @@ func formatFloat(f float64) string {
return strconv.FormatFloat(f, 'g', -1, 64)
}
// seed is called when the AssemblyScript's random number generator needs to be seeded
// seed is called when the AssemblyScript's random number generator needs to be
// seeded.
//
// Here's the import in a user's module that ends up using this, in WebAssembly 1.0 (MVP) Text Format:
// Here's the import in a user's module that ends up using this, in WebAssembly
// 1.0 (MVP) Text Format:
// (import "env" "seed" (func $~lib/builtins/seed (result f64)))
//
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111

View File

@@ -0,0 +1,45 @@
package assemblyscript_test
import (
"context"
_ "embed"
"log"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/assemblyscript"
)
// This shows how to instantiate AssemblyScript's special imports.
func Example_instantiate() {
ctx := context.Background()
r := wazero.NewRuntime()
defer r.Close(ctx) // This closes everything this Runtime created.
// This adds the "env" module to the runtime, with AssemblyScript's special
// function imports.
if _, err := assemblyscript.Instantiate(ctx, r); err != nil {
log.Panicln(err)
}
// Output:
}
// This shows how to instantiate AssemblyScript's special imports when you also
// need other functions in the "env" module.
func Example_functionExporter() {
ctx := context.Background()
r := wazero.NewRuntime()
defer r.Close(ctx) // This closes everything this Runtime created.
// First construct your own module builder for "env"
envBuilder := r.NewModuleBuilder("env").
ExportFunction("get_int", func() uint32 { return 1 })
// Now, add AssemblyScript special function imports into it.
envBuilder.ExportFunctions(assemblyscript.NewFunctionExporter().
WithAbortMessageDisabled().ExportFunctions())
// Output:
}

View File

@@ -41,17 +41,17 @@ var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
func TestAbort(t *testing.T) {
tests := []struct {
name string
enabled bool
exporter FunctionExporter
expected string
}{
{
name: "enabled",
enabled: true,
exporter: NewFunctionExporter(),
expected: "message at filename:1:2\n",
},
{
name: "disabled",
enabled: false,
exporter: NewFunctionExporter().WithAbortMessageDisabled(),
expected: "",
},
}
@@ -65,13 +65,10 @@ func TestAbort(t *testing.T) {
out := &bytes.Buffer{}
if tc.enabled {
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
} else {
_, err := NewBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx, r)
require.NoError(t, err)
}
_, err := r.NewModuleBuilder("env").
ExportFunctions(tc.exporter.ExportFunctions()).
Instantiate(testCtx, r)
require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err)
@@ -93,6 +90,54 @@ func TestAbort(t *testing.T) {
}
}
func TestAbort_Error(t *testing.T) {
tests := []struct {
name string
messageUTF16 []byte
fileNameUTF16 []byte
}{
{
name: "bad message",
messageUTF16: encodeUTF16("message")[:5],
fileNameUTF16: encodeUTF16("filename"),
},
{
name: "bad filename",
messageUTF16: encodeUTF16("message"),
fileNameUTF16: encodeUTF16("filename")[:5],
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
out := &bytes.Buffer{}
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out)
mod, err := r.InstantiateModule(testCtx, compiled, exporter)
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.NoError(t, err)
require.Equal(t, "", out.String()) // nothing output if strings fail to read.
})
}
}
var unreachableAfterAbort = `(module
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
(func $main
@@ -106,12 +151,15 @@ var unreachableAfterAbort = `(module
(start $main)
)`
// TestAbort_StopsExecution ensures code that follows an abort isn't invoked.
func TestAbort_StopsExecution(t *testing.T) {
// TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked.
func TestAbort_UnreachableAfter(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := NewBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx, r)
_, err := r.NewModuleBuilder("env").
// Disable the abort message as we are passing invalid memory offsets.
ExportFunctions(NewFunctionExporter().WithAbortMessageDisabled().ExportFunctions()).
Instantiate(testCtx, r)
require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort)
@@ -149,47 +197,95 @@ func TestSeed(t *testing.T) {
require.Equal(t, uint64(506097522914230528), res[0])
}
func TestSeed_error(t *testing.T) {
tests := []struct {
name string
source io.Reader
expectedErr string
}{
{
name: "not 8 bytes",
source: bytes.NewReader([]byte{0, 1}),
expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
{
name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading random seed: ice cream (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
mod, err := r.InstantiateModule(testCtx, compiled, exporter)
require.NoError(t, err)
_, err = mod.ExportedFunction("seed").Call(testCtx)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func TestTrace(t *testing.T) {
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
tests := []struct {
name string
mode traceMode
exporter FunctionExporter
params []uint64
expected string
}{
{
name: "stderr",
mode: traceStderr,
params: noArgs,
expected: "trace: hello\n",
},
{
name: "stdout",
mode: traceStdout,
params: noArgs,
expected: "trace: hello\n",
},
{
name: "disabled",
mode: traceDisabled,
exporter: NewFunctionExporter(),
params: noArgs,
expected: "",
},
{
name: "one",
mode: traceStdout,
name: "ToStderr",
exporter: NewFunctionExporter().WithTraceToStderr(),
params: noArgs,
expected: "trace: hello\n",
},
{
name: "ToStdout - no args",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: noArgs,
expected: "trace: hello\n",
},
{
name: "ToStdout - one arg",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
expected: "trace: hello 1\n",
},
{
name: "two",
mode: traceStdout,
name: "ToStdout - two args",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
expected: "trace: hello 1,2\n",
},
{
name: "five",
mode: traceStdout,
name: "ToStdout - five args",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{
4,
5,
@@ -210,24 +306,9 @@ func TestTrace(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
out := &bytes.Buffer{}
as := NewBuilder(r)
modConfig := wazero.NewModuleConfig()
switch tc.mode {
case traceStderr:
as = as.WithTraceToStderr()
modConfig = modConfig.WithStderr(out)
case traceStdout:
as = as.WithTraceToStdout()
modConfig = modConfig.WithStdout(out)
case traceDisabled:
// Set but not used
modConfig = modConfig.WithStderr(out)
modConfig = modConfig.WithStdout(out)
}
_, err := as.Instantiate(testCtx, r)
_, err := r.NewModuleBuilder("env").
ExportFunctions(tc.exporter.ExportFunctions()).
Instantiate(testCtx, r)
require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat)
@@ -236,7 +317,15 @@ func TestTrace(t *testing.T) {
code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, modConfig)
out := &bytes.Buffer{}
config := wazero.NewModuleConfig()
if tc.name == "ToStderr" {
config.WithStderr(out)
} else {
config.WithStdout(out)
}
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStdout(out).WithStderr(out))
require.NoError(t, err)
message := encodeUTF16("hello")
@@ -252,7 +341,65 @@ func TestTrace(t *testing.T) {
}
}
func TestReadAssemblyScriptString(t *testing.T) {
func TestTrace_error(t *testing.T) {
tests := []struct {
name string
message []byte
out io.Writer
expectedErr string
}{
{
name: "not 8 bytes",
message: encodeUTF16("hello")[:5],
out: &bytes.Buffer{},
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
{
name: "error writing",
message: encodeUTF16("hello"),
out: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := r.NewModuleBuilder("env").
ExportFunctions(NewFunctionExporter().WithTraceToStdout().ExportFunctions()).
Instantiate(testCtx, r)
require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
exporter := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
mod, err := r.InstantiateModule(testCtx, compiled, exporter)
require.NoError(t, err)
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func Test_readAssemblyScriptString(t *testing.T) {
tests := []struct {
name string
memory func(api.Memory)
@@ -325,54 +472,6 @@ func TestReadAssemblyScriptString(t *testing.T) {
}
}
func TestAbort_error(t *testing.T) {
tests := []struct {
name string
messageUTF16 []byte
fileNameUTF16 []byte
}{
{
name: "bad message",
messageUTF16: encodeUTF16("message")[:5],
fileNameUTF16: encodeUTF16("filename"),
},
{
name: "bad filename",
messageUTF16: encodeUTF16("message"),
fileNameUTF16: encodeUTF16("filename")[:5],
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
out := &bytes.Buffer{}
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.NoError(t, err)
require.Equal(t, "", out.String()) // nothing output if strings fail to read.
})
}
}
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
off := 0
ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16)))
@@ -391,110 +490,6 @@ func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fi
return messageOff, filenameOff
}
func TestSeed_error(t *testing.T) {
tests := []struct {
name string
source io.Reader
expectedErr string
}{
{
name: "not 8 bytes",
source: bytes.NewReader([]byte{0, 1}),
expectedErr: `error reading random seed: unexpected EOF (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
{
name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading random seed: ice cream (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
seedWasm, err := watzero.Wat2Wasm(seedWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err)
config := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
_, err = mod.ExportedFunction("seed").Call(testCtx)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func TestTrace_error(t *testing.T) {
tests := []struct {
name string
message []byte
out io.Writer
expectedErr string
}{
{
name: "not 8 bytes",
message: encodeUTF16("hello")[:5],
out: &bytes.Buffer{},
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
{
name: "error writing",
message: encodeUTF16("hello"),
out: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := NewBuilder(r).WithTraceToStdout().Instantiate(testCtx, r)
require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func encodeUTF16(s string) []byte {
runes := utf16.Encode([]rune(s))
b := make([]byte, len(runes)*2)

View File

@@ -291,32 +291,23 @@ func (c *compiledModule) Close(_ context.Context) error {
// CompileConfig allows you to override what was decoded from wasm, prior to compilation (ModuleBuilder.Compile or
// Runtime.CompileModule).
//
// For example, WithImportRenamer allows you to override hard-coded names that don't match your requirements.
// For example, WithMemorySizer allows you to override memoryc size that doesn't match your requirements.
//
// Note: CompileConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
type CompileConfig interface {
// WithImportRenamer can rename imports or break them into different modules. No default.
// A nil function is invalid and ignored.
//
// Note: This is currently not relevant for ModuleBuilder as it has no means to define imports.
WithImportRenamer(api.ImportRenamer) CompileConfig
// WithMemorySizer are the allocation parameters used for a Wasm memory.
// The default is to set cap=min and max=65536 if unset. A nil function is invalid and ignored.
WithMemorySizer(api.MemorySizer) CompileConfig
}
type compileConfig struct {
importRenamer api.ImportRenamer
memorySizer api.MemorySizer
memorySizer api.MemorySizer
}
// NewCompileConfig returns a CompileConfig that can be used for configuring module compilation.
func NewCompileConfig() CompileConfig {
return &compileConfig{
importRenamer: nil,
memorySizer: wasm.MemorySizer,
memorySizer: wasm.MemorySizer,
}
}
@@ -326,16 +317,6 @@ func (c *compileConfig) clone() *compileConfig {
return &ret
}
// WithImportRenamer implements CompileConfig.WithImportRenamer
func (c *compileConfig) WithImportRenamer(importRenamer api.ImportRenamer) CompileConfig {
if importRenamer == nil {
return c
}
ret := c.clone()
ret.importRenamer = importRenamer
return ret
}
// WithMemorySizer implements CompileConfig.WithMemorySizer
func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConfig {
if memorySizer == nil {

View File

@@ -9,7 +9,6 @@ import (
"testing"
"testing/fstest"
"github.com/tetratelabs/wazero/api"
internalsys "github.com/tetratelabs/wazero/internal/sys"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
@@ -190,12 +189,6 @@ func TestRuntimeConfig_FeatureToggle(t *testing.T) {
}
func TestCompileConfig(t *testing.T) {
im := func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) {
return "a", oldName
}
im2 := func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) {
return "b", oldName
}
mp := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 0, 1, 1
}
@@ -204,20 +197,6 @@ func TestCompileConfig(t *testing.T) {
with func(CompileConfig) CompileConfig
expected *compileConfig
}{
{
name: "WithImportRenamer",
with: func(c CompileConfig) CompileConfig {
return c.WithImportRenamer(im)
},
expected: &compileConfig{importRenamer: im},
},
{
name: "WithImportRenamer twice",
with: func(c CompileConfig) CompileConfig {
return c.WithImportRenamer(im).WithImportRenamer(im2)
},
expected: &compileConfig{importRenamer: im2},
},
{
name: "WithMemorySizer",
with: func(c CompileConfig) CompileConfig {
@@ -242,7 +221,6 @@ func TestCompileConfig(t *testing.T) {
// We cannot compare func, but we can compare reflect.Value
// See https://go.dev/ref/spec#Comparison_operators
require.Equal(t, reflect.ValueOf(tc.expected.importRenamer), reflect.ValueOf(rc.importRenamer))
require.Equal(t, reflect.ValueOf(tc.expected.memorySizer), reflect.ValueOf(rc.memorySizer))
// The source wasn't modified
require.Equal(t, &compileConfig{}, input)

View File

@@ -1 +0,0 @@
replace-import

View File

@@ -1,4 +0,0 @@
## Replace import example
This example shows how to override a module name hard-coded in a WebAssembly
module. This is similar to what some tools call "linking".

View File

@@ -1,65 +0,0 @@
package main
import (
"context"
_ "embed"
"fmt"
"log"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// needsImportWasm was generated by the following:
// cd testdata; wat2wasm --debug-names needs_import.wat
//go:embed testdata/needs_import.wasm
var needsImportWasm []byte
// main shows how to override a module or function name hard-coded in a
// WebAssembly module. This is similar to what some tools call "linking".
func main() {
// Choose the context to use for function calls.
ctx := context.Background()
// Create a new WebAssembly Runtime.
r := wazero.NewRuntime()
defer r.Close(ctx) // This closes everything this Runtime created.
// Instantiate a Go-defined module named "assemblyscript" that exports a
// function to close the module that calls "abort".
host, err := r.NewModuleBuilder("assemblyscript").
ExportFunction("abort", func(ctx context.Context, m api.Module, messageOffset, fileNameOffset, line, col uint32) {
_ = m.CloseWithExitCode(ctx, 255)
}).Instantiate(ctx, r)
if err != nil {
log.Panicln(err)
}
defer host.Close(ctx)
// Compile the WebAssembly module, replacing the import "env.abort" with
// "assemblyscript.abort".
compileConfig := wazero.NewCompileConfig().
WithImportRenamer(func(externType api.ExternType, oldModule, oldName string) (newModule, newName string) {
if oldModule == "env" && oldName == "abort" {
return "assemblyscript", "abort"
}
return oldModule, oldName
})
code, err := r.CompileModule(ctx, needsImportWasm, compileConfig)
if err != nil {
log.Panicln(err)
}
defer code.Close(ctx)
// Instantiate the WebAssembly module.
mod, err := r.InstantiateModule(ctx, code, wazero.NewModuleConfig())
if err != nil {
log.Panicln(err)
}
defer mod.Close(ctx)
// Since the above worked, the exported function closes the module.
_, err = mod.ExportedFunction("abort").Call(ctx, 0, 0, 0, 0)
fmt.Println(err)
}

View File

@@ -1,16 +0,0 @@
package main
import (
"testing"
"github.com/tetratelabs/wazero/internal/testing/maintester"
"github.com/tetratelabs/wazero/internal/testing/require"
)
// Test_main ensures the following will work:
//
// go run replace-import.go
func Test_main(t *testing.T) {
stdout, _ := maintester.TestMain(t, main, "replace-import")
require.Equal(t, "module \"needs-import\" closed with exit_code(255)\n", stdout)
}

View File

@@ -1,3 +0,0 @@
(module $needs-import
(func (export "abort") (import "env" "abort") (param i32 i32 i32 i32))
)

View File

@@ -174,13 +174,6 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte, cConfig Comp
return nil, err
}
// Replace imports if any configuration exists to do so.
if importRenamer := config.importRenamer; importRenamer != nil {
for _, i := range internal.ImportSection {
i.Module, i.Name = importRenamer(i.Type, i.Module, i.Name)
}
}
internal.AssignModuleID(binary)
if err = r.store.Engine.CompileModule(ctx, internal); err != nil {

View File

@@ -78,56 +78,6 @@ func TestRuntime_CompileModule(t *testing.T) {
Max: 3,
}, code.module.MemorySection)
})
t.Run("WithImportReplacements", func(t *testing.T) {
testBin, err := watzero.Wat2Wasm(`(module
(import "js" "increment" (func $increment (result i32)))
(import "js" "decrement" (func $decrement (result i32)))
(import "js" "wasm_increment" (func $wasm_increment (result i32)))
(import "js" "wasm_decrement" (func $wasm_decrement (result i32)))
)`)
require.NoError(t, err)
m, err := r.CompileModule(testCtx, testBin, NewCompileConfig().
WithImportRenamer(func(externType api.ExternType, oldModule, oldName string) (string, string) {
if externType != api.ExternTypeFunc {
return oldModule, oldName
}
switch oldName {
case "increment", "decrement":
return "go", oldName
case "wasm_increment", "wasm_decrement":
return "wasm", oldName
default:
return oldModule, oldName
}
}))
require.NoError(t, err)
code := m.(*compiledModule)
require.Equal(t, []*wasm.Import{
{
Module: "go", Name: "increment",
Type: wasm.ExternTypeFunc,
DescFunc: 0,
},
{
Module: "go", Name: "decrement",
Type: wasm.ExternTypeFunc,
DescFunc: 0,
},
{
Module: "wasm", Name: "wasm_increment",
Type: wasm.ExternTypeFunc,
DescFunc: 0,
},
{
Module: "wasm", Name: "wasm_decrement",
Type: wasm.ExternTypeFunc,
DescFunc: 0,
},
}, code.module.ImportSection)
})
}
func TestRuntime_CompileModule_Errors(t *testing.T) {