diff --git a/bom/pom.xml b/bom/pom.xml index c98a8d3b9..aad4e3126 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -59,6 +59,11 @@ runtime ${project.version} + + com.dylibso.chicory + rustc-demangle + ${project.version} + com.dylibso.chicory simd diff --git a/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt b/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt index 9d155aa8a..caaf849b5 100644 --- a/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt +++ b/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt @@ -3,6 +3,8 @@ private final Lcom/dylibso/chicory/runtime/Instance; instance private final Lcom/dylibso/chicory/runtime/internal/CompilerInterpreterMachine; compilerInterpreterMachine } + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + private final static Z memCopyWorkaround } diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Emitters.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Emitters.java index fc12e592c..0138239f2 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Emitters.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Emitters.java @@ -69,6 +69,8 @@ public static Builder builder() { } public static void TRAP(Context ctx, CompilerInstruction ins, InstructionAdapter asm) { + asm.load(ctx.instanceSlot(), OBJECT_TYPE); + asm.visitLdcInsn(ctx.internalClassName()); emitInvokeStatic(asm, ShadedRefs.THROW_TRAP_EXCEPTION); asm.athrow(); } diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java index 4b02b2061..9085ae96e 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java @@ -13,6 +13,7 @@ import com.dylibso.chicory.wasm.ChicoryException; import com.dylibso.chicory.wasm.InvalidException; import com.dylibso.chicory.wasm.types.FunctionType; +import java.util.Arrays; /** * This class will get shaded into the compiled code. @@ -183,8 +184,34 @@ public static RuntimeException throwOutOfBoundsMemoryAccess() { throw new WasmRuntimeException("out of bounds memory access"); } - public static RuntimeException throwTrapException() { - throw new TrapException("Trapped on unreachable instruction"); + public static RuntimeException throwTrapException(Instance instance, String internalClassName) { + StackTraceElement[] trace = new Throwable().getStackTrace(); + var originalClassName = internalClassName.replace('/', '.'); + + StackTraceElement[] filtered = + Arrays.stream(trace) + .filter( + stackTraceElement -> + stackTraceElement + .getClassName() + .startsWith(originalClassName) + && stackTraceElement + .getMethodName() + .startsWith("func_")) + .flatMap( + stackTraceElement -> { + var funcId = + Integer.parseInt( + stackTraceElement + .getMethodName() + .replace("func_", "")); + + return instance.computeStackFrame(funcId).stream(); + }) + .toArray(StackTraceElement[]::new); + + throw new TrapException( + "Trapped on unreachable instruction in: " + originalClassName, filtered); } public static RuntimeException throwUnknownFunction(int index) { diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java index 8973b7e6e..9f18d3e5c 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java @@ -155,7 +155,8 @@ public final class ShadedRefs { Shaded.class.getMethod("throwIndirectCallTypeMismatch"); THROW_OUT_OF_BOUNDS_MEMORY_ACCESS = Shaded.class.getMethod("throwOutOfBoundsMemoryAccess"); - THROW_TRAP_EXCEPTION = Shaded.class.getMethod("throwTrapException"); + THROW_TRAP_EXCEPTION = + Shaded.class.getMethod("throwTrapException", Instance.class, String.class); THROW_UNKNOWN_FUNCTION = Shaded.class.getMethod("throwUnknownFunction", int.class); AOT_INTERPRETER_MACHINE_CALL = diff --git a/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyI32Renamed.approved.txt b/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyI32Renamed.approved.txt index d23140ddd..abd04097b 100644 --- a/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyI32Renamed.approved.txt +++ b/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyI32Renamed.approved.txt @@ -69,6 +69,8 @@ public final class FOO implements com/dylibso/chicory/runtime/Machine { final class FOOShaded { + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + private final static Z memCopyWorkaround private ()V @@ -473,13 +475,63 @@ final class FOOShaded { INVOKESPECIAL com/dylibso/chicory/runtime/WasmRuntimeException. (Ljava/lang/String;)V ATHROW - public static throwTrapException()Ljava/lang/RuntimeException; + public static throwTrapException(Lcom/dylibso/chicory/runtime/Instance;Ljava/lang/String;)Ljava/lang/RuntimeException; L0 + NEW java/lang/Throwable + DUP + INVOKESPECIAL java/lang/Throwable. ()V + INVOKEVIRTUAL java/lang/Throwable.getStackTrace ()[Ljava/lang/StackTraceElement; + ASTORE 2 + L1 + ALOAD 1 + BIPUSH 47 + BIPUSH 46 + INVOKEVIRTUAL java/lang/String.replace (CC)Ljava/lang/String; + ASTORE 3 + L2 + ALOAD 2 + L3 + INVOKESTATIC java/util/Arrays.stream ([Ljava/lang/Object;)Ljava/util/stream/Stream; + ALOAD 3 + INVOKEDYNAMIC test(Ljava/lang/String;)Ljava/util/function/Predicate; [ + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + (Ljava/lang/Object;)Z, + FOOShaded.lambda$throwTrapException$0(Ljava/lang/String;Ljava/lang/StackTraceElement;)Z, + (Ljava/lang/StackTraceElement;)Z + ] + L4 + INVOKEINTERFACE java/util/stream/Stream.filter (Ljava/util/function/Predicate;)Ljava/util/stream/Stream; (itf) + ALOAD 0 + INVOKEDYNAMIC apply(Lcom/dylibso/chicory/runtime/Instance;)Ljava/util/function/Function; [ + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + (Ljava/lang/Object;)Ljava/lang/Object;, + FOOShaded.lambda$throwTrapException$1(Lcom/dylibso/chicory/runtime/Instance;Ljava/lang/StackTraceElement;)Ljava/util/stream/Stream;, + (Ljava/lang/StackTraceElement;)Ljava/util/stream/Stream; + ] + L5 + INVOKEINTERFACE java/util/stream/Stream.flatMap (Ljava/util/function/Function;)Ljava/util/stream/Stream; (itf) + INVOKEDYNAMIC apply()Ljava/util/function/IntFunction; [ + java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; + (I)Ljava/lang/Object;, + FOOShaded.lambda$throwTrapException$2(I)[Ljava/lang/StackTraceElement;, + (I)[Ljava/lang/StackTraceElement; + ] + L6 + INVOKEINTERFACE java/util/stream/Stream.toArray (Ljava/util/function/IntFunction;)[Ljava/lang/Object; (itf) + CHECKCAST [Ljava/lang/StackTraceElement; + ASTORE 4 + L7 NEW com/dylibso/chicory/runtime/TrapException DUP - LDC "Trapped on unreachable instruction" - INVOKESPECIAL com/dylibso/chicory/runtime/TrapException. (Ljava/lang/String;)V + ALOAD 3 + INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [ + java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; + "Trapped on unreachable instruction in: \u0001" + ] + ALOAD 4 + INVOKESPECIAL com/dylibso/chicory/runtime/TrapException. (Ljava/lang/String;[Ljava/lang/StackTraceElement;)V ATHROW + L8 public static throwUnknownFunction(I)Ljava/lang/RuntimeException; L0 @@ -532,6 +584,57 @@ final class FOOShaded { RETURN L2 + private static synthetic lambda$throwTrapException$2(I)[Ljava/lang/StackTraceElement; + L0 + ILOAD 0 + ANEWARRAY java/lang/StackTraceElement + ARETURN + L1 + + private static synthetic lambda$throwTrapException$1(Lcom/dylibso/chicory/runtime/Instance;Ljava/lang/StackTraceElement;)Ljava/util/stream/Stream; + L0 + ALOAD 1 + L1 + INVOKEVIRTUAL java/lang/StackTraceElement.getMethodName ()Ljava/lang/String; + LDC "func_" + LDC "" + L2 + INVOKEVIRTUAL java/lang/String.replace (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String; + L3 + INVOKESTATIC java/lang/Integer.parseInt (Ljava/lang/String;)I + ISTORE 2 + L4 + ALOAD 0 + ILOAD 2 + INVOKEVIRTUAL com/dylibso/chicory/runtime/Instance.computeStackFrame (I)Ljava/util/List; + INVOKEINTERFACE java/util/List.stream ()Ljava/util/stream/Stream; (itf) + ARETURN + L5 + + private static synthetic lambda$throwTrapException$0(Ljava/lang/String;Ljava/lang/StackTraceElement;)Z + L0 + ALOAD 1 + L1 + INVOKEVIRTUAL java/lang/StackTraceElement.getClassName ()Ljava/lang/String; + ALOAD 0 + L2 + INVOKEVIRTUAL java/lang/String.startsWith (Ljava/lang/String;)Z + IFEQ L3 + ALOAD 1 + L4 + INVOKEVIRTUAL java/lang/StackTraceElement.getMethodName ()Ljava/lang/String; + LDC "func_" + L5 + INVOKEVIRTUAL java/lang/String.startsWith (Ljava/lang/String;)Z + IFEQ L3 + ICONST_1 + GOTO L6 + L3 + ICONST_0 + L6 + IRETURN + L7 + static ()V L0 LDC "chicory.memCopyWorkaround" diff --git a/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyTrap.approved.txt b/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyTrap.approved.txt index 9eeb822f4..dcbd76a85 100644 --- a/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyTrap.approved.txt +++ b/compiler/src/test/resources/com/dylibso/chicory/approvals/ApprovalTest.verifyTrap.approved.txt @@ -78,7 +78,9 @@ public final class com/dylibso/chicory/$gen/CompiledMachine implements com/dylib final class com/dylibso/chicory/$gen/CompiledMachineFuncGroup_0 { public static func_0(Lcom/dylibso/chicory/runtime/Memory;Lcom/dylibso/chicory/runtime/Instance;)V - INVOKESTATIC com/dylibso/chicory/$gen/CompiledMachineShaded.throwTrapException ()Ljava/lang/RuntimeException; + ALOAD 1 + LDC "com/dylibso/chicory/$gen/CompiledMachine" + INVOKESTATIC com/dylibso/chicory/$gen/CompiledMachineShaded.throwTrapException (Lcom/dylibso/chicory/runtime/Instance;Ljava/lang/String;)Ljava/lang/RuntimeException; ATHROW L0 ATHROW diff --git a/machine-tests/pom.xml b/machine-tests/pom.xml index 3825c1129..324831dcd 100644 --- a/machine-tests/pom.xml +++ b/machine-tests/pom.xml @@ -26,6 +26,11 @@ compiler test + + com.dylibso.chicory + rustc-demangle + test + com.dylibso.chicory wasi @@ -84,6 +89,16 @@ ${project.basedir}/../wabt/src/main/resources/wat2wasm + + count_vowels + + compile + + + com.dylibso.chicory.testing.CountVowels + ${project.basedir}/../wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm + + diff --git a/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java b/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java index fc1d09b89..edac2452d 100644 --- a/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java +++ b/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.dylibso.chicory.compiler.MachineFactoryCompiler; +import com.dylibso.chicory.demangle.Demangler; import com.dylibso.chicory.runtime.ImportTable; import com.dylibso.chicory.runtime.ImportValues; import com.dylibso.chicory.runtime.Instance; @@ -32,11 +33,14 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; import org.junit.jupiter.api.Test; public final class MachinesTest { @@ -225,7 +229,7 @@ public void shouldUseMachineCallOnlyForExport() throws Exception { } @Test - public void shouldCallIndirectInterpreterToAot() { + public void shouldCallIndirectInterpreterToCompiler() { var store = new Store(); var table = new TableInstance( @@ -242,6 +246,7 @@ public void shouldCallIndirectInterpreterToAot() { Instance.builder(loadModule("compiled/call_indirect-import.wat.wasm")) .withImportValues(store.toImportValues()) .withMachineFactory(MachineFactoryCompiler::compile) + .withExceptionConverter(demanglerExceptionConverter("wasm-compiled-module")) .build(); assertEquals(42, instance.export("call-self").apply()[0]); @@ -249,11 +254,11 @@ public void shouldCallIndirectInterpreterToAot() { var ex = assertThrows(TrapException.class, instance.export("call-other-fail")::apply); var className = ex.getStackTrace()[0].getClassName(); - assertTrue(className.contains("CompiledMachine"), className); + assertTrue(className.contains("wasm-compiled-module"), className); } @Test - public void shouldCallIndirectAotToInterpreter() { + public void shouldCallIndirectCompilerToInterpreter() { var store = new Store(); var table = new TableInstance( @@ -270,6 +275,7 @@ public void shouldCallIndirectAotToInterpreter() { Instance.builder(loadModule("compiled/call_indirect-import.wat.wasm")) .withImportValues(store.toImportValues()) .withMachineFactory(InterpreterMachine::new) + .withExceptionConverter(demanglerExceptionConverter("wasm-interpreted-module")) .build(); assertEquals(42, instance.export("call-self").apply()[0]); @@ -277,6 +283,88 @@ public void shouldCallIndirectAotToInterpreter() { var ex = assertThrows(TrapException.class, instance.export("call-other-fail")::apply); var className = ex.getStackTrace()[0].getClassName(); - assertTrue(className.contains("InterpreterMachine"), className); + assertTrue(className.contains("wasm-interpreted-module"), className); + } + + private String readStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + private static List demanglerExceptionConverter( + Instance instance, int funcIdx) { + return demanglerExceptionConverter("wasm-module").apply(instance, funcIdx); + } + + private static BiFunction> + demanglerExceptionConverter(String fallbackModuleName) { + return (instance, funcIdx) -> { + var moduleName = instance.module().moduleName().orElse(fallbackModuleName); + var funcName = + instance.module() + .functionName(funcIdx) + .map(str -> Demangler.demangle(str)) + .orElse("function[" + funcIdx + "]"); + return List.of(new StackTraceElement(moduleName, funcName, null, -1)); + }; + } + + private static BiFunction> + demanglerExceptionConverter(WasmModule debugModule) { + return (instance, funcIdx) -> { + var moduleName = debugModule.moduleName().orElse("wasm-module"); + var funcName = + debugModule + .functionName(funcIdx) + .map(str -> Demangler.demangle(str)) + .orElse("function[" + funcIdx + "]"); + return List.of(new StackTraceElement(moduleName, funcName, null, -1)); + }; + } + + @Test + public void shouldEmitUnderstandableStackTracesWithInterpreter() throws Exception { + var instance = + Instance.builder(loadModule("compiled/count_vowels.rs.wasm")) + .withExceptionConverter(MachinesTest::demanglerExceptionConverter) + .build(); + var countVowels = instance.export("count_vowels"); + var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1)); + var exceptionTxt = readStackTrace(exception); + + assertTrue(exceptionTxt.contains("count_vowels.wasm.count_vowels")); + assertTrue(exceptionTxt.contains("count_vowels.wasm.std::panicking::rust_panic_with_hook")); + } + + @Test + public void shouldEmitUnderstandableStackTracesRuntimeCompiled() throws Exception { + var instance = + Instance.builder(loadModule("compiled/count_vowels.rs.wasm")) + .withMachineFactory(MachineFactoryCompiler::compile) + .withExceptionConverter(MachinesTest::demanglerExceptionConverter) + .build(); + var countVowels = instance.export("count_vowels"); + var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1)); + var exceptionTxt = readStackTrace(exception); + + assertTrue(exceptionTxt.contains("count_vowels.wasm.count_vowels")); + assertTrue(exceptionTxt.contains("count_vowels.wasm.std::panicking::rust_panic_with_hook")); + } + + @Test + public void shouldEmitUnderstandableStackTracesBuildTimeCompiled() throws Exception { + var debugModule = loadModule("compiled/count_vowels.rs.wasm"); + var instance = + Instance.builder(CountVowels.load()) + .withMachineFactory(CountVowels::create) + .withExceptionConverter(demanglerExceptionConverter(debugModule)) + .build(); + var countVowels = instance.export("count_vowels"); + var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1)); + var exceptionTxt = readStackTrace(exception); + + assertTrue(exceptionTxt.contains("count_vowels.wasm.count_vowels")); + assertTrue(exceptionTxt.contains("count_vowels.wasm.std::panicking::rust_panic_with_hook")); } } diff --git a/pom.xml b/pom.xml index 7b3cbb668..442451db1 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,11 @@ runtime ${project.version} + + com.dylibso.chicory + rustc-demangle + ${project.version} + com.dylibso.chicory simd @@ -882,6 +887,7 @@ machine-tests runtime runtime-tests + rustc-demangle test-gen-lib test-gen-plugin wabt @@ -912,6 +918,7 @@ compiler-maven-plugin log runtime + rustc-demangle wabt wasi wasm diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java index 693ec5324..866be4110 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java @@ -44,6 +44,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -67,6 +69,9 @@ public class Instance { private final ExecutionListener listener; private final Exports fluentExports; + private final Optional>> + exceptionConverter; + private final Map exnRefs; Instance( @@ -85,8 +90,10 @@ public class Instance { Function machineFactory, boolean initialize, boolean start, - ExecutionListener listener) { + ExecutionListener listener, + Optional>> exceptionConverter) { this.module = module; + this.exceptionConverter = exceptionConverter; this.globalInitializers = globalInitializers.clone(); this.globals = new GlobalInstance[globalInitializers.length]; this.memory = memory; @@ -334,6 +341,14 @@ public WasmException exn(int idx) { return exnRefs.get(idx); } + public List computeStackFrame(int funcIdx) { + if (exceptionConverter.isPresent()) { + return exceptionConverter.get().apply(this, funcIdx); + } else { + return List.of(); + } + } + public Machine getMachine() { return machine; } @@ -358,6 +373,7 @@ public static final class Builder { private ExecutionListener listener; private ImportValues importValues; private Function machineFactory; + private BiFunction> exceptionConverter; private Builder(WasmModule module) { this.module = Objects.requireNonNull(module); @@ -401,6 +417,26 @@ public Builder withMachineFactory(Function machineFactory) { return this; } + public Builder withExceptionConverter( + BiFunction> exceptionConverter) { + this.exceptionConverter = exceptionConverter; + return this; + } + + public Builder withDefaultExceptionConverter() { + this.exceptionConverter = + (inst, funcIdx) -> { + var funcName = + inst.module() + .functionName(funcIdx) + .orElse("function[" + funcIdx + "]"); + var moduleName = inst.module().moduleName().orElse("wasm-module"); + + return List.of(new StackTraceElement(moduleName, funcName, null, -1)); + }; + return this; + } + private boolean checkExternalFunctionSignature(FunctionImport imprt, ImportFunction f) { try { validateExternalFunctionSignature(imprt, f); @@ -884,7 +920,8 @@ public Instance build() { machineFactory, initialize, start, - listener); + listener, + Optional.ofNullable(exceptionConverter)); } } } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java index 575a842cf..23a4d0f71 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java @@ -13,6 +13,7 @@ import com.dylibso.chicory.wasm.types.ValType; import com.dylibso.chicory.wasm.types.Value; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.List; @@ -168,7 +169,23 @@ protected void eval(MStack stack, Instance instance, Deque callStack instance.onExecution(instruction, stack); switch (opcode) { case UNREACHABLE: - throw new TrapException("Trapped on unreachable instruction"); + { + List elements = new ArrayList<>(); + int last = -1; + while (frame != null) { + while (frame != null && frame.ctrlStackSize() > 0) { + if (frame.funcId() != last) { + elements.addAll(instance.computeStackFrame(frame.funcId())); + last = frame.funcId(); + } + frame = callStack.isEmpty() ? null : callStack.pop(); + } + } + + throw new TrapException( + "Trapped on unreachable instruction in InterpreterMachine", + elements.toArray(StackTraceElement[]::new)); + } case NOP: break; case LOOP: diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java b/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java index a7155687b..9571707b0 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java @@ -6,4 +6,9 @@ public class TrapException extends ChicoryException { public TrapException(String msg) { super(msg); } + + public TrapException(String msg, StackTraceElement[] elements) { + super(msg); + setStackTrace(elements); + } } diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java index 68bbe87b6..6dd387f17 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/WasmModuleTest.java @@ -20,6 +20,8 @@ import com.dylibso.chicory.wasm.types.TagType; import com.dylibso.chicory.wasm.types.ValType; import com.dylibso.chicory.wasm.types.Value; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -565,4 +567,26 @@ public void timeoutExecution() throws Exception { var future = service.submit(() -> function.apply()); assertThrows(TimeoutException.class, () -> future.get(100, TimeUnit.MILLISECONDS)); } + + private String readStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + @Test + public void shouldEmitUnderstandableStackTraces() throws Exception { + var instance = + Instance.builder(loadModule("compiled/count_vowels.rs.wasm")) + .withDefaultExceptionConverter() + .build(); + var countVowels = instance.export("count_vowels"); + var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1)); + var exceptionTxt = readStackTrace(exception); + + System.out.println(); + + assertTrue(exceptionTxt.contains("count_vowels.wasm.count_vowels")); + assertTrue(exceptionTxt.contains("rust_panic")); + } } diff --git a/rustc-demangle/pom.xml b/rustc-demangle/pom.xml new file mode 100644 index 000000000..4f0cdb737 --- /dev/null +++ b/rustc-demangle/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + com.dylibso.chicory + chicory + 999-SNAPSHOT + ../pom.xml + + rustc-demangle + jar + Chicory - RustC Demangle + Chicory - Rustc demangle + + + + com.dylibso.chicory + annotations + + + com.dylibso.chicory + runtime + + + com.dylibso.chicory + wasm + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + + + + com.dylibso.chicory + chicory-compiler-maven-plugin + + + compile-rustc-demangle + + compile + + + com.dylibso.chicory.demangle.RustCDemangle + src/main/resources/rustc_demangle.wasm + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.dylibso.chicory + annotations-processor + ${project.version} + + + + + + + diff --git a/rustc-demangle/src/main/java/com/dylibso/chicory/demangle/Demangler.java b/rustc-demangle/src/main/java/com/dylibso/chicory/demangle/Demangler.java new file mode 100644 index 000000000..6ab9cfa50 --- /dev/null +++ b/rustc-demangle/src/main/java/com/dylibso/chicory/demangle/Demangler.java @@ -0,0 +1,63 @@ +package com.dylibso.chicory.demangle; + +import com.dylibso.chicory.annotations.WasmModuleInterface; +import com.dylibso.chicory.runtime.ByteArrayMemory; +import com.dylibso.chicory.runtime.Instance; + +@WasmModuleInterface("rustc_demangle.wasm") +public final class Demangler { + + private static final Instance INSTANCE = + Instance.builder(RustCDemangle.load()) + .withMemoryFactory(ByteArrayMemory::new) + .withMachineFactory(RustCDemangle::create) + .build(); + private static final Demangler_ModuleExports exports = new Demangler_ModuleExports(INSTANCE); + + private Demangler() {} + + // Demangles symbol given in `mangled` argument into `out` buffer + // + // Returns 0 if `mangled` is not Rust symbol or if `out` buffer is too small + // Returns 1 otherwise + // int rustc_demangle(const char *mangled, char *out, size_t out_size); + private static String rustcDemangle(String mangled) { + int mangledPtr = 0; + int mangledSize = mangled.length() + 1; + int outPtr = 0; + int outSize = 0; + + try { + mangledPtr = exports.alloc(mangledSize); + exports.memory().writeCString(mangledPtr, mangled); + + outSize = exports.rustcDemangleLen(mangledPtr); + // Not a Rust symbol + if (outSize == 0) { + return mangled; + } else { + outPtr = exports.alloc(outSize); + var result = exports.rustcDemangle(mangledPtr, outPtr, outSize); + if (result == 0) { + throw new RuntimeException( + "Failed to demangle '" + + mangled + + "' is not Rust symbol or the out buffer is too small"); + } else { + return exports.memory().readCString(outPtr); + } + } + } finally { + if (mangledPtr != 0) { + exports.dealloc(mangledPtr, mangledSize); + } + if (outPtr != 0 && outSize != 0) { + exports.dealloc(outPtr, outSize); + } + } + } + + public static String demangle(String str) { + return rustcDemangle(str); + } +} diff --git a/rustc-demangle/src/main/resources/rustc_demangle.wasm b/rustc-demangle/src/main/resources/rustc_demangle.wasm new file mode 100755 index 000000000..8689dccf5 Binary files /dev/null and b/rustc-demangle/src/main/resources/rustc_demangle.wasm differ diff --git a/rustc-demangle/src/test/java/com/dylibso/chicory/demangle/DemanglerTest.java b/rustc-demangle/src/test/java/com/dylibso/chicory/demangle/DemanglerTest.java new file mode 100644 index 000000000..26d3ece8b --- /dev/null +++ b/rustc-demangle/src/test/java/com/dylibso/chicory/demangle/DemanglerTest.java @@ -0,0 +1,17 @@ +package com.dylibso.chicory.demangle; + +import static com.dylibso.chicory.demangle.Demangler.demangle; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class DemanglerTest { + + @Test + public void basicDemanglingExamples() { + // https://docs.rs/rustc-demangle/latest/rustc_demangle/#examples + assertEquals("test", demangle("_ZN4testE")); + assertEquals("foo::bar", demangle("_ZN3foo3barE")); + assertEquals("foo", demangle("foo")); + } +} diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/WasmModule.java b/wasm/src/main/java/com/dylibso/chicory/wasm/WasmModule.java index 601ed8fe9..1024c3823 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/WasmModule.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/WasmModule.java @@ -143,6 +143,31 @@ public List ignoredSections() { return ignoredSections; } + // Helper functions + public Optional functionName(int idx) { + if (idx < importSection.importCount()) { + return Optional.of(importSection.getImport(idx).name()); + } else { + var customSection = customSections.get("name"); + if (customSection != null && customSection instanceof NameCustomSection) { + var nameCustomSection = (NameCustomSection) customSection; + var originalName = nameCustomSection.nameOfFunction(idx); + return Optional.ofNullable(originalName); + } + return Optional.empty(); + } + } + + public Optional moduleName() { + var customSection = customSections.get("name"); + if (customSection != null && customSection instanceof NameCustomSection) { + var nameCustomSection = (NameCustomSection) customSection; + return nameCustomSection.moduleName(); + } else { + return Optional.empty(); + } + } + public static Builder builder() { return new Builder(); }