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) { function jsoImports(imports) {
let javaObjectSymbol = Symbol("javaObject"); let javaObjectSymbol = Symbol("javaObject");
let functionsSymbol = Symbol("functions"); let functionsSymbol = Symbol("functions");
let functionOriginSymbol = Symbol("functionOrigin"); let functionOriginSymbol = Symbol("functionOrigin");
let javaExceptionSymbol = Symbol("javaException");
let jsWrappers = new WeakMap(); let jsWrappers = new WeakMap();
let javaWrappers = new WeakMap(); let javaWrappers = new WeakMap();
let primitiveWrappers = new Map(); let primitiveWrappers = new Map();
let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token)); let primitiveFinalization = new FinalizationRegistry(token => primitiveFinalization.delete(token));
let hashCodes = new WeakMap(); let hashCodes = new WeakMap();
let javaExceptionWrappers = new WeakMap();
let lastHashCode = 2463534242; let lastHashCode = 2463534242;
let nextHashCode = () => { let nextHashCode = () => {
let x = lastHashCode; let x = lastHashCode;
@ -156,6 +157,53 @@ TeaVM.wasm = function() {
obj[prop] = value; 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 = { imports.teavmJso = {
emptyString: () => "", emptyString: () => "",
stringFromCharCode: code => String.fromCharCode(code), stringFromCharCode: code => String.fromCharCode(code),
@ -166,11 +214,17 @@ TeaVM.wasm = function() {
appendToArray: (array, e) => array.push(e), appendToArray: (array, e) => array.push(e),
unwrapBoolean: value => value ? 1 : 0, unwrapBoolean: value => value ? 1 : 0,
wrapBoolean: value => !!value, wrapBoolean: value => !!value,
getProperty: (obj, prop) => obj !== null ? obj[prop] : getGlobalName(prop), getProperty: getProperty,
getPropertyPure: (obj, prop) => obj !== null ? obj[prop] : getGlobalName(prop), getPropertyPure: getProperty,
setProperty: setProperty, setProperty: setProperty,
setPropertyPure: setProperty, setPropertyPure: setProperty,
global: getGlobalName, global(name) {
try {
return getGlobalName(name);
} catch (e) {
rethrowJsAsJava(e);
}
},
createClass(name) { createClass(name) {
let fn = new Function( let fn = new Function(
"javaObjectSymbol", "javaObjectSymbol",
@ -184,18 +238,30 @@ TeaVM.wasm = function() {
}, },
defineMethod(cls, name, fn) { defineMethod(cls, name, fn) {
cls.prototype[name] = function(...args) { cls.prototype[name] = function(...args) {
return fn(this, ...args); try {
return fn(this, ...args);
} catch (e) {
rethrowJavaAsJs(e);
}
} }
}, },
defineProperty(cls, name, getFn, setFn) { defineProperty(cls, name, getFn, setFn) {
let descriptor = { let descriptor = {
get() { get() {
return getFn(this); try {
return getFn(this);
} catch (e) {
rethrowJavaAsJs(e);
}
} }
}; };
if (setFn !== null) { if (setFn !== null) {
descriptor.set = function(value) { descriptor.set = function(value) {
setFn(this, value); try {
setFn(this, value);
} catch (e) {
rethrowJavaAsJs(e);
}
} }
} }
Object.defineProperty(cls.prototype, name, descriptor); Object.defineProperty(cls.prototype, name, descriptor);
@ -239,7 +305,15 @@ TeaVM.wasm = function() {
return origin; return origin;
} }
} }
return { [property]: fn }; return {
[property]: function(...args) {
try {
return fn(...args);
} catch (e) {
rethrowJavaAsJs(e);
}
}
};
}, },
wrapObject(obj) { wrapObject(obj) {
if (obj === null) { if (obj === null) {
@ -298,25 +372,47 @@ TeaVM.wasm = function() {
} }
}, },
apply: (instance, method, args) => { apply: (instance, method, args) => {
if (instance === null) { try {
let fn = getGlobalName(method); if (instance === null) {
return fn(...args); let fn = getGlobalName(method);
} else { return fn(...args);
return instance[method](...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", 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; imports.teavmJso[name] = identity;
} }
for (let i = 0; i < 32; ++i) { for (let i = 0; i < 32; ++i) {
imports.teavmJso["createFunction" + i] = (...args) => new Function(...args); imports.teavmJso["createFunction" + i] = (...args) => new Function(...args);
imports.teavmJso["callFunction" + i] = (fn, ...args) => fn(...args); imports.teavmJso["callFunction" + i] = (fn, ...args) => {
imports.teavmJso["callMethod" + i] = (instance, method, ...args) => try {
instance !== null ? instance[method](...args) : getGlobalName(method)(...args); return fn(...args);
imports.teavmJso["construct" + i] = (constructor, ...args) => new constructor(...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 imports.teavmJso["arrayOf" + i] = (...args) => args
} }
} }

View File

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

View File

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

View File

@ -68,7 +68,7 @@ class JSValueMarshaller {
String className = ((ValueType.Object) type).getClassName(); String className = ((ValueType.Object) type).getClassName();
ClassReader cls = classSource.get(className); ClassReader cls = classSource.get(className);
if (cls != null && cls.getAnnotations().get(JSFunctor.class.getName()) != null) { 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); return wrap(var, type, jsType, location.getSourceLocation(), byRef);
@ -83,20 +83,22 @@ class JSValueMarshaller {
.count() == 1; .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)) { if (!isProperFunctor(type)) {
diagnostics.error(location, "Wrong functor: {{c0}}", type.getName()); diagnostics.error(location, "Wrong functor: {{c0}}", type.getName());
return var; return var;
} }
var unwrapNative = new InvokeInstruction(); if (jsType == JSType.JAVA) {
unwrapNative.setLocation(location.getSourceLocation()); var unwrapNative = new InvokeInstruction();
unwrapNative.setType(InvocationType.SPECIAL); unwrapNative.setLocation(location.getSourceLocation());
unwrapNative.setMethod(JSMethods.UNWRAP); unwrapNative.setType(InvocationType.SPECIAL);
unwrapNative.setArguments(var); unwrapNative.setMethod(JSMethods.UNWRAP);
unwrapNative.setReceiver(program.createVariable()); unwrapNative.setArguments(var);
replacement.add(unwrapNative); unwrapNative.setReceiver(program.createVariable());
var = unwrapNative.getReceiver(); replacement.add(unwrapNative);
var = unwrapNative.getReceiver();
}
String name = type.getMethods().stream() String name = type.getMethods().stream()
.filter(method -> method.hasModifier(ElementModifier.ABSTRACT)) .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 { class WasmGCJSDependencies extends AbstractDependencyListener {
@Override @Override
public void started(DependencyAgent agent) { public void started(DependencyAgent agent) {
agent.linkMethod(STRING_TO_JS) reachUtilities(agent);
.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();
} }
@Override @Override
@ -46,6 +37,26 @@ class WasmGCJSDependencies extends AbstractDependencyListener {
if (method.getMethod().getName().equals("jsArrayItem")) { if (method.getMethod().getName().equals("jsArrayItem")) {
method.getVariable(1).getArrayItem().connect(method.getResult()); 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") @Import(name = "charAt", module = "teavmJso")
static native char charAt(JSObject str, int index); 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.javascript.rendering.AstWriter;
import org.teavm.backend.wasm.model.WasmFunction; import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmGlobal; 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.WasmType;
import org.teavm.backend.wasm.model.expression.WasmCall; 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.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.WasmGetGlobal;
import org.teavm.backend.wasm.model.expression.WasmGetLocal;
import org.teavm.backend.wasm.model.expression.WasmNullConstant; import org.teavm.backend.wasm.model.expression.WasmNullConstant;
import org.teavm.backend.wasm.model.expression.WasmSetGlobal; 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.JSBodyAstEmitter;
import org.teavm.jso.impl.JSBodyBloatedEmitter; import org.teavm.jso.impl.JSBodyBloatedEmitter;
import org.teavm.jso.impl.JSBodyEmitter; import org.teavm.jso.impl.JSBodyEmitter;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;
class WasmGCJsoCommonGenerator { class WasmGCJsoCommonGenerator {
private WasmGCJSFunctions jsFunctions; private WasmGCJSFunctions jsFunctions;
private boolean initialized; private boolean initialized;
private List<Consumer<WasmFunction>> initializerParts = new ArrayList<>(); private List<Consumer<WasmFunction>> initializerParts = new ArrayList<>();
private boolean rethrowExported;
WasmGCJsoCommonGenerator(WasmGCJSFunctions jsFunctions) { WasmGCJsoCommonGenerator(WasmGCJSFunctions jsFunctions) {
this.jsFunctions = jsFunctions; this.jsFunctions = jsFunctions;
@ -47,6 +57,7 @@ class WasmGCJsoCommonGenerator {
} }
initialized = true; initialized = true;
context.addToInitializer(this::writeToInitializer); context.addToInitializer(this::writeToInitializer);
exportRethrowException(context);
} }
private void writeToInitializer(WasmFunction function) { 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); initializerParts.add(part);
} }
@ -114,4 +126,37 @@ class WasmGCJsoCommonGenerator {
WasmExpression stringToJs(WasmGCJsoContext context, WasmExpression str) { WasmExpression stringToJs(WasmGCJsoContext context, WasmExpression str) {
return new WasmCall(stringToJsFunction(context), 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.BaseWasmFunctionRepository;
import org.teavm.backend.wasm.WasmFunctionTypes; import org.teavm.backend.wasm.WasmFunctionTypes;
import org.teavm.backend.wasm.generate.gc.WasmGCNameProvider; 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.generate.gc.strings.WasmGCStringProvider;
import org.teavm.backend.wasm.generators.gc.WasmGCCustomGeneratorContext; import org.teavm.backend.wasm.generators.gc.WasmGCCustomGeneratorContext;
import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsicContext; import org.teavm.backend.wasm.intrinsics.gc.WasmGCIntrinsicContext;
import org.teavm.backend.wasm.model.WasmFunction; import org.teavm.backend.wasm.model.WasmFunction;
import org.teavm.backend.wasm.model.WasmModule; import org.teavm.backend.wasm.model.WasmModule;
import org.teavm.backend.wasm.model.WasmTag;
interface WasmGCJsoContext { interface WasmGCJsoContext {
WasmModule module(); WasmModule module();
@ -36,6 +38,10 @@ interface WasmGCJsoContext {
WasmGCStringProvider strings(); WasmGCStringProvider strings();
WasmGCTypeMapper typeMapper();
WasmTag exceptionTag();
void addToInitializer(Consumer<WasmFunction> initializerContributor); void addToInitializer(Consumer<WasmFunction> initializerContributor);
static WasmGCJsoContext wrap(WasmGCIntrinsicContext context) { static WasmGCJsoContext wrap(WasmGCIntrinsicContext context) {
@ -65,6 +71,16 @@ interface WasmGCJsoContext {
return context.strings(); return context.strings();
} }
@Override
public WasmGCTypeMapper typeMapper() {
return context.typeMapper();
}
@Override
public WasmTag exceptionTag() {
return context.exceptionTag();
}
@Override @Override
public void addToInitializer(Consumer<WasmFunction> initializerContributor) { public void addToInitializer(Consumer<WasmFunction> initializerContributor) {
context.addToInitializer(initializerContributor); context.addToInitializer(initializerContributor);
@ -99,6 +115,16 @@ interface WasmGCJsoContext {
return context.strings(); return context.strings();
} }
@Override
public WasmGCTypeMapper typeMapper() {
return context.typeMapper();
}
@Override
public WasmTag exceptionTag() {
return context.exceptionTag();
}
@Override @Override
public void addToInitializer(Consumer<WasmFunction> initializerContributor) { public void addToInitializer(Consumer<WasmFunction> initializerContributor) {
context.addToInitializer(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.EachTestCompiledSeparately;
import org.teavm.junit.OnlyPlatform; import org.teavm.junit.OnlyPlatform;
import org.teavm.junit.SkipJVM; import org.teavm.junit.SkipJVM;
import org.teavm.junit.SkipPlatform;
import org.teavm.junit.TeaVMTestRunner; import org.teavm.junit.TeaVMTestRunner;
import org.teavm.junit.TestPlatform; import org.teavm.junit.TestPlatform;
@RunWith(TeaVMTestRunner.class) @RunWith(TeaVMTestRunner.class)
@SkipJVM @SkipJVM
@OnlyPlatform(TestPlatform.JAVASCRIPT) @OnlyPlatform({TestPlatform.JAVASCRIPT, TestPlatform.WEBASSEMBLY_GC})
@EachTestCompiledSeparately @EachTestCompiledSeparately
public class ExceptionsTest { public class ExceptionsTest {
@Test @Test
@ -102,6 +103,7 @@ public class ExceptionsTest {
} }
@Test @Test
@SkipPlatform(TestPlatform.WEBASSEMBLY_GC)
public void catchNativeExceptionAsRuntimeException() { public void catchNativeExceptionAsRuntimeException() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
try { try {