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) 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. // 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. // 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 // 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. // * "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 // Relationship to WASI
// //
// A program compiled to use WASI, via "import wasi" in any file, won't import these functions. // A program compiled to use WASI, via "import wasi" in any file, won't import
// See wasi_snapshot_preview1.InstantiateSnapshotPreview1 // 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 package assemblyscript
import ( import (
@@ -31,44 +36,46 @@ import (
"github.com/tetratelabs/wazero/sys" "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 // Notes
// //
// * Closing the wazero.Runtime has the same effect as closing the result. // * 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) { 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. // FunctionExporter configures the functions in the "env" module used by
type Builder interface { // AssemblyScript.
// WithAbortMessageDisabled configures the AssemblyScript abort function to discard any message. type FunctionExporter interface {
WithAbortMessageDisabled() Builder // 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 // WithTraceToStdout configures the AssemblyScript trace function to output
// wazero.ModuleConfig WithStdout. // messages to Stdout, as configured by wazero.ModuleConfig WithStdout.
WithTraceToStdout() Builder WithTraceToStdout() FunctionExporter
// WithTraceToStderr configures the AssemblyScript trace function to output messages to Stderr, as configured by // WithTraceToStderr configures the AssemblyScript trace function to output
// wazero.ModuleConfig WithStderr. Because of the potential volume of trace messages, it is often more appropriate // messages to Stderr, as configured by wazero.ModuleConfig WithStderr.
// to use WithTraceToStdout instead.
WithTraceToStderr() Builder
// Compile compiles the "env" module that can instantiated in any namespace (wazero.Namespace).
// //
// Note: This has the same effect as the same function on wazero.ModuleBuilder. // Because of the potential volume of trace messages, it is often more
Compile(context.Context, wazero.CompileConfig) (wazero.CompiledModule, error) // appropriate to use WithTraceToStdout instead.
WithTraceToStderr() FunctionExporter
// Instantiate instantiates the "env" module into the provided namespace. // ExportFunctions builds functions to export with a wazero.ModuleBuilder
// // named "env".
// Note: This has the same effect as the same function on wazero.ModuleBuilder. ExportFunctions() (nameToGoFunc map[string]interface{})
Instantiate(context.Context, wazero.Namespace) (api.Closer, error)
} }
// NewBuilder returns a new Builder with trace disabled. // NewFunctionExporter returns a FunctionExporter object with trace disabled.
func NewBuilder(r wazero.Runtime) Builder { func NewFunctionExporter() FunctionExporter {
return &builder{r: r, traceMode: traceDisabled} return &functionExporter{traceMode: traceDisabled}
} }
type traceMode int type traceMode int
@@ -79,54 +86,44 @@ const (
traceStderr traceMode = 2 traceStderr traceMode = 2
) )
type builder struct { type functionExporter struct {
r wazero.Runtime
abortMessageDisabled bool abortMessageDisabled bool
traceMode traceMode traceMode traceMode
} }
// WithAbortMessageDisabled implements Builder.WithAbortMessageDisabled // WithAbortMessageDisabled implements FunctionExporter.WithAbortMessageDisabled
func (b *builder) WithAbortMessageDisabled() Builder { func (e *functionExporter) WithAbortMessageDisabled() FunctionExporter {
ret := *b // copy ret := *e // copy
ret.abortMessageDisabled = true ret.abortMessageDisabled = true
return &ret return &ret
} }
// WithTraceToStdout implements Builder.WithTraceToStdout // WithTraceToStdout implements FunctionExporter.WithTraceToStdout
func (b *builder) WithTraceToStdout() Builder { func (e *functionExporter) WithTraceToStdout() FunctionExporter {
ret := *b // copy ret := *e // copy
ret.traceMode = traceStdout ret.traceMode = traceStdout
return &ret return &ret
} }
// WithTraceToStderr implements Builder.WithTraceToStderr // WithTraceToStderr implements FunctionExporter.WithTraceToStderr
func (b *builder) WithTraceToStderr() Builder { func (e *functionExporter) WithTraceToStderr() FunctionExporter {
ret := *b // copy ret := *e // copy
ret.traceMode = traceStderr ret.traceMode = traceStderr
return &ret return &ret
} }
// moduleBuilder returns a new wazero.ModuleBuilder // ExportFunctions implements FunctionExporter.ExportFunctions
func (b *builder) moduleBuilder() wazero.ModuleBuilder { func (e *functionExporter) ExportFunctions() (nameToGoFunc map[string]interface{}) {
env := &assemblyscript{abortMessageDisabled: b.abortMessageDisabled, traceMode: b.traceMode} env := &assemblyscript{abortMessageDisabled: e.abortMessageDisabled, traceMode: e.traceMode}
return b.r.NewModuleBuilder("env"). return map[string]interface{}{
ExportFunction("abort", env.abort). "abort": env.abort,
ExportFunction("trace", env.trace). "trace": env.trace,
ExportFunction("seed", env.seed) "seed": env.seed,
}
} }
// Compile implements Builder.Compile // assemblyScript includes "Special imports" only used In AssemblyScript when a
func (b *builder) Compile(ctx context.Context, config wazero.CompileConfig) (wazero.CompiledModule, error) { // user didn't add `import "wasi"` to their entry file.
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.
// //
// See https://www.assemblyscript.org/concepts.html#special-imports // See https://www.assemblyscript.org/concepts.html#special-imports
// See https://www.assemblyscript.org/concepts.html#targeting-wasi // See https://www.assemblyscript.org/concepts.html#targeting-wasi
@@ -137,13 +134,15 @@ type assemblyscript struct {
traceMode traceMode traceMode traceMode
} }
// abort is called on unrecoverable errors. This is typically present in Wasm compiled from AssemblyScript, if // abort is called on unrecoverable errors. This is typically present in Wasm
// assertions are enabled or errors are thrown. // 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 // The implementation writes the message to stderr, unless
// 255. // 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))) // (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 // 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)) 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))) // (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 // 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) 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))) // (import "env" "seed" (func $~lib/builtins/seed (result f64)))
// //
// See https://github.com/AssemblyScript/assemblyscript/blob/fa14b3b03bd4607efa52aaff3132bea0c03a7989/std/assembly/wasi/index.ts#L111 // 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) { func TestAbort(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
enabled bool exporter FunctionExporter
expected string expected string
}{ }{
{ {
name: "enabled", name: "enabled",
enabled: true, exporter: NewFunctionExporter(),
expected: "message at filename:1:2\n", expected: "message at filename:1:2\n",
}, },
{ {
name: "disabled", name: "disabled",
enabled: false, exporter: NewFunctionExporter().WithAbortMessageDisabled(),
expected: "", expected: "",
}, },
} }
@@ -65,13 +65,10 @@ func TestAbort(t *testing.T) {
out := &bytes.Buffer{} out := &bytes.Buffer{}
if tc.enabled { _, err := r.NewModuleBuilder("env").
_, err := Instantiate(testCtx, r) ExportFunctions(tc.exporter.ExportFunctions()).
require.NoError(t, err) Instantiate(testCtx, r)
} else { require.NoError(t, err)
_, err := NewBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx, r)
require.NoError(t, err)
}
abortWasm, err := watzero.Wat2Wasm(abortWat) abortWasm, err := watzero.Wat2Wasm(abortWat)
require.NoError(t, err) 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 var unreachableAfterAbort = `(module
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32))) (import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
(func $main (func $main
@@ -106,12 +151,15 @@ var unreachableAfterAbort = `(module
(start $main) (start $main)
)` )`
// TestAbort_StopsExecution ensures code that follows an abort isn't invoked. // TestAbort_UnreachableAfter ensures code that follows an abort isn't invoked.
func TestAbort_StopsExecution(t *testing.T) { func TestAbort_UnreachableAfter(t *testing.T) {
r := wazero.NewRuntime() r := wazero.NewRuntime()
defer r.Close(testCtx) 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) require.NoError(t, err)
abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort) abortWasm, err := watzero.Wat2Wasm(unreachableAfterAbort)
@@ -149,47 +197,95 @@ func TestSeed(t *testing.T) {
require.Equal(t, uint64(506097522914230528), res[0]) 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) { func TestTrace(t *testing.T) {
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0} noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
tests := []struct { tests := []struct {
name string name string
mode traceMode exporter FunctionExporter
params []uint64 params []uint64
expected string expected string
}{ }{
{
name: "stderr",
mode: traceStderr,
params: noArgs,
expected: "trace: hello\n",
},
{
name: "stdout",
mode: traceStdout,
params: noArgs,
expected: "trace: hello\n",
},
{ {
name: "disabled", name: "disabled",
mode: traceDisabled, exporter: NewFunctionExporter(),
params: noArgs, params: noArgs,
expected: "", expected: "",
}, },
{ {
name: "one", name: "ToStderr",
mode: traceStdout, 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}, params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
expected: "trace: hello 1\n", expected: "trace: hello 1\n",
}, },
{ {
name: "two", name: "ToStdout - two args",
mode: traceStdout, exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0}, params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
expected: "trace: hello 1,2\n", expected: "trace: hello 1,2\n",
}, },
{ {
name: "five", name: "ToStdout - five args",
mode: traceStdout, exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{ params: []uint64{
4, 4,
5, 5,
@@ -210,24 +306,9 @@ func TestTrace(t *testing.T) {
r := wazero.NewRuntime() r := wazero.NewRuntime()
defer r.Close(testCtx) defer r.Close(testCtx)
out := &bytes.Buffer{} _, err := r.NewModuleBuilder("env").
ExportFunctions(tc.exporter.ExportFunctions()).
as := NewBuilder(r) Instantiate(testCtx, 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)
require.NoError(t, err) require.NoError(t, err)
traceWasm, err := watzero.Wat2Wasm(traceWat) traceWasm, err := watzero.Wat2Wasm(traceWat)
@@ -236,7 +317,15 @@ func TestTrace(t *testing.T) {
code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig()) code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err) 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) require.NoError(t, err)
message := encodeUTF16("hello") 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 { tests := []struct {
name string name string
memory func(api.Memory) 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) { func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
off := 0 off := 0
ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16))) 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 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 { func encodeUTF16(s string) []byte {
runes := utf16.Encode([]rune(s)) runes := utf16.Encode([]rune(s))
b := make([]byte, len(runes)*2) 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 // CompileConfig allows you to override what was decoded from wasm, prior to compilation (ModuleBuilder.Compile or
// Runtime.CompileModule). // 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. // Note: CompileConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
type CompileConfig interface { 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. // 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. // The default is to set cap=min and max=65536 if unset. A nil function is invalid and ignored.
WithMemorySizer(api.MemorySizer) CompileConfig WithMemorySizer(api.MemorySizer) CompileConfig
} }
type compileConfig struct { type compileConfig struct {
importRenamer api.ImportRenamer memorySizer api.MemorySizer
memorySizer api.MemorySizer
} }
// NewCompileConfig returns a CompileConfig that can be used for configuring module compilation. // NewCompileConfig returns a CompileConfig that can be used for configuring module compilation.
func NewCompileConfig() CompileConfig { func NewCompileConfig() CompileConfig {
return &compileConfig{ return &compileConfig{
importRenamer: nil, memorySizer: wasm.MemorySizer,
memorySizer: wasm.MemorySizer,
} }
} }
@@ -326,16 +317,6 @@ func (c *compileConfig) clone() *compileConfig {
return &ret 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 // WithMemorySizer implements CompileConfig.WithMemorySizer
func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConfig { func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConfig {
if memorySizer == nil { if memorySizer == nil {

View File

@@ -9,7 +9,6 @@ import (
"testing" "testing"
"testing/fstest" "testing/fstest"
"github.com/tetratelabs/wazero/api"
internalsys "github.com/tetratelabs/wazero/internal/sys" internalsys "github.com/tetratelabs/wazero/internal/sys"
testfs "github.com/tetratelabs/wazero/internal/testing/fs" testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/testing/require"
@@ -190,12 +189,6 @@ func TestRuntimeConfig_FeatureToggle(t *testing.T) {
} }
func TestCompileConfig(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) { mp := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
return 0, 1, 1 return 0, 1, 1
} }
@@ -204,20 +197,6 @@ func TestCompileConfig(t *testing.T) {
with func(CompileConfig) CompileConfig with func(CompileConfig) CompileConfig
expected *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", name: "WithMemorySizer",
with: func(c CompileConfig) CompileConfig { with: func(c CompileConfig) CompileConfig {
@@ -242,7 +221,6 @@ func TestCompileConfig(t *testing.T) {
// We cannot compare func, but we can compare reflect.Value // We cannot compare func, but we can compare reflect.Value
// See https://go.dev/ref/spec#Comparison_operators // 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)) require.Equal(t, reflect.ValueOf(tc.expected.memorySizer), reflect.ValueOf(rc.memorySizer))
// The source wasn't modified // The source wasn't modified
require.Equal(t, &compileConfig{}, input) 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 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) internal.AssignModuleID(binary)
if err = r.store.Engine.CompileModule(ctx, internal); err != nil { if err = r.store.Engine.CompileModule(ctx, internal); err != nil {

View File

@@ -78,56 +78,6 @@ func TestRuntime_CompileModule(t *testing.T) {
Max: 3, Max: 3,
}, code.module.MemorySection) }, 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) { func TestRuntime_CompileModule_Errors(t *testing.T) {