Wasm: support displaying objects in debugger

This commit is contained in:
Alexey Andreev 2022-12-20 10:01:47 +01:00
parent 7b3905246b
commit 9a9e7561b7
14 changed files with 847 additions and 100 deletions

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -140,8 +140,8 @@ public class Promise<T> {
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<T> {
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<T> {
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<T> {
this.value = value;
if (thenList != null) {
List<Then<T>> list = thenList;
var list = thenList;
thenList = null;
for (Then<T> then : list) {
for (var then : list) {
if (then.promise) {
passValueAsync((Function<T, Promise<Object>>) then.f, (Promise<Object>) then.target);
} else {

View File

@ -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<String, ? extends JavaScriptVariable> 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);
}

View File

@ -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<String> getRepresentation() {
return jsValue.getRepresentation();
}
@Override
Promise<String> 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<Map<String, Variable>> 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<String, Variable>();
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<String, Variable> fillArray(Map<String, ? extends JavaScriptVariable> jsVariables) {
var vars = new HashMap<String, Variable>();
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<Boolean> hasInnerStructure() {
return getType().then(value -> !value.equals("long") && jsValue.hasInnerStructure());
}
@Override
public Promise<String> getInstanceId() {
return getType().then(value -> value.equals("long") ? null : jsValue.getInstanceId());
}
@Override
public JavaScriptValue getOriginalValue() {
return jsValue;
}
}

View File

@ -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<Map<String, Variable>> properties;
private Promise<String> 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<String> getRepresentation() {
return jsValue.getRepresentation();
}
public abstract Promise<String> getRepresentation();
public Promise<String> 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<String> prepareType();
public Promise<Map<String, Variable>> 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<String, Variable> vars = new HashMap<>();
for (Map.Entry<String, ? extends JavaScriptVariable> 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<String, Variable> fillArray(Map<String, ? extends JavaScriptVariable> jsVariables) {
Map<String, Variable> vars = new HashMap<>();
for (Map.Entry<String, ? extends JavaScriptVariable> 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<Map<String, Variable>> prepareProperties();
public Promise<Boolean> hasInnerStructure() {
return getType().then(value -> !value.equals("long") && jsValue.hasInnerStructure());
}
public abstract Promise<Boolean> hasInnerStructure();
public Promise<String> getInstanceId() {
return getType().then(value -> value.equals("long") ? null : jsValue.getInstanceId());
}
public abstract Promise<String> getInstanceId();
public JavaScriptValue getOriginalValue() {
return jsValue;
}
public abstract JavaScriptValue getOriginalValue();
}

View File

@ -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<TypeLayout> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<TypeLayout> 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<String> 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<Map<String, Variable>> 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<Map<String, Variable>> 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<String, Variable>();
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<Map<String, Variable>> 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<String, Variable>();
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<String, Variable> 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<Boolean> hasInnerStructure() {
return getCalculatedType().then(type -> {
switch (type.kind()) {
case CLASS:
case ARRAY:
return true;
default:
return false;
}
});
}
@Override
public Promise<String> getInstanceId() {
return Promise.of(null);
}
@Override
public JavaScriptValue getOriginalValue() {
return null;
}
}

View File

@ -28,4 +28,6 @@ public interface JavaScriptCallFrame {
JavaScriptValue getThisVariable();
JavaScriptValue getClosureVariable();
Promise<byte[]> getMemory(int address, int count);
}

View File

@ -19,6 +19,8 @@ import java.util.Map;
import org.teavm.common.Promise;
public interface JavaScriptValue {
String getSimpleRepresentation();
Promise<String> getRepresentation();
Promise<String> getClassName();

View File

@ -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<Void> 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<Void> 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<List<RDPLocalVariable>> 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<List<RDPLocalVariable>> 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<String> getClassName(String objectId) {
CallFunctionCommand params = new CallFunctionCommand();
CallArgumentDTO arg = new CallArgumentDTO();
@ -407,6 +444,34 @@ public class ChromeRDPDebugger extends BaseChromeRDPDebugger implements JavaScri
});
}
Promise<byte[]> 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<String> 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) {

View File

@ -29,16 +29,19 @@ class RDPCallFrame implements JavaScriptCallFrame {
private JavaScriptLocation location;
private Promise<Map<String, ? extends JavaScriptVariable>> variables;
private JavaScriptValue thisObject;
private JavaScriptValue moduleObject;
private Promise<JavaScriptValue> 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<JavaScriptValue> getMemoryObject() {
if (memoryObject == null) {
memoryObject = getPropertyIfExists(moduleObject, "memories")
.thenAsync(x -> getPropertyIfExists(x, "$memory"))
.thenAsync(x -> getPropertyIfExists(x, "buffer"));
}
return memoryObject;
}
@Override
public Promise<byte[]> getMemory(int address, int count) {
return getMemoryObject().thenAsync(buf -> buf != null
? debugger.getMemory(buf.getInstanceId(), address, count)
: null);
}
private Promise<JavaScriptValue> 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;
});
}
}

View File

@ -42,6 +42,11 @@ class RDPValue implements JavaScriptValue {
defaultRepresentation = representation;
}
@Override
public String getSimpleRepresentation() {
return defaultRepresentation;
}
@Override
public Promise<String> getRepresentation() {
if (representation == null) {

View File

@ -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;
}

View File

@ -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;
}