diff --git a/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js b/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js index 1570bf2aa..38300a970 100644 --- a/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js +++ b/core/src/main/resources/org/teavm/backend/wasm/wasm-gc-runtime.js @@ -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 } } diff --git a/jso/core/build.gradle.kts b/jso/core/build.gradle.kts index 4d0279616..eb6c274be 100644 --- a/jso/core/build.gradle.kts +++ b/jso/core/build.gradle.kts @@ -28,6 +28,7 @@ configurations { dependencies { "teavm"(project(":jso:impl")) + compileOnly(project(":interop:core")) } teavmPublish { diff --git a/jso/core/src/main/java/org/teavm/jso/JSExceptions.java b/jso/core/src/main/java/org/teavm/jso/JSExceptions.java index 26e259da1..f7525f0cb 100644 --- a/jso/core/src/main/java/org/teavm/jso/JSExceptions.java +++ b/jso/core/src/main/java/org/teavm/jso/JSExceptions.java @@ -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); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java index b3c5e590c..313cb6b13 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -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)) diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCExceptionWrapper.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCExceptionWrapper.java new file mode 100644 index 000000000..46e6bdfcd --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCExceptionWrapper.java @@ -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; + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSDependencies.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSDependencies.java index cb7f74379..80f80f6c8 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSDependencies.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSDependencies.java @@ -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(); + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java index 9ad78bc4d..ff206a548 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJSRuntime.java @@ -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; + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoCommonGenerator.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoCommonGenerator.java index a453081a8..e6fd4b064 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoCommonGenerator.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoCommonGenerator.java @@ -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> 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 part) { + void addInitializerPart(WasmGCJsoContext context, Consumer 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); + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoContext.java b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoContext.java index 22dff5a72..a29e014bf 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoContext.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/wasmgc/WasmGCJsoContext.java @@ -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 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 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 initializerContributor) { context.addToInitializer(initializerContributor); diff --git a/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java b/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java index 3266bfeb1..f0d8e6dd3 100644 --- a/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java +++ b/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java @@ -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 {