Makes wazero.CompiledCode an interface instead of a struct (#519)

This makes wazero.CompiledCode an interface instead of a struct to
prevent it from being used incorrectly. For example, even though the
fields are not exported, someone can mistakenly instantiate this
when it is a struct, and in doing so violate internal assumptions.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-05-02 11:44:01 +08:00
committed by GitHub
parent a91140f7f7
commit fbea2de984
8 changed files with 61 additions and 36 deletions

View File

@@ -153,7 +153,7 @@ type ModuleBuilder interface {
ExportGlobalF64(name string, v float64) ModuleBuilder ExportGlobalF64(name string, v float64) ModuleBuilder
// Build returns a module to instantiate, or returns an error if any of the configuration is invalid. // Build returns a module to instantiate, or returns an error if any of the configuration is invalid.
Build(context.Context) (*CompiledCode, error) Build(context.Context) (CompiledCode, error)
// Instantiate is a convenience that calls Build, then Runtime.InstantiateModule // Instantiate is a convenience that calls Build, then Runtime.InstantiateModule
// //
@@ -250,7 +250,7 @@ func (b *moduleBuilder) ExportGlobalF64(name string, v float64) ModuleBuilder {
} }
// Build implements ModuleBuilder.Build // Build implements ModuleBuilder.Build
func (b *moduleBuilder) Build(ctx context.Context) (*CompiledCode, error) { func (b *moduleBuilder) Build(ctx context.Context) (CompiledCode, error) {
// Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule // Verify the maximum limit here, so we don't have to pass it to wasm.NewHostModule
memoryLimitPages := b.r.memoryLimitPages memoryLimitPages := b.r.memoryLimitPages
for name, mem := range b.nameToMemory { for name, mem := range b.nameToMemory {
@@ -271,19 +271,19 @@ func (b *moduleBuilder) Build(ctx context.Context) (*CompiledCode, error) {
return nil, err return nil, err
} }
return &CompiledCode{module: module, compiledEngine: b.r.store.Engine}, nil return &compiledCode{module: module, compiledEngine: b.r.store.Engine}, nil
} }
// Instantiate implements ModuleBuilder.Instantiate // Instantiate implements ModuleBuilder.Instantiate
func (b *moduleBuilder) Instantiate(ctx context.Context) (api.Module, error) { func (b *moduleBuilder) Instantiate(ctx context.Context) (api.Module, error) {
if module, err := b.Build(ctx); err != nil { if compiled, err := b.Build(ctx); err != nil {
return nil, err return nil, err
} else { } else {
if err = b.r.store.Engine.CompileModule(ctx, module.module); err != nil { if err = b.r.store.Engine.CompileModule(ctx, compiled.(*compiledCode).module); err != nil {
return nil, err return nil, err
} }
// *wasm.ModuleInstance cannot be tracked, so we release the cache inside this function. // *wasm.ModuleInstance cannot be tracked, so we release the cache inside this function.
defer module.Close(ctx) defer compiled.Close(ctx)
return b.r.InstantiateModuleWithConfig(ctx, module, NewModuleConfig().WithName(b.moduleName)) return b.r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig().WithName(b.moduleName))
} }
} }

View File

@@ -344,8 +344,9 @@ func TestNewModuleBuilder_Build(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
b := tc.input(NewRuntime()).(*moduleBuilder) b := tc.input(NewRuntime()).(*moduleBuilder)
m, err := b.Build(testCtx) compiled, err := b.Build(testCtx)
require.NoError(t, err) require.NoError(t, err)
m := compiled.(*compiledCode)
requireHostModuleEquals(t, tc.expected, m.module) requireHostModuleEquals(t, tc.expected, m.module)

View File

@@ -238,22 +238,27 @@ func (c *runtimeConfig) WithWasmCore2() RuntimeConfig {
return ret return ret
} }
// CompiledCode is a WebAssembly 1.0 (20191205) module ready to be instantiated (Runtime.InstantiateModule) as an\ // CompiledCode is a WebAssembly 1.0 (20191205) module ready to be instantiated (Runtime.InstantiateModule) as an
// api.Module. // api.Module.
// //
// Note: In WebAssembly language, this is a decoded, validated, and possibly also compiled module. wazero avoids using // Note: In WebAssembly language, this is a decoded, validated, and possibly also compiled module. wazero avoids using
// the name "Module" for both before and after instantiation as the name conflation has caused confusion. // the name "Module" for both before and after instantiation as the name conflation has caused confusion.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#semantic-phases%E2%91%A0 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#semantic-phases%E2%91%A0
type CompiledCode struct { type CompiledCode interface {
// Close releases all the allocated resources for this CompiledCode.
//
// Note: It is safe to call Close while having outstanding calls from Modules instantiated from this CompiledCode.
Close(context.Context) error
}
type compiledCode struct {
module *wasm.Module module *wasm.Module
// compiledEngine holds an engine on which `module` is compiled. // compiledEngine holds an engine on which `module` is compiled.
compiledEngine wasm.Engine compiledEngine wasm.Engine
} }
// Close releases all the allocated resources for this CompiledCode. // Close implements CompiledCode.Close
// func (c *compiledCode) Close(_ context.Context) error {
// Note: It is safe to call Close while having outstanding calls from Modules instantiated from this *CompiledCode.
func (c *CompiledCode) Close(_ context.Context) error {
// Note: If you use the context.Context param, don't forget to coerce nil to context.Background()! // Note: If you use the context.Context param, don't forget to coerce nil to context.Background()!
c.compiledEngine.DeleteCompiledModule(c.module) c.compiledEngine.DeleteCompiledModule(c.module)

View File

@@ -853,12 +853,12 @@ func TestCompiledCode_Close(t *testing.T) {
for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil! for _, ctx := range []context.Context{nil, testCtx} { // Ensure it doesn't crash on nil!
e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}} e := &mockEngine{name: "1", cachedModules: map[*wasm.Module]struct{}{}}
var cs []*CompiledCode var cs []*compiledCode
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
m := &wasm.Module{} m := &wasm.Module{}
err := e.CompileModule(ctx, m) err := e.CompileModule(ctx, m)
require.NoError(t, err) require.NoError(t, err)
cs = append(cs, &CompiledCode{module: m, compiledEngine: e}) cs = append(cs, &compiledCode{module: m, compiledEngine: e})
} }
// Before Close. // Before Close.

View File

@@ -364,7 +364,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var importingCode, importedCode *wazero.CompiledCode var importingCode, importedCode wazero.CompiledCode
var imported, importing api.Module var imported, importing api.Module
var err error var err error
closeAndReturn := func(ctx context.Context, x uint32) uint32 { closeAndReturn := func(ctx context.Context, x uint32) uint32 {

View File

@@ -54,7 +54,7 @@ type wazeroRuntime struct {
config wazero.RuntimeConfig config wazero.RuntimeConfig
runtime wazero.Runtime runtime wazero.Runtime
logFn func([]byte) error logFn func([]byte) error
env, compiled *wazero.CompiledCode env, compiled wazero.CompiledCode
} }
type wazeroModule struct { type wazeroModule struct {

27
wasm.go
View File

@@ -49,7 +49,7 @@ type Runtime interface {
// Note: When the context is nil, it defaults to context.Background. // Note: When the context is nil, it defaults to context.Background.
// Note: The resulting module name defaults to what was binary from the custom name section. // Note: The resulting module name defaults to what was binary from the custom name section.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0 // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
CompileModule(ctx context.Context, source []byte) (*CompiledCode, error) CompileModule(ctx context.Context, source []byte) (CompiledCode, error)
// InstantiateModuleFromCode instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or // InstantiateModuleFromCode instantiates a module from the WebAssembly 1.0 (20191205) text or binary source or
// errs if invalid. // errs if invalid.
@@ -94,7 +94,7 @@ type Runtime interface {
// * The module has a start function, and it failed to execute. // * The module has a start function, and it failed to execute.
// //
// Note: When the context is nil, it defaults to context.Background. // Note: When the context is nil, it defaults to context.Background.
InstantiateModule(ctx context.Context, compiled *CompiledCode) (api.Module, error) InstantiateModule(ctx context.Context, compiled CompiledCode) (api.Module, error)
// InstantiateModuleWithConfig is like InstantiateModule, except you can override configuration such as the module // InstantiateModuleWithConfig is like InstantiateModule, except you can override configuration such as the module
// name or ENV variables. // name or ENV variables.
@@ -114,7 +114,7 @@ type Runtime interface {
// //
// Note: When the context is nil, it defaults to context.Background. // Note: When the context is nil, it defaults to context.Background.
// Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result. // Note: Config is copied during instantiation: Later changes to config do not affect the instantiated result.
InstantiateModuleWithConfig(ctx context.Context, compiled *CompiledCode, config *ModuleConfig) (mod api.Module, err error) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config *ModuleConfig) (mod api.Module, err error)
} }
func NewRuntime() Runtime { func NewRuntime() Runtime {
@@ -149,7 +149,7 @@ func (r *runtime) Module(moduleName string) api.Module {
} }
// CompileModule implements Runtime.CompileModule // CompileModule implements Runtime.CompileModule
func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCode, error) { func (r *runtime) CompileModule(ctx context.Context, source []byte) (CompiledCode, error) {
if source == nil { if source == nil {
return nil, errors.New("source == nil") return nil, errors.New("source == nil")
} }
@@ -202,7 +202,7 @@ func (r *runtime) CompileModule(ctx context.Context, source []byte) (*CompiledCo
return nil, err return nil, err
} }
return &CompiledCode{module: internal, compiledEngine: r.store.Engine}, nil return &compiledCode{module: internal, compiledEngine: r.store.Engine}, nil
} }
// InstantiateModuleFromCode implements Runtime.InstantiateModuleFromCode // InstantiateModuleFromCode implements Runtime.InstantiateModuleFromCode
@@ -228,23 +228,28 @@ func (r *runtime) InstantiateModuleFromCodeWithConfig(ctx context.Context, sourc
} }
// InstantiateModule implements Runtime.InstantiateModule // InstantiateModule implements Runtime.InstantiateModule
func (r *runtime) InstantiateModule(ctx context.Context, compiled *CompiledCode) (mod api.Module, err error) { func (r *runtime) InstantiateModule(ctx context.Context, compiled CompiledCode) (mod api.Module, err error) {
return r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig()) return r.InstantiateModuleWithConfig(ctx, compiled, NewModuleConfig())
} }
// InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig // InstantiateModuleWithConfig implements Runtime.InstantiateModuleWithConfig
func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled *CompiledCode, config *ModuleConfig) (mod api.Module, err error) { func (r *runtime) InstantiateModuleWithConfig(ctx context.Context, compiled CompiledCode, config *ModuleConfig) (mod api.Module, err error) {
var sysCtx *wasm.SysContext var sysCtx *wasm.SysContext
if sysCtx, err = config.toSysContext(); err != nil { if sysCtx, err = config.toSysContext(); err != nil {
return return
} }
name := config.name code, ok := compiled.(*compiledCode)
if name == "" && compiled.module.NameSection != nil && compiled.module.NameSection.ModuleName != "" { if !ok {
name = compiled.module.NameSection.ModuleName panic(fmt.Errorf("unsupported wazero.CompiledCode implementation: %#v", compiled))
} }
module := config.replaceImports(compiled.module) name := config.name
if name == "" && code.module.NameSection != nil && code.module.NameSection.ModuleName != "" {
name = code.module.NameSection.ModuleName
}
module := config.replaceImports(code.module)
var functionListenerFactory experimentalapi.FunctionListenerFactory var functionListenerFactory experimentalapi.FunctionListenerFactory
if ctx != nil { // Test to see if internal code are using an experimental feature. if ctx != nil { // Test to see if internal code are using an experimental feature.

View File

@@ -19,7 +19,7 @@ import (
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
func TestNewRuntimeWithConfig_PanicsOnWrongImpl(t *testing.T) { func TestNewRuntimeWithConfig_PanicsOnWrongImpl(t *testing.T) {
// It is too burdensome to define an impl of RuntimeConfig in tests just to verify the error when it is wrong. // It causes maintenance to define an impl of RuntimeConfig in tests just to verify the error when it is wrong.
// Instead, we pass nil which is implicitly the wrong type, as that's less work! // Instead, we pass nil which is implicitly the wrong type, as that's less work!
err := require.CapturePanic(func() { err := require.CapturePanic(func() {
NewRuntimeWithConfig(nil) NewRuntimeWithConfig(nil)
@@ -68,8 +68,9 @@ func TestRuntime_CompileModule(t *testing.T) {
tc := tt tc := tt
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
code, err := r.CompileModule(testCtx, tc.source) m, err := r.CompileModule(testCtx, tc.source)
require.NoError(t, err) require.NoError(t, err)
code := m.(*compiledCode)
defer code.Close(testCtx) defer code.Close(testCtx)
if tc.expectedName != "" { if tc.expectedName != "" {
require.Equal(t, tc.expectedName, code.module.NameSection.ModuleName) require.Equal(t, tc.expectedName, code.module.NameSection.ModuleName)
@@ -84,8 +85,9 @@ func TestRuntime_CompileModule(t *testing.T) {
source := []byte(`(module (memory 1 3))`) source := []byte(`(module (memory 1 3))`)
code, err := r.CompileModule(testCtx, source) m, err := r.CompileModule(testCtx, source)
require.NoError(t, err) require.NoError(t, err)
code := m.(*compiledCode)
defer code.Close(testCtx) defer code.Close(testCtx)
require.Equal(t, &wasm.Memory{ require.Equal(t, &wasm.Memory{
@@ -312,12 +314,13 @@ func TestModule_Global(t *testing.T) {
r := NewRuntime().(*runtime) r := NewRuntime().(*runtime)
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
var code *CompiledCode var m CompiledCode
if tc.module != nil { if tc.module != nil {
code = &CompiledCode{module: tc.module} m = &compiledCode{module: tc.module}
} else { } else {
code, _ = tc.builder(r).Build(testCtx) m, _ = tc.builder(r).Build(testCtx)
} }
code := m.(*compiledCode)
err := r.store.Engine.CompileModule(testCtx, code.module) err := r.store.Engine.CompileModule(testCtx, code.module)
require.NoError(t, err) require.NoError(t, err)
@@ -463,6 +466,17 @@ func TestRuntime_InstantiateModuleFromCode_UsesContext(t *testing.T) {
require.True(t, calledStart) require.True(t, calledStart)
} }
func TestInstantiateModuleWithConfig_PanicsOnWrongCompiledCodeImpl(t *testing.T) {
// It causes maintenance to define an impl of CompiledCode in tests just to verify the error when it is wrong.
// Instead, we pass nil which is implicitly the wrong type, as that's less work!
r := NewRuntime()
err := require.CapturePanic(func() {
_, _ = r.InstantiateModuleWithConfig(testCtx, nil, NewModuleConfig())
})
require.EqualError(t, err, "unsupported wazero.CompiledCode implementation: <nil>")
}
// TestInstantiateModuleWithConfig_WithName tests that we can pre-validate (cache) a module and instantiate it under // TestInstantiateModuleWithConfig_WithName tests that we can pre-validate (cache) a module and instantiate it under
// different names. This pattern is used in wapc-go. // different names. This pattern is used in wapc-go.
func TestInstantiateModuleWithConfig_WithName(t *testing.T) { func TestInstantiateModuleWithConfig_WithName(t *testing.T) {
@@ -471,7 +485,7 @@ func TestInstantiateModuleWithConfig_WithName(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer base.Close(testCtx) defer base.Close(testCtx)
require.Equal(t, "0", base.module.NameSection.ModuleName) require.Equal(t, "0", base.(*compiledCode).module.NameSection.ModuleName)
// Use the same runtime to instantiate multiple modules // Use the same runtime to instantiate multiple modules
internal := r.(*runtime).store internal := r.(*runtime).store