From a3dfc0c48696195194622ff69f365eb630c55c15 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 23 Nov 2018 19:54:37 +0300 Subject: [PATCH] Add API to catch native JS exceptions --- .../org/teavm/backend/javascript/runtime.js | 8 ++ .../main/java/org/teavm/jso/core/JSError.java | 53 ++++++++ .../main/java/org/teavm/jso/JSExceptions.java | 25 ++++ .../org/teavm/jso/impl/JSBodyInlineUtil.java | 3 - .../org/teavm/jso/impl/JSBodyRepository.java | 1 - .../impl/JSExceptionsDependencyListener.java | 51 ++++++++ .../teavm/jso/impl/JSExceptionsGenerator.java | 39 ++++++ .../java/org/teavm/jso/impl/JSOPlugin.java | 10 ++ .../org/teavm/jso/test/ExceptionsTest.java | 123 ++++++++++++++++++ 9 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSError.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSExceptions.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsDependencyListener.java create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsGenerator.java create mode 100644 tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index c0a0d55ae..022021dda 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -611,6 +611,14 @@ function $rt_intBitsToFloat(n) { return $rt_numberConversionView.getFloat32(0); } +function $rt_javaException(e) { + return e instanceof Error && typeof e.$javaException === 'object' ? e.$javaException : null; +} + +function $rt_jsException(e) { + return typeof e.$jsException === 'object' ? e.$jsException : null; +} + function $dbg_class(obj) { var cls = obj.constructor; var arrayDegree = 0; diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSError.java b/jso/apis/src/main/java/org/teavm/jso/core/JSError.java new file mode 100644 index 000000000..5883eb14e --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSError.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 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.core; + +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; + +public abstract class JSError implements JSObject { + @JSBody(params = { "tryClause", "catchClause" }, script = "" + + "try {" + + "return tryClause();" + + "} catch (e) {" + + "return catchClause(e);" + + "}") + public static native T catchNative(TryClause tryClause, CatchClause catchClause); + + @JSBody(params = "object", script = "return object instanceof Error;") + public static native boolean isError(JSObject object); + + @JSProperty + public abstract String getStack(); + + @JSProperty + public abstract String getMessage(); + + @JSProperty + public abstract String getName(); + + @JSFunctor + public interface TryClause extends JSObject { + T run(); + } + + @JSFunctor + public interface CatchClause extends JSObject { + T accept(JSObject e); + } +} diff --git a/jso/core/src/main/java/org/teavm/jso/JSExceptions.java b/jso/core/src/main/java/org/teavm/jso/JSExceptions.java new file mode 100644 index 000000000..26e259da1 --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSExceptions.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 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; + +public final class JSExceptions { + private JSExceptions() { + } + + 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/JSBodyInlineUtil.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyInlineUtil.java index 206d1fef8..6f194b44c 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyInlineUtil.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyInlineUtil.java @@ -23,7 +23,6 @@ import org.mozilla.javascript.ast.ExpressionStatement; import org.mozilla.javascript.ast.Name; import org.mozilla.javascript.ast.NodeVisitor; import org.mozilla.javascript.ast.ReturnStatement; -import org.mozilla.javascript.ast.ThrowStatement; import org.teavm.model.MethodReference; import org.teavm.model.ValueType; @@ -63,8 +62,6 @@ final class JSBodyInlineUtil { if (method.getReturnType() == ValueType.VOID) { if (statement instanceof ExpressionStatement) { return ((ExpressionStatement) statement).getExpression(); - } else if (statement instanceof ThrowStatement) { - return ((ThrowStatement) statement).getExpression(); } } else { if (statement instanceof ReturnStatement) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java index 5580eafed..24b803226 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSBodyRepository.java @@ -28,5 +28,4 @@ class JSBodyRepository { public final Set inlineMethods = new HashSet<>(); public final Map callbackCallees = new HashMap<>(); public final Map> callbackMethods = new HashMap<>(); - public final Map> callbackMethodsDeps = new HashMap<>(); } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsDependencyListener.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsDependencyListener.java new file mode 100644 index 000000000..60e84b743 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsDependencyListener.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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; + +import org.teavm.dependency.AbstractDependencyListener; +import org.teavm.dependency.DependencyAgent; +import org.teavm.dependency.DependencyNode; +import org.teavm.dependency.MethodDependency; +import org.teavm.jso.JSExceptions; +import org.teavm.model.CallLocation; + +public class JSExceptionsDependencyListener extends AbstractDependencyListener { + private DependencyNode allExceptions; + + @Override + public void started(DependencyAgent agent) { + allExceptions = agent.createNode(); + } + + @Override + public void methodReached(DependencyAgent agent, MethodDependency method, CallLocation location) { + if (method.getReference().getClassName().equals(JSExceptions.class.getName())) { + if (method.getReference().getName().equals("getJavaException")) { + allExceptions.connect(method.getResult()); + } + } else if (method.getReference().getClassName().equals(JS.class.getName())) { + switch (method.getReference().getName()) { + case "get": + case "set": + case "invoke": + allExceptions.connect(method.getThrown()); + break; + } + } else { + method.getThrown().connect(allExceptions); + } + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsGenerator.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsGenerator.java new file mode 100644 index 000000000..8a4936828 --- /dev/null +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSExceptionsGenerator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 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; + +import java.io.IOException; +import org.teavm.backend.javascript.spi.Injector; +import org.teavm.backend.javascript.spi.InjectorContext; +import org.teavm.model.MethodReference; + +public class JSExceptionsGenerator implements Injector { + @Override + public void generate(InjectorContext context, MethodReference methodRef) throws IOException { + switch (methodRef.getName()) { + case "getJavaException": + context.getWriter().append("$rt_javaException("); + context.writeExpr(context.getArgument(0)); + context.getWriter().append(")"); + break; + case "getJSException": + context.getWriter().append("$rt_jsException("); + context.writeExpr(context.getArgument(0)); + context.getWriter().append(")"); + break; + } + } +} diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java index 08f62c680..e9dbf0aa4 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSOPlugin.java @@ -16,6 +16,9 @@ package org.teavm.jso.impl; import org.teavm.backend.javascript.TeaVMJavaScriptHost; +import org.teavm.jso.JSExceptions; +import org.teavm.jso.JSObject; +import org.teavm.model.MethodReference; import org.teavm.vm.TeaVMPluginUtil; import org.teavm.vm.spi.TeaVMHost; import org.teavm.vm.spi.TeaVMPlugin; @@ -34,6 +37,7 @@ public class JSOPlugin implements TeaVMPlugin { JSDependencyListener dependencyListener = new JSDependencyListener(repository); JSAliasRenderer aliasRenderer = new JSAliasRenderer(); host.add(dependencyListener); + host.add(new JSExceptionsDependencyListener()); jsHost.add(aliasRenderer); jsHost.addGeneratorProvider(new GeneratorAnnotationInstaller<>(new JSBodyGenerator(), @@ -42,6 +46,12 @@ public class JSOPlugin implements TeaVMPlugin { DynamicInjector.class.getName())); jsHost.addVirtualMethods(aliasRenderer); + JSExceptionsGenerator exceptionsGenerator = new JSExceptionsGenerator(); + jsHost.add(new MethodReference(JSExceptions.class, "getJavaException", JSObject.class, Throwable.class), + exceptionsGenerator); + jsHost.add(new MethodReference(JSExceptions.class, "getJSException", Throwable.class, JSObject.class), + exceptionsGenerator); + TeaVMPluginUtil.handleNatives(host, JS.class); } } diff --git a/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java b/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java new file mode 100644 index 000000000..c3de654bf --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/ExceptionsTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2018 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.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSExceptions; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; +import org.teavm.jso.core.JSError; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +@SkipJVM +public class ExceptionsTest { + @Test + public void throwExceptionThroughJSCode() { + JSRunnable[] actions = new JSRunnable[] { + () -> { + throw new CustomException1(); + }, + () -> { + throw new CustomException2(); + } + }; + + StringBuilder sb = new StringBuilder(); + for (JSRunnable action : actions) { + try { + runJsCode(action); + } catch (RuntimeException e) { + sb.append(e.getMessage()); + } + } + + assertEquals("foobar", sb.toString()); + } + + @Test + public void catchNativeException() { + StringBuilder sb = new StringBuilder(); + JSError.catchNative(() -> { + throwNativeException(); + return null; + }, e -> { + sb.append("caught"); + assertTrue("Should catch Error", JSError.isError(e)); + + JSError error = (JSError) e; + assertEquals("foo", error.getMessage()); + return null; + }); + assertEquals("caught", sb.toString()); + } + + @Test + public void catchThrowableAsNativeException() { + JSRunnable[] actions = new JSRunnable[] { + () -> { + throw new CustomException1(); + }, + () -> { + throw new CustomException2(); + } + }; + + StringBuilder sb = new StringBuilder(); + for (JSRunnable action : actions) { + JSError.catchNative(() -> { + runJsCode(action); + return null; + }, e -> { + Throwable t = JSExceptions.getJavaException(e); + sb.append(t.getMessage()); + return null; + }); + } + + assertEquals("foobar", sb.toString()); + } + + @JSBody(params = "runnable", script = "runnable();") + private static native void runJsCode(JSRunnable runnable); + + @JSBody(script = "throw new Error('foo');") + private static native void throwNativeException(); + + @JSFunctor + interface JSRunnable extends JSObject { + void run(); + } + + static class CustomException1 extends RuntimeException { + @Override + public String getMessage() { + return "foo"; + } + } + + static class CustomException2 extends RuntimeException { + @Override + public String getMessage() { + return "bar"; + } + } +}