wa-lang.org/wazero@v1.0.2/internal/testing/enginetest/enginetest.go (about) 1 // Package enginetest contains tests common to any wasm.Engine implementation. Defining these as top-level 2 // functions is less burden than copy/pasting the implementations, while still allowing test caching to operate. 3 // 4 // In simplest case, dispatch: 5 // 6 // func TestModuleEngine_Call(t *testing.T) { 7 // enginetest.RunTestModuleEngine_Call(t, NewEngine) 8 // } 9 // 10 // Some tests using the Compiler Engine may need to guard as they use compiled features: 11 // 12 // func TestModuleEngine_Call(t *testing.T) { 13 // requireSupportedOSArch(t) 14 // enginetest.RunTestModuleEngine_Call(t, NewEngine) 15 // } 16 // 17 // Note: These tests intentionally avoid using wasm.Store as it is important to know both the dependencies and 18 // the capabilities at the wasm.Engine abstraction. 19 package enginetest 20 21 import ( 22 "context" 23 "errors" 24 "math" 25 "testing" 26 27 "wa-lang.org/wazero/api" 28 "wa-lang.org/wazero/experimental" 29 "wa-lang.org/wazero/internal/testing/require" 30 "wa-lang.org/wazero/internal/u64" 31 "wa-lang.org/wazero/internal/wasm" 32 "wa-lang.org/wazero/internal/wasmruntime" 33 ) 34 35 const ( 36 i32, i64 = wasm.ValueTypeI32, wasm.ValueTypeI64 37 ) 38 39 var ( 40 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 41 testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 42 // v_v is a nullary function type (void -> void) 43 v_v = &wasm.FunctionType{} 44 ) 45 46 type EngineTester interface { 47 // IsCompiler returns true if this engine is a compiler. 48 IsCompiler() bool 49 50 NewEngine(enabledFeatures api.CoreFeatures) wasm.Engine 51 52 ListenerFactory() experimental.FunctionListenerFactory 53 54 // CompiledFunctionPointerValue returns the opaque compiledFunction's pointer for the `funcIndex`. 55 CompiledFunctionPointerValue(tme wasm.ModuleEngine, funcIndex wasm.Index) uint64 56 } 57 58 func RunTestEngine_NewModuleEngine(t *testing.T, et EngineTester) { 59 e := et.NewEngine(api.CoreFeaturesV1) 60 61 t.Run("error before instantiation", func(t *testing.T) { 62 _, err := e.NewModuleEngine("mymod", &wasm.Module{}, nil, nil) 63 require.EqualError(t, err, "source module for mymod must be compiled before instantiation") 64 }) 65 66 t.Run("sets module name", func(t *testing.T) { 67 m := &wasm.Module{} 68 err := e.CompileModule(testCtx, m, nil) 69 require.NoError(t, err) 70 me, err := e.NewModuleEngine(t.Name(), m, nil, nil) 71 require.NoError(t, err) 72 require.Equal(t, t.Name(), me.Name()) 73 }) 74 } 75 76 func RunTestEngine_InitializeFuncrefGlobals(t *testing.T, et EngineTester) { 77 e := et.NewEngine(api.CoreFeaturesV2) 78 79 i64 := i64 80 m := &wasm.Module{ 81 TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}}}, 82 FunctionSection: []wasm.Index{0, 0, 0}, 83 CodeSection: []*wasm.Code{ 84 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{i64}}, 85 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{i64}}, 86 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}, LocalTypes: []wasm.ValueType{i64}}, 87 }, 88 } 89 m.BuildFunctionDefinitions() 90 err := e.CompileModule(testCtx, m, nil) 91 require.NoError(t, err) 92 93 // To use the function, we first need to add it to a module. 94 instance := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} 95 fns := instance.BuildFunctions(m) 96 me, err := e.NewModuleEngine(t.Name(), m, nil, fns) 97 require.NoError(t, err) 98 99 nullRefVal := wasm.GlobalInstanceNullFuncRefValue 100 globals := []*wasm.GlobalInstance{ 101 {Val: 10, Type: &wasm.GlobalType{ValType: i32}}, 102 {Val: uint64(nullRefVal), Type: &wasm.GlobalType{ValType: wasm.ValueTypeFuncref}}, 103 {Val: uint64(2), Type: &wasm.GlobalType{ValType: wasm.ValueTypeFuncref}}, 104 {Val: uint64(1), Type: &wasm.GlobalType{ValType: wasm.ValueTypeFuncref}}, 105 {Val: uint64(0), Type: &wasm.GlobalType{ValType: wasm.ValueTypeFuncref}}, 106 } 107 me.InitializeFuncrefGlobals(globals) 108 109 // Non-funcref values must be intact. 110 require.Equal(t, uint64(10), globals[0].Val) 111 // The second global had wasm.GlobalInstanceNullFuncRefValue, so that value must be translated as null reference (uint64(0)). 112 require.Zero(t, globals[1].Val) 113 // Non GlobalInstanceNullFuncRefValue valued globals must result in having the valid compiled function's pointers. 114 require.Equal(t, et.CompiledFunctionPointerValue(me, 2), globals[2].Val) 115 require.Equal(t, et.CompiledFunctionPointerValue(me, 1), globals[3].Val) 116 require.Equal(t, et.CompiledFunctionPointerValue(me, 0), globals[4].Val) 117 } 118 119 func RunTestModuleEngine_Call(t *testing.T, et EngineTester) { 120 e := et.NewEngine(api.CoreFeaturesV2) 121 122 // Define a basic function which defines two parameters and two results. 123 // This is used to test results when incorrect arity is used. 124 m := &wasm.Module{ 125 TypeSection: []*wasm.FunctionType{ 126 { 127 Params: []wasm.ValueType{i64, i64}, 128 Results: []wasm.ValueType{i64, i64}, 129 ParamNumInUint64: 2, 130 ResultNumInUint64: 2, 131 }, 132 }, 133 FunctionSection: []wasm.Index{0}, 134 CodeSection: []*wasm.Code{ 135 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeEnd}}, 136 }, 137 } 138 139 m.BuildFunctionDefinitions() 140 listeners := buildListeners(et.ListenerFactory(), m) 141 err := e.CompileModule(testCtx, m, listeners) 142 require.NoError(t, err) 143 144 // To use the function, we first need to add it to a module. 145 module := &wasm.ModuleInstance{Name: t.Name(), TypeIDs: []wasm.FunctionTypeID{0}} 146 module.Functions = module.BuildFunctions(m) 147 148 // Compile the module 149 me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions) 150 require.NoError(t, err) 151 linkModuleToEngine(module, me) 152 153 // Ensure the base case doesn't fail: A single parameter should work as that matches the function signature. 154 fn := module.Functions[0] 155 156 ce, err := me.NewCallEngine(module.CallCtx, fn) 157 require.NoError(t, err) 158 159 results, err := ce.Call(testCtx, module.CallCtx, []uint64{1, 2}) 160 require.NoError(t, err) 161 require.Equal(t, []uint64{1, 2}, results) 162 163 t.Run("errs when not enough parameters", func(t *testing.T) { 164 ce, err := me.NewCallEngine(module.CallCtx, fn) 165 require.NoError(t, err) 166 167 _, err = ce.Call(testCtx, module.CallCtx, nil) 168 require.EqualError(t, err, "expected 2 params, but passed 0") 169 }) 170 171 t.Run("errs when too many parameters", func(t *testing.T) { 172 ce, err := me.NewCallEngine(module.CallCtx, fn) 173 require.NoError(t, err) 174 175 _, err = ce.Call(testCtx, module.CallCtx, []uint64{1, 2, 3}) 176 require.EqualError(t, err, "expected 2 params, but passed 3") 177 }) 178 } 179 180 func RunTestModuleEngine_LookupFunction(t *testing.T, et EngineTester) { 181 e := et.NewEngine(api.CoreFeaturesV1) 182 183 mod := &wasm.Module{ 184 TypeSection: []*wasm.FunctionType{{}, {Params: []wasm.ValueType{wasm.ValueTypeV128}}}, 185 FunctionSection: []wasm.Index{0, 0, 0}, 186 CodeSection: []*wasm.Code{ 187 { 188 Body: []byte{wasm.OpcodeEnd}, 189 }, {Body: []byte{wasm.OpcodeEnd}}, {Body: []byte{wasm.OpcodeEnd}}, 190 }, 191 } 192 193 mod.BuildFunctionDefinitions() 194 err := e.CompileModule(testCtx, mod, nil) 195 require.NoError(t, err) 196 m := &wasm.ModuleInstance{TypeIDs: []wasm.FunctionTypeID{0, 1}} 197 m.Tables = []*wasm.TableInstance{ 198 {Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeFuncref}, 199 {Min: 2, References: make([]wasm.Reference, 2), Type: wasm.RefTypeExternref}, 200 {Min: 10, References: make([]wasm.Reference, 10), Type: wasm.RefTypeFuncref}, 201 } 202 m.Functions = m.BuildFunctions(mod) 203 204 me, err := e.NewModuleEngine(m.Name, mod, nil, m.Functions) 205 require.NoError(t, err) 206 linkModuleToEngine(m, me) 207 208 t.Run("null reference", func(t *testing.T) { 209 _, err := me.LookupFunction(m.Tables[0], m.TypeIDs[0], 0) // offset 0 is not initialized yet. 210 require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err) 211 _, err = me.LookupFunction(m.Tables[0], m.TypeIDs[0], 1) // offset 1 is not initialized yet. 212 require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err) 213 }) 214 215 m.Tables[0].References[0] = me.FunctionInstanceReference(2) 216 m.Tables[0].References[1] = me.FunctionInstanceReference(0) 217 218 t.Run("initialized", func(t *testing.T) { 219 index, err := me.LookupFunction(m.Tables[0], m.TypeIDs[0], 0) // offset 0 is now initialized. 220 require.NoError(t, err) 221 require.Equal(t, wasm.Index(2), index) 222 index, err = me.LookupFunction(m.Tables[0], m.TypeIDs[0], 1) // offset 1 is now initialized. 223 require.NoError(t, err) 224 require.Equal(t, wasm.Index(0), index) 225 }) 226 227 t.Run("out of range", func(t *testing.T) { 228 _, err := me.LookupFunction(m.Tables[0], m.TypeIDs[0], 100 /* out of range */) 229 require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err) 230 }) 231 232 t.Run("access to externref table", func(t *testing.T) { 233 _, err := me.LookupFunction(m.Tables[1], /* table[1] has externref type. */ 234 m.TypeIDs[0], 0) 235 require.Equal(t, wasmruntime.ErrRuntimeInvalidTableAccess, err) 236 }) 237 238 t.Run("access to externref table", func(t *testing.T) { 239 _, err := me.LookupFunction(m.Tables[0], /* type mismatch */ 240 m.TypeIDs[1], 0) 241 require.Equal(t, wasmruntime.ErrRuntimeIndirectCallTypeMismatch, err) 242 }) 243 244 m.Tables[2].References[0] = me.FunctionInstanceReference(1) 245 m.Tables[2].References[5] = me.FunctionInstanceReference(2) 246 t.Run("initialized - tables[2]", func(t *testing.T) { 247 index, err := me.LookupFunction(m.Tables[2], m.TypeIDs[0], 0) 248 require.NoError(t, err) 249 require.Equal(t, wasm.Index(1), index) 250 index, err = me.LookupFunction(m.Tables[2], m.TypeIDs[0], 5) 251 require.NoError(t, err) 252 require.Equal(t, wasm.Index(2), index) 253 }) 254 } 255 256 func runTestModuleEngine_Call_HostFn_Mem(t *testing.T, et EngineTester, readMem *wasm.Code) { 257 e := et.NewEngine(api.CoreFeaturesV1) 258 _, importing, done := setupCallMemTests(t, e, readMem, et.ListenerFactory()) 259 defer done() 260 261 importingMemoryVal := uint64(6) 262 importing.Memory = &wasm.MemoryInstance{Buffer: u64.LeBytes(importingMemoryVal), Min: 1, Cap: 1, Max: 1} 263 264 tests := []struct { 265 name string 266 fn *wasm.FunctionInstance 267 expected uint64 268 }{ 269 { 270 name: callImportReadMemName, 271 fn: importing.Exports[callImportReadMemName].Function, 272 expected: importingMemoryVal, 273 }, 274 { 275 name: callImportCallReadMemName, 276 fn: importing.Exports[callImportCallReadMemName].Function, 277 expected: importingMemoryVal, 278 }, 279 } 280 for _, tt := range tests { 281 tc := tt 282 283 t.Run(tc.name, func(t *testing.T) { 284 ce, err := tc.fn.Module.Engine.NewCallEngine(tc.fn.Module.CallCtx, tc.fn) 285 require.NoError(t, err) 286 287 results, err := ce.Call(testCtx, importing.CallCtx, nil) 288 require.NoError(t, err) 289 require.Equal(t, tc.expected, results[0]) 290 }) 291 } 292 } 293 294 func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) { 295 t.Run("wasm", func(t *testing.T) { 296 runTestModuleEngine_Call_HostFn(t, et, hostDivByWasm) 297 runTestModuleEngine_Call_HostFn_Mem(t, et, hostReadMemWasm) 298 }) 299 t.Run("go", func(t *testing.T) { 300 runTestModuleEngine_Call_HostFn(t, et, hostDivByGo) 301 runTestModuleEngine_Call_HostFn_Mem(t, et, hostReadMemGo) 302 }) 303 } 304 305 func runTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester, hostDivBy *wasm.Code) { 306 e := et.NewEngine(api.CoreFeaturesV1) 307 308 _, imported, importing, done := setupCallTests(t, e, hostDivBy, et.ListenerFactory()) 309 defer done() 310 311 // Ensure the base case doesn't fail: A single parameter should work as that matches the function signature. 312 tests := []struct { 313 name string 314 module *wasm.CallContext 315 fn *wasm.FunctionInstance 316 }{ 317 { 318 name: divByWasmName, 319 module: imported.CallCtx, 320 fn: imported.Exports[divByWasmName].Function, 321 }, 322 { 323 name: callDivByGoName, 324 module: imported.CallCtx, 325 fn: imported.Exports[callDivByGoName].Function, 326 }, 327 { 328 name: callImportCallDivByGoName, 329 module: importing.CallCtx, 330 fn: importing.Exports[callImportCallDivByGoName].Function, 331 }, 332 } 333 for _, tt := range tests { 334 tc := tt 335 336 t.Run(tc.name, func(t *testing.T) { 337 m := tc.module 338 f := tc.fn 339 340 ce, err := f.Module.Engine.NewCallEngine(m, f) 341 require.NoError(t, err) 342 343 results, err := ce.Call(testCtx, m, []uint64{1}) 344 require.NoError(t, err) 345 require.Equal(t, uint64(1), results[0]) 346 347 results2, err := ce.Call(testCtx, m, []uint64{1}) 348 require.NoError(t, err) 349 require.Equal(t, results, results2) 350 351 // Ensure the result slices are unique 352 results[0] = 255 353 require.Equal(t, uint64(1), results2[0]) 354 }) 355 } 356 } 357 358 func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) { 359 e := et.NewEngine(api.CoreFeaturesV1) 360 361 _, imported, importing, done := setupCallTests(t, e, hostDivByGo, et.ListenerFactory()) 362 defer done() 363 364 tests := []struct { 365 name string 366 module *wasm.CallContext 367 fn *wasm.FunctionInstance 368 input []uint64 369 expectedErr string 370 }{ 371 { 372 name: "wasm function not enough parameters", 373 input: []uint64{}, 374 module: imported.CallCtx, 375 fn: imported.Exports[divByWasmName].Function, 376 expectedErr: `expected 1 params, but passed 0`, 377 }, 378 { 379 name: "wasm function too many parameters", 380 input: []uint64{1, 2}, 381 module: imported.CallCtx, 382 fn: imported.Exports[divByWasmName].Function, 383 expectedErr: `expected 1 params, but passed 2`, 384 }, 385 { 386 name: "wasm function panics with wasmruntime.Error", 387 input: []uint64{0}, 388 module: imported.CallCtx, 389 fn: imported.Exports[divByWasmName].Function, 390 expectedErr: `wasm error: integer divide by zero 391 wasm stack trace: 392 imported.div_by.wasm(i32) i32`, 393 }, 394 { 395 name: "wasm calls host function that panics", 396 input: []uint64{math.MaxUint32}, 397 module: imported.CallCtx, 398 fn: imported.Exports[callDivByGoName].Function, 399 expectedErr: `host-function panic (recovered by wazero) 400 wasm stack trace: 401 host.div_by.go(i32) i32 402 imported.call->div_by.go(i32) i32`, 403 }, 404 { 405 name: "wasm calls imported wasm that calls host function panics with runtime.Error", 406 input: []uint64{0}, 407 module: importing.CallCtx, 408 fn: importing.Exports[callImportCallDivByGoName].Function, 409 expectedErr: `runtime error: integer divide by zero (recovered by wazero) 410 wasm stack trace: 411 host.div_by.go(i32) i32 412 imported.call->div_by.go(i32) i32 413 importing.call_import->call->div_by.go(i32) i32`, 414 }, 415 { 416 name: "wasm calls imported wasm that calls host function that panics", 417 input: []uint64{math.MaxUint32}, 418 module: importing.CallCtx, 419 fn: importing.Exports[callImportCallDivByGoName].Function, 420 expectedErr: `host-function panic (recovered by wazero) 421 wasm stack trace: 422 host.div_by.go(i32) i32 423 imported.call->div_by.go(i32) i32 424 importing.call_import->call->div_by.go(i32) i32`, 425 }, 426 { 427 name: "wasm calls imported wasm calls host function panics with runtime.Error", 428 input: []uint64{0}, 429 module: importing.CallCtx, 430 fn: importing.Exports[callImportCallDivByGoName].Function, 431 expectedErr: `runtime error: integer divide by zero (recovered by wazero) 432 wasm stack trace: 433 host.div_by.go(i32) i32 434 imported.call->div_by.go(i32) i32 435 importing.call_import->call->div_by.go(i32) i32`, 436 }, 437 } 438 for _, tt := range tests { 439 tc := tt 440 t.Run(tc.name, func(t *testing.T) { 441 m := tc.module 442 f := tc.fn 443 444 ce, err := f.Module.Engine.NewCallEngine(m, f) 445 require.NoError(t, err) 446 447 _, err = ce.Call(testCtx, m, tc.input) 448 require.EqualError(t, err, tc.expectedErr) 449 450 // Ensure the module still works 451 results, err := ce.Call(testCtx, m, []uint64{1}) 452 require.NoError(t, err) 453 require.Equal(t, uint64(1), results[0]) 454 }) 455 } 456 } 457 458 // RunTestModuleEngine_Memory shows that the byte slice returned from api.Memory Read is not a copy, rather a re-slice 459 // of the underlying memory. This allows both host and Wasm to see each other's writes, unless one side changes the 460 // capacity of the slice. 461 // 462 // Known cases that change the slice capacity: 463 // * Host code calls append on a byte slice returned by api.Memory Read 464 // * Wasm code calls wasm.OpcodeMemoryGrowName and this changes the capacity (by default, it will). 465 func RunTestModuleEngine_Memory(t *testing.T, et EngineTester) { 466 e := et.NewEngine(api.CoreFeaturesV2) 467 468 wasmPhrase := "Well, that'll be the day when you say goodbye." 469 wasmPhraseSize := uint32(len(wasmPhrase)) 470 471 // Define a basic function which defines one parameter. This is used to test results when incorrect arity is used. 472 one := uint32(1) 473 m := &wasm.Module{ 474 TypeSection: []*wasm.FunctionType{{Params: []api.ValueType{api.ValueTypeI32}, ParamNumInUint64: 1}, v_v}, 475 FunctionSection: []wasm.Index{0, 1}, 476 MemorySection: &wasm.Memory{Min: 1, Cap: 1, Max: 2}, 477 DataSection: []*wasm.DataSegment{ 478 { 479 OffsetExpression: nil, // passive 480 Init: []byte(wasmPhrase), 481 }, 482 }, 483 DataCountSection: &one, 484 CodeSection: []*wasm.Code{ 485 {Body: []byte{ // "grow" 486 wasm.OpcodeLocalGet, 0, // how many pages to grow (param) 487 wasm.OpcodeMemoryGrow, 0, // memory index zero 488 wasm.OpcodeDrop, // drop the previous page count (or -1 if grow failed) 489 wasm.OpcodeEnd, 490 }}, 491 {Body: []byte{ // "init" 492 wasm.OpcodeI32Const, 0, // target offset 493 wasm.OpcodeI32Const, 0, // source offset 494 wasm.OpcodeI32Const, byte(wasmPhraseSize), // len 495 wasm.OpcodeMiscPrefix, wasm.OpcodeMiscMemoryInit, 0, 0, // segment 0, memory 0 496 wasm.OpcodeEnd, 497 }}, 498 }, 499 ExportSection: []*wasm.Export{ 500 {Name: "grow", Type: wasm.ExternTypeFunc, Index: 0}, 501 {Name: "init", Type: wasm.ExternTypeFunc, Index: 1}, 502 }, 503 } 504 m.BuildFunctionDefinitions() 505 listeners := buildListeners(et.ListenerFactory(), m) 506 507 err := e.CompileModule(testCtx, m, listeners) 508 require.NoError(t, err) 509 510 // Assign memory to the module instance 511 module := &wasm.ModuleInstance{ 512 Name: t.Name(), 513 Memory: wasm.NewMemoryInstance(m.MemorySection), 514 DataInstances: []wasm.DataInstance{m.DataSection[0].Init}, 515 TypeIDs: []wasm.FunctionTypeID{0, 1}, 516 } 517 var memory api.Memory = module.Memory 518 519 // To use functions, we need to instantiate them (associate them with a ModuleInstance). 520 module.Functions = module.BuildFunctions(m) 521 module.BuildExports(m.ExportSection) 522 grow, init := module.Functions[0], module.Functions[1] 523 524 // Compile the module 525 me, err := e.NewModuleEngine(module.Name, m, nil, module.Functions) 526 require.NoError(t, err) 527 linkModuleToEngine(module, me) 528 529 buf, ok := memory.Read(testCtx, 0, wasmPhraseSize) 530 require.True(t, ok) 531 require.Equal(t, make([]byte, wasmPhraseSize), buf) 532 533 // Initialize the memory using Wasm. This copies the test phrase. 534 initCallEngine, err := me.NewCallEngine(module.CallCtx, init) 535 require.NoError(t, err) 536 _, err = initCallEngine.Call(testCtx, module.CallCtx, nil) 537 require.NoError(t, err) 538 539 // We expect the same []byte read earlier to now include the phrase in wasm. 540 require.Equal(t, wasmPhrase, string(buf)) 541 542 hostPhrase := "Goodbye, cruel world. I'm off to join the circus." // Intentionally slightly longer. 543 hostPhraseSize := uint32(len(hostPhrase)) 544 545 // Copy over the buffer, which should stop at the current length. 546 copy(buf, hostPhrase) 547 require.Equal(t, "Goodbye, cruel world. I'm off to join the circ", string(buf)) 548 549 // The underlying memory should be updated. This proves that Memory.Read returns a re-slice, not a copy, and that 550 // programs can rely on this (for example, to update shared state in Wasm and view that in Go and visa versa). 551 buf2, ok := memory.Read(testCtx, 0, wasmPhraseSize) 552 require.True(t, ok) 553 require.Equal(t, buf, buf2) 554 555 // Now, append to the buffer we got from Wasm. As this changes capacity, it should result in a new byte slice. 556 buf = append(buf, 'u', 's', '.') 557 require.Equal(t, hostPhrase, string(buf)) 558 559 // To prove the above, we re-read the memory and should not see the appended bytes (rather zeros instead). 560 buf2, ok = memory.Read(testCtx, 0, hostPhraseSize) 561 require.True(t, ok) 562 hostPhraseTruncated := "Goodbye, cruel world. I'm off to join the circ" + string([]byte{0, 0, 0}) 563 require.Equal(t, hostPhraseTruncated, string(buf2)) 564 565 // Now, we need to prove the other direction, that when Wasm changes the capacity, the host's buffer is unaffected. 566 growCallEngine, err := me.NewCallEngine(module.CallCtx, grow) 567 require.NoError(t, err) 568 _, err = growCallEngine.Call(testCtx, module.CallCtx, []uint64{1}) 569 require.NoError(t, err) 570 571 // The host buffer should still contain the same bytes as before grow 572 require.Equal(t, hostPhraseTruncated, string(buf2)) 573 574 // Re-initialize the memory in wasm, which overwrites the region. 575 initCallEngine2, err := me.NewCallEngine(module.CallCtx, init) 576 require.NoError(t, err) 577 _, err = initCallEngine2.Call(testCtx, module.CallCtx, nil) 578 require.NoError(t, err) 579 580 // The host was not affected because it is a different slice due to "memory.grow" affecting the underlying memory. 581 require.Equal(t, hostPhraseTruncated, string(buf2)) 582 } 583 584 const ( 585 divByWasmName = "div_by.wasm" 586 divByGoName = "div_by.go" 587 callDivByGoName = "call->" + divByGoName 588 callImportCallDivByGoName = "call_import->" + callDivByGoName 589 ) 590 591 func divByGo(d uint32) uint32 { 592 if d == math.MaxUint32 { 593 panic(errors.New("host-function panic")) 594 } 595 return 1 / d // go panics if d == 0 596 } 597 598 var hostDivByGo = wasm.MustParseGoReflectFuncCode(divByGo) 599 600 // (func (export "div_by.wasm") (param i32) (result i32) (i32.div_u (i32.const 1) (local.get 0))) 601 var ( 602 divByWasm = []byte{wasm.OpcodeI32Const, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeI32DivU, wasm.OpcodeEnd} 603 hostDivByWasm = &wasm.Code{IsHostFunction: true, Body: divByWasm} 604 ) 605 606 const ( 607 readMemName = "read_mem" 608 callReadMemName = "call->read_mem" 609 callImportReadMemName = "call_import->read_mem" 610 callImportCallReadMemName = "call_import->call->read_mem" 611 ) 612 613 func readMemGo(ctx context.Context, m api.Module) uint64 { 614 ret, ok := m.Memory().ReadUint64Le(ctx, 0) 615 if !ok { 616 panic("couldn't read memory") 617 } 618 return ret 619 } 620 621 var hostReadMemGo = wasm.MustParseGoReflectFuncCode(readMemGo) 622 623 // (func (export "wasm_read_mem") (result i64) i32.const 0 i64.load) 624 var ( 625 readMemWasm = []byte{wasm.OpcodeI32Const, 0, wasm.OpcodeI64Load, 0x3, 0x0, wasm.OpcodeEnd} 626 hostReadMemWasm = &wasm.Code{IsHostFunction: true, Body: readMemWasm} 627 ) 628 629 func setupCallTests(t *testing.T, e wasm.Engine, divBy *wasm.Code, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, *wasm.ModuleInstance, func()) { 630 ft := &wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}, ParamNumInUint64: 1, ResultNumInUint64: 1} 631 632 divByName := divByWasmName 633 if divBy.GoFunc != nil { 634 divByName = divByGoName 635 } 636 hostModule := &wasm.Module{ 637 TypeSection: []*wasm.FunctionType{ft}, 638 FunctionSection: []wasm.Index{0}, 639 CodeSection: []*wasm.Code{divBy}, 640 ExportSection: []*wasm.Export{{Name: divByGoName, Type: wasm.ExternTypeFunc, Index: 0}}, 641 NameSection: &wasm.NameSection{ 642 ModuleName: "host", 643 FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: divByName}}, 644 }, 645 ID: wasm.ModuleID{0}, 646 } 647 hostModule.BuildFunctionDefinitions() 648 lns := buildListeners(fnlf, hostModule) 649 err := e.CompileModule(testCtx, hostModule, lns) 650 require.NoError(t, err) 651 host := &wasm.ModuleInstance{Name: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} 652 host.Functions = host.BuildFunctions(hostModule) 653 host.BuildExports(hostModule.ExportSection) 654 hostFn := host.Exports[divByGoName].Function 655 656 hostME, err := e.NewModuleEngine(host.Name, hostModule, nil, host.Functions) 657 require.NoError(t, err) 658 linkModuleToEngine(host, hostME) 659 660 importedModule := &wasm.Module{ 661 ImportSection: []*wasm.Import{{}}, 662 TypeSection: []*wasm.FunctionType{ft}, 663 FunctionSection: []wasm.Index{0, 0}, 664 CodeSection: []*wasm.Code{ 665 {Body: divByWasm}, 666 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, byte(0), // Calling imported host function ^. 667 wasm.OpcodeEnd}}, 668 }, 669 ExportSection: []*wasm.Export{ 670 {Name: divByWasmName, Type: wasm.ExternTypeFunc, Index: 1}, 671 {Name: callDivByGoName, Type: wasm.ExternTypeFunc, Index: 2}, 672 }, 673 NameSection: &wasm.NameSection{ 674 ModuleName: "imported", 675 FunctionNames: wasm.NameMap{ 676 {Index: wasm.Index(1), Name: divByWasmName}, 677 {Index: wasm.Index(2), Name: callDivByGoName}, 678 }, 679 }, 680 ID: wasm.ModuleID{1}, 681 } 682 importedModule.BuildFunctionDefinitions() 683 lns = buildListeners(fnlf, importedModule) 684 err = e.CompileModule(testCtx, importedModule, lns) 685 require.NoError(t, err) 686 687 imported := &wasm.ModuleInstance{Name: importedModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} 688 importedFunctions := imported.BuildFunctions(importedModule) 689 imported.Functions = append([]*wasm.FunctionInstance{hostFn}, importedFunctions...) 690 imported.BuildExports(importedModule.ExportSection) 691 callHostFn := imported.Exports[callDivByGoName].Function 692 693 // Compile the imported module 694 importedMe, err := e.NewModuleEngine(imported.Name, importedModule, []*wasm.FunctionInstance{hostFn}, importedFunctions) 695 require.NoError(t, err) 696 linkModuleToEngine(imported, importedMe) 697 698 // To test stack traces, call the same function from another module 699 importingModule := &wasm.Module{ 700 TypeSection: []*wasm.FunctionType{ft}, 701 ImportSection: []*wasm.Import{{}}, 702 FunctionSection: []wasm.Index{0}, 703 CodeSection: []*wasm.Code{ 704 {Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0 /* only one imported function */, wasm.OpcodeEnd}}, 705 }, 706 ExportSection: []*wasm.Export{ 707 {Name: callImportCallDivByGoName, Type: wasm.ExternTypeFunc, Index: 1}, 708 }, 709 NameSection: &wasm.NameSection{ 710 ModuleName: "importing", 711 FunctionNames: wasm.NameMap{{Index: wasm.Index(1), Name: callImportCallDivByGoName}}, 712 }, 713 ID: wasm.ModuleID{2}, 714 } 715 importingModule.BuildFunctionDefinitions() 716 lns = buildListeners(fnlf, importingModule) 717 err = e.CompileModule(testCtx, importingModule, lns) 718 require.NoError(t, err) 719 720 // Add the exported function. 721 importing := &wasm.ModuleInstance{Name: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} 722 importingFunctions := importing.BuildFunctions(importingModule) 723 importing.Functions = append([]*wasm.FunctionInstance{callHostFn}, importingFunctions...) 724 importing.BuildExports(importingModule.ExportSection) 725 726 // Compile the importing module 727 importingMe, err := e.NewModuleEngine(importing.Name, importingModule, []*wasm.FunctionInstance{callHostFn}, importingFunctions) 728 require.NoError(t, err) 729 linkModuleToEngine(importing, importingMe) 730 731 return host, imported, importing, func() { 732 e.DeleteCompiledModule(hostModule) 733 e.DeleteCompiledModule(importedModule) 734 e.DeleteCompiledModule(importingModule) 735 } 736 } 737 738 func setupCallMemTests(t *testing.T, e wasm.Engine, readMem *wasm.Code, fnlf experimental.FunctionListenerFactory) (*wasm.ModuleInstance, *wasm.ModuleInstance, func()) { 739 ft := &wasm.FunctionType{Results: []wasm.ValueType{i64}, ResultNumInUint64: 1} 740 741 callReadMem := &wasm.Code{ // shows indirect calls still use the same memory 742 IsHostFunction: true, 743 Body: []byte{ 744 wasm.OpcodeCall, 1, 745 // On the return from the another host function, 746 // we should still be able to access the memory. 747 wasm.OpcodeI32Const, 0, 748 wasm.OpcodeI32Load, 0x2, 0x0, 749 wasm.OpcodeEnd, 750 }, 751 } 752 hostModule := &wasm.Module{ 753 TypeSection: []*wasm.FunctionType{ft}, 754 FunctionSection: []wasm.Index{0, 0}, 755 CodeSection: []*wasm.Code{callReadMem, readMem}, 756 ExportSection: []*wasm.Export{ 757 {Name: callReadMemName, Type: wasm.ExternTypeFunc, Index: 0}, 758 {Name: readMemName, Type: wasm.ExternTypeFunc, Index: 1}, 759 }, 760 NameSection: &wasm.NameSection{ 761 ModuleName: "host", 762 FunctionNames: wasm.NameMap{{Index: 0, Name: readMemName}, {Index: 1, Name: callReadMemName}}, 763 }, 764 ID: wasm.ModuleID{0}, 765 } 766 hostModule.BuildFunctionDefinitions() 767 err := e.CompileModule(testCtx, hostModule, nil) 768 require.NoError(t, err) 769 host := &wasm.ModuleInstance{Name: hostModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} 770 host.Functions = host.BuildFunctions(hostModule) 771 host.BuildExports(hostModule.ExportSection) 772 readMemFn := host.Exports[readMemName].Function 773 callReadMemFn := host.Exports[callReadMemName].Function 774 775 hostME, err := e.NewModuleEngine(host.Name, hostModule, nil, host.Functions) 776 require.NoError(t, err) 777 linkModuleToEngine(host, hostME) 778 779 importingModule := &wasm.Module{ 780 TypeSection: []*wasm.FunctionType{ft}, 781 ImportSection: []*wasm.Import{ 782 // Placeholder for two import functions from `importedModule`. 783 {Type: wasm.ExternTypeFunc, DescFunc: 0}, 784 {Type: wasm.ExternTypeFunc, DescFunc: 0}, 785 }, 786 FunctionSection: []wasm.Index{0, 0}, 787 ExportSection: []*wasm.Export{ 788 {Name: callImportReadMemName, Type: wasm.ExternTypeFunc, Index: 2}, 789 {Name: callImportCallReadMemName, Type: wasm.ExternTypeFunc, Index: 3}, 790 }, 791 CodeSection: []*wasm.Code{ 792 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Calling the index 0 = callReadMemFn. 793 {Body: []byte{wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Calling the index 1 = readMemFn. 794 }, 795 NameSection: &wasm.NameSection{ 796 ModuleName: "importing", 797 FunctionNames: wasm.NameMap{ 798 {Index: 2, Name: callImportReadMemName}, 799 {Index: 3, Name: callImportCallReadMemName}, 800 }, 801 }, 802 // Indicates that this module has a memory so that compilers are able to assembe memory-related initialization. 803 MemorySection: &wasm.Memory{Min: 1}, 804 ID: wasm.ModuleID{1}, 805 } 806 importingModule.BuildFunctionDefinitions() 807 err = e.CompileModule(testCtx, importingModule, nil) 808 require.NoError(t, err) 809 810 // Add the exported function. 811 importing := &wasm.ModuleInstance{Name: importingModule.NameSection.ModuleName, TypeIDs: []wasm.FunctionTypeID{0}} 812 importingFunctions := importing.BuildFunctions(importingModule) 813 // Note: adds imported functions readMemFn and callReadMemFn at index 0 and 1. 814 importing.Functions = append([]*wasm.FunctionInstance{callReadMemFn, readMemFn}, importingFunctions...) 815 importing.BuildExports(importingModule.ExportSection) 816 817 // Compile the importing module 818 importingMe, err := e.NewModuleEngine(importing.Name, importingModule, []*wasm.FunctionInstance{readMemFn, callReadMemFn}, importingFunctions) 819 require.NoError(t, err) 820 linkModuleToEngine(importing, importingMe) 821 822 return host, importing, func() { 823 e.DeleteCompiledModule(hostModule) 824 e.DeleteCompiledModule(importingModule) 825 } 826 } 827 828 // linkModuleToEngine assigns fields that wasm.Store would on instantiation. These includes fields both interpreter and 829 // Compiler needs as well as fields only needed by Compiler. 830 // 831 // Note: This sets fields that are not needed in the interpreter, but are required by code compiled by Compiler. If a new 832 // test here passes in the interpreter and segmentation faults in Compiler, check for a new field offset or a change in Compiler 833 // (e.g. compiler.TestVerifyOffsetValue). It is possible for all other tests to pass as that field is implicitly set by 834 // wasm.Store: store isn't used here for unit test precision. 835 func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) { 836 module.Engine = me // for Compiler, links the module to the module-engine compiled from it (moduleInstanceEngineOffset). 837 // callEngineModuleContextModuleInstanceAddressOffset 838 module.CallCtx = wasm.NewCallContext(nil, module, nil) 839 } 840 841 func buildListeners(factory experimental.FunctionListenerFactory, m *wasm.Module) []experimental.FunctionListener { 842 if factory == nil || len(m.FunctionSection) == 0 { 843 return nil 844 } 845 listeners := make([]experimental.FunctionListener, len(m.FunctionSection)) 846 importCount := m.ImportFuncCount() 847 for i := 0; i < len(listeners); i++ { 848 listeners[i] = factory.NewListener(m.FunctionDefinitionSection[uint32(i)+importCount]) 849 } 850 return listeners 851 }