From 9a9e7561b747e22ae03dde2bef70fd705218871c Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Tue, 20 Dec 2022 10:01:47 +0100 Subject: [PATCH] Wasm: support displaying objects in debugger --- .../backend/wasm/debug/info/VariableType.java | 21 +- .../wasm/generate/WasmClassGenerator.java | 11 +- .../main/java/org/teavm/common/Promise.java | 10 +- .../java/org/teavm/debugging/Debugger.java | 13 +- .../java/org/teavm/debugging/JsValueImpl.java | 120 +++++ .../main/java/org/teavm/debugging/Value.java | 99 +--- .../org/teavm/debugging/WasmValueImpl.java | 503 ++++++++++++++++++ .../javascript/JavaScriptCallFrame.java | 2 + .../debugging/javascript/JavaScriptValue.java | 2 + .../teavm/chromerdp/ChromeRDPDebugger.java | 76 ++- .../org/teavm/chromerdp/RDPCallFrame.java | 35 +- .../java/org/teavm/chromerdp/RDPValue.java | 5 + .../chromerdp/data/ExceptionDetailsDTO.java | 24 + .../chromerdp/messages/RunScriptResponse.java | 26 + 14 files changed, 847 insertions(+), 100 deletions(-) create mode 100644 core/src/main/java/org/teavm/debugging/JsValueImpl.java create mode 100644 core/src/main/java/org/teavm/debugging/WasmValueImpl.java create mode 100644 tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/ExceptionDetailsDTO.java create mode 100644 tools/chrome-rdp/src/main/java/org/teavm/chromerdp/messages/RunScriptResponse.java diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/info/VariableType.java b/core/src/main/java/org/teavm/backend/wasm/debug/info/VariableType.java index 953d0f1e1..3a7c844e6 100644 --- a/core/src/main/java/org/teavm/backend/wasm/debug/info/VariableType.java +++ b/core/src/main/java/org/teavm/backend/wasm/debug/info/VariableType.java @@ -22,5 +22,24 @@ public enum VariableType { DOUBLE, OBJECT, ADDRESS, - UNDEFINED + UNDEFINED; + + public FieldType asFieldType() { + switch (this) { + case INT: + return FieldType.INT; + case LONG: + return FieldType.LONG; + case FLOAT: + return FieldType.FLOAT; + case DOUBLE: + return FieldType.DOUBLE; + case OBJECT: + return FieldType.OBJECT; + case ADDRESS: + return FieldType.ADDRESS; + default: + return FieldType.UNDEFINED; + } + } } diff --git a/core/src/main/java/org/teavm/backend/wasm/generate/WasmClassGenerator.java b/core/src/main/java/org/teavm/backend/wasm/generate/WasmClassGenerator.java index 5db1527e4..1ce343108 100644 --- a/core/src/main/java/org/teavm/backend/wasm/generate/WasmClassGenerator.java +++ b/core/src/main/java/org/teavm/backend/wasm/generate/WasmClassGenerator.java @@ -714,7 +714,16 @@ public class WasmClassGenerator { debug.writeArray(indexes.get(itemType), data.start); } else if (data.type instanceof ValueType.Object) { var className = ((ValueType.Object) data.type).getClassName(); - if (isManagedClass(className)) { + if (className.equals("java.lang.Class")) { + int headerSize = 8; + debug.startClass(className, indexes.get(ValueType.object("java.lang.Object")), data.start, 60); + debug.instanceField("size", 8, FieldType.INT); + debug.instanceField("flags", 12, FieldType.INT); + debug.instanceField("name", 24, FieldType.OBJECT); + debug.instanceField("itemType", 32, FieldType.OBJECT); + debug.instanceField("parent", 56, FieldType.OBJECT); + debug.endClass(); + } else if (isManagedClass(className)) { var parent = data.cls.getParent() != null ? indexes.get(ValueType.object(data.cls.getParent())) : -1; diff --git a/core/src/main/java/org/teavm/common/Promise.java b/core/src/main/java/org/teavm/common/Promise.java index f2be6fe7a..5ef438e28 100644 --- a/core/src/main/java/org/teavm/common/Promise.java +++ b/core/src/main/java/org/teavm/common/Promise.java @@ -140,8 +140,8 @@ public class Promise { if (state == State.PENDING || state == State.WAITING_PROMISE) { if (thenList == null) { thenList = new ArrayList<>(); - thenList.add(new Then<>(f, result, false)); } + thenList.add(new Then<>(f, result, false)); } else { passValue(f, result); } @@ -160,8 +160,8 @@ public class Promise { if (state == State.PENDING || state == State.WAITING_PROMISE) { if (thenList == null) { thenList = new ArrayList<>(); - thenList.add(new Then<>(f, result, true)); } + thenList.add(new Then<>(f, result, true)); } else if (state == State.COMPLETED) { passValueAsync(f, result); } @@ -173,8 +173,8 @@ public class Promise { if (state == State.PENDING || state == State.WAITING_PROMISE) { if (catchList == null) { catchList = new ArrayList<>(); - catchList.add(new Catch(f, result)); } + catchList.add(new Catch(f, result)); } else if (state == State.ERRORED) { passError(f, result); } @@ -266,9 +266,9 @@ public class Promise { this.value = value; if (thenList != null) { - List> list = thenList; + var list = thenList; thenList = null; - for (Then then : list) { + for (var then : list) { if (then.promise) { passValueAsync((Function>) then.f, (Promise) then.target); } else { diff --git a/core/src/main/java/org/teavm/debugging/Debugger.java b/core/src/main/java/org/teavm/debugging/Debugger.java index 793579d56..cafafbb73 100644 --- a/core/src/main/java/org/teavm/debugging/Debugger.java +++ b/core/src/main/java/org/teavm/debugging/Debugger.java @@ -47,6 +47,7 @@ import org.teavm.debugging.javascript.JavaScriptDebugger; import org.teavm.debugging.javascript.JavaScriptDebuggerListener; import org.teavm.debugging.javascript.JavaScriptLocation; import org.teavm.debugging.javascript.JavaScriptScript; +import org.teavm.debugging.javascript.JavaScriptValue; import org.teavm.debugging.javascript.JavaScriptVariable; import org.teavm.model.MethodReference; import org.teavm.model.ValueType; @@ -516,7 +517,7 @@ public class Debugger { for (Map.Entry entry : jsVariables.entrySet()) { JavaScriptVariable jsVar = entry.getValue(); String[] names = mapVariable(entry.getKey(), jsFrame.getLocation()); - Value value = new Value(this, debugInformation, jsVar.getValue()); + Value value = new JsValueImpl(this, debugInformation, jsVar.getValue()); for (String name : names) { if (name == null) { name = "js:" + jsVar.getName(); @@ -543,9 +544,15 @@ public class Debugger { var variable = prop.get("value"); return variable != null ? variable.getValue() : null; }) - .thenAsync(value -> { + .thenAsync((JavaScriptValue value) -> { if (value != null) { - var varValue = new Value(this, debugInfo, value); + var repr = value.getSimpleRepresentation(); + if (repr.endsWith("n")) { + repr = repr.substring(0, repr.length() - 1); + } + var longValue = Long.parseLong(repr); + var varValue = new WasmValueImpl(this, debugInfo, + range.variable().type().asFieldType(), jsFrame, longValue); var variable = new Variable(range.variable().name(), varValue); vars.put(variable.getName(), variable); } diff --git a/core/src/main/java/org/teavm/debugging/JsValueImpl.java b/core/src/main/java/org/teavm/debugging/JsValueImpl.java new file mode 100644 index 000000000..434e5d968 --- /dev/null +++ b/core/src/main/java/org/teavm/debugging/JsValueImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright 2022 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.debugging; + +import java.util.HashMap; +import java.util.Map; +import org.teavm.common.Promise; +import org.teavm.debugging.information.DebugInformation; +import org.teavm.debugging.javascript.JavaScriptValue; +import org.teavm.debugging.javascript.JavaScriptVariable; + +class JsValueImpl extends Value { + private DebugInformation debugInformation; + private JavaScriptValue jsValue; + + JsValueImpl(Debugger debugger, DebugInformation debugInformation, JavaScriptValue jsValue) { + super(debugger); + this.debugInformation = debugInformation; + this.jsValue = jsValue; + } + + @Override + public Promise getRepresentation() { + return jsValue.getRepresentation(); + } + + @Override + Promise prepareType() { + return jsValue.getClassName().then(className -> { + if (className.startsWith("a/")) { + className = className.substring(2); + String origClassName = className; + int degree = 0; + while (className.endsWith("[]")) { + className = className.substring(0, className.length() - 2); + ++degree; + } + String javaClassName = debugInformation.getClassNameByJsName(className); + if (javaClassName != null) { + if (degree > 0) { + StringBuilder sb = new StringBuilder(javaClassName); + for (int i = 0; i < degree; ++i) { + sb.append("[]"); + } + javaClassName = sb.toString(); + } + className = javaClassName; + } else { + className = origClassName; + } + } + return className; + }); + } + + @Override + Promise> prepareProperties() { + return jsValue.getProperties().thenAsync(jsVariables -> { + return getType().thenAsync(className -> { + if (!className.startsWith("@") && className.endsWith("[]") && jsVariables.containsKey("data")) { + return jsVariables.get("data").getValue().getProperties() + .then(arrayData -> fillArray(arrayData)); + } + var vars = new HashMap(); + for (var entry : jsVariables.entrySet()) { + var jsVar = entry.getValue(); + String name; + name = debugger.mapField(className, entry.getKey()); + if (name == null) { + continue; + } + var value = new JsValueImpl(debugger, debugInformation, jsVar.getValue()); + vars.put(name, new Variable(name, value)); + } + return Promise.of(vars); + }); + }); + } + + private Map fillArray(Map jsVariables) { + var vars = new HashMap(); + for (var entry : jsVariables.entrySet()) { + var jsVar = entry.getValue(); + if (!isNumeric(entry.getKey())) { + continue; + } + Value value = new JsValueImpl(debugger, debugInformation, jsVar.getValue()); + vars.put(entry.getKey(), new Variable(entry.getKey(), value)); + } + return vars; + } + + @Override + public Promise hasInnerStructure() { + return getType().then(value -> !value.equals("long") && jsValue.hasInnerStructure()); + } + + @Override + public Promise getInstanceId() { + return getType().then(value -> value.equals("long") ? null : jsValue.getInstanceId()); + } + + @Override + public JavaScriptValue getOriginalValue() { + return jsValue; + } +} diff --git a/core/src/main/java/org/teavm/debugging/Value.java b/core/src/main/java/org/teavm/debugging/Value.java index 70563c134..cce44721b 100644 --- a/core/src/main/java/org/teavm/debugging/Value.java +++ b/core/src/main/java/org/teavm/debugging/Value.java @@ -15,35 +15,22 @@ */ package org.teavm.debugging; -import java.util.HashMap; import java.util.Map; import org.teavm.backend.wasm.debug.info.DebugInfo; import org.teavm.common.Promise; -import org.teavm.debugging.information.DebugInformation; import org.teavm.debugging.javascript.JavaScriptValue; -import org.teavm.debugging.javascript.JavaScriptVariable; -public class Value { - private Debugger debugger; - private DebugInformation debugInformation; +public abstract class Value { + Debugger debugger; private DebugInfo wasmDebugInfo; - private JavaScriptValue jsValue; private Promise> properties; private Promise type; - Value(Debugger debugger, DebugInformation debugInformation, JavaScriptValue jsValue) { + Value(Debugger debugger) { this.debugger = debugger; - this.debugInformation = debugInformation; - this.jsValue = jsValue; } - Value(Debugger debugger, DebugInfo wasmDebugInfo, JavaScriptValue jsValue) { - this.debugger = debugger; - this.wasmDebugInfo = wasmDebugInfo; - this.jsValue = jsValue; - } - - private static boolean isNumeric(String str) { + static boolean isNumeric(String str) { for (int i = 0; i < str.length(); ++i) { char c = str.charAt(i); if (c < '0' || c > '9') { @@ -53,89 +40,29 @@ public class Value { return true; } - public Promise getRepresentation() { - return jsValue.getRepresentation(); - } + public abstract Promise getRepresentation(); public Promise getType() { if (type == null) { - type = jsValue.getClassName().then(className -> { - if (className.startsWith("a/")) { - className = className.substring(2); - String origClassName = className; - int degree = 0; - while (className.endsWith("[]")) { - className = className.substring(0, className.length() - 2); - ++degree; - } - String javaClassName = debugInformation.getClassNameByJsName(className); - if (javaClassName != null) { - if (degree > 0) { - StringBuilder sb = new StringBuilder(javaClassName); - for (int i = 0; i < degree; ++i) { - sb.append("[]"); - } - javaClassName = sb.toString(); - } - className = javaClassName; - } else { - className = origClassName; - } - } - return className; - }); + type = prepareType(); } return type; } + abstract Promise prepareType(); + public Promise> getProperties() { if (properties == null) { - properties = jsValue.getProperties().thenAsync(jsVariables -> { - return getType().thenAsync(className -> { - if (!className.startsWith("@") && className.endsWith("[]") && jsVariables.containsKey("data")) { - return jsVariables.get("data").getValue().getProperties() - .then(arrayData -> fillArray(arrayData)); - } - Map vars = new HashMap<>(); - for (Map.Entry entry : jsVariables.entrySet()) { - JavaScriptVariable jsVar = entry.getValue(); - String name; - name = debugger.mapField(className, entry.getKey()); - if (name == null) { - continue; - } - Value value = new Value(debugger, debugInformation, jsVar.getValue()); - vars.put(name, new Variable(name, value)); - } - return Promise.of(vars); - }); - }); + properties = prepareProperties(); } return properties; } - private Map fillArray(Map jsVariables) { - Map vars = new HashMap<>(); - for (Map.Entry entry : jsVariables.entrySet()) { - JavaScriptVariable jsVar = entry.getValue(); - if (!isNumeric(entry.getKey())) { - continue; - } - Value value = new Value(debugger, debugInformation, jsVar.getValue()); - vars.put(entry.getKey(), new Variable(entry.getKey(), value)); - } - return vars; - } + abstract Promise> prepareProperties(); - public Promise hasInnerStructure() { - return getType().then(value -> !value.equals("long") && jsValue.hasInnerStructure()); - } + public abstract Promise hasInnerStructure(); - public Promise getInstanceId() { - return getType().then(value -> value.equals("long") ? null : jsValue.getInstanceId()); - } + public abstract Promise getInstanceId(); - public JavaScriptValue getOriginalValue() { - return jsValue; - } + public abstract JavaScriptValue getOriginalValue(); } diff --git a/core/src/main/java/org/teavm/debugging/WasmValueImpl.java b/core/src/main/java/org/teavm/debugging/WasmValueImpl.java new file mode 100644 index 000000000..844b58699 --- /dev/null +++ b/core/src/main/java/org/teavm/debugging/WasmValueImpl.java @@ -0,0 +1,503 @@ +/* + * Copyright 2022 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.debugging; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.teavm.backend.wasm.debug.info.ArrayLayout; +import org.teavm.backend.wasm.debug.info.ClassLayout; +import org.teavm.backend.wasm.debug.info.DebugInfo; +import org.teavm.backend.wasm.debug.info.FieldType; +import org.teavm.backend.wasm.debug.info.InterfaceLayout; +import org.teavm.backend.wasm.debug.info.PrimitiveLayout; +import org.teavm.backend.wasm.debug.info.TypeLayout; +import org.teavm.common.Promise; +import org.teavm.debugging.javascript.JavaScriptCallFrame; +import org.teavm.debugging.javascript.JavaScriptValue; +import org.teavm.model.PrimitiveType; + +class WasmValueImpl extends Value { + private static final String CLASS_PROP = "__class"; + private static final String ADDRESS_PROP = "__address"; + private DebugInfo debugInfo; + private FieldType type; + private JavaScriptCallFrame callFrame; + private long longValue; + private Promise calculatedType; + + WasmValueImpl(Debugger debugger, DebugInfo debugInfo, FieldType type, JavaScriptCallFrame callFrame, + long longValue) { + super(debugger); + this.debugInfo = debugInfo; + this.type = type; + this.callFrame = callFrame; + this.longValue = longValue; + } + + @Override + public Promise getRepresentation() { + switch (type) { + case BOOLEAN: + return Promise.of(longValue != 0 ? "true" : "false"); + case BYTE: + return Promise.of(Byte.toString((byte) longValue)); + case SHORT: + return Promise.of(Short.toString((short) longValue)); + case CHAR: { + var sb = new StringBuilder("'"); + appendChar(sb, (char) longValue); + sb.append("'"); + return Promise.of(sb.toString()); + } + case INT: + return Promise.of(Integer.toString((int) longValue)); + case LONG: + return Promise.of(Long.toString(longValue)); + case FLOAT: + return Promise.of(Float.toString(Float.intBitsToFloat((int) longValue))); + case DOUBLE: + return Promise.of(Double.toString(Double.longBitsToDouble(longValue))); + case OBJECT: + return buildObjectRepresentation(); + case ADDRESS: + return Promise.of("0x" + Integer.toHexString((int) longValue)); + default: + return Promise.of("undefined"); + } + } + + private void appendChar(StringBuilder sb, char c) { + switch (c) { + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\b': + sb.append("\\b"); + break; + case '\f': + sb.append("\\f"); + break; + case '\'': + sb.append("\\\'"); + break; + case '\"': + sb.append("\\\""); + break; + case '\\': + sb.append("\\\\"); + break; + default: + if (c < 32) { + sb.append("\\u00").append(Character.forDigit(c / 16, 16)) + .append(Character.forDigit(c % 16, 16)); + } else { + sb.append(c); + } + break; + } + } + + private Promise buildObjectRepresentation() { + if (longValue == 0) { + return Promise.of("null"); + } + return getCalculatedType().thenAsync(cls -> { + if (cls == null) { + return Promise.of("error"); + } + return typeRepresentation(cls, (int) longValue); + }); + } + + private Promise typeRepresentation(TypeLayout type, int address) { + switch (type.kind()) { + case CLASS: + return objectRepresentation((ClassLayout) type, address); + case ARRAY: + return arrayRepresentation((ArrayLayout) type, address); + default: + break; + } + return Promise.of(classToString(type)); + } + + private Promise objectRepresentation(ClassLayout cls, int address) { + if (cls.classRef().fullName().equals("java.lang.String")) { + var stringRepr = decodeString(cls, address); + if (stringRepr != null) { + return stringRepr.then(result -> result != null ? result : classToString(cls)); + } + } else if (cls.classRef().fullName().equals("java.lang.Class")) { + var stringRepr = decodeClass(address); + if (stringRepr != null) { + return Promise.of(stringRepr); + } + } + return Promise.of(classToString(cls)); + } + + private Promise arrayRepresentation(ArrayLayout arrayType, int address) { + return callFrame.getMemory(address + 8, 4).then(data -> { + if (data == null) { + return classToString(arrayType); + } + var length = readInt(data, 0); + return classToString(arrayType.elementType()) + "[" + length + "]"; + }); + } + + private Promise decodeString(ClassLayout cls, int address) { + for (var field : cls.instanceFields()) { + if (field.name().equals("characters") && field.type() == FieldType.OBJECT) { + return callFrame.getMemory(address + field.address(), 4).thenAsync(data -> { + var charsAddress = readInt(data, 0); + return decodeChars(charsAddress); + }); + } + } + return null; + } + + private Promise decodeChars(int address) { + return callFrame.getMemory(address, 12).thenAsync(data -> { + if (data == null) { + return null; + } + var classPtr = readInt(data, 0) << 3; + var type = debugInfo.classLayoutInfo().find(classPtr); + if (!(type instanceof ArrayLayout)) { + return null; + } + var elementType = ((ArrayLayout) type).elementType(); + if (!(elementType instanceof PrimitiveLayout)) { + return null; + } + var primitiveType = ((PrimitiveLayout) elementType).primitiveType(); + if (primitiveType != PrimitiveType.CHARACTER) { + return null; + } + var length = readInt(data, 8); + return callFrame.getMemory(address + 12, length * 2).then(charsData -> { + if (charsData == null) { + return null; + } + var sb = new StringBuilder("\""); + for (var i = 0; i < length; ++i) { + appendChar(sb, (char) readShort(charsData, i * 2)); + } + sb.append("\""); + return sb.toString(); + }); + }); + } + + private String decodeClass(int address) { + var type = debugInfo.classLayoutInfo().find(address); + return type != null ? classToString(type) : null; + } + + @Override + Promise prepareType() { + switch (type) { + case BOOLEAN: + return Promise.of("boolean"); + case BYTE: + return Promise.of("byte"); + case SHORT: + return Promise.of("short"); + case CHAR: + return Promise.of("char"); + case INT: + return Promise.of("int"); + case LONG: + return Promise.of("long"); + case FLOAT: + return Promise.of("float"); + case DOUBLE: + return Promise.of("double"); + case ADDRESS: + return Promise.of("address"); + case OBJECT: + return fetchObjectType(); + default: + return Promise.of("undefined"); + } + } + + private Promise getCalculatedType() { + if (calculatedType == null) { + calculatedType = callFrame.getMemory((int) longValue, 4).then(data -> { + if (data == null) { + return null; + } + var header = readInt(data, 0); + var classPtr = header << 3; + var classes = debugInfo.classLayoutInfo(); + if (classes == null) { + return null; + } + return classes.find(classPtr); + }); + } + return calculatedType; + } + + private Promise fetchObjectType() { + if (longValue == 0) { + return Promise.of("null"); + } + return getCalculatedType().then(cls -> { + if (cls == null) { + return "error"; + } + return classToString(cls); + }); + } + + private String classToString(TypeLayout type) { + switch (type.kind()) { + case PRIMITIVE: + switch (((PrimitiveLayout) type).primitiveType()) { + case BOOLEAN: + return "boolean"; + case BYTE: + return "byte"; + case SHORT: + return "short"; + case CHARACTER: + return "char"; + case INTEGER: + return "int"; + case LONG: + return "long"; + case FLOAT: + return "float"; + case DOUBLE: + return "double"; + default: + break; + } + break; + case CLASS: + return ((ClassLayout) type).classRef().fullName(); + case INTERFACE: + return ((InterfaceLayout) type).classRef().fullName(); + case ARRAY: + return classToString(((ArrayLayout) type).elementType()) + "[]"; + default: + break; + } + return "unknown"; + } + + @Override + Promise> prepareProperties() { + return getCalculatedType().thenAsync(cls -> { + if (cls != null) { + switch (cls.kind()) { + case CLASS: + return fetchObjectProperties((ClassLayout) cls); + case ARRAY: + return fetchArrayProperties((ArrayLayout) cls); + default: + break; + } + } + return Promise.of(Collections.emptyMap()); + }); + } + + private Promise> fetchObjectProperties(ClassLayout cls) { + if (longValue == 0) { + return Promise.of(Collections.emptyMap()); + } + return callFrame.getMemory((int) longValue, cls.size()).then(data -> { + if (data == null) { + return Collections.emptyMap(); + } + var properties = new LinkedHashMap(); + for (var field : cls.instanceFields()) { + long longValue; + switch (field.type()) { + case BOOLEAN: + case BYTE: + longValue = data[field.address()]; + break; + case SHORT: + case CHAR: + longValue = readShort(data, field.address()); + break; + case INT: + case FLOAT: + case ADDRESS: + case OBJECT: + longValue = readInt(data, field.address()); + break; + case LONG: + case DOUBLE: + longValue = readLong(data, field.address()); + break; + default: + longValue = 0; + break; + } + var value = new WasmValueImpl(debugger, debugInfo, field.type(), callFrame, longValue); + properties.put(field.name(), new Variable(field.name(), value)); + } + addCommonProperties(properties, cls); + return properties; + }); + } + + private Promise> fetchArrayProperties(ArrayLayout type) { + if (longValue == 0) { + return Promise.of(Collections.emptyMap()); + } + return callFrame.getMemory((int) longValue + 8, 4).thenAsync(data -> { + if (data == null) { + return Promise.of(Collections.emptyMap()); + } + var length = readInt(data, 0); + var offset = 12; + int itemSize; + FieldType elementType; + if (type.elementType() instanceof PrimitiveLayout) { + switch (((PrimitiveLayout) type.elementType()).primitiveType()) { + case BOOLEAN: + elementType = FieldType.BOOLEAN; + itemSize = 0; + break; + case BYTE: + elementType = FieldType.BYTE; + itemSize = 0; + break; + case SHORT: + elementType = FieldType.SHORT; + itemSize = 1; + break; + case CHARACTER: + elementType = FieldType.CHAR; + itemSize = 1; + break; + case INTEGER: + elementType = FieldType.INT; + itemSize = 2; + break; + case LONG: + elementType = FieldType.LONG; + itemSize = 3; + break; + case FLOAT: + elementType = FieldType.FLOAT; + itemSize = 2; + break; + case DOUBLE: + elementType = FieldType.DOUBLE; + offset = 16; + itemSize = 3; + break; + default: + itemSize = 1; + elementType = FieldType.UNDEFINED; + break; + } + } else { + elementType = FieldType.OBJECT; + itemSize = 2; + } + return callFrame.getMemory((int) longValue + offset, length << itemSize).then(arrayData -> { + var properties = new LinkedHashMap(); + for (var i = 0; i < length; ++i) { + var name = String.valueOf(i); + long longValue; + switch (itemSize) { + case 0: + longValue = arrayData[i]; + break; + case 1: + longValue = readShort(arrayData, i * 2); + break; + case 2: + longValue = readInt(arrayData, i * 4); + break; + default: + longValue = readLong(arrayData, i * 8); + break; + } + var value = new WasmValueImpl(debugger, debugInfo, elementType, callFrame, longValue); + properties.put(name, new Variable(name, value)); + } + properties.put("length", new Variable("length", new WasmValueImpl(debugger, debugInfo, + FieldType.INT, callFrame, length))); + addCommonProperties(properties, type); + return properties; + }); + }); + } + + private void addCommonProperties(Map properties, TypeLayout cls) { + properties.put(CLASS_PROP, new Variable(CLASS_PROP, new WasmValueImpl(debugger, debugInfo, + FieldType.OBJECT, callFrame, cls.address()))); + properties.put(ADDRESS_PROP, new Variable(ADDRESS_PROP, new WasmValueImpl(debugger, debugInfo, + FieldType.ADDRESS, callFrame, longValue))); + } + + private int readInt(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) + | ((data[offset + 2] & 0xFF) << 16) | ((data[offset + 3] & 0xFF) << 24); + } + + private int readShort(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8); + } + + private long readLong(byte[] data, int offset) { + return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) + | ((data[offset + 2] & 0xFF) << 16) | ((data[offset + 3] & 0xFF) << 24) + | ((data[offset + 4] & 0xFFL) << 32) | ((data[offset + 5] & 0xFF) << 40) + | ((data[offset + 6] & 0xFFL) << 48) | ((data[offset + 7] & 0xFF) << 56); + } + + @Override + public Promise hasInnerStructure() { + return getCalculatedType().then(type -> { + switch (type.kind()) { + case CLASS: + case ARRAY: + return true; + default: + return false; + } + }); + } + + @Override + public Promise getInstanceId() { + return Promise.of(null); + } + + @Override + public JavaScriptValue getOriginalValue() { + return null; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java index c27c1d14c..ed1f3d3e6 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptCallFrame.java @@ -28,4 +28,6 @@ public interface JavaScriptCallFrame { JavaScriptValue getThisVariable(); JavaScriptValue getClosureVariable(); + + Promise getMemory(int address, int count); } diff --git a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java index 5ead7ec78..f0cd2044d 100644 --- a/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java +++ b/core/src/main/java/org/teavm/debugging/javascript/JavaScriptValue.java @@ -19,6 +19,8 @@ import java.util.Map; import org.teavm.common.Promise; public interface JavaScriptValue { + String getSimpleRepresentation(); + Promise getRepresentation(); Promise getClassName(); diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java index d0d6ea55a..52570a9ad 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/ChromeRDPDebugger.java @@ -15,10 +15,12 @@ */ package org.teavm.chromerdp; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.NullNode; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -108,6 +110,25 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri }); } + private Promise injectWasmFunctions(int contextId) { + return enableRuntime() + .thenAsync(v -> { + var compileParams = new CompileScriptCommand(); + compileParams.expression = "" + + "$dbg_memory = function(buffer, offset, count) { return btoa(" + + "String.fromCharCode.apply(null, new Uint8Array(buffer, offset, count))) };\n"; + compileParams.sourceURL = "file://fake-wasm"; + compileParams.persistScript = true; + compileParams.executionContextId = contextId; + return callMethodAsync("Runtime.compileScript", CompileScriptResponse.class, compileParams); + }) + .thenAsync(response -> { + var runParams = new RunScriptCommand(); + runParams.scriptId = response.scriptId; + return callMethodAsync("Runtime.runScript", void.class, runParams); + }); + } + private Promise enableRuntime() { if (runtimeEnabledPromise == null) { runtimeEnabledPromise = callMethodAsync("Runtime.enable", void.class, null); @@ -191,7 +212,7 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri } var script = new ChromeRDPScript(this, params.getScriptId(), language, params.getUrl()); scripts.put(script.getId(), script); - if (params.getUrl().equals("file://fake")) { + if (params.getUrl().startsWith("file://fake")) { return Promise.VOID; } for (var listener : getListeners()) { @@ -199,6 +220,8 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri } if (language == JavaScriptLanguage.JS) { return injectFunctions(params.getExecutionContextId()); + } else if (language == JavaScriptLanguage.WASM) { + return injectWasmFunctions(params.getExecutionContextId()); } return Promise.VOID; } @@ -359,7 +382,7 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri } Promise> getScope(String scopeId) { - GetPropertiesCommand params = new GetPropertiesCommand(); + var params = new GetPropertiesCommand(); params.setObjectId(scopeId); params.setOwnProperties(true); @@ -392,6 +415,20 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri }); } + Promise> getSpecialScope(String scopeId) { + var params = new GetPropertiesCommand(); + params.setObjectId(scopeId); + params.setOwnProperties(false); + + return callMethodAsync("Runtime.getProperties", GetPropertiesResponse.class, params) + .then(response -> { + if (response == null) { + return Collections.emptyList(); + } + return parseProperties(scopeId, response.getResult(), null); + }); + } + Promise getClassName(String objectId) { CallFunctionCommand params = new CallFunctionCommand(); CallArgumentDTO arg = new CallArgumentDTO(); @@ -407,6 +444,34 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri }); } + Promise getMemory(String objectId, int start, int count) { + var params = new CallFunctionCommand(); + params.setObjectId(objectId); + params.setArguments(new CallArgumentDTO[] { objArg(objectId), intArg(start), intArg(count) }); + params.setFunctionDeclaration("$dbg_memory"); + + return callMethodAsync("Runtime.callFunctionOn", CallFunctionResponse.class, params) + .then(response -> { + var result = response != null ? response.getResult() : null; + if (result.getValue() == null) { + return null; + } + return Base64.getDecoder().decode(result.getValue().textValue()); + }); + } + + private CallArgumentDTO objArg(String objectId) { + var arg = new CallArgumentDTO(); + arg.setObjectId(objectId); + return arg; + } + + private CallArgumentDTO intArg(int value) { + var arg = new CallArgumentDTO(); + arg.setValue(new IntNode(value)); + return arg; + } + Promise getRepresentation(String objectId) { CallFunctionCommand params = new CallFunctionCommand(); CallArgumentDTO arg = new CallArgumentDTO(); @@ -495,6 +560,7 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri String scopeId = null; RDPValue thisObject = null; RDPValue closure = null; + RDPValue module = null; for (ScopeDTO scope : dto.getScopeChain()) { switch (scope.getType()) { case "local": @@ -508,10 +574,14 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri thisObject = new RDPValue(this, scope.getObject().getDescription(), scope.getObject().getType(), scope.getObject().getObjectId(), true); break; + case "module": + module = new RDPValue(this, scope.getObject().getDescription(), scope.getObject().getType(), + scope.getObject().getObjectId(), true); + break; } } return new RDPCallFrame(this, dto.getCallFrameId(), map(dto.getLocation()), scopeId, - thisObject, closure); + thisObject, module, closure); } private JavaScriptLocation map(LocationDTO dto) { diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java index 84b330e6c..d99c26d0e 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPCallFrame.java @@ -29,16 +29,19 @@ class RDPCallFrame implements JavaScriptCallFrame { private JavaScriptLocation location; private Promise> variables; private JavaScriptValue thisObject; + private JavaScriptValue moduleObject; + private Promise memoryObject; private JavaScriptValue closure; private String scopeId; RDPCallFrame(ChromeRDPDebugger debugger, String chromeId, JavaScriptLocation location, String scopeId, - JavaScriptValue thisObject, JavaScriptValue closure) { + JavaScriptValue thisObject, JavaScriptValue moduleObject, JavaScriptValue closure) { this.debugger = debugger; this.chromeId = chromeId; this.location = location; this.scopeId = scopeId; this.thisObject = thisObject; + this.moduleObject = moduleObject; this.closure = closure; } @@ -73,4 +76,34 @@ class RDPCallFrame implements JavaScriptCallFrame { public JavaScriptValue getClosureVariable() { return closure; } + + private Promise getMemoryObject() { + if (memoryObject == null) { + memoryObject = getPropertyIfExists(moduleObject, "memories") + .thenAsync(x -> getPropertyIfExists(x, "$memory")) + .thenAsync(x -> getPropertyIfExists(x, "buffer")); + } + return memoryObject; + } + + @Override + public Promise getMemory(int address, int count) { + return getMemoryObject().thenAsync(buf -> buf != null + ? debugger.getMemory(buf.getInstanceId(), address, count) + : null); + } + + private Promise getPropertyIfExists(JavaScriptValue value, String name) { + if (value == null) { + return Promise.of(null); + } + return debugger.getSpecialScope(value.getInstanceId()).then(properties -> { + for (var property : properties) { + if (property.getName().equals(name)) { + return property.getValue(); + } + } + return null; + }); + } } diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java index 61f493842..ac77c942b 100644 --- a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/RDPValue.java @@ -42,6 +42,11 @@ class RDPValue implements JavaScriptValue { defaultRepresentation = representation; } + @Override + public String getSimpleRepresentation() { + return defaultRepresentation; + } + @Override public Promise getRepresentation() { if (representation == null) { diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/ExceptionDetailsDTO.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/ExceptionDetailsDTO.java new file mode 100644 index 000000000..be5f66a4f --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/data/ExceptionDetailsDTO.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.chromerdp.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExceptionDetailsDTO { + public int exceptionId; + public String text; +} diff --git a/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/messages/RunScriptResponse.java b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/messages/RunScriptResponse.java new file mode 100644 index 000000000..365297e3c --- /dev/null +++ b/tools/chrome-rdp/src/main/java/org/teavm/chromerdp/messages/RunScriptResponse.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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.chromerdp.messages; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.teavm.chromerdp.data.ExceptionDetailsDTO; +import org.teavm.chromerdp.data.RemoteObjectDTO; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class RunScriptResponse { + public RemoteObjectDTO result; + public ExceptionDetailsDTO exceptionDetails; +}