diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java index d0cd0562d..8c8b83fa5 100644 --- a/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TThrowable.java @@ -19,9 +19,13 @@ import java.io.PrintStream; import java.io.PrintWriter; import org.teavm.classlib.PlatformDetector; import org.teavm.classlib.java.util.TArrays; +import org.teavm.interop.Import; import org.teavm.interop.Remove; import org.teavm.interop.Rename; import org.teavm.interop.Superclass; +import org.teavm.jso.JSIndexer; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; import org.teavm.runtime.ExceptionHandling; @Superclass("java.lang.Object") @@ -33,6 +37,7 @@ public class TThrowable extends RuntimeException { private boolean writableStackTrace; private TThrowable[] suppressed = new TThrowable[0]; private TStackTraceElement[] stackTrace; + private LazyStackSupplier lazyStackTrace; @SuppressWarnings("unused") @Rename("fakeInit") @@ -104,10 +109,35 @@ public class TThrowable extends RuntimeException { public Throwable fillInStackTrace() { if (PlatformDetector.isLowLevel()) { stackTrace = (TStackTraceElement[]) (Object) ExceptionHandling.fillStackTrace(); + } else if (PlatformDetector.isWebAssemblyGC()) { + lazyStackTrace = takeWasmGCStack(); } return this; } + private void ensureStackTrace() { + if (PlatformDetector.isWebAssemblyGC()) { + if (lazyStackTrace != null) { + var supplier = lazyStackTrace; + lazyStackTrace = null; + var nativeStack = supplier.getStack(); + if (nativeStack == null) { + return; + } + var stack = new TStackTraceElement[nativeStack.getLength()]; + for (var i = 0; i < nativeStack.getLength(); ++i) { + var frame = nativeStack.get(i); + stack[i] = new TStackTraceElement(frame.getClassName(), frame.getMethod(), frame.getFile(), + frame.getLine()); + } + stackTrace = stack; + } + } + } + + @Import(name = "takeStackTrace") + private native LazyStackSupplier takeWasmGCStack(); + @Rename("getMessage") public String getMessage0() { return message; @@ -158,6 +188,7 @@ public class TThrowable extends RuntimeException { stream.print(": " + message); } stream.println(); + ensureStackTrace(); if (stackTrace != null) { for (TStackTraceElement element : stackTrace) { stream.print("\tat "); @@ -177,6 +208,7 @@ public class TThrowable extends RuntimeException { stream.print(": " + message); } stream.println(); + ensureStackTrace(); if (stackTrace != null) { for (TStackTraceElement element : stackTrace) { stream.print("\tat "); @@ -191,10 +223,14 @@ public class TThrowable extends RuntimeException { @Rename("getStackTrace") public TStackTraceElement[] getStackTrace0() { + ensureStackTrace(); return stackTrace != null ? stackTrace.clone() : new TStackTraceElement[0]; } public void setStackTrace(@SuppressWarnings("unused") TStackTraceElement[] stackTrace) { + if (PlatformDetector.isWebAssemblyGC()) { + lazyStackTrace = null; + } this.stackTrace = stackTrace.clone(); } @@ -210,4 +246,30 @@ public class TThrowable extends RuntimeException { suppressed = TArrays.copyOf(suppressed, suppressed.length + 1); suppressed[suppressed.length - 1] = exception; } + + interface LazyStackSupplier extends JSObject { + StackFrames getStack(); + } + + interface StackFrames extends JSObject { + @JSProperty + int getLength(); + + @JSIndexer + StackFrame get(int index); + } + + interface StackFrame extends JSObject { + @JSProperty + String getClassName(); + + @JSProperty + String getMethod(); + + @JSProperty + String getFile(); + + @JSProperty + int getLine(); + } } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/DeobfuscatedLocation.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/DeobfuscatedLocation.java new file mode 100644 index 000000000..550d31455 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/DeobfuscatedLocation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.debug.info; + +public class DeobfuscatedLocation { + public final FileInfo file; + public final MethodInfo method; + public final int line; + + public DeobfuscatedLocation(FileInfo file, MethodInfo method, int line) { + this.file = file; + this.method = method; + this.line = line; + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java index bc2e8960d..9363fe597 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfo.java @@ -16,6 +16,7 @@ package org.teavm.backend.wasm.debug.info; import java.io.PrintStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -34,6 +35,44 @@ public class LineInfo { return sequenceList; } + public DeobfuscatedLocation[] deobfuscate(int[] addresses) { + var result = new ArrayList(); + for (var address : addresses) { + var part = deobfuscateSingle(address); + if (part != null) { + result.addAll(List.of(part)); + } + } + return result.toArray(new DeobfuscatedLocation[0]); + } + + public DeobfuscatedLocation[] deobfuscateSingle(int address) { + var sequence = find(address); + if (sequence == null) { + return null; + } + var instructionLoc = sequence.unpack().find(address); + if (instructionLoc == null) { + return null; + } + var location = instructionLoc.location(); + if (location == null) { + return null; + } + var result = new DeobfuscatedLocation[location.depth()]; + var method = sequence.method(); + var i = 0; + while (true) { + result[i++] = new DeobfuscatedLocation(location.file(), method, location.line()); + if (i >= result.length) { + break; + } + method = location.inlining().method(); + location = location.inlining().location(); + } + return result; + } + public LineInfoSequence find(int address) { var index = CollectionUtil.binarySearch(sequenceList, address, LineInfoSequence::endAddress); if (index < 0) { diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java index 8e16601b1..b1b096ccf 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/LineInfoUnpackedSequence.java @@ -54,7 +54,7 @@ public class LineInfoUnpackedSequence { } public int findIndex(int address) { - if (address < startAddress || address >= endAddress) { + if (address < startAddress || address >= endAddress || locations.isEmpty()) { return -1; } var index = CollectionUtil.binarySearch(locations, address, InstructionLocation::address); diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/Location.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/Location.java index a0cb2d008..d8a654c42 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/Location.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/Location.java @@ -37,4 +37,14 @@ public class Location { public InliningLocation inlining() { return inlining; } + + public int depth() { + var result = 0; + var loc = this; + while (loc != null) { + ++result; + loc = loc.inlining != null ? loc.inlining.location() : null; + } + return result; + } } diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/parser/LinesDeobfuscationParser.java b/core/src/main/java/org/teavm/backend/wasm/debug/parser/LinesDeobfuscationParser.java new file mode 100644 index 000000000..6b4ff9e80 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/debug/parser/LinesDeobfuscationParser.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.debug.parser; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import org.teavm.backend.wasm.debug.info.LineInfo; + +public class LinesDeobfuscationParser { + private Map sectionParsers = new LinkedHashMap<>(); + private DebugLinesParser lines; + + public LinesDeobfuscationParser() { + var strings = addSection(new DebugStringParser()); + var files = addSection(new DebugFileParser(strings)); + var packages = addSection(new DebugPackageParser(strings)); + var classes = addSection(new DebugClassParser(strings, packages)); + var methods = addSection(new DebugMethodParser(strings, classes)); + lines = addSection(new DebugLinesParser(files, methods)); + } + + private T addSection(T section) { + sectionParsers.put(section.name(), section); + return section; + } + + public boolean canHandleSection(String name) { + return sectionParsers.keySet().contains(name); + } + + public void applySection(String name, byte[] data) { + var parser = sectionParsers.get(name); + if (parser != null) { + parser.parse(data); + } + } + + public void pullSections(Function provider) { + for (var parser : sectionParsers.values()) { + var section = provider.apply(parser.name()); + if (section != null) { + parser.parse(section); + } + } + } + + public LineInfo getLineInfo() { + return lines.getLineInfo(); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java index eb3325bce..b184d993e 100644 --- a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java +++ b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java @@ -28,7 +28,6 @@ public abstract class DisassemblyWriter { private PrintWriter out; private boolean withAddress; private int indentLevel; - private int addressWithinSection; private int address; private boolean hasAddress; private boolean lineStarted; @@ -55,7 +54,6 @@ public abstract class DisassemblyWriter { } public void startSection() { - addressWithinSection = -1; currentSequenceIndex = 0; } @@ -105,13 +103,13 @@ public abstract class DisassemblyWriter { return; } if (currentCommandIndex < 0) { - if (addressWithinSection < debugLines.sequences().get(currentSequenceIndex).startAddress()) { + if (address < debugLines.sequences().get(currentSequenceIndex).startAddress()) { return; } currentCommandIndex = 0; printSingleDebugAnnotation("start debug line sequence"); } else { - if (addressWithinSection >= debugLines.sequences().get(currentSequenceIndex).endAddress()) { + if (address >= debugLines.sequences().get(currentSequenceIndex).endAddress()) { printSingleDebugAnnotation("end debug line sequence"); ++currentSequenceIndex; currentCommandIndex = -1; @@ -123,7 +121,7 @@ public abstract class DisassemblyWriter { var sequence = debugLines.sequences().get(currentSequenceIndex); while (currentCommandIndex < sequence.commands().size()) { var command = sequence.commands().get(currentCommandIndex); - if (addressWithinSection < command.address()) { + if (address < command.address()) { break; } @@ -219,7 +217,6 @@ public abstract class DisassemblyWriter { public final AddressListener addressListener = new AddressListener() { @Override public void address(int address) { - addressWithinSection = address; DisassemblyWriter.this.address = address + addressOffset; } }; diff --git a/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java b/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java index 89b03b8eb..35be5415f 100644 --- a/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java +++ b/core/src/main/java/org/teavm/backend/wasm/render/WasmBinaryRenderer.java @@ -336,8 +336,9 @@ public class WasmBinaryRenderer { .collect(Collectors.toList()); section.writeLEB(functions.size()); + var sectionOffset = output.getPosition() + 4; for (var function : functions) { - var body = renderFunction(module, function, section.getPosition() + 4); + var body = renderFunction(module, function, section.getPosition() + 4, sectionOffset); var startPos = section.getPosition(); section.writeLEB4(body.length); section.writeBytes(body); @@ -351,10 +352,10 @@ public class WasmBinaryRenderer { dwarfGenerator.setCodeSize(section.getPosition()); } - writeSection(SECTION_CODE, "code", section.getData()); + writeSection(SECTION_CODE, "code", section.getData(), true); } - private byte[] renderFunction(WasmModule module, WasmFunction function, int offset) { + private byte[] renderFunction(WasmModule module, WasmFunction function, int offset, int sectionOffset) { var code = new WasmBinaryWriter(); var dwarfSubprogram = dwarfClassGen != null ? dwarfClassGen.getSubprogram(function.getName()) : null; @@ -393,7 +394,7 @@ public class WasmBinaryRenderer { } var visitor = new WasmBinaryRenderingVisitor(code, module, dwarfGenerator, - function.getJavaMethod() != null ? debugLines : null, offset); + function.getJavaMethod() != null ? debugLines : null, offset + sectionOffset); for (var part : function.getBody()) { visitor.preprocess(part); } @@ -408,7 +409,7 @@ public class WasmBinaryRenderer { dwarfSubprogram.endOffset = code.getPosition() + offset; } if (debugVariables != null) { - writeDebugVariables(function, offset, code.getPosition()); + writeDebugVariables(function, offset + sectionOffset, code.getPosition()); } return code.getData(); @@ -600,13 +601,21 @@ public class WasmBinaryRenderer { } private void writeSection(int id, String name, byte[] data) { + writeSection(id, name, data, false); + } + + private void writeSection(int id, String name, byte[] data, boolean constantSizeLength) { var start = output.getPosition(); output.writeByte(id); int length = data.length; if (id == 0) { length += name.length() + 1; } - output.writeLEB(length); + if (constantSizeLength) { + output.writeLEB4(length); + } else { + output.writeLEB(length); + } if (id == 0) { output.writeAsciiString(name); } diff --git a/core/src/main/js/wasm-gc-runtime/runtime.js b/core/src/main/js/wasm-gc-runtime/runtime.js index 88dd98f21..21584a9dd 100644 --- a/core/src/main/js/wasm-gc-runtime/runtime.js +++ b/core/src/main/js/wasm-gc-runtime/runtime.js @@ -32,7 +32,8 @@ let setGlobalName = function(name, value) { function defaults(imports) { let context = { - exports: null + exports: null, + stackDeobfuscator: null }; dateImports(imports); consoleImports(imports, context); @@ -41,7 +42,10 @@ function defaults(imports) { imports.teavmMath = Math; return { supplyExports(exports) { - context.exports = exports + context.exports = exports; + }, + supplyStackDeobfuscator(deobfuscator) { + context.stackDeobfuscator = deobfuscator; } } } @@ -142,26 +146,39 @@ function coreImports(imports, context) { return weakRef; }, stringDeref: weakRef => weakRef.deref(), - currentStackTrace() { - if (stackDeobfuscator) { - return; - } - let reportCallFrame = context.exports["teavm.reportCallFrame"]; - if (typeof reportCallFrame !== "function") { - return; - } + takeStackTrace() { let stack = new Error().stack; - for (let line in stack.split("\n")) { + let addresses = []; + for (let line of stack.split("\n")) { let match = chromeExceptionRegex.exec(line); - if (match !== null) { - let address = parseInt(match.groups[1], 16); - let frames = stackDeobfuscator(address); - for (let frame of frames) { - let line = frame.line; - reportCallFrame(file, method, cls, line); - } + if (match !== null && match.length >= 2) { + let address = parseInt(match[1], 16); + addresses.push(address); } } + return { + getStack() { + let result; + if (context.stackDeobfuscator) { + try { + result = context.stackDeobfuscator(addresses); + } catch (e) { + console.warn("Could not deobfuscate stack", e); + } + } + if (!result) { + result = addresses.map(address => { + return { + className: "java.lang.Throwable$FakeClass", + method: "fakeMethod", + file: "Throwable.java", + line: address + }; + }); + } + return result; + } + }; } }; } @@ -175,7 +192,7 @@ function jsoImports(imports, context) { let jsWrappers = new WeakMap(); let javaWrappers = new WeakMap(); let primitiveWrappers = new Map(); - let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token)); + let primitiveFinalization = new FinalizationRegistry(token => primitiveWrappers.delete(token)); let hashCodes = new WeakMap(); let javaExceptionWrappers = new WeakMap(); let lastHashCode = 2463534242; @@ -576,34 +593,74 @@ function jsoImports(imports, context) { } } -function load(path, options) { +async function load(path, options) { if (!options) { options = {}; } const importObj = {}; - const defaultsResults = defaults(importObj); + const defaultsResult = defaults(importObj); if (typeof options.installImports !== "undefined") { options.installImports(importObj); } - return WebAssembly.instantiateStreaming(fetch(path), importObj) - .then(r => { - defaultsResults.supplyExports(r.instance.exports); - let userExports = {}; - let teavm = { - exports: userExports, - instance: r.instance, - module: r.module - }; - for (let key in r.instance.exports) { - let exportObj = r.instance.exports[key]; - if (exportObj instanceof WebAssembly.Global) { - Object.defineProperty(userExports, key, { - get: () => exportObj.value - }); - } + let [deobfuscatorFactory, { module, instance }] = await Promise.all([ + options.attachStackDeobfuscator ? getDeobfuscator(path, options) : Promise.resolve(null), + WebAssembly.instantiateStreaming(fetch(path), importObj) + ]); + + defaultsResult.supplyExports(instance.exports); + if (deobfuscatorFactory) { + let deobfuscator = createDeobfuscator(module, deobfuscatorFactory); + if (deobfuscator !== null) { + defaultsResult.supplyStackDeobfuscator(deobfuscator); + } + } + let userExports = {}; + let teavm = { + exports: userExports, + instance: instance, + module: module + }; + for (let key in instance.exports) { + let exportObj = instance.exports[key]; + if (exportObj instanceof WebAssembly.Global) { + Object.defineProperty(userExports, key, { + get: () => exportObj.value + }); + } + } + return teavm; +} + +async function getDeobfuscator(path, options) { + try { + const importObj = {}; + const defaultsResult = defaults(importObj, {}); + const { instance } = await WebAssembly.instantiateStreaming(fetch(path + "-deobfuscator.wasm"), importObj); + defaultsResult.supplyExports(instance.exports) + return instance; + } catch (e) { + console.warn("Could not load deobfuscator", e); + return null; + } +} + +function createDeobfuscator(module, deobfuscatorFactory) { + let deobfuscator = null; + let deobfuscatorInitialized = false; + function ensureDeobfuscator() { + if (!deobfuscatorInitialized) { + deobfuscatorInitialized = true; + try { + deobfuscator = deobfuscatorFactory.exports.createForModule.value(module); + } catch (e) { + console.warn("Could not load create deobfuscator", e); } - return teavm; - }); + } + } + return addresses => { + ensureDeobfuscator(); + return deobfuscator !== null ? deobfuscator.deobfuscate(addresses) : []; + } } \ No newline at end of file diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java index d1c5fa02a..066499ae7 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java @@ -245,7 +245,8 @@ class JSObjectClassTransformer implements ClassHolderTransformer { invocation.setReceiver(result); resultType = method.getResultType(); } - exit.setValueToReturn(marshaller.wrapArgument(callLocation, result, resultType, JSType.JAVA, false)); + exit.setValueToReturn(marshaller.wrapArgument(callLocation, result, resultType, + typeHelper.mapType(method.getResultType()), false)); basicBlock.addAll(marshallInstructions); marshallInstructions.clear(); } @@ -322,7 +323,7 @@ class JSObjectClassTransformer implements ClassHolderTransformer { if (method.getResultType() != ValueType.VOID) { invocation.setReceiver(program.createVariable()); exit.setValueToReturn(marshaller.wrapArgument(callLocation, invocation.getReceiver(), - method.getResultType(), JSType.JAVA, false)); + method.getResultType(), typeHelper.mapType(method.getResultType()), false)); basicBlock.addAll(marshallInstructions); marshallInstructions.clear(); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java index 24da0bce7..d2c4a174a 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java @@ -23,6 +23,9 @@ final class WasmGCJSRuntime { } static JSObject stringToJs(String str) { + if (str == null) { + return null; + } if (str.isEmpty()) { return emptyString(); } @@ -34,6 +37,9 @@ final class WasmGCJSRuntime { } static String jsToString(JSObject obj) { + if (obj == null) { + return null; + } var length = stringLength(obj); if (length == 0) { return ""; diff --git a/settings.gradle.kts b/settings.gradle.kts index ad4938d45..7c56260d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,7 @@ include("classlib") include("tools:core") include("tools:browser-runner") include("tools:deobfuscator-js") +include("tools:deobfuscator-wasm-gc") include("tools:junit") include("tools:devserver") include("tools:c-incremental") diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java index edf3965f1..b3a2d0908 100644 --- a/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithExportedClassMembers.java @@ -15,6 +15,7 @@ */ package org.teavm.jso.export; +import java.util.Arrays; import org.teavm.jso.JSExport; import org.teavm.jso.JSProperty; @@ -60,5 +61,10 @@ public final class ModuleWithExportedClassMembers { public static String staticProp() { return "I'm static"; } + + @JSExport + public String consumeIntArray(int[] array) { + return "accepted int array: " + Arrays.toString(array); + } } } diff --git a/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java b/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java index 7b80b2597..0115d486a 100644 --- a/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java +++ b/tests/src/test/java/org/teavm/jso/export/ModuleWithPrimitiveTypes.java @@ -15,6 +15,7 @@ */ package org.teavm.jso.export; +import java.util.Arrays; import org.teavm.jso.JSExport; public final class ModuleWithPrimitiveTypes { @@ -125,4 +126,9 @@ public final class ModuleWithPrimitiveTypes { public static String stringParam(String param) { return "string:" + param; } + + @JSExport + public static String intArrayParam(int[] param) { + return "intArray:" + Arrays.toString(param); + } } diff --git a/tests/src/test/java/org/teavm/jso/export/SimpleModule.java b/tests/src/test/java/org/teavm/jso/export/SimpleModule.java index 7ec62be97..a81258ccc 100644 --- a/tests/src/test/java/org/teavm/jso/export/SimpleModule.java +++ b/tests/src/test/java/org/teavm/jso/export/SimpleModule.java @@ -16,6 +16,8 @@ package org.teavm.jso.export; import org.teavm.jso.JSExport; +import org.teavm.jso.core.JSArray; +import org.teavm.jso.core.JSString; public final class SimpleModule { private SimpleModule() { @@ -25,4 +27,13 @@ public final class SimpleModule { public static int foo() { return 23; } + + @JSExport + public static JSArray bar(int count) { + var array = new JSArray(); + for (var i = 0; i < count; ++i) { + array.push(JSString.valueOf("item" + i)); + } + return array; + } } diff --git a/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js b/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js index 47769e207..ddad09f88 100644 --- a/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js +++ b/tests/src/test/resources/org/teavm/jso/export/exportClassMembers.js @@ -22,4 +22,5 @@ export async function test() { assertEquals("consumeObject:qwe:42", consumeObject(o)); assertEquals(99, C.baz()); assertEquals("I'm static", C.staticProp); + assertEquals("accepted int array: [2, 3, 5]", o.consumeIntArray([2, 3, 5])); } \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/primitives.js b/tests/src/test/resources/org/teavm/jso/export/primitives.js index 7fd5d54aa..c7581504d 100644 --- a/tests/src/test/resources/org/teavm/jso/export/primitives.js +++ b/tests/src/test/resources/org/teavm/jso/export/primitives.js @@ -45,8 +45,13 @@ function testConsumePrimitives() { assertEquals("string:q", java.stringParam("q")); } +function testConsumePrimitiveArrays() { + assertEquals("intArray:[2, 3, 5]", java.intArrayParam([2, 3, 5])); +} + export async function test() { testReturnPrimitives(); testReturnArrays(); testConsumePrimitives(); + testConsumePrimitiveArrays(); } \ No newline at end of file diff --git a/tests/src/test/resources/org/teavm/jso/export/simple.js b/tests/src/test/resources/org/teavm/jso/export/simple.js index b93131974..0a6f94277 100644 --- a/tests/src/test/resources/org/teavm/jso/export/simple.js +++ b/tests/src/test/resources/org/teavm/jso/export/simple.js @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const { foo } = await (await import('/tests/simple/provider.js')).default; +const { foo, bar } = await (await import('/tests/simple/provider.js')).default; export async function test() { assertEquals(23, foo()); + assertEquals(["item0", "item1"], bar(2)); } \ No newline at end of file diff --git a/tools/browser-runner/src/main/resources/test-server/frame.js b/tools/browser-runner/src/main/resources/test-server/frame.js index c37f98ebd..a4e722c06 100644 --- a/tools/browser-runner/src/main/resources/test-server/frame.js +++ b/tools/browser-runner/src/main/resources/test-server/frame.js @@ -211,6 +211,7 @@ function launchWasmGCTest(file, argument, callback) { } TeaVM.wasmGC.load(file.path, { + attachStackDeobfuscator: true, installImports: function(o) { o.teavmConsole.putcharStdout = putchar; o.teavmConsole.putcharStderr = putcharStderr; diff --git a/tools/deobfuscator-wasm-gc/build.gradle.kts b/tools/deobfuscator-wasm-gc/build.gradle.kts new file mode 100644 index 000000000..aae25913c --- /dev/null +++ b/tools/deobfuscator-wasm-gc/build.gradle.kts @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + `java-library` +} + +description = "JavaScript deobfuscator" + +configurations { + val teavmCompile = create("teavmCompile") + compileClasspath.configure { + extendsFrom(teavmCompile) + } + create("wasm") +} + +dependencies { + compileOnly(project(":jso:apis")) + "teavmCompile"(project(":classlib")) + "teavmCompile"(project(":tools:core")) +} + +val generateWasm by tasks.register("generateWasm") { + outputs.dir(layout.buildDirectory.dir("teavm")) + dependsOn(tasks.classes) + classpath += configurations["teavmCompile"] + classpath += java.sourceSets.main.get().output.classesDirs + mainClass = "org.teavm.tooling.deobfuscate.wasmgc.Compiler" + args( + "org.teavm.tooling.deobfuscate.wasmgc.DeobfuscatorFactory", + layout.buildDirectory.dir("teavm").get().asFile.absolutePath, + "deobfuscator.wasm" + ) +} + +val zipWithWasm by tasks.register("zipWithWasm") { + dependsOn(generateWasm) + archiveClassifier = "wasm" + from(layout.buildDirectory.dir("teavm"), layout.buildDirectory.dir("teavm-lib")) + include("*.wasm") + entryCompression = ZipEntryCompression.DEFLATED +} + +tasks.assemble.configure { + dependsOn(zipWithWasm) +} + +artifacts.add("wasm", zipWithWasm) \ No newline at end of file diff --git a/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Compiler.java b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Compiler.java new file mode 100644 index 000000000..47b0e03b0 --- /dev/null +++ b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Compiler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.tooling.deobfuscate.wasmgc; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.file.Files; +import org.teavm.backend.wasm.disasm.Disassembler; +import org.teavm.backend.wasm.disasm.DisassemblyHTMLWriter; +import org.teavm.tooling.ConsoleTeaVMToolLog; +import org.teavm.tooling.TeaVMProblemRenderer; +import org.teavm.tooling.TeaVMTargetType; +import org.teavm.tooling.TeaVMTool; +import org.teavm.tooling.TeaVMToolException; +import org.teavm.vm.TeaVMOptimizationLevel; + +public final class Compiler { + private Compiler() { + } + + public static void main(String[] args) throws TeaVMToolException, IOException { + var tool = new TeaVMTool(); + var log = new ConsoleTeaVMToolLog(false); + tool.setTargetType(TeaVMTargetType.WEBASSEMBLY_GC); + tool.setMainClass(args[0]); + tool.setTargetDirectory(new File(args[1])); + tool.setTargetFileName(args[2]); + tool.setObfuscated(true); + tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); + + tool.generate(); + TeaVMProblemRenderer.describeProblems(tool.getDependencyInfo().getCallGraph(), tool.getProblemProvider(), log); + if (!tool.getProblemProvider().getSevereProblems().isEmpty()) { + System.exit(1); + } + + var fileName = args[2]; + var disassemblyOutputFile = new File(args[1] + "/" + fileName.substring(0, fileName.length() - 5) + + ".wast.html"); + try (var disassemblyWriter = new PrintWriter(new OutputStreamWriter( + new FileOutputStream(disassemblyOutputFile)))) { + var htmlWriter = new DisassemblyHTMLWriter(disassemblyWriter); + htmlWriter.setWithAddress(true); + var disassembler = new Disassembler(htmlWriter); + var inputFile = new File(new File(args[1]), args[2]); + disassembler.disassemble(Files.readAllBytes(inputFile.toPath())); + } + } +} diff --git a/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Deobfuscator.java b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Deobfuscator.java new file mode 100644 index 000000000..7f1a6e12f --- /dev/null +++ b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Deobfuscator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 konsoletyper. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.tooling.deobfuscate.wasmgc; + +import org.teavm.backend.wasm.debug.info.LineInfo; +import org.teavm.jso.JSExport; +import org.teavm.jso.core.JSArray; +import org.teavm.jso.core.JSArrayReader; + +public final class Deobfuscator { + private LineInfo debugInformation; + + Deobfuscator(LineInfo debugInformation) { + this.debugInformation = debugInformation; + } + + @JSExport + public JSArrayReader deobfuscate(int[] addresses) { + var locations = debugInformation.deobfuscate(addresses); + var frames = new JSArray(); + for (var location : locations) { + var frame = new Frame(location.method.cls().fullName(), location.method.name(), + location.file != null ? location.file.fullName() : null, location.line); + frames.push(frame); + } + return frames; + } +} diff --git a/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/DeobfuscatorFactory.java b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/DeobfuscatorFactory.java new file mode 100644 index 000000000..79f3fc20e --- /dev/null +++ b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/DeobfuscatorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.tooling.deobfuscate.wasmgc; + +import org.teavm.backend.wasm.debug.parser.LinesDeobfuscationParser; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSExport; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSArrayReader; +import org.teavm.jso.typedarrays.ArrayBuffer; +import org.teavm.jso.typedarrays.Int8Array; + +public final class DeobfuscatorFactory { + private DeobfuscatorFactory() { + } + + @JSExport + public static Deobfuscator createForModule(JSObject module) { + var parser = new LinesDeobfuscationParser(); + parser.pullSections(name -> { + var result = getSection(module, name); + if (result == null || result.getLength() != 1) { + return null; + } + var data = new Int8Array(result.get(0)); + var bytes = new byte[data.getLength()]; + for (var i = 0; i < data.getLength(); ++i) { + bytes[i] = data.get(i); + } + return bytes; + }); + return new Deobfuscator(parser.getLineInfo()); + } + + @JSBody(params = { "module", "name"}, script = "return WebAssembly.Module.customSections(module, name);") + private static native JSArrayReader getSection(JSObject module, String name); +} diff --git a/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Frame.java b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Frame.java new file mode 100644 index 000000000..eaceee449 --- /dev/null +++ b/tools/deobfuscator-wasm-gc/src/main/java/org/teavm/tooling/deobfuscate/wasmgc/Frame.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 konsoletyper. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.tooling.deobfuscate.wasmgc; + +import org.teavm.jso.JSExport; +import org.teavm.jso.JSProperty; + +public class Frame { + private String className; + private String fileName; + private String methodName; + private int lineNumber; + + public Frame(String className, String methodName, String fileName, int lineNumber) { + this.className = className; + this.methodName = methodName; + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + @JSExport + @JSProperty + public String getClassName() { + return className; + } + + @JSExport + @JSProperty + public String getFile() { + return fileName; + } + + @JSExport + @JSProperty + public String getMethod() { + return methodName; + } + + @JSExport + @JSProperty + public int getLine() { + return lineNumber; + } +} diff --git a/tools/junit/build.gradle.kts b/tools/junit/build.gradle.kts index 638a49280..2d0074643 100644 --- a/tools/junit/build.gradle.kts +++ b/tools/junit/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(project(":core")) implementation(project(":tools:core")) implementation(project(":tools:browser-runner")) + runtimeOnly(project(":tools:deobfuscator-wasm-gc", "wasm")) } teavmPublish { diff --git a/tools/junit/src/main/java/org/teavm/junit/TestWasmGCEntryPoint.java b/tools/junit/src/main/java/org/teavm/junit/TestWasmGCEntryPoint.java index c2e77410d..81e93138d 100644 --- a/tools/junit/src/main/java/org/teavm/junit/TestWasmGCEntryPoint.java +++ b/tools/junit/src/main/java/org/teavm/junit/TestWasmGCEntryPoint.java @@ -32,7 +32,6 @@ final class TestWasmGCEntryPoint { e.printStackTrace(out); reportFailure(JSString.valueOf(out.toString())); } - TestEntryPoint.run(args.length > 0 ? args[0] : null); } @Import(module = "teavmTest", name = "success") diff --git a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java index 87e20c6a1..6b895e92b 100644 --- a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java +++ b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java @@ -126,8 +126,11 @@ class WebAssemblyGCPlatformSupport extends TestPlatformSupport { htmlOutput(outputPath, outputPathForMethod, configuration, reference, "teavm-run-test-wasm-gc.html"); var testPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), getExtension() + "-runtime.js"); + var testDeobfuscatorPath = getOutputFile(outputPath, "classTest", configuration.getSuffix(), + getExtension() + "-deobfuscator.wasm"); try { TestUtil.resourceToFile("org/teavm/backend/wasm/wasm-gc-runtime.js", testPath, Map.of()); + TestUtil.resourceToFile("deobfuscator.wasm", testDeobfuscatorPath, Map.of()); } catch (IOException e) { throw new RuntimeException(e); } @@ -142,8 +145,11 @@ class WebAssemblyGCPlatformSupport extends TestPlatformSupport { htmlSingleTestOutput(outputPathForMethod, configuration, "teavm-run-test-wasm-gc.html"); var testPath = getOutputFile(outputPathForMethod, "test", configuration.getSuffix(), getExtension() + "-runtime.js"); + var testDeobfuscatorPath = getOutputFile(outputPathForMethod, "test", configuration.getSuffix(), + getExtension() + "-deobfuscator.wasm"); try { TestUtil.resourceToFile("org/teavm/backend/wasm/wasm-gc-runtime.js", testPath, Map.of()); + TestUtil.resourceToFile("deobfuscator.wasm", testDeobfuscatorPath, Map.of()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/tools/junit/src/main/resources/teavm-run-test-wasm-gc.html b/tools/junit/src/main/resources/teavm-run-test-wasm-gc.html index 8a0b52b02..e70e638be 100644 --- a/tools/junit/src/main/resources/teavm-run-test-wasm-gc.html +++ b/tools/junit/src/main/resources/teavm-run-test-wasm-gc.html @@ -25,6 +25,7 @@