Skip to content

Commit

Permalink
Adds MemoryDefinition to CompiledModule and Memory (#817)
Browse files Browse the repository at this point in the history
It is more often the case that projects are enabling a freestanding
target, and that may or may not have an exporting memory depending on
how that's interpreted. This adds the ability to inspect memories
similar to how you can already inspect compiled code prior to
instantiation. For example, you can enforce an ABI constraint that
"memory" must be exported even if WASI is not in use.

Signed-off-by: Adrian Cole <[email protected]>
  • Loading branch information
codefromthecrypt authored Sep 29, 2022
1 parent 761347d commit 9a623c4
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 62 deletions.
66 changes: 46 additions & 20 deletions api/wasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,21 +179,57 @@ type Closer interface {
Close(context.Context) error
}

// FunctionDefinition is a WebAssembly function exported in a module (wazero.CompiledModule).
// ExportDefinition is a WebAssembly type exported in a module
// (wazero.CompiledModule).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type FunctionDefinition interface {
type ExportDefinition interface {
// ModuleName is the possibly empty name of the module defining this
// function.
// export.
//
// Note: This may be different from Module.Name, because a compiled module
// can be instantiated multiple times as different names.
ModuleName() string

// Index is the position in the module's function index namespace, imports
// first.
// Index is the position in the module's index namespace, imports first.
Index() uint32

// Import returns true with the module and name when this was imported.
// Otherwise, it returns false.
//
// Note: Empty string is valid for both names in the WebAssembly Core
// Specification, so "" "" is possible.
Import() (moduleName, name string, isImport bool)

// ExportNames include all exported names.
//
// Note: The empty name is allowed in the WebAssembly Core Specification,
// so "" is possible.
ExportNames() []string
}

// MemoryDefinition is a WebAssembly memory exported in a module
// (wazero.CompiledModule). Units are in pages (64KB).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type MemoryDefinition interface {
ExportDefinition

// Min returns the possibly zero initial count of 64KB pages.
Min() uint32

// Max returns the possibly zero max count of 64KB pages, or false if
// unbounded.
Max() (uint32, bool)
}

// FunctionDefinition is a WebAssembly function exported in a module
// (wazero.CompiledModule).
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#exports%E2%91%A0
type FunctionDefinition interface {
ExportDefinition

// Name is the module-defined name of the function, which is not necessarily
// the same as its export name.
Name() string
Expand All @@ -215,19 +251,6 @@ type FunctionDefinition interface {
// and not the imported function name.
DebugName() string

// Import returns true with the module and function name when this function
// is imported. Otherwise, it returns false.
//
// Note: Empty string is valid for both the imported module and function
// name in the WebAssembly specification.
Import() (moduleName, name string, isImport bool)

// ExportNames include all exported names for the given function.
//
// Note: The empty name is allowed in the WebAssembly specification, so ""
// is possible.
ExportNames() []string

// GoFunc is present when the function was implemented by the embedder
// (ex via wazero.HostModuleBuilder) instead of a wasm binary.
//
Expand Down Expand Up @@ -326,15 +349,18 @@ type MutableGlobal interface {
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#storage%E2%91%A0
type Memory interface {
// Definition is metadata about this memory from its defining module.
Definition() MemoryDefinition

// Size returns the size in bytes available. Ex. If the underlying memory has 1 page: 65536
// Size returns the size in bytes available. Ex. If the underlying memory
// has 1 page: 65536
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-memorymathsfmemorysize%E2%91%A0
Size(context.Context) uint32

// Grow increases memory by the delta in pages (65536 bytes per page).
// The return val is the previous memory size in pages, or false if the
// delta was ignored as it exceeds max memory.
// delta was ignored as it exceeds MemoryDefinition.Max.
//
// # Notes
//
Expand Down
29 changes: 29 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,32 @@ type CompiledModule interface {
// (api.FunctionDefinition) in this module keyed on export name.
ExportedFunctions() map[string]api.FunctionDefinition

// ImportedMemories returns all the imported memories
// (api.MemoryDefinition) in this module or nil if there are none.
//
// ## Notes
// - As of WebAssembly Core Specification 2.0, there can be at most one
// memory.
// - Unlike ExportedMemories, there is no unique constraint on imports.
ImportedMemories() []api.MemoryDefinition

// ExportedMemories returns all the exported memories
// (api.MemoryDefinition) in this module keyed on export name.
//
// Note: As of WebAssembly Core Specification 2.0, there can be at most one
// memory.
ExportedMemories() map[string]api.MemoryDefinition

// Close releases all the allocated resources for this CompiledModule.
//
// Note: It is safe to call Close while having outstanding calls from an
// api.Module instantiated from this.
Close(context.Context) error
}

// compile-time check to ensure compiledModule implements CompiledModule
var _ CompiledModule = &compiledModule{}

type compiledModule struct {
module *wasm.Module
// compiledEngine holds an engine on which `module` is compiled.
Expand Down Expand Up @@ -210,6 +229,16 @@ func (c *compiledModule) ExportedFunctions() map[string]api.FunctionDefinition {
return c.module.ExportedFunctions()
}

// ImportedMemories implements CompiledModule.ImportedMemories
func (c *compiledModule) ImportedMemories() []api.MemoryDefinition {
return c.module.ImportedMemories()
}

// ExportedMemories implements CompiledModule.ExportedMemories
func (c *compiledModule) ExportedMemories() map[string]api.MemoryDefinition {
return c.module.ExportedMemories()
}

// ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
// system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated
// multiple times.
Expand Down
2 changes: 1 addition & 1 deletion internal/engine/compiler/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
fn := ce.fn
stackBasePointer := int(ce.stackBasePointerInBytes >> 3)
for {
def := fn.source.FunctionDefinition
def := fn.source.Definition
builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())

callFrameOffset := callFrameOffset(fn.source.Type)
Expand Down
12 changes: 6 additions & 6 deletions internal/engine/compiler/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,16 +311,16 @@ func ptrAsUint64(f *function) uint64 {

func TestCallEngine_deferredOnCall(t *testing.T) {
f1 := &function{source: &wasm.FunctionInstance{
FunctionDefinition: newMockFunctionDefinition("1"),
Type: &wasm.FunctionType{ParamNumInUint64: 2},
Definition: newMockFunctionDefinition("1"),
Type: &wasm.FunctionType{ParamNumInUint64: 2},
}}
f2 := &function{source: &wasm.FunctionInstance{
FunctionDefinition: newMockFunctionDefinition("2"),
Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
Definition: newMockFunctionDefinition("2"),
Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
}}
f3 := &function{source: &wasm.FunctionInstance{
FunctionDefinition: newMockFunctionDefinition("3"),
Type: &wasm.FunctionType{ResultNumInUint64: 1},
Definition: newMockFunctionDefinition("3"),
Type: &wasm.FunctionType{ResultNumInUint64: 1},
}}

ce := &callEngine{
Expand Down
16 changes: 8 additions & 8 deletions internal/engine/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) {
frameCount := len(ce.frames)
for i := 0; i < frameCount; i++ {
frame := ce.popFrame()
def := frame.f.source.FunctionDefinition
def := frame.f.source.Definition
builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes())
}
err = builder.FromRecovered(v)
Expand All @@ -826,7 +826,7 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) {
func (ce *callEngine) callFunction(ctx context.Context, callCtx *wasm.CallContext, f *function) {
if f.hostFn != nil {
ce.callGoFuncWithStack(ctx, callCtx, f)
} else if lsn := f.source.FunctionListener; lsn != nil {
} else if lsn := f.source.Listener; lsn != nil {
ce.callNativeFuncWithListener(ctx, callCtx, f, lsn)
} else {
ce.callNativeFunc(ctx, callCtx, f)
Expand All @@ -835,16 +835,16 @@ func (ce *callEngine) callFunction(ctx context.Context, callCtx *wasm.CallContex

func (ce *callEngine) callGoFunc(ctx context.Context, callCtx *wasm.CallContext, f *function, params []uint64) (results []uint64) {
callCtx = callCtx.WithMemory(ce.callerMemory())
if f.source.FunctionListener != nil {
ctx = f.source.FunctionListener.Before(ctx, f.source.FunctionDefinition, params)
if f.source.Listener != nil {
ctx = f.source.Listener.Before(ctx, f.source.Definition, params)
}
frame := &callFrame{f: f}
ce.pushFrame(frame)
results = wasm.CallGoFunc(ctx, callCtx, f.source, params)
ce.popFrame()
if f.source.FunctionListener != nil {
if f.source.Listener != nil {
// TODO: This doesn't get the error due to use of panic to propagate them.
f.source.FunctionListener.After(ctx, f.source.FunctionDefinition, nil, results)
f.source.Listener.After(ctx, f.source.Definition, nil, results)
}
return
}
Expand Down Expand Up @@ -4327,10 +4327,10 @@ func i32Abs(v uint32) uint32 {
}

func (ce *callEngine) callNativeFuncWithListener(ctx context.Context, callCtx *wasm.CallContext, f *function, fnl experimental.FunctionListener) context.Context {
ctx = fnl.Before(ctx, f.source.FunctionDefinition, ce.peekValues(len(f.source.Type.Params)))
ctx = fnl.Before(ctx, f.source.Definition, ce.peekValues(len(f.source.Type.Params)))
ce.callNativeFunc(ctx, callCtx, f)
// TODO: This doesn't get the error due to use of panic to propagate them.
fnl.After(ctx, f.source.FunctionDefinition, nil, ce.peekValues(len(f.source.Type.Results)))
fnl.After(ctx, f.source.Definition, nil, ce.peekValues(len(f.source.Type.Results)))
return ctx
}

Expand Down
4 changes: 2 additions & 2 deletions internal/wasm/call_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ type function struct {

// Definition implements the same method as documented on api.FunctionDefinition.
func (f *function) Definition() api.FunctionDefinition {
return f.fi.FunctionDefinition
return f.fi.Definition
}

// Call implements the same method as documented on api.Function.
Expand All @@ -180,7 +180,7 @@ type importedFn struct {

// Definition implements the same method as documented on api.Function.
func (f *importedFn) Definition() api.FunctionDefinition {
return f.importedFn.FunctionDefinition
return f.importedFn.Definition
}

// Call implements the same method as documented on api.Function.
Expand Down
7 changes: 7 additions & 0 deletions internal/wasm/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type MemoryInstance struct {
Min, Cap, Max uint32
// mux is used to prevent overlapping calls to Grow.
mux sync.RWMutex
// definition is known at compile time.
definition api.MemoryDefinition
}

// NewMemoryInstance creates a new instance based on the parameters in the SectionIDMemory.
Expand All @@ -51,6 +53,11 @@ func NewMemoryInstance(memSec *Memory) *MemoryInstance {
}
}

// Definition implements the same method as documented on api.Memory.
func (m *MemoryInstance) Definition() api.MemoryDefinition {
return m.definition
}

// Size implements the same method as documented on api.Memory.
func (m *MemoryInstance) Size(_ context.Context) uint32 {
return m.size()
Expand Down
119 changes: 119 additions & 0 deletions internal/wasm/memory_definition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package wasm

import "github.com/tetratelabs/wazero/api"

// ImportedMemories implements the same method as documented on wazero.CompiledModule.
func (m *Module) ImportedMemories() (ret []api.MemoryDefinition) {
for _, d := range m.MemoryDefinitionSection {
if d.importDesc != nil {
ret = append(ret, d)
}
}
return
}

// ExportedMemories implements the same method as documented on wazero.CompiledModule.
func (m *Module) ExportedMemories() map[string]api.MemoryDefinition {
ret := map[string]api.MemoryDefinition{}
for _, d := range m.MemoryDefinitionSection {
for _, e := range d.exportNames {
ret[e] = d
}
}
return ret
}

// BuildMemoryDefinitions generates memory metadata that can be parsed from
// the module. This must be called after all validation.
//
// Note: This is exported for wazero.Runtime `CompileModule`.
func (m *Module) BuildMemoryDefinitions() {
var moduleName string
if m.NameSection != nil {
moduleName = m.NameSection.ModuleName
}

memoryCount := m.ImportMemoryCount()
if m.MemorySection != nil {
memoryCount++
}

if memoryCount == 0 {
return
}

m.MemoryDefinitionSection = make([]*MemoryDefinition, 0, memoryCount)
importMemIdx := Index(0)
for _, i := range m.ImportSection {
if i.Type != ExternTypeMemory {
continue
}

m.MemoryDefinitionSection = append(m.MemoryDefinitionSection, &MemoryDefinition{
importDesc: &[2]string{i.Module, i.Name},
index: importMemIdx,
memory: i.DescMem,
})
importMemIdx++
}

if m.MemorySection != nil {
m.MemoryDefinitionSection = append(m.MemoryDefinitionSection, &MemoryDefinition{
index: importMemIdx,
memory: m.MemorySection,
})
}

for _, d := range m.MemoryDefinitionSection {
d.moduleName = moduleName
for _, e := range m.ExportSection {
if e.Type == ExternTypeMemory && e.Index == d.index {
d.exportNames = append(d.exportNames, e.Name)
}
}
}
}

// MemoryDefinition implements api.MemoryDefinition
type MemoryDefinition struct {
moduleName string
index Index
importDesc *[2]string
exportNames []string
memory *Memory
}

// ModuleName implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) ModuleName() string {
return f.moduleName
}

// Index implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) Index() uint32 {
return f.index
}

// Import implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) Import() (moduleName, name string, isImport bool) {
if importDesc := f.importDesc; importDesc != nil {
moduleName, name, isImport = importDesc[0], importDesc[1], true
}
return
}

// ExportNames implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) ExportNames() []string {
return f.exportNames
}

// Min implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) Min() uint32 {
return f.memory.Min
}

// Max implements the same method as documented on api.MemoryDefinition.
func (f *MemoryDefinition) Max() (max uint32, encoded bool) {
max = f.memory.Max
encoded = f.memory.IsMaxEncoded
return
}
Loading

0 comments on commit 9a623c4

Please sign in to comment.