|
46 | 46 | import java.nio.ByteBuffer; |
47 | 47 | import java.nio.ByteOrder; |
48 | 48 | import java.util.Arrays; |
| 49 | +import java.util.EnumSet; |
49 | 50 | import java.util.HashMap; |
50 | 51 | import java.util.Objects; |
51 | 52 | import java.util.concurrent.Callable; |
|
79 | 80 | import org.graalvm.wasm.constants.Sizes; |
80 | 81 | import org.graalvm.wasm.exception.WasmException; |
81 | 82 | import org.graalvm.wasm.exception.WasmJsApiException; |
| 83 | +import org.graalvm.wasm.exception.WasmRuntimeException; |
82 | 84 | import org.graalvm.wasm.globals.WasmGlobal; |
83 | 85 | import org.graalvm.wasm.memory.WasmMemory; |
84 | 86 | import org.graalvm.wasm.memory.WasmMemoryLibrary; |
| 87 | +import org.graalvm.wasm.utils.WasmBinaryTools; |
85 | 88 | import org.junit.Assert; |
86 | 89 | import org.junit.Test; |
87 | 90 |
|
|
101 | 104 |
|
102 | 105 | public class WasmJsApiSuite { |
103 | 106 | private static final String REF_TYPES_OPTION = "wasm.BulkMemoryAndRefTypes"; |
| 107 | + private static final String EXCEPTIONS_OPTION = "wasm.Exceptions"; |
104 | 108 |
|
105 | 109 | private static WasmFunctionInstance createWasmFunctionInstance(WasmContext context, int[] paramTypes, int[] resultTypes, RootNode functionRootNode) { |
106 | 110 | WasmModule module = WasmModule.createBuiltin("dummyModule"); |
@@ -2514,6 +2518,121 @@ public void testUncachedIndirectCallException() throws IOException, InterruptedE |
2514 | 2518 | }); |
2515 | 2519 | } |
2516 | 2520 |
|
| 2521 | + /** |
| 2522 | + * Tests that an exception thrown by WebAssembly can be caught in the embedder. Checks that the |
| 2523 | + * correct tag and exception fields are present. |
| 2524 | + */ |
| 2525 | + @Test |
| 2526 | + public void testExceptionFromInsideWasm() throws IOException, InterruptedException { |
| 2527 | + final byte[] binary = compileWat("exceptions-inside", """ |
| 2528 | + (module |
| 2529 | + (tag $E (export "E") (param i32 externref)) |
| 2530 | + (func (export "raise") (param i32 externref) |
| 2531 | + (throw $E (local.get 0) (local.get 1)) |
| 2532 | + ) |
| 2533 | + )""", EnumSet.of(WasmBinaryTools.WabtOption.EXCEPTIONS)); |
| 2534 | + runTest(builder -> builder.option(EXCEPTIONS_OPTION, "true"), context -> { |
| 2535 | + final WebAssembly wasm = new WebAssembly(context); |
| 2536 | + final WasmInstance instance = moduleInstantiate(wasm, binary, null); |
| 2537 | + final Object raise = WebAssembly.instanceExport(instance, "raise"); |
| 2538 | + final Object tag = WebAssembly.instanceExport(instance, "E"); |
| 2539 | + |
| 2540 | + final Object extern = "foo"; |
| 2541 | + |
| 2542 | + try { |
| 2543 | + try { |
| 2544 | + // Trigger the exception with fields: 7 (i32) and extern (externref) |
| 2545 | + InteropLibrary.getUncached(raise).execute(raise, 7, extern); |
| 2546 | + Assert.fail("Expected a WebAssembly exception but none was thrown."); |
| 2547 | + } catch (WasmRuntimeException ex) { |
| 2548 | + InteropLibrary exInterop = InteropLibrary.getUncached(ex); |
| 2549 | + // Verify it's an interop exception |
| 2550 | + Assert.assertTrue("Should be an interop exception", exInterop.isException(ex)); |
| 2551 | + |
| 2552 | + // Verify the tag |
| 2553 | + final Object exnTag = WebAssembly.exnTag(new Object[]{ex}); |
| 2554 | + Assert.assertEquals("Unexpected exception tag", tag, exnTag); |
| 2555 | + |
| 2556 | + // Verify the fields |
| 2557 | + Assert.assertTrue("Wasm exception should have fields as array elements", exInterop.hasArrayElements(ex)); |
| 2558 | + Assert.assertEquals("Expected 2 exception fields", 2, exInterop.getArraySize(ex)); |
| 2559 | + Assert.assertEquals("First field mismatch", 7, InteropLibrary.getUncached().asInt(exInterop.readArrayElement(ex, 0))); |
| 2560 | + Assert.assertSame("Second field mismatch", extern, exInterop.readArrayElement(ex, 1)); |
| 2561 | + } |
| 2562 | + } catch (InteropException e) { |
| 2563 | + throw new AssertionError(e); |
| 2564 | + } |
| 2565 | + }); |
| 2566 | + } |
| 2567 | + |
| 2568 | + /** |
| 2569 | + * Tests that GraalWasm can catch wasm exceptions instantiated and thrown by the host. Also |
| 2570 | + * checks that the correct tag and exception fields are present. |
| 2571 | + */ |
| 2572 | + @Test |
| 2573 | + public void testExceptionFromOutsideWasm() throws IOException, InterruptedException { |
| 2574 | + // 1) Module that defines and exports the tag. |
| 2575 | + final byte[] tagModule = compileWat("exceptions-tag", """ |
| 2576 | + (module |
| 2577 | + (tag $E (export "E") (param i32 externref)) |
| 2578 | + )""", EnumSet.of(WasmBinaryTools.WabtOption.EXCEPTIONS)); |
| 2579 | + |
| 2580 | + // 2) Module that imports the tag and a host function. It calls the host function |
| 2581 | + // and catches exceptions with the imported tag, returning the exception fields. |
| 2582 | + final byte[] consumerModule = compileWat("exceptions-outside-consumer", """ |
| 2583 | + (module |
| 2584 | + (type $host_t (func)) |
| 2585 | + (tag $E (import "m" "E") (param i32 externref)) |
| 2586 | + (func $host (import "m" "host") (type $host_t)) |
| 2587 | + (func (export "callAndHandle") (result i32 externref) |
| 2588 | + (block $h (result i32 externref) |
| 2589 | + (try_table (result i32 externref) (catch $E $h) |
| 2590 | + (call $host) |
| 2591 | + ;; In case the host does not throw, produce dummy values (won't be reached in this test). |
| 2592 | + (i32.const -1) |
| 2593 | + (ref.null extern) |
| 2594 | + ) |
| 2595 | + ) |
| 2596 | + ) |
| 2597 | + )""", EnumSet.of(WasmBinaryTools.WabtOption.EXCEPTIONS)); |
| 2598 | + |
| 2599 | + runTest(builder -> builder.option(EXCEPTIONS_OPTION, "true"), context -> { |
| 2600 | + final WebAssembly wasm = new WebAssembly(context); |
| 2601 | + |
| 2602 | + // Instantiate the tag-defining module and obtain the tag object. |
| 2603 | + final WasmInstance tagModuleInstance = moduleInstantiate(wasm, tagModule, null); |
| 2604 | + final Object tag = WebAssembly.instanceExport(tagModuleInstance, "E"); |
| 2605 | + |
| 2606 | + final Object extern = "foo"; |
| 2607 | + |
| 2608 | + // Import object providing the tag and the throwing host callback. |
| 2609 | + final Dictionary importObject = Dictionary.create(new Object[]{ |
| 2610 | + "m", Dictionary.create(new Object[]{ |
| 2611 | + "E", tag, |
| 2612 | + "host", new Executable(args -> { |
| 2613 | + throw wasm.exnAlloc(new Object[]{tag, 7, extern}); |
| 2614 | + }) |
| 2615 | + }), |
| 2616 | + }); |
| 2617 | + |
| 2618 | + // Instantiate the consumer and call its function that catches our exception. |
| 2619 | + final WasmInstance consumer = moduleInstantiate(wasm, consumerModule, importObject); |
| 2620 | + final Object callAndHandle = WebAssembly.instanceExport(consumer, "callAndHandle"); |
| 2621 | + try { |
| 2622 | + final InteropLibrary interop = InteropLibrary.getUncached(); |
| 2623 | + final Object result = interop.execute(callAndHandle); |
| 2624 | + |
| 2625 | + // The catch $E returns the fields; verify them. |
| 2626 | + Assert.assertTrue("Result must be a multi-value array", interop.hasArrayElements(result)); |
| 2627 | + Assert.assertEquals("Expected 2 results", 2, interop.getArraySize(result)); |
| 2628 | + Assert.assertEquals("First field mismatch", 7, interop.asInt(interop.readArrayElement(result, 0))); |
| 2629 | + Assert.assertSame("Second field mismatch", extern, interop.readArrayElement(result, 1)); |
| 2630 | + } catch (InteropException e) { |
| 2631 | + throw new AssertionError(e); |
| 2632 | + } |
| 2633 | + }); |
| 2634 | + } |
| 2635 | + |
2517 | 2636 | private static void runMemoryTest(Consumer<WasmContext> testCase) throws IOException { |
2518 | 2637 | runTest(null, testCase); |
2519 | 2638 | runTest(options -> options.option("wasm.UseUnsafeMemory", "true"), testCase); |
|
0 commit comments