wasm gc: support JS exceptions

This commit is contained in:
Alexey Andreev 2024-10-04 11:49:17 +02:00
parent 1d47146f43
commit 3218a00eb9
10 changed files with 270 additions and 41 deletions

View File

@ -110,16 +110,17 @@ TeaVM.wasm = function() {
}
function jsoImports(imports) {
let javaObjectSymbol = Symbol("javaObject");
let functionsSymbol = Symbol("functions");
let functionOriginSymbol = Symbol("functionOrigin");
let javaExceptionSymbol = Symbol("javaException");
let jsWrappers = new WeakMap();
let javaWrappers = new WeakMap();
let primitiveWrappers = new Map();
let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token));
let hashCodes = new WeakMap();
let javaExceptionWrappers = new WeakMap();
let lastHashCode = 2463534242;
let nextHashCode = () => {
let x = lastHashCode;
@ -156,6 +157,53 @@ TeaVM.wasm = function() {
obj[prop] = value;
}
}
function javaExceptionToJs(e) {
if (e instanceof WebAssembly.Exception) {
let tag = exports["javaException"];
if (e.is(tag)) {
let javaException = e.getArg(tag, 0);
let extracted = extractException(javaException);
if (extracted !== null) {
return extracted;
}
let wrapperRef = javaExceptionWrappers.get(javaException);
if (typeof wrapperRef != "undefined") {
let wrapper = wrapperRef.deref();
if (typeof wrapper !== "undefined") {
return wrapper;
}
}
let wrapper = new Error();
javaExceptionWrappers.set(javaException, new WeakRef(wrapper));
wrapper[javaExceptionSymbol] = javaException;
return wrapper;
}
}
return e;
}
function jsExceptionAsJava(e) {
if (javaExceptionSymbol in e) {
return e[javaExceptionSymbol];
} else {
return exports["teavm.js.wrapException"](e);
}
}
function rethrowJsAsJava(e) {
exports["teavm.js.throwException"](jsExceptionAsJava(e));
}
function extractException(e) {
return exports["teavm.js.extractException"](e);
}
function rethrowJavaAsJs(e) {
throw javaExceptionToJs(e);
}
function getProperty(obj, prop) {
try {
return obj !== null ? obj[prop] : getGlobalName(prop)
} catch (e) {
rethrowJsAsJava(e);
}
}
imports.teavmJso = {
emptyString: () => "",
stringFromCharCode: code => String.fromCharCode(code),
@ -166,11 +214,17 @@ TeaVM.wasm = function() {
appendToArray: (array, e) => array.push(e),
unwrapBoolean: value => value ? 1 : 0,
wrapBoolean: value => !!value,
getProperty: (obj, prop) => obj !== null ? obj[prop] : getGlobalName(prop),
getPropertyPure: (obj, prop) => obj !== null ? obj[prop] : getGlobalName(prop),
getProperty: getProperty,
getPropertyPure: getProperty,
setProperty: setProperty,
setPropertyPure: setProperty,
global: getGlobalName,
global(name) {
try {
return getGlobalName(name);
} catch (e) {
rethrowJsAsJava(e);
}
},
createClass(name) {
let fn = new Function(
"javaObjectSymbol",
@ -184,18 +238,30 @@ TeaVM.wasm = function() {
},
defineMethod(cls, name, fn) {
cls.prototype[name] = function(...args) {
return fn(this, ...args);
try {
return fn(this, ...args);
} catch (e) {
rethrowJavaAsJs(e);
}
}
},
defineProperty(cls, name, getFn, setFn) {
let descriptor = {
get() {
return getFn(this);
try {
return getFn(this);
} catch (e) {
rethrowJavaAsJs(e);
}
}
};
if (setFn !== null) {
descriptor.set = function(value) {
setFn(this, value);
try {
setFn(this, value);
} catch (e) {
rethrowJavaAsJs(e);
}
}
}
Object.defineProperty(cls.prototype, name, descriptor);
@ -239,7 +305,15 @@ TeaVM.wasm = function() {
return origin;
}
}
return { [property]: fn };
return {
[property]: function(...args) {
try {
return fn(...args);
} catch (e) {
rethrowJavaAsJs(e);
}
}
};
},
wrapObject(obj) {
if (obj === null) {
@ -298,25 +372,47 @@ TeaVM.wasm = function() {
}
},
apply: (instance, method, args) => {
if (instance === null) {
let fn = getGlobalName(method);
return fn(...args);
} else {
return instance[method](...args);
try {
if (instance === null) {
let fn = getGlobalName(method);
return fn(...args);
} else {
return instance[method](...args);
}
} catch (e) {
rethrowJsAsJava(e);
}
},
concatArray: (a, b) => a.concat(b)
concatArray: (a, b) => a.concat(b),
getJavaException: e => e[javaExceptionSymbol]
};
for (let name of ["wrapByte", "wrapShort", "wrapChar", "wrapInt", "wrapFloat", "wrapDouble", "unwrapByte",
"unwrapShort", "unwrapChar", "unwrapInt", "unwrapFloat", "unwrapDouble"]) {
"unwrapShort", "unwrapChar", "unwrapInt", "unwrapFloat", "unwrapDouble"]) {
imports.teavmJso[name] = identity;
}
for (let i = 0; i < 32; ++i) {
imports.teavmJso["createFunction" + i] = (...args) => new Function(...args);
imports.teavmJso["callFunction" + i] = (fn, ...args) => fn(...args);
imports.teavmJso["callMethod" + i] = (instance, method, ...args) =>
instance !== null ? instance[method](...args) : getGlobalName(method)(...args);
imports.teavmJso["construct" + i] = (constructor, ...args) => new constructor(...args);
imports.teavmJso["callFunction" + i] = (fn, ...args) => {
try {
return fn(...args);
} catch (e) {
rethrowJsAsJava(e);
}
};
imports.teavmJso["callMethod" + i] = (instance, method, ...args) => {
try {
return instance !== null ? instance[method](...args) : getGlobalName(method)(...args);
} catch (e) {
rethrowJsAsJava(e);
}
}
imports.teavmJso["construct" + i] = (constructor, ...args) => {
try {
return new constructor(...args);
} catch (e) {
rethrowJsAsJava(e);
}
}
imports.teavmJso["arrayOf" + i] = (...args) => args
}
}

View File

@ -28,6 +28,7 @@ configurations {
dependencies {
"teavm"(project(":jso:impl"))
compileOnly(project(":interop:core"))
}
teavmPublish {

View File

@ -15,10 +15,13 @@
*/
package org.teavm.jso;
import org.teavm.interop.Import;
public final class JSExceptions {
private JSExceptions() {
}
@Import(name = "getJavaException", module = "teavmJso")
public static native Throwable getJavaException(JSObject e);
public static native JSObject getJSException(Throwable e);

View File

@ -68,7 +68,7 @@ class JSValueMarshaller {
String className = ((ValueType.Object) type).getClassName();
ClassReader cls = classSource.get(className);
if (cls != null && cls.getAnnotations().get(JSFunctor.class.getName()) != null) {
return wrapFunctor(location, var, cls);
return wrapFunctor(location, var, cls, jsType);
}
}
return wrap(var, type, jsType, location.getSourceLocation(), byRef);
@ -83,20 +83,22 @@ class JSValueMarshaller {
.count() == 1;
}
private Variable wrapFunctor(CallLocation location, Variable var, ClassReader type) {
private Variable wrapFunctor(CallLocation location, Variable var, ClassReader type, JSType jsType) {
if (!isProperFunctor(type)) {
diagnostics.error(location, "Wrong functor: {{c0}}", type.getName());
return var;
}
var unwrapNative = new InvokeInstruction();
unwrapNative.setLocation(location.getSourceLocation());
unwrapNative.setType(InvocationType.SPECIAL);
unwrapNative.setMethod(JSMethods.UNWRAP);
unwrapNative.setArguments(var);
unwrapNative.setReceiver(program.createVariable());
replacement.add(unwrapNative);
var = unwrapNative.getReceiver();
if (jsType == JSType.JAVA) {
var unwrapNative = new InvokeInstruction();
unwrapNative.setLocation(location.getSourceLocation());
unwrapNative.setType(InvocationType.SPECIAL);
unwrapNative.setMethod(JSMethods.UNWRAP);
unwrapNative.setArguments(var);
unwrapNative.setReceiver(program.createVariable());
replacement.add(unwrapNative);
var = unwrapNative.getReceiver();
}
String name = type.getMethods().stream()
.filter(method -> method.hasModifier(ElementModifier.ABSTRACT))

View File

@ -0,0 +1,35 @@
/*
* 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.jso.impl.wasmgc;
import org.teavm.jso.JSObject;
import org.teavm.jso.core.JSError;
class WasmGCExceptionWrapper extends RuntimeException {
final JSObject jsException;
WasmGCExceptionWrapper(JSObject jsException) {
this.jsException = jsException;
}
@Override
public String getMessage() {
var message = jsException instanceof JSError
? ((JSError) jsException).getMessage()
: jsException.toString();
return "(JavaScript) Error: " + message;
}
}

View File

@ -28,16 +28,7 @@ import org.teavm.model.MethodReference;
class WasmGCJSDependencies extends AbstractDependencyListener {
@Override
public void started(DependencyAgent agent) {
agent.linkMethod(STRING_TO_JS)
.propagate(1, agent.getType("java.lang.String"))
.use();
var jsToString = agent.linkMethod(JS_TO_STRING);
jsToString.getResult().propagate(agent.getType("java.lang.String"));
jsToString.use();
agent.linkMethod(new MethodReference(JSWrapper.class, "createWrapper", JSObject.class, Object.class))
.use();
reachUtilities(agent);
}
@Override
@ -46,6 +37,26 @@ class WasmGCJSDependencies extends AbstractDependencyListener {
if (method.getMethod().getName().equals("jsArrayItem")) {
method.getVariable(1).getArrayItem().connect(method.getResult());
}
} else if (method.getMethod().getOwnerName().equals(JSWrapper.class.getName())) {
if (method.getMethod().getName().equals("wrap")) {
agent.linkMethod(new MethodReference(JSWrapper.class, "createWrapper", JSObject.class, Object.class))
.use();
}
}
}
private void reachUtilities(DependencyAgent agent) {
agent.linkMethod(STRING_TO_JS)
.propagate(1, agent.getType("java.lang.String"))
.use();
var jsToString = agent.linkMethod(JS_TO_STRING);
jsToString.getResult().propagate(agent.getType("java.lang.String"));
jsToString.use();
agent.linkMethod(new MethodReference(WasmGCJSRuntime.class, "wrapException", JSObject.class, Throwable.class))
.use();
agent.linkMethod(new MethodReference(WasmGCJSRuntime.class, "extractException", Throwable.class,
JSObject.class)).use();
}
}

View File

@ -65,4 +65,12 @@ final class WasmGCJSRuntime {
@Import(name = "charAt", module = "teavmJso")
static native char charAt(JSObject str, int index);
static Throwable wrapException(JSObject obj) {
return new WasmGCExceptionWrapper(obj);
}
static JSObject extractException(Throwable e) {
return e instanceof WasmGCExceptionWrapper ? ((WasmGCExceptionWrapper) e).jsException : null;
}
}

View File

@ -22,20 +22,30 @@ import java.util.function.Consumer;
import org.teavm.backend.javascript.rendering.AstWriter;
import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmGlobal;
import org.teavm.backend.wasm.model.WasmLocal;
import org.teavm.backend.wasm.model.WasmType;
import org.teavm.backend.wasm.model.expression.WasmCall;
import org.teavm.backend.wasm.model.expression.WasmCast;
import org.teavm.backend.wasm.model.expression.WasmExpression;
import org.teavm.backend.wasm.model.expression.WasmExternConversion;
import org.teavm.backend.wasm.model.expression.WasmExternConversionType;
import org.teavm.backend.wasm.model.expression.WasmGetGlobal;
import org.teavm.backend.wasm.model.expression.WasmGetLocal;
import org.teavm.backend.wasm.model.expression.WasmNullConstant;
import org.teavm.backend.wasm.model.expression.WasmSetGlobal;
import org.teavm.backend.wasm.model.expression.WasmThrow;
import org.teavm.jso.JSObject;
import org.teavm.jso.impl.JSBodyAstEmitter;
import org.teavm.jso.impl.JSBodyBloatedEmitter;
import org.teavm.jso.impl.JSBodyEmitter;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;
class WasmGCJsoCommonGenerator {
private WasmGCJSFunctions jsFunctions;
private boolean initialized;
private List<Consumer<WasmFunction>> initializerParts = new ArrayList<>();
private boolean rethrowExported;
WasmGCJsoCommonGenerator(WasmGCJSFunctions jsFunctions) {
this.jsFunctions = jsFunctions;
@ -47,6 +57,7 @@ class WasmGCJsoCommonGenerator {
}
initialized = true;
context.addToInitializer(this::writeToInitializer);
exportRethrowException(context);
}
private void writeToInitializer(WasmFunction function) {
@ -55,7 +66,8 @@ class WasmGCJsoCommonGenerator {
}
}
void addInitializerPart(Consumer<WasmFunction> part) {
void addInitializerPart(WasmGCJsoContext context, Consumer<WasmFunction> part) {
initialize(context);
initializerParts.add(part);
}
@ -114,4 +126,37 @@ class WasmGCJsoCommonGenerator {
WasmExpression stringToJs(WasmGCJsoContext context, WasmExpression str) {
return new WasmCall(stringToJsFunction(context), str);
}
private void exportRethrowException(WasmGCJsoContext context) {
if (rethrowExported) {
return;
}
rethrowExported = true;
var fn = context.functions().forStaticMethod(new MethodReference(WasmGCJSRuntime.class, "wrapException",
JSObject.class, Throwable.class));
fn.setExportName("teavm.js.wrapException");
fn = context.functions().forStaticMethod(new MethodReference(WasmGCJSRuntime.class, "extractException",
Throwable.class, JSObject.class));
fn.setExportName("teavm.js.extractException");
createThrowExceptionFunction(context);
}
private void createThrowExceptionFunction(WasmGCJsoContext context) {
var fn = new WasmFunction(context.functionTypes().of(null, WasmType.Reference.EXTERN));
fn.setName(context.names().topLevel("teavm@throwException"));
fn.setExportName("teavm.js.throwException");
context.module().functions.add(fn);
var exceptionLocal = new WasmLocal(WasmType.Reference.EXTERN);
fn.add(exceptionLocal);
var asAny = new WasmExternConversion(WasmExternConversionType.EXTERN_TO_ANY, new WasmGetLocal(exceptionLocal));
var throwableType = (WasmType.Reference) context.typeMapper().mapType(ValueType.parse(Throwable.class));
var asThrowable = new WasmCast(asAny, throwableType);
var throwExpr = new WasmThrow(context.exceptionTag());
throwExpr.getArguments().add(asThrowable);
fn.getBody().add(throwExpr);
}
}

View File

@ -19,11 +19,13 @@ import java.util.function.Consumer;
import org.teavm.backend.wasm.BaseWasmFunctionRepository;
import org.teavm.backend.wasm.WasmFunctionTypes;
import org.teavm.backend.wasm.generate.gc.WasmGCNameProvider;
import org.teavm.backend.wasm.generate.gc.classes.WasmGCTypeMapper;
import org.teavm.backend.wasm.generate.gc.strings.WasmGCStringProvider;
import org.teavm.backend.wasm.generators.gc.WasmGCCustomGeneratorContext;
import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsicContext;
import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmModule;
import org.teavm.backend.wasm.model.WasmTag;
interface WasmGCJsoContext {
WasmModule module();
@ -36,6 +38,10 @@ interface WasmGCJsoContext {
WasmGCStringProvider strings();
WasmGCTypeMapper typeMapper();
WasmTag exceptionTag();
void addToInitializer(Consumer<WasmFunction> initializerContributor);
static WasmGCJsoContext wrap(WasmGCIntrinsicContext context) {
@ -65,6 +71,16 @@ interface WasmGCJsoContext {
return context.strings();
}
@Override
public WasmGCTypeMapper typeMapper() {
return context.typeMapper();
}
@Override
public WasmTag exceptionTag() {
return context.exceptionTag();
}
@Override
public void addToInitializer(Consumer<WasmFunction> initializerContributor) {
context.addToInitializer(initializerContributor);
@ -99,6 +115,16 @@ interface WasmGCJsoContext {
return context.strings();
}
@Override
public WasmGCTypeMapper typeMapper() {
return context.typeMapper();
}
@Override
public WasmTag exceptionTag() {
return context.exceptionTag();
}
@Override
public void addToInitializer(Consumer<WasmFunction> initializerContributor) {
context.addToInitializer(initializerContributor);

View File

@ -27,12 +27,13 @@ import org.teavm.jso.core.JSError;
import org.teavm.junit.EachTestCompiledSeparately;
import org.teavm.junit.OnlyPlatform;
import org.teavm.junit.SkipJVM;
import org.teavm.junit.SkipPlatform;
import org.teavm.junit.TeaVMTestRunner;
import org.teavm.junit.TestPlatform;
@RunWith(TeaVMTestRunner.class)
@SkipJVM
@OnlyPlatform(TestPlatform.JAVASCRIPT)
@OnlyPlatform({TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY_GC})
@EachTestCompiledSeparately
public class ExceptionsTest {
@Test
@ -102,6 +103,7 @@ public class ExceptionsTest {
}
@Test
@SkipPlatform(TestPlatform.WEBASSEMBLY_GC)
public void catchNativeExceptionAsRuntimeException() {
StringBuilder sb = new StringBuilder();
try {