github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/engine/wazevo/module_engine.go (about) 1 package wazevo 2 3 import ( 4 "encoding/binary" 5 "unsafe" 6 7 "github.com/wasilibs/wazerox/api" 8 "github.com/wasilibs/wazerox/experimental" 9 "github.com/wasilibs/wazerox/internal/engine/wazevo/wazevoapi" 10 "github.com/wasilibs/wazerox/internal/wasm" 11 "github.com/wasilibs/wazerox/internal/wasmruntime" 12 ) 13 14 type ( 15 // moduleEngine implements wasm.ModuleEngine. 16 moduleEngine struct { 17 // opaquePtr equals &opaque[0]. 18 opaquePtr *byte 19 parent *compiledModule 20 module *wasm.ModuleInstance 21 opaque moduleContextOpaque 22 localFunctionInstances []*functionInstance 23 importedFunctions []importedFunction 24 listeners []experimental.FunctionListener 25 } 26 27 functionInstance struct { 28 executable *byte 29 moduleContextOpaquePtr *byte 30 typeID wasm.FunctionTypeID 31 indexInModule wasm.Index 32 } 33 34 importedFunction struct { 35 me *moduleEngine 36 indexInModule wasm.Index 37 } 38 39 // moduleContextOpaque is the opaque byte slice of Module instance specific contents whose size 40 // is only Wasm-compile-time known, hence dynamic. Its contents are basically the pointers to the module instance, 41 // specific objects as well as functions. This is sometimes called "VMContext" in other Wasm runtimes. 42 // 43 // Internally, the buffer is structured as follows: 44 // 45 // type moduleContextOpaque struct { 46 // moduleInstance *wasm.ModuleInstance 47 // localMemoryBufferPtr *byte (optional) 48 // localMemoryLength uint64 (optional) 49 // importedMemoryInstance *wasm.MemoryInstance (optional) 50 // importedMemoryOwnerOpaqueCtx *byte (optional) 51 // importedFunctions [# of importedFunctions]functionInstance 52 // globals []*wasm.GlobalInstance (optional) 53 // typeIDsBegin &wasm.ModuleInstance.TypeIDs[0] (optional) 54 // tables []*wasm.TableInstance (optional) 55 // beforeListenerTrampolines1stElement **byte (optional) 56 // afterListenerTrampolines1stElement **byte (optional) 57 // dataInstances1stElement []wasm.DataInstance (optional) 58 // elementInstances1stElement []wasm.ElementInstance (optional) 59 // } 60 // 61 // See wazevoapi.NewModuleContextOffsetData for the details of the offsets. 62 // 63 // Note that for host modules, the structure is entirely different. See buildHostModuleOpaque. 64 moduleContextOpaque []byte 65 ) 66 67 func putLocalMemory(opaque []byte, offset wazevoapi.Offset, mem *wasm.MemoryInstance) { 68 s := uint64(len(mem.Buffer)) 69 var b uint64 70 if len(mem.Buffer) > 0 { 71 b = uint64(uintptr(unsafe.Pointer(&mem.Buffer[0]))) 72 } 73 binary.LittleEndian.PutUint64(opaque[offset:], b) 74 binary.LittleEndian.PutUint64(opaque[offset+8:], s) 75 } 76 77 func (m *moduleEngine) setupOpaque() { 78 inst := m.module 79 offsets := &m.parent.offsets 80 opaque := m.opaque 81 82 binary.LittleEndian.PutUint64(opaque[offsets.ModuleInstanceOffset:], 83 uint64(uintptr(unsafe.Pointer(m.module))), 84 ) 85 86 if lm := offsets.LocalMemoryBegin; lm >= 0 { 87 putLocalMemory(opaque, lm, inst.MemoryInstance) 88 } 89 90 // Note: imported memory is resolved in ResolveImportedFunction. 91 92 // Note: imported functions are resolved in ResolveImportedFunction. 93 94 if globalOffset := offsets.GlobalsBegin; globalOffset >= 0 { 95 for _, g := range inst.Globals { 96 binary.LittleEndian.PutUint64(opaque[globalOffset:], uint64(uintptr(unsafe.Pointer(g)))) 97 globalOffset += 8 98 } 99 } 100 101 if tableOffset := offsets.TablesBegin; tableOffset >= 0 { 102 // First we write the first element's address of typeIDs. 103 if len(inst.TypeIDs) > 0 { 104 binary.LittleEndian.PutUint64(opaque[offsets.TypeIDs1stElement:], uint64(uintptr(unsafe.Pointer(&inst.TypeIDs[0])))) 105 } 106 107 // Then we write the table addresses. 108 for _, table := range inst.Tables { 109 binary.LittleEndian.PutUint64(opaque[tableOffset:], uint64(uintptr(unsafe.Pointer(table)))) 110 tableOffset += 8 111 } 112 } 113 114 if beforeListenerOffset := offsets.BeforeListenerTrampolines1stElement; beforeListenerOffset >= 0 { 115 binary.LittleEndian.PutUint64(opaque[beforeListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0])))) 116 } 117 if afterListenerOffset := offsets.AfterListenerTrampolines1stElement; afterListenerOffset >= 0 { 118 binary.LittleEndian.PutUint64(opaque[afterListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerAfterTrampolines[0])))) 119 } 120 if len(inst.DataInstances) > 0 { 121 binary.LittleEndian.PutUint64(opaque[offsets.DataInstances1stElement:], uint64(uintptr(unsafe.Pointer(&inst.DataInstances[0])))) 122 } 123 if len(inst.ElementInstances) > 0 { 124 binary.LittleEndian.PutUint64(opaque[offsets.ElementInstances1stElement:], uint64(uintptr(unsafe.Pointer(&inst.ElementInstances[0])))) 125 } 126 } 127 128 // NewFunction implements wasm.ModuleEngine. 129 func (m *moduleEngine) NewFunction(index wasm.Index) api.Function { 130 if wazevoapi.PrintMachineCodeHexPerFunctionDisassemblable { 131 panic("When PrintMachineCodeHexPerFunctionDisassemblable enabled, functions must not be called") 132 } 133 134 localIndex := index 135 if importedFnCount := m.module.Source.ImportFunctionCount; index < importedFnCount { 136 imported := &m.importedFunctions[index] 137 return imported.me.NewFunction(imported.indexInModule) 138 } else { 139 localIndex -= importedFnCount 140 } 141 142 src := m.module.Source 143 typIndex := src.FunctionSection[localIndex] 144 typ := src.TypeSection[typIndex] 145 sizeOfParamResultSlice := typ.ResultNumInUint64 146 if ps := typ.ParamNumInUint64; ps > sizeOfParamResultSlice { 147 sizeOfParamResultSlice = ps 148 } 149 p := m.parent 150 offset := p.functionOffsets[localIndex] 151 152 ce := &callEngine{ 153 indexInModule: index, 154 executable: &p.executable[offset], 155 parent: m, 156 preambleExecutable: &m.parent.entryPreambles[typIndex][0], 157 sizeOfParamResultSlice: sizeOfParamResultSlice, 158 requiredParams: typ.ParamNumInUint64, 159 numberOfResults: typ.ResultNumInUint64, 160 } 161 162 ce.execCtx.memoryGrowTrampolineAddress = &m.parent.sharedFunctions.memoryGrowExecutable[0] 163 ce.execCtx.stackGrowCallTrampolineAddress = &m.parent.sharedFunctions.stackGrowExecutable[0] 164 ce.execCtx.checkModuleExitCodeTrampolineAddress = &m.parent.sharedFunctions.checkModuleExitCode[0] 165 ce.execCtx.tableGrowTrampolineAddress = &m.parent.sharedFunctions.tableGrowExecutable[0] 166 ce.execCtx.refFuncTrampolineAddress = &m.parent.sharedFunctions.refFuncExecutable[0] 167 ce.execCtx.memmoveAddress = memmovPtr 168 ce.init() 169 return ce 170 } 171 172 // ResolveImportedFunction implements wasm.ModuleEngine. 173 func (m *moduleEngine) ResolveImportedFunction(index, indexInImportedModule wasm.Index, importedModuleEngine wasm.ModuleEngine) { 174 executableOffset, moduleCtxOffset, typeIDOffset := m.parent.offsets.ImportedFunctionOffset(index) 175 importedME := importedModuleEngine.(*moduleEngine) 176 177 if int(indexInImportedModule) >= len(importedME.importedFunctions) { 178 indexInImportedModule -= wasm.Index(len(importedME.importedFunctions)) 179 } else { 180 imported := &importedME.importedFunctions[indexInImportedModule] 181 m.ResolveImportedFunction(index, imported.indexInModule, imported.me) 182 return // Recursively resolve the imported function. 183 } 184 185 offset := importedME.parent.functionOffsets[indexInImportedModule] 186 typeID := getTypeIDOf(indexInImportedModule, importedME.module) 187 executable := &importedME.parent.executable[offset] 188 // Write functionInstance. 189 binary.LittleEndian.PutUint64(m.opaque[executableOffset:], uint64(uintptr(unsafe.Pointer(executable)))) 190 binary.LittleEndian.PutUint64(m.opaque[moduleCtxOffset:], uint64(uintptr(unsafe.Pointer(importedME.opaquePtr)))) 191 binary.LittleEndian.PutUint64(m.opaque[typeIDOffset:], uint64(typeID)) 192 193 // Write importedFunction so that it can be used by NewFunction. 194 m.importedFunctions[index] = importedFunction{me: importedME, indexInModule: indexInImportedModule} 195 } 196 197 func getTypeIDOf(funcIndex wasm.Index, m *wasm.ModuleInstance) wasm.FunctionTypeID { 198 source := m.Source 199 200 var typeIndex wasm.Index 201 if funcIndex >= source.ImportFunctionCount { 202 funcIndex -= source.ImportFunctionCount 203 typeIndex = source.FunctionSection[funcIndex] 204 } else { 205 var cnt wasm.Index 206 for i := range source.ImportSection { 207 if source.ImportSection[i].Type == wasm.ExternTypeFunc { 208 if cnt == funcIndex { 209 typeIndex = source.ImportSection[i].DescFunc 210 break 211 } 212 cnt++ 213 } 214 } 215 } 216 return m.TypeIDs[typeIndex] 217 } 218 219 // ResolveImportedMemory implements wasm.ModuleEngine. 220 func (m *moduleEngine) ResolveImportedMemory(importedModuleEngine wasm.ModuleEngine) { 221 importedME := importedModuleEngine.(*moduleEngine) 222 inst := importedME.module 223 224 var memInstPtr uint64 225 var memOwnerOpaquePtr uint64 226 if offs := importedME.parent.offsets; offs.ImportedMemoryBegin >= 0 { 227 offset := offs.ImportedMemoryBegin 228 memInstPtr = binary.LittleEndian.Uint64(importedME.opaque[offset:]) 229 memOwnerOpaquePtr = binary.LittleEndian.Uint64(importedME.opaque[offset+8:]) 230 } else { 231 memInstPtr = uint64(uintptr(unsafe.Pointer(inst.MemoryInstance))) 232 memOwnerOpaquePtr = uint64(uintptr(unsafe.Pointer(importedME.opaquePtr))) 233 } 234 offset := m.parent.offsets.ImportedMemoryBegin 235 binary.LittleEndian.PutUint64(m.opaque[offset:], memInstPtr) 236 binary.LittleEndian.PutUint64(m.opaque[offset+8:], memOwnerOpaquePtr) 237 } 238 239 // DoneInstantiation implements wasm.ModuleEngine. 240 func (m *moduleEngine) DoneInstantiation() { 241 if !m.module.Source.IsHostModule { 242 m.setupOpaque() 243 } 244 } 245 246 // FunctionInstanceReference implements wasm.ModuleEngine. 247 func (m *moduleEngine) FunctionInstanceReference(funcIndex wasm.Index) wasm.Reference { 248 if funcIndex < m.module.Source.ImportFunctionCount { 249 begin, _, _ := m.parent.offsets.ImportedFunctionOffset(funcIndex) 250 return uintptr(unsafe.Pointer(&m.opaque[begin])) 251 } 252 localIndex := funcIndex - m.module.Source.ImportFunctionCount 253 p := m.parent 254 executable := &p.executable[p.functionOffsets[localIndex]] 255 typeID := m.module.TypeIDs[m.module.Source.FunctionSection[localIndex]] 256 257 lf := &functionInstance{ 258 executable: executable, 259 moduleContextOpaquePtr: m.opaquePtr, 260 typeID: typeID, 261 indexInModule: funcIndex, 262 } 263 m.localFunctionInstances = append(m.localFunctionInstances, lf) 264 return uintptr(unsafe.Pointer(lf)) 265 } 266 267 // LookupFunction implements wasm.ModuleEngine. 268 func (m *moduleEngine) LookupFunction(t *wasm.TableInstance, typeId wasm.FunctionTypeID, tableOffset wasm.Index) (*wasm.ModuleInstance, wasm.Index) { 269 if tableOffset >= uint32(len(t.References)) || t.Type != wasm.RefTypeFuncref { 270 panic(wasmruntime.ErrRuntimeInvalidTableAccess) 271 } 272 rawPtr := t.References[tableOffset] 273 if rawPtr == 0 { 274 panic(wasmruntime.ErrRuntimeInvalidTableAccess) 275 } 276 277 tf := functionFromUintptr(rawPtr) 278 if tf.typeID != typeId { 279 panic(wasmruntime.ErrRuntimeIndirectCallTypeMismatch) 280 } 281 return moduleInstanceFromOpaquePtr(tf.moduleContextOpaquePtr), tf.indexInModule 282 } 283 284 // functionFromUintptr resurrects the original *function from the given uintptr 285 // which comes from either funcref table or OpcodeRefFunc instruction. 286 func functionFromUintptr(ptr uintptr) *functionInstance { 287 // Wraps ptrs as the double pointer in order to avoid the unsafe access as detected by race detector. 288 // 289 // For example, if we have (*function)(unsafe.Pointer(ptr)) instead, then the race detector's "checkptr" 290 // subroutine wanrs as "checkptr: pointer arithmetic result points to invalid allocation" 291 // https://github.com/golang/go/blob/1ce7fcf139417d618c2730010ede2afb41664211/src/runtime/checkptr.go#L69 292 var wrapped *uintptr = &ptr 293 return *(**functionInstance)(unsafe.Pointer(wrapped)) 294 } 295 296 func moduleInstanceFromOpaquePtr(ptr *byte) *wasm.ModuleInstance { 297 return *(**wasm.ModuleInstance)(unsafe.Pointer(ptr)) 298 }