Add API to catch native JS exceptions

This commit is contained in:
Alexey Andreev 2018-11-23 19:54:37 +03:00
parent cf9090e0fa
commit a3dfc0c486
9 changed files with 309 additions and 4 deletions

View File

@ -611,6 +611,14 @@ function $rt_intBitsToFloat(n) {
return $rt_numberConversionView.getFloat32(0); 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) { function $dbg_class(obj) {
var cls = obj.constructor; var cls = obj.constructor;
var arrayDegree = 0; var arrayDegree = 0;

View File

@ -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 extends JSObject> T catchNative(TryClause<T> tryClause, CatchClause<T> 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<T extends JSObject> extends JSObject {
T run();
}
@JSFunctor
public interface CatchClause<T extends JSObject> extends JSObject {
T accept(JSObject e);
}
}

View File

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

View File

@ -23,7 +23,6 @@ import org.mozilla.javascript.ast.ExpressionStatement;
import org.mozilla.javascript.ast.Name; import org.mozilla.javascript.ast.Name;
import org.mozilla.javascript.ast.NodeVisitor; import org.mozilla.javascript.ast.NodeVisitor;
import org.mozilla.javascript.ast.ReturnStatement; import org.mozilla.javascript.ast.ReturnStatement;
import org.mozilla.javascript.ast.ThrowStatement;
import org.teavm.model.MethodReference; import org.teavm.model.MethodReference;
import org.teavm.model.ValueType; import org.teavm.model.ValueType;
@ -63,8 +62,6 @@ final class JSBodyInlineUtil {
if (method.getReturnType() == ValueType.VOID) { if (method.getReturnType() == ValueType.VOID) {
if (statement instanceof ExpressionStatement) { if (statement instanceof ExpressionStatement) {
return ((ExpressionStatement) statement).getExpression(); return ((ExpressionStatement) statement).getExpression();
} else if (statement instanceof ThrowStatement) {
return ((ThrowStatement) statement).getExpression();
} }
} else { } else {
if (statement instanceof ReturnStatement) { if (statement instanceof ReturnStatement) {

View File

@ -28,5 +28,4 @@ class JSBodyRepository {
public final Set<MethodReference> inlineMethods = new HashSet<>(); public final Set<MethodReference> inlineMethods = new HashSet<>();
public final Map<MethodReference, MethodReference> callbackCallees = new HashMap<>(); public final Map<MethodReference, MethodReference> callbackCallees = new HashMap<>();
public final Map<MethodReference, Set<MethodReference>> callbackMethods = new HashMap<>(); public final Map<MethodReference, Set<MethodReference>> callbackMethods = new HashMap<>();
public final Map<MethodReference, Set<MethodReference>> callbackMethodsDeps = new HashMap<>();
} }

View File

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

View File

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

View File

@ -16,6 +16,9 @@
package org.teavm.jso.impl; package org.teavm.jso.impl;
import org.teavm.backend.javascript.TeaVMJavaScriptHost; 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.TeaVMPluginUtil;
import org.teavm.vm.spi.TeaVMHost; import org.teavm.vm.spi.TeaVMHost;
import org.teavm.vm.spi.TeaVMPlugin; import org.teavm.vm.spi.TeaVMPlugin;
@ -34,6 +37,7 @@ public class JSOPlugin implements TeaVMPlugin {
JSDependencyListener dependencyListener = new JSDependencyListener(repository); JSDependencyListener dependencyListener = new JSDependencyListener(repository);
JSAliasRenderer aliasRenderer = new JSAliasRenderer(); JSAliasRenderer aliasRenderer = new JSAliasRenderer();
host.add(dependencyListener); host.add(dependencyListener);
host.add(new JSExceptionsDependencyListener());
jsHost.add(aliasRenderer); jsHost.add(aliasRenderer);
jsHost.addGeneratorProvider(new GeneratorAnnotationInstaller<>(new JSBodyGenerator(), jsHost.addGeneratorProvider(new GeneratorAnnotationInstaller<>(new JSBodyGenerator(),
@ -42,6 +46,12 @@ public class JSOPlugin implements TeaVMPlugin {
DynamicInjector.class.getName())); DynamicInjector.class.getName()));
jsHost.addVirtualMethods(aliasRenderer); 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); TeaVMPluginUtil.handleNatives(host, JS.class);
} }
} }

View File

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