github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/imports/emscripten/emscripten_test.go (about) 1 package emscripten 2 3 import ( 4 "bytes" 5 "context" 6 _ "embed" 7 "testing" 8 9 wazero "github.com/wasilibs/wazerox" 10 "github.com/wasilibs/wazerox/api" 11 "github.com/wasilibs/wazerox/experimental" 12 "github.com/wasilibs/wazerox/experimental/logging" 13 "github.com/wasilibs/wazerox/imports/wasi_snapshot_preview1" 14 internal "github.com/wasilibs/wazerox/internal/emscripten" 15 "github.com/wasilibs/wazerox/internal/testing/binaryencoding" 16 "github.com/wasilibs/wazerox/internal/testing/require" 17 "github.com/wasilibs/wazerox/internal/wasm" 18 ) 19 20 const ( 21 i64 = wasm.ValueTypeI64 22 f32 = wasm.ValueTypeF32 23 f64 = wasm.ValueTypeF64 24 ) 25 26 // growWasm was compiled from testdata/grow.cc 27 // 28 //go:embed testdata/grow.wasm 29 var growWasm []byte 30 31 // invokeWasm was generated by the following: 32 // 33 // cd testdata; wat2wasm --debug-names invoke.wat 34 // 35 //go:embed testdata/invoke.wasm 36 var invokeWasm []byte 37 38 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 39 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 40 41 // TestGrow is an integration test until we have an Emscripten example. 42 func TestGrow(t *testing.T) { 43 var log bytes.Buffer 44 45 // Set context to one that has an experimental listener 46 ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{}, 47 logging.NewHostLoggingListenerFactory(&log, logging.LogScopeMemory)) 48 49 r := wazero.NewRuntime(ctx) 50 defer r.Close(ctx) 51 52 wasi_snapshot_preview1.MustInstantiate(ctx, r) 53 54 _, err := Instantiate(ctx, r) 55 require.NoError(t, err) 56 57 // Emscripten exits main with zero by default, which coerces to nul. 58 _, err = r.Instantiate(ctx, growWasm) 59 require.Nil(t, err) 60 61 // We expect the memory no-op memory growth hook to be invoked as wasm. 62 require.Contains(t, log.String(), "==> env.emscripten_notify_memory_growth(memory_index=0)") 63 } 64 65 func TestNewFunctionExporterForModule(t *testing.T) { 66 tests := []struct { 67 name string 68 input *wasm.Module 69 expected emscriptenFns 70 }{ 71 { 72 name: "empty", 73 input: &wasm.Module{}, 74 expected: emscriptenFns{}, 75 }, 76 { 77 name: internal.FunctionNotifyMemoryGrowth, 78 input: &wasm.Module{ 79 TypeSection: []wasm.FunctionType{ 80 {Params: []wasm.ValueType{i32}}, 81 }, 82 ImportSection: []wasm.Import{ 83 { 84 Module: "env", Name: internal.FunctionNotifyMemoryGrowth, 85 Type: wasm.ExternTypeFunc, 86 DescFunc: 0, 87 }, 88 }, 89 }, 90 expected: []*wasm.HostFunc{internal.NotifyMemoryGrowth}, 91 }, 92 { 93 name: "all result types", 94 input: &wasm.Module{ 95 TypeSection: []wasm.FunctionType{ 96 {Params: []wasm.ValueType{i32}}, 97 {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, 98 {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i64}}, 99 {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f32}}, 100 {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{f64}}, 101 }, 102 ImportSection: []wasm.Import{ 103 { 104 Module: "env", Name: "invoke_v", 105 Type: wasm.ExternTypeFunc, 106 DescFunc: 0, 107 }, 108 { 109 Module: "env", Name: "invoke_i", 110 Type: wasm.ExternTypeFunc, 111 DescFunc: 1, 112 }, 113 { 114 Module: "env", Name: "invoke_p", 115 Type: wasm.ExternTypeFunc, 116 DescFunc: 1, 117 }, 118 { 119 Module: "env", Name: "invoke_j", 120 Type: wasm.ExternTypeFunc, 121 DescFunc: 2, 122 }, 123 { 124 Module: "env", Name: "invoke_f", 125 Type: wasm.ExternTypeFunc, 126 DescFunc: 3, 127 }, 128 { 129 Module: "env", Name: "invoke_d", 130 Type: wasm.ExternTypeFunc, 131 DescFunc: 4, 132 }, 133 }, 134 }, 135 expected: []*wasm.HostFunc{ 136 { 137 ExportName: "invoke_v", 138 ParamTypes: []api.ValueType{i32}, 139 ParamNames: []string{"index"}, 140 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, 141 }, 142 { 143 ExportName: "invoke_i", 144 ParamTypes: []api.ValueType{i32}, 145 ParamNames: []string{"index"}, 146 ResultTypes: []api.ValueType{i32}, 147 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}}, 148 }, 149 { 150 ExportName: "invoke_p", 151 ParamTypes: []api.ValueType{i32}, 152 ParamNames: []string{"index"}, 153 ResultTypes: []api.ValueType{i32}, 154 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i32}}}}, 155 }, 156 { 157 ExportName: "invoke_j", 158 ParamTypes: []api.ValueType{i32}, 159 ParamNames: []string{"index"}, 160 ResultTypes: []api.ValueType{i64}, 161 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{i64}}}}, 162 }, 163 { 164 ExportName: "invoke_f", 165 ParamTypes: []api.ValueType{i32}, 166 ParamNames: []string{"index"}, 167 ResultTypes: []api.ValueType{f32}, 168 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f32}}}}, 169 }, 170 { 171 ExportName: "invoke_d", 172 ParamTypes: []api.ValueType{i32}, 173 ParamNames: []string{"index"}, 174 ResultTypes: []api.ValueType{f64}, 175 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Results: []api.ValueType{f64}}}}, 176 }, 177 }, 178 }, 179 { 180 name: "ignores other imports", 181 input: &wasm.Module{ 182 TypeSection: []wasm.FunctionType{ 183 {Params: []wasm.ValueType{i32}}, 184 }, 185 ImportSection: []wasm.Import{ 186 { 187 Module: "anv", Name: "invoke_v", 188 Type: wasm.ExternTypeFunc, 189 DescFunc: 0, 190 }, 191 { 192 Module: "env", Name: "invoke_v", 193 Type: wasm.ExternTypeFunc, 194 DescFunc: 0, 195 }, 196 { 197 Module: "env", Name: "grow", 198 Type: wasm.ExternTypeFunc, 199 DescFunc: 0, 200 }, 201 }, 202 }, 203 expected: []*wasm.HostFunc{ 204 { 205 ExportName: "invoke_v", 206 ParamTypes: []api.ValueType{i32}, 207 ParamNames: []string{"index"}, 208 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, 209 }, 210 }, 211 }, 212 { 213 name: "invoke_v and " + internal.FunctionNotifyMemoryGrowth, 214 input: &wasm.Module{ 215 TypeSection: []wasm.FunctionType{{Params: []wasm.ValueType{i32}}}, 216 ImportSection: []wasm.Import{ 217 { 218 Module: "env", Name: "invoke_v", 219 Type: wasm.ExternTypeFunc, 220 DescFunc: 0, 221 }, 222 { 223 Module: "env", Name: internal.FunctionNotifyMemoryGrowth, 224 Type: wasm.ExternTypeFunc, 225 DescFunc: 0, 226 }, 227 }, 228 }, 229 expected: []*wasm.HostFunc{ 230 { 231 ExportName: "invoke_v", 232 ParamTypes: []api.ValueType{i32}, 233 ParamNames: []string{"index"}, 234 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{}}}, 235 }, 236 internal.NotifyMemoryGrowth, 237 }, 238 }, 239 { 240 name: "invoke_vi", 241 input: &wasm.Module{ 242 TypeSection: []wasm.FunctionType{ 243 {Params: []wasm.ValueType{i32, i32}}, 244 }, 245 ImportSection: []wasm.Import{ 246 { 247 Module: "env", Name: "invoke_vi", 248 Type: wasm.ExternTypeFunc, 249 DescFunc: 0, 250 }, 251 }, 252 }, 253 expected: []*wasm.HostFunc{ 254 { 255 ExportName: "invoke_vi", 256 ParamTypes: []api.ValueType{i32, i32}, 257 ParamNames: []string{"index", "a1"}, 258 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{Params: []api.ValueType{i32}}}}, 259 }, 260 }, 261 }, 262 { 263 name: "invoke_iiiii", 264 input: &wasm.Module{ 265 TypeSection: []wasm.FunctionType{ 266 { 267 Params: []wasm.ValueType{i32, i32, i32, i32, i32}, 268 Results: []wasm.ValueType{i32}, 269 }, 270 }, 271 ImportSection: []wasm.Import{ 272 { 273 Module: "env", Name: "invoke_iiiii", 274 Type: wasm.ExternTypeFunc, 275 DescFunc: 0, 276 }, 277 }, 278 }, 279 expected: []*wasm.HostFunc{ 280 { 281 ExportName: "invoke_iiiii", 282 ParamTypes: []api.ValueType{i32, i32, i32, i32, i32}, 283 ParamNames: []string{"index", "a1", "a2", "a3", "a4"}, 284 ResultTypes: []wasm.ValueType{i32}, 285 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{ 286 Params: []api.ValueType{i32, i32, i32, i32}, 287 Results: []api.ValueType{i32}, 288 }}}, 289 }, 290 }, 291 }, 292 { 293 name: "invoke_viiiddiiiiii", 294 input: &wasm.Module{ 295 TypeSection: []wasm.FunctionType{ 296 { 297 Params: []wasm.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, 298 }, 299 }, 300 ImportSection: []wasm.Import{ 301 { 302 Module: "env", Name: "invoke_viiiddiiiiii", 303 Type: wasm.ExternTypeFunc, 304 DescFunc: 0, 305 }, 306 }, 307 }, 308 expected: []*wasm.HostFunc{ 309 { 310 ExportName: "invoke_viiiddiiiiii", 311 ParamTypes: []api.ValueType{i32, i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, 312 ParamNames: []string{"index", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10", "a11"}, 313 Code: wasm.Code{GoFunc: &internal.InvokeFunc{FunctionType: &wasm.FunctionType{ 314 Params: []api.ValueType{i32, i32, i32, f64, f64, i32, i32, i32, i32, i32, i32}, 315 }}}, 316 }, 317 }, 318 }, 319 } 320 321 for _, tt := range tests { 322 tc := tt 323 324 t.Run(tc.name, func(t *testing.T) { 325 r := wazero.NewRuntime(testCtx) 326 defer r.Close(testCtx) 327 328 guest, err := r.CompileModule(testCtx, binaryencoding.EncodeModule(tc.input)) 329 require.NoError(t, err) 330 331 exporter, err := NewFunctionExporterForModule(guest) 332 require.NoError(t, err) 333 actual := exporter.(emscriptenFns) 334 335 require.Equal(t, len(tc.expected), len(actual)) 336 for i, expected := range tc.expected { 337 require.Equal(t, expected, actual[i], actual[i].ExportName) 338 } 339 }) 340 } 341 } 342 343 func TestInstantiateForModule(t *testing.T) { 344 var log bytes.Buffer 345 346 // Set context to one that has an experimental listener 347 ctx := context.WithValue(testCtx, experimental.FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&log)) 348 349 r := wazero.NewRuntime(ctx) 350 defer r.Close(ctx) 351 352 compiled, err := r.CompileModule(ctx, invokeWasm) 353 require.NoError(t, err) 354 355 _, err = InstantiateForModule(ctx, r, compiled) 356 require.NoError(t, err) 357 358 mod, err := r.InstantiateModule(ctx, compiled, wazero.NewModuleConfig()) 359 require.NoError(t, err) 360 361 tests := []struct { 362 name, funcName string 363 tableOffset int 364 params, expectedResults []uint64 365 expectedLog string 366 }{ 367 { 368 name: "invoke_i", 369 funcName: "call_v_i32", 370 expectedResults: []uint64{42}, 371 expectedLog: `--> .call_v_i32(0) 372 ==> env.invoke_i(index=0) 373 --> .stackSave() 374 <-- 65536 375 --> .v_i32() 376 <-- 42 377 <== 42 378 <-- 42 379 `, 380 }, 381 { 382 name: "invoke_ii", 383 funcName: "call_i32_i32", 384 tableOffset: 2, 385 params: []uint64{42}, 386 expectedResults: []uint64{42}, 387 expectedLog: `--> .call_i32_i32(2,42) 388 ==> env.invoke_ii(index=2,a1=42) 389 --> .stackSave() 390 <-- 65536 391 --> .i32_i32(42) 392 <-- 42 393 <== 42 394 <-- 42 395 `, 396 }, 397 { 398 name: "invoke_iii", 399 funcName: "call_i32i32_i32", 400 tableOffset: 4, 401 params: []uint64{1, 2}, 402 expectedResults: []uint64{3}, 403 expectedLog: `--> .call_i32i32_i32(4,1,2) 404 ==> env.invoke_iii(index=4,a1=1,a2=2) 405 --> .stackSave() 406 <-- 65536 407 --> .i32i32_i32(1,2) 408 <-- 3 409 <== 3 410 <-- 3 411 `, 412 }, 413 { 414 name: "invoke_iiii", 415 funcName: "call_i32i32i32_i32", 416 tableOffset: 6, 417 params: []uint64{1, 2, 4}, 418 expectedResults: []uint64{7}, 419 expectedLog: `--> .call_i32i32i32_i32(6,1,2,4) 420 ==> env.invoke_iiii(index=6,a1=1,a2=2,a3=4) 421 --> .stackSave() 422 <-- 65536 423 --> .i32i32i32_i32(1,2,4) 424 <-- 7 425 <== 7 426 <-- 7 427 `, 428 }, 429 { 430 name: "invoke_iiiii", 431 funcName: "calli32_i32i32i32i32_i32", 432 tableOffset: 8, 433 params: []uint64{1, 2, 4, 8}, 434 expectedResults: []uint64{15}, 435 expectedLog: `--> .calli32_i32i32i32i32_i32(8,1,2,4,8) 436 ==> env.invoke_iiiii(index=8,a1=1,a2=2,a3=4,a4=8) 437 --> .stackSave() 438 <-- 65536 439 --> .i32i32i32i32_i32(1,2,4,8) 440 <-- 15 441 <== 15 442 <-- 15 443 `, 444 }, 445 { 446 name: "invoke_v", 447 funcName: "call_v_v", 448 tableOffset: 10, 449 expectedLog: `--> .call_v_v(10) 450 ==> env.invoke_v(index=10) 451 --> .stackSave() 452 <-- 65536 453 --> .v_v() 454 <-- 455 <== 456 <-- 457 `, 458 }, 459 { 460 name: "invoke_vi", 461 funcName: "call_i32_v", 462 tableOffset: 12, 463 params: []uint64{42}, 464 expectedLog: `--> .call_i32_v(12,42) 465 ==> env.invoke_vi(index=12,a1=42) 466 --> .stackSave() 467 <-- 65536 468 --> .i32_v(42) 469 <-- 470 <== 471 <-- 472 `, 473 }, 474 { 475 name: "invoke_vii", 476 funcName: "call_i32i32_v", 477 tableOffset: 14, 478 params: []uint64{1, 2}, 479 expectedLog: `--> .call_i32i32_v(14,1,2) 480 ==> env.invoke_vii(index=14,a1=1,a2=2) 481 --> .stackSave() 482 <-- 65536 483 --> .i32i32_v(1,2) 484 <-- 485 <== 486 <-- 487 `, 488 }, 489 { 490 name: "invoke_viii", 491 funcName: "call_i32i32i32_v", 492 tableOffset: 16, 493 params: []uint64{1, 2, 4}, 494 expectedLog: `--> .call_i32i32i32_v(16,1,2,4) 495 ==> env.invoke_viii(index=16,a1=1,a2=2,a3=4) 496 --> .stackSave() 497 <-- 65536 498 --> .i32i32i32_v(1,2,4) 499 <-- 500 <== 501 <-- 502 `, 503 }, 504 { 505 name: "invoke_viiii", 506 funcName: "calli32_i32i32i32i32_v", 507 tableOffset: 18, 508 params: []uint64{1, 2, 4, 8}, 509 expectedLog: `--> .calli32_i32i32i32i32_v(18,1,2,4,8) 510 ==> env.invoke_viiii(index=18,a1=1,a2=2,a3=4,a4=8) 511 --> .stackSave() 512 <-- 65536 513 --> .i32i32i32i32_v(1,2,4,8) 514 <-- 515 <== 516 <-- 517 `, 518 }, 519 { 520 name: "invoke_v_with_longjmp", 521 funcName: "call_invoke_v_with_longjmp_throw", 522 tableOffset: 20, 523 params: []uint64{}, 524 expectedLog: `--> .call_invoke_v_with_longjmp_throw(20) 525 ==> env.invoke_v(index=20) 526 --> .stackSave() 527 <-- 42 528 --> .call_longjmp_throw() 529 ==> env._emscripten_throw_longjmp() 530 --> .stackRestore(42) 531 <-- 532 --> .setThrew(1,0) 533 <-- 534 <== 535 <-- 536 `, 537 }, 538 } 539 540 for _, tt := range tests { 541 tc := tt 542 543 t.Run(tc.name, func(t *testing.T) { 544 defer log.Reset() 545 546 params := tc.params 547 params = append([]uint64{uint64(tc.tableOffset)}, params...) 548 549 results, err := mod.ExportedFunction(tc.funcName).Call(testCtx, params...) 550 require.NoError(t, err) 551 require.Equal(t, tc.expectedResults, results) 552 553 // We expect to see the dynamic function call target 554 require.Equal(t, tc.expectedLog, log.String()) 555 556 // We expect an unreachable function to err 557 params[0]++ 558 _, err = mod.ExportedFunction(tc.funcName).Call(testCtx, params...) 559 require.Error(t, err) 560 }) 561 } 562 }