wasi: stops enforcing _start function export (#409)
Currently, we have custom code in wapc-go because our library forces a failure when a module that uses WASI doesn't define a "_start" function. Using the same pragmatism that resulted in us not enforcing the WASI table, this makes the "_start" function optional. This doesn't add a flag as the spec is not a proper version anyway (snapshot-01), so there's no need to further complicate configuration. If a "_start" function exists, we enforce it is of the proper signature and succeeds. Otherwise, we allow it to be absent. Signed-off-by: Adrian Cole <adrian@tetrate.io> Co-authored-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
17
RATIONALE.md
17
RATIONALE.md
@@ -87,7 +87,22 @@ runtime vs interpreting Wasm directly (the `naivevm` interpreter).
|
||||
Note: `microwasm` was never specified formally, and only exists in a historical codebase of wasmtime:
|
||||
https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/microwasm.rs
|
||||
|
||||
## Why is `SysConfig` decoupled from WASI?
|
||||
## WASI
|
||||
|
||||
### Why aren't all WASI rules enforced?
|
||||
|
||||
The [snapshot-01](https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md) version of WASI has a
|
||||
number of rules for a "command module", but only the memory export rule is enforced. If a "_start" function exists, it
|
||||
is enforced to be the correct signature and succeed, but the export itself isn't enforced. It follows that this means
|
||||
exports are not required to be contained to a "_start" function invocation. Finally, the "__indirect_function_table"
|
||||
export is also not enforced.
|
||||
|
||||
The reason for the exceptions are that implementations aren't following the rules. For example, TinyGo doesn't export
|
||||
"__indirect_function_table", so crashing on this would make wazero unable to run TinyGo modules. Similarly, modules
|
||||
loaded by wapc-go don't always define a "_start" function. Since "snapshot-01" is not a proper version, and certainly
|
||||
not a W3C recommendation, there's no sense in breaking users over matters like this.
|
||||
|
||||
### Why is `SysConfig` decoupled from WASI?
|
||||
|
||||
WebAssembly System Interfaces (WASI) is a formalization of a practice that can be done anyway: Define a host function to
|
||||
access a system interface, such as writing to STDOUT. WASI stalled at snapshot-01 and as of early 2022, is being
|
||||
|
||||
@@ -1505,9 +1505,9 @@ func openFileEntry(rootFS fs.FS, pathName string) (*internalwasm.FileEntry, wasi
|
||||
}
|
||||
|
||||
func ValidateWASICommand(module *internalwasm.Module, moduleName string) error {
|
||||
if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err != nil {
|
||||
return err
|
||||
} else {
|
||||
// The snapshot-01 specification requires a "_start" function, but we don't enforce it is present. We only enforce
|
||||
// if present, it is valid. In practice, not all modules importing WASI define a "_start" function (ex. wapc-go).
|
||||
if start, err := requireExport(module, moduleName, FunctionStart, internalwasm.ExternTypeFunc); err == nil {
|
||||
// TODO: this should be verified during decode so that errors have the correct source positions
|
||||
ft := module.TypeOfFunction(start.Index)
|
||||
if ft == nil {
|
||||
|
||||
34
wasi.go
34
wasi.go
@@ -40,9 +40,9 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// StartWASICommand instantiates the module and starts its WASI Command function ("_start"). The return value are all
|
||||
// exported functions in the module. This errs if the module doesn't comply with prerequisites, or any instantiation
|
||||
// or function call error.
|
||||
// StartWASICommand instantiates the module and starts its WASI Command function ("_start") if present. The return value
|
||||
// are all exported functions in the module. This errs if the module doesn't export a memory named "memory", or there
|
||||
// are any instantiation or function call errors. On success, other modules can import wasi.ModuleSnapshotPreview1.
|
||||
//
|
||||
// Ex.
|
||||
// r := wazero.NewRuntime()
|
||||
@@ -53,13 +53,17 @@ func StartWASICommandFromSource(r Runtime, source []byte) (wasm.Module, error) {
|
||||
// module, _ := StartWASICommand(r, decoded)
|
||||
// defer module.Close()
|
||||
//
|
||||
// Prerequisites of the "Current Unstable ABI" from wasi.ModuleSnapshotPreview1:
|
||||
// * "_start" is an exported nullary function and does not export "_initialize"
|
||||
// * "memory" is an exported memory.
|
||||
// ## "memory" export
|
||||
// WASI snapshot-01 requires exporting a memory named "memory", and wazero enforces this as nearly all functions use
|
||||
// memory to implement multiple returns. StartWASICommand errs if there is no memory exported as "memory".
|
||||
//
|
||||
// ## "_start" function export
|
||||
// WASI snapshot-01 requires exporting a function named "_start" for WASI command, but wazero does not enforce this. If it is defined,
|
||||
// it is called directly after any module-defined start function, in the runtime context (RuntimeConfig.WithContext).
|
||||
//
|
||||
// ## "__indirect_function_table" function export
|
||||
// WASI snapshot-01 requires exporting a table named "__indirect_function_table", but wazero does not enforce this.
|
||||
//
|
||||
// Note: "_start" is invoked in the RuntimeConfig.Context.
|
||||
// Note: Exporting "__indirect_function_table" is mentioned as required, but not enforced here.
|
||||
// Note: The wasm.Functions return value does not restrict exports after "_start" as allowed in the specification.
|
||||
// Note: All TinyGo Wasm are WASI commands. They initialize memory on "_start" and import "fd_write" to implement panic.
|
||||
// See StartWASICommandWithConfig
|
||||
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/design/application-abi.md#current-unstable-abi
|
||||
@@ -97,7 +101,8 @@ func startWASICommandWithSysContext(r Runtime, module *Module, sys *internalwasm
|
||||
|
||||
internal, ok := r.(*runtime)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported Runtime implementation: %s", r)
|
||||
err = fmt.Errorf("unsupported Runtime implementation: %s", r)
|
||||
return
|
||||
}
|
||||
|
||||
if mod, err = internal.store.Instantiate(internal.ctx, module.module, module.name, sys); err != nil {
|
||||
@@ -105,8 +110,11 @@ func startWASICommandWithSysContext(r Runtime, module *Module, sys *internalwasm
|
||||
}
|
||||
|
||||
start := mod.ExportedFunction(internalwasi.FunctionStart)
|
||||
if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil {
|
||||
return nil, fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
|
||||
if start == nil {
|
||||
return
|
||||
}
|
||||
return mod, nil
|
||||
if _, err = start.Call(mod.WithContext(internal.ctx)); err != nil {
|
||||
err = fmt.Errorf("module[%s] function[%s] failed: %w", module.name, internalwasi.FunctionStart, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
32
wasi_test.go
32
wasi_test.go
@@ -11,7 +11,24 @@ import (
|
||||
"github.com/tetratelabs/wazero/wasm"
|
||||
)
|
||||
|
||||
func TestStartWASICommand_UsesStoreContext(t *testing.T) {
|
||||
// TestStartWASICommand_DoesntEnforce_Start ensures wapc-go work when modules import WASI, but don't export "_start".
|
||||
func TestStartWASICommand_DoesntEnforce_Start(t *testing.T) {
|
||||
r := NewRuntime()
|
||||
|
||||
wasi, err := r.InstantiateModule(WASISnapshotPreview1())
|
||||
require.NoError(t, err)
|
||||
defer wasi.Close()
|
||||
|
||||
// Start the module as a WASI command. This will fail if the context wasn't as intended.
|
||||
mod, err := StartWASICommandFromSource(r, []byte(`(module $wasi_test.go
|
||||
(memory 1)
|
||||
(export "memory" (memory 0))
|
||||
)`))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mod.Close())
|
||||
}
|
||||
|
||||
func TestStartWASICommand_UsesRuntimeContext(t *testing.T) {
|
||||
type key string
|
||||
config := NewRuntimeConfig().WithContext(context.WithValue(context.Background(), key("wa"), "zero"))
|
||||
r := NewRuntimeWithConfig(config)
|
||||
@@ -23,23 +40,24 @@ func TestStartWASICommand_UsesStoreContext(t *testing.T) {
|
||||
require.Equal(t, config.ctx, ctx.Context())
|
||||
}
|
||||
|
||||
_, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
|
||||
host, err := r.NewModuleBuilder("").ExportFunction("start", start).Instantiate()
|
||||
require.NoError(t, err)
|
||||
defer host.Close()
|
||||
|
||||
_, err = r.InstantiateModule(WASISnapshotPreview1())
|
||||
wasi, err := r.InstantiateModule(WASISnapshotPreview1())
|
||||
require.NoError(t, err)
|
||||
defer wasi.Close()
|
||||
|
||||
decoded, err := r.CompileModule([]byte(`(module $wasi_test.go
|
||||
// Start the module as a WASI command. This will fail if the context wasn't as intended.
|
||||
mod, err := StartWASICommandFromSource(r, []byte(`(module $wasi_test.go
|
||||
(import "" "start" (func $start))
|
||||
(memory 1)
|
||||
(export "_start" (func $start))
|
||||
(export "memory" (memory 0))
|
||||
)`))
|
||||
require.NoError(t, err)
|
||||
defer mod.Close()
|
||||
|
||||
// Start the module as a WASI command. This will fail if the context wasn't as intended.
|
||||
_, err = StartWASICommand(r, decoded)
|
||||
require.NoError(t, err)
|
||||
require.True(t, calledStart)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user