Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.koushikdutta.quack.QuackContext Maven / Gradle / Ivy
Go to download
The quickjs embeddable Javascript engine packaged for Java.
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 com.koushikdutta.quack;
import static java.lang.System.getProperty;
import static java.nio.file.Files.copy;
import static java.nio.file.Files.createDirectory;
import static java.nio.file.Files.createFile;
import static java.nio.file.Files.exists;
import static java.nio.file.Paths.get;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Locale.ENGLISH;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
/** A simple EMCAScript (Javascript) interpreter. */
@SuppressWarnings({"unchecked", "rawtypes"})
public final class QuackContext implements Closeable {
// mapped java objects are held as weak keys to strong javascript object references.
// ie, a js ArrayBuffer or Uint8Array value will be mapped from the java DirectByteBuffer key.
private final WeakExactHashMap nativeMappings = new WeakExactHashMap<>();
private final Map JavaScriptToJavaCoercions = new LinkedHashMap<>();
private final Map JavaToJavascriptCoercions = new LinkedHashMap<>();
final Map JavaScriptToJavaMethodCoercions = new LinkedHashMap<>();
final Map JavaToJavascriptMethodCoercions = new LinkedHashMap<>();
private QuackInvocationHandlerWrapper invocationHandlerWrapper;
private static boolean loaded = false;
private static final String OS_NAME = getProperty("os.name").toLowerCase(ENGLISH);
// temporary directory location
private static final Path tmpdir = get(getProperty("java.io.tmpdir")).toAbsolutePath();
private static final boolean WINDOWS = OS_NAME.startsWith("windows");
private static final boolean MAC = OS_NAME.contains("mac");
private static final String version = "1.0.0";
static {
loadJni();
}
public static synchronized boolean loadJni() {
if (loaded) {
return true;
}
ClassLoader cl = QuackContext.class.getClassLoader();
String name = WINDOWS ? "quickjs.dll" : MAC ? "libquickjs.dylib" : "libquickjs.so";
Path libFile = tmpdir.resolve("quickjs-" + version).resolve(name);
if (!exists(libFile)) {
try (InputStream is = cl.getResourceAsStream("META-INF/" + name)) {
if (is == null) {
throw new RuntimeException("resource not found: META-INF/" + name);
}
if (!exists(libFile.getParent())) {
createDirectory(libFile.getParent());
}
if (!exists(libFile)) {
createFile(libFile);
}
copy(is, libFile, REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.load(libFile.toString());
return loaded = true;
}
static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
static boolean isNumberClass(Class> c) {
return c == byte.class || c == Byte.class || c == short.class || c == Short.class || c == int.class || c == Integer.class
|| c == long.class || c == Long.class || c == float.class || c == Float.class || c == double.class || c == Double.class;
}
public void setInvocationHandlerWrapper(QuackInvocationHandlerWrapper invocationHandlerWrapper) {
this.invocationHandlerWrapper = invocationHandlerWrapper;
}
// trap for Object methods.
private static InvocationHandler wrapObjectInvocationHandler(JavaScriptObject jo, InvocationHandler handler) {
return (proxy, method, args) -> {
if (method.getDeclaringClass() == Object.class)
return method.invoke(jo, args);
return handler.invoke(proxy, method, args);
};
}
InvocationHandler getWrappedInvocationHandler(JavaScriptObject javaScriptObject, InvocationHandler handler) {
// trap Object methods before allowing it to go to the JavaScriptObject or the JavaScriptObject single method lambda.
// higher level wrappers may trap the Object methods themselves.
handler = wrapObjectInvocationHandler(javaScriptObject, handler);
if (invocationHandlerWrapper == null)
return handler;
InvocationHandler wrapped = invocationHandlerWrapper.wrapInvocationHandler(javaScriptObject, handler);
if (wrapped != null)
return wrapped;
return handler;
}
/**
* Register a function that coerces values JavaScript values into an object of type
* {@code clazz} before being passed along to Java.
*/
public synchronized void putJavaScriptToJavaCoercion(Class clazz, QuackCoercion coercion) {
JavaScriptToJavaCoercions.put(clazz, coercion);
}
/**
* Register a function that coerces Java values of type {@code clazz} into a JavaScript object
* before being passed along to QuickJS.
* @param clazz
* @param coercion
* @param
*/
public synchronized void putJavaToJavaScriptCoercion(Class clazz, QuackCoercion coercion) {
JavaToJavascriptCoercions.put(clazz, coercion);
}
/**
* Coerce a Java value into an equivalent JavaScript object.
*/
public Object coerceJavaToJavaScript(Object o) {
if (o == null)
return null;
return coerceJavaToJavaScript(o.getClass(), o);
}
/**
* Coerce Java args to Javascript object args.
* @param args
*/
public Object[] coerceJavaArgsToJavaScript(Object... args) {
if (args != null) {
for (int i = 0; i < args.length; i++) {
args[i] = coerceJavaToJavaScript(args[i]);
}
}
return args;
}
/**
* Coerce a Java value into an equivalent JavaScript object.
*/
public Object coerceJavaToJavaScript(Class clazz, Object o) {
if (o == null)
return null;
while (o instanceof QuackJavaObject) {
Object coerced = ((QuackJavaObject)o).getObject();;
if (o == coerced)
break;
o = coerced;
}
Object ret = coerceJavaToJavaScript(JavaToJavascriptCoercions, o, clazz);
if (ret != null)
return ret;
// automatically coerce functional interfaces into functions
Method method = getLambdaMethod(clazz);
if (method != null) {
final Object thiz = o;
return new JavaMethodObject(this, thiz, method.getName()) {
@Override
public Object callMethod(Object thiz, Object... args) {
return super.callMethod(thiz, args);
}
@Override
protected Method[] getMethods(Object thiz) {
return clazz.getMethods();
}
};
}
return o;
}
private static Method getLambdaMethod(Class clazz) {
if (!clazz.isInterface())
return null;
Method match = null;
for (Method method: clazz.getMethods()) {
if (!Modifier.isStatic(method.getModifiers())) {
if (match != null)
return null;
match = method;
}
}
return match;
}
/**
* Coerce a JavaScript value into an equivalent Java object.
*/
public Object coerceJavaScriptToJava(Class> clazz, Object o) {
if (o == null)
return null;
while (o instanceof QuackJavaObject) {
Object coerced = ((QuackJavaObject)o).getObject();;
if (o == coerced)
break;
o = coerced;
}
if (clazz == null)
return o;
if (clazz.isInstance(o))
return o;
// unbox needs no coercion.
if (clazz == boolean.class && o instanceof Boolean)
return o;
if (clazz == byte.class && o instanceof Byte)
return o;
if (clazz == short.class && o instanceof Short)
return o;
if (clazz == int.class && o instanceof Integer)
return o;
if (clazz == long.class && o instanceof Long)
return o;
if (clazz == float.class && o instanceof Float)
return o;
if (clazz == double.class && o instanceof Double)
return o;
// javascript only uses doubles.
if ((clazz == byte.class || clazz == Byte.class) && o instanceof Double)
return ((Double)o).byteValue();
if ((clazz == short.class || clazz == Short.class) && o instanceof Double)
return ((Double)o).shortValue();
if ((clazz == int.class || clazz == Integer.class) && o instanceof Double)
return ((Double)o).intValue();
if ((clazz == float.class || clazz == Float.class) && o instanceof Double)
return ((Double)o).floatValue();
if ((clazz == long.class || clazz == Long.class) && o instanceof Double)
return ((Double)o).longValue();
if (clazz.isArray() && o instanceof JavaScriptObject) {
JavaScriptObject jo = (JavaScriptObject)o;
int length = ((Number)jo.get("length")).intValue();
Class componentType = clazz.getComponentType();
Object ret = Array.newInstance(componentType, length);
for (int i = 0; i < length; i++) {
Array.set(ret, i, coerceJavaScriptToJava(componentType, jo.get(i)));
}
return ret;
}
Object ret = coerceJavaScriptToJava(JavaScriptToJavaCoercions, o, clazz);
if (ret != null)
return ret;
// convert javascript objects proxy objects that implement interfaces
if (clazz.isInterface() && o instanceof JavaScriptObject) {
JavaScriptObject jo = (JavaScriptObject)o;
// single method arguments are simply callbacks
Method lambda = getLambdaMethod(clazz);
if (lambda != null) {
return Proxy.newProxyInstance(QuackJavaScriptObject.class.getClassLoader(), new Class[]{QuackJavaScriptObject.class, clazz},
jo.getWrappedInvocationHandler((proxy, method, args) ->
coerceJavaScriptToJava(method.getReturnType(), jo.call(JavaScriptObject.coerceArgs(this, method, args)))));
}
else {
InvocationHandler handler = jo.createInvocationHandler();
return Proxy.newProxyInstance(QuackJavaScriptObject.class.getClassLoader(), new Class[] { QuackJavaScriptObject.class, clazz }, handler);
}
}
// coercion was a failure, and returning the input value may cause a class
// cast exception if the caller is expecting a specific type.
return o;
}
public interface JavaMethodReference {
void invoke(T thiz) throws Exception;
}
public interface JavaMethodReference0 {
void invoke(T thiz, A arg0) throws Exception;
}
public interface JavaMethodReference1 {
void invoke(T thiz, A arg0, B arg1) throws Exception;
}
public interface JavaMethodReference2 {
void invoke(T thiz, A arg0, B arg1, C arg2) throws Exception;
}
public interface JavaMethodReference3 {
void invoke(T thiz, A arg0, B arg1, C arg2, D arg3) throws Exception;
}
public interface JavaMethodReference4 {
void invoke(T thiz, A arg0, B arg1, C arg2, D arg3, E arg4) throws Exception;
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference0 ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference1 ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference2 ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference3 ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
public static Method getInterfaceMethod(Class clazz, JavaMethodReference4 ref) {
return invokeMethodReferenceProxy(clazz, ref);
}
static Memoize javaObjectFields = new Memoize<>();
static Memoize javaObjectMethods = new Memoize<>();
static Memoize javaObjectGetter = new Memoize<>();
static Memoize javaObjectSetter = new Memoize<>();
static Memoize javaObjectMethodCandidates = new Memoize<>();
static Memoize javaObjectConstructorCandidates = new Memoize<>();
static Memoize interfaceMethods = new Memoize<>();
static Method getInterfaceMethod(Method method) {
return interfaceMethods.memoize(() -> {
if (method.getDeclaringClass().isInterface())
return method;
Class c = method.getDeclaringClass();
for (Class iface: c.getInterfaces()) {
for (Method m: iface.getDeclaredMethods()) {
if (m.getParameterTypes().length != method.getParameterTypes().length)
continue;
if (!m.getName().equals(method.getName()))
continue;
if (!m.getReturnType().isAssignableFrom(method.getReturnType()))
continue;
boolean paramMatch = true;
for (int i = 0; i < method.getParameterTypes().length; i++) {
if (!m.getParameterTypes()[i].isAssignableFrom(method.getParameterTypes()[i])) {
paramMatch = false;
break;
}
}
if (paramMatch)
return m;
}
}
return null;
}, method);
}
public synchronized void putJavaScriptToJavaMethodCoercion(Method method, QuackMethodCoercion coercion) {
JavaScriptToJavaMethodCoercions.put(method, coercion);
interfaceMethods.clear();
}
public synchronized void putJavaToJavaScriptMethodCoercion(Method method, QuackMethodCoercion coercion) {
JavaToJavascriptMethodCoercions.put(method, coercion);
interfaceMethods.clear();
}
private static class MethodException extends Exception {
/**
*
*/
private static final long serialVersionUID = -1432377890337490927L;
Method method;
MethodException(Method method) {
this.method = method;
}
}
private static Object throwInvokedMethod(Object proxy, Method method, Object[] args) throws MethodException {
throw new MethodException(method);
}
private static T createMethodInterceptProxy(Class clazz) {
return (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, QuackContext::throwInvokedMethod);
}
private static Method invokeMethodReferenceProxy(Class clazz, Object ref) {
try {
if (ref.getClass().getDeclaredMethods().length != 1)
throw new Exception("expecting lambda with 1 method: getInterfaceMethod(Foo.class, Foo::bar)");
Method method = ref.getClass().getDeclaredMethods()[0];
Object[] args = new Object[method.getParameterTypes().length];
// first arg is "this" for the lambda
args[0] = createMethodInterceptProxy(clazz);
method.invoke(ref, args);
}
catch (Exception e) {
if (e instanceof InvocationTargetException) {
InvocationTargetException invocationTargetException = (InvocationTargetException)e;
if (invocationTargetException.getTargetException() instanceof UndeclaredThrowableException) {
UndeclaredThrowableException undeclaredThrowableException = (UndeclaredThrowableException)invocationTargetException.getTargetException();
if (undeclaredThrowableException.getUndeclaredThrowable() instanceof MethodException) {
return ((MethodException)undeclaredThrowableException.getUndeclaredThrowable()).method;
}
}
else if (invocationTargetException.getTargetException() instanceof NullPointerException) {
throw new IllegalArgumentException("lambdas with primitive arguments must be invoked with default values: getInterfaceMethod(Foo.class, thiz -> thiz.setInt(0))");
}
}
throw new IllegalArgumentException(e);
}
throw new IllegalArgumentException("interface method was not called by lambda.");
}
private static Object coerceJavaToJavaScript(Map coerce, Object o, Class> clazz) {
QuackCoercion coercion = coerce.get(clazz);
if (coercion != null) {
return coercion.coerce(clazz, o);
}
// check to see if there is a superclass converter (ie, Enum.class as a catch all).
for (Map.Entry check: coerce.entrySet()) {
if (check.getKey().isAssignableFrom(clazz))
return check.getValue().coerce(clazz, o);
}
return null;
}
private static Object coerceJavaScriptToJava(Map coerce, Object o, Class> clazz) {
QuackCoercion coercion = coerce.get(clazz);
if (coercion != null) {
return coercion.coerce(clazz, o);
}
// check to see if there exists a more specific superclass converter.
for (Map.Entry check: coerce.entrySet()) {
if (clazz.isAssignableFrom(check.getKey())) {
throw new AssertionError("Superclass converter not implemented.");
}
}
// check to see if there is a subclass converter (ie, Enum.class as a catch all).
for (Map.Entry check: coerce.entrySet()) {
if (check.getKey().isAssignableFrom(clazz))
return check.getValue().coerce(clazz, o);
}
return null;
}
/**
* Create a new interpreter instance. Calls to this method must matched with
* calls to {@link #close()} on the returned instance to avoid leaking native memory.
*/
private boolean useQuickJS;
public static QuackContext create(boolean useQuickJS) {
QuackContext quack = new QuackContext(useQuickJS);
// context will hold a weak ref, so this doesn't matter if it fails.
long context = createContext(quack, useQuickJS);
if (context == 0) {
throw new OutOfMemoryError("Cannot create QuickJS instance");
}
quack.context = context;
quack.useQuickJS = useQuickJS;
return quack;
}
public static QuackContext create() {
return create(true);
}
private long context;
private QuackContext(boolean useQuickJS) {
// coercing javascript string into an enum for java
JavaScriptToJavaCoercions.put(Enum.class, (QuackCoercion) (clazz, o) -> {
if (o == null)
return null;
return Enum.valueOf(clazz, o.toString());
});
// coerce JavaScript Numbers. quack supports ints and doubles natively.
putJavaScriptToJavaCoercion(Byte.class, (clazz, o) -> o instanceof Number ? ((Number)o).byteValue() : o instanceof String ? Byte.parseByte(o.toString()) : (Byte)o);
JavaScriptToJavaCoercions.put(byte.class, (clazz, o) -> o instanceof Number ? ((Number)o).byteValue() : o instanceof String ? Byte.parseByte(o.toString()) : o);
// bytes become ints
putJavaToJavaScriptCoercion(byte.class, (clazz, o) -> o.intValue());
putJavaToJavaScriptCoercion(Byte.class, (clazz, o) -> o.intValue());
JavaScriptToJavaCoercions.put(Short.class, (clazz, o) -> o instanceof Number ? ((Number)o).shortValue() : o instanceof String ? Short.parseShort(o.toString()) : o);
JavaScriptToJavaCoercions.put(short.class, (clazz, o) -> o instanceof Number ? ((Number)o).shortValue() : o instanceof String ? Short.parseShort(o.toString()) : o);
// shorts become ints
putJavaToJavaScriptCoercion(short.class, (clazz, o) -> o.intValue());
putJavaToJavaScriptCoercion(Short.class, (clazz, o) -> o.intValue());
JavaScriptToJavaCoercions.put(Integer.class, (clazz, o) -> o instanceof Number ? ((Number)o).intValue() : o instanceof String ? Integer.parseInt(o.toString()) : o);
JavaScriptToJavaCoercions.put(int.class, (clazz, o) -> o instanceof Number ? ((Number)o).intValue() : o instanceof String ? Integer.parseInt(o.toString()) : o);
JavaScriptToJavaCoercions.put(Long.class, (clazz, o) -> o instanceof Number ? ((Number)o).longValue() : o instanceof String ? Long.parseLong(o.toString()) : o);
JavaScriptToJavaCoercions.put(long.class, (clazz, o) -> o instanceof Number ? ((Number)o).longValue() : o instanceof String ? Long.parseLong(o.toString()) : o);
// by default longs become strings, precision loss going to double. that's no good.
// coercions can be used to get numbers if necessary.
if (!useQuickJS) {
putJavaToJavaScriptCoercion(long.class, (clazz, o) -> o.toString());
putJavaToJavaScriptCoercion(Long.class, (clazz, o) -> o.toString());
}
JavaScriptToJavaCoercions.put(Float.class, (clazz, o) -> o instanceof Number ? ((Number)o).floatValue() : o instanceof String ? Float.parseFloat(o.toString()) : o);
JavaScriptToJavaCoercions.put(float.class, (clazz, o) -> o instanceof Number ? ((Number)o).floatValue() : o instanceof String ? Float.parseFloat(o.toString()) : o);
// floats become doubles
putJavaToJavaScriptCoercion(float.class, (clazz, o) -> o.doubleValue());
putJavaToJavaScriptCoercion(Float.class, (clazz, o) -> o.doubleValue());
JavaScriptToJavaCoercions.put(Double.class, (clazz, o) -> o instanceof Number ? ((Number)o).doubleValue() : o instanceof String ? Double.parseDouble(o.toString()) : o);
JavaScriptToJavaCoercions.put(double.class, (clazz, o) -> o instanceof Number ? ((Number)o).doubleValue() : o instanceof String ? Double.parseDouble(o.toString()) : o);
// coercing a java enum into javascript string
putJavaToJavaScriptCoercion(Enum.class, (clazz, o) -> o.toString());
putJavaToJavaScriptCoercion(ByteBuffer.class, (clazz, o) -> {
// send as is to quickjs, it can handle direct buffers at any position/limit.
if (o.isDirect() && useQuickJS)
return o;
ByteBuffer direct = ByteBuffer.allocateDirect(o.remaining());
direct.put(o);
direct.flip();
return direct;
});
}
private long totalElapsedScriptExecutionMs;
/**
* Profiling tool. Get the total time spent evaluating JavaScript. This iincludes
* calls back out to Java.
* @return
*/
public long getTotalScriptExecutionTime() {
return totalElapsedScriptExecutionMs;
}
/**
* Profiling tool. Reset the total time spent evaluating JavaScript.
*/
public void resetTotalScriptExecutionTime() {
totalElapsedScriptExecutionMs = 0;
}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized T evaluate(Class clazz, String script, String fileName) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return (T)coerceJavaScriptToJava(clazz, evaluate(context, script, fileName));
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized Object evaluate(String script, String fileName) {
return evaluate(null, script, fileName);
}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized JavaScriptObject evaluateModule(String script, String fileName) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return (JavaScriptObject)coerceJavaScriptToJava(JavaScriptObject.class, evaluateModule(context, script, fileName));
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
/**
* Evaluate {@code script} and return a result. {@code fileName} will be used in error
* reporting.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized JavaScriptObject evaluateModule(String script) {
return evaluateModule(script, "?");
}
/**
* Evaluate {@code script} and return a result.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized Object evaluate(String script) {
return evaluate(script, "?");
}
/**
* Evaluate {@code script} and return the expected result of a specific type.
* @param script
* @param clazz
* @param
* @return
*/
public synchronized T evaluate(String script, Class clazz) {
return (T)coerceJavaScriptToJava(clazz, evaluate(script));
}
/**
* Evaluate {@code script} and return the expected result of a JavaScriptObject.
* @param script
* @return
*/
public synchronized JavaScriptObject evaluateForJavaScriptObject(String script) {
return evaluate(script, JavaScriptObject.class);
}
/**
* Compile a JavaScript function and return of JavaScriptObject as the resulting function.
*
* @throws QuackException if there is an error evaluating the script.
*/
public synchronized JavaScriptObject compileFunction(String script, String fileName) {
return compileFunction(context, script, fileName);
}
/**
* Release the native resources associated with this object. You must call this
* method for each instance to avoid leaking native memory.
*/
@Override public synchronized void close() {
if (context != 0) {
long contextToClose = context;
context = 0;
destroyContext(contextToClose);
}
nativeMappings.clear();
}
@Override protected synchronized void finalize() throws Throwable {
// this isn't THAT bad, as JavaScriptObjects may be passed around without concern for the
// QuickJS collection.
if (context != 0) {
Logger.getLogger(getClass().getName()).warning("QuickJS instance leaked!");
}
// definitely close it though.
close();
}
synchronized public JavaScriptObject getGlobalObject() {
return getGlobalObject(context);
}
/**
* Notify any attached debugger to process pending any debugging requests. When
* cooperateDebuger is invoked by the caller, the caller must ensure no calls into
* the QuickJS during that time.
*/
public synchronized void cooperateDebugger() {
if (context == 0)
return;
cooperateDebugger(context);
}
/**
* Wait for a debugging connection on port 9091.
*/
public void waitForDebugger(String connectionString) {
if (context == 0)
return;
waitForDebugger(context, connectionString);
}
/**
* Check if a debugger is currently attached.
*/
public boolean isDebugging() {
if (context == 0)
return false;
return isDebugging(context);
}
/**
* Send an custom app notification to any connected debugging client.
* @param args
*/
public synchronized void debuggerAppNotify(Object... args) {
if (context == 0)
return;
debuggerAppNotify(context, args);
}
synchronized Object getKeyObject(long object, Object key) {
if (context == 0)
return null;
return getKeyObject(context, object, key);
}
synchronized Object getKeyString(long object, String key) {
if (context == 0)
return null;
return getKeyString(context, object, key);
}
synchronized Object getKeyInteger(long object, int index) {
if (context == 0)
return null;
return getKeyInteger(context, object, index);
}
synchronized boolean setKeyObject(long object, Object key, Object value) {
if (context == 0)
return false;
return setKeyObject(context, object, key, value);
}
synchronized boolean setKeyString(long object, String key, Object value) {
if (context == 0)
return false;
return setKeyString(context, object, key, value);
}
synchronized boolean setKeyInteger(long object, int index, Object value) {
if (context == 0)
return false;
return setKeyInteger(context, object, index, value);
}
synchronized Object call(long object, Object... args) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return call(context, object, args);
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
synchronized Object callConstructor(long object, Object... args) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return callConstructor(context, object, args);
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
synchronized Object callMethod(long object, Object thiz, Object... args) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return callMethod(context, object, thiz, args);
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
synchronized Object callProperty(long object, Object property, Object... args) {
if (context == 0)
return null;
long start = System.nanoTime() / 1000000;
try {
return callProperty(context, object, property, args);
}
finally {
totalElapsedScriptExecutionMs += System.nanoTime() / 1000000 - start;
handlePostInvocation();
}
}
synchronized String stringify(long object) {
if (context == 0)
return null;
return stringify(context, object);
}
public synchronized long getHeapSize() {
if (context == 0)
return 0;
return getHeapSize(context);
}
private interface Thrower {
void doThrow() throws Throwable;
}
private interface Catcher {
JavaScriptObject doCatch(Thrower thrower);
}
public synchronized JavaScriptObject newError(Throwable t) {
if (context == 0)
return null;
try {
Thrower thrower = () -> {
throw t;
};
Catcher catcher = evaluate("(function(t) { try { t(); } catch (e) { return e } })", Catcher.class);
return catcher.doCatch(thrower);
}
catch (Throwable unexpected) {
return null;
}
}
public synchronized void throwObject(Object o) {
if (context == 0)
return;
evaluateForJavaScriptObject("(function(t) { throw t; })").call(o);
}
// to prevent from blocking the JavaScriptObject finalizer, create
// a finalization queue for the JS side.
final ArrayList finalizationQueue = new ArrayList<>();
// this method should NOT be synchronized at the QuackContext level, so it never blocks a finalizer
void finalizeJavaScriptObject(long object) {
if (context == 0)
return;
synchronized (finalizationQueue) {
finalizationQueue.add(object);
}
}
synchronized private void finalizeJavaScriptObjects() {
long[] copy;
synchronized (finalizationQueue) {
if (finalizationQueue.isEmpty())
return;
copy = new long[finalizationQueue.size()];
for (int i = 0; i < finalizationQueue.size(); i++) {
copy[i] = finalizationQueue.get(i);
}
finalizationQueue.clear();
}
if (context == 0)
return;
finalizeJavaScriptObjects(context, copy);
}
synchronized private boolean hasPostInvocationTasks() {
synchronized (finalizationQueue) {
if (!finalizationQueue.isEmpty())
return true;
}
// this should be called outside of the queue check to prevent weird blocking.
return hasPendingJobs(context);
}
synchronized private void handlePostInvocation() {
if (!hasPostInvocationTasks())
return;
if (jobExecutor == null) {
runPostInvocation();
return;
}
jobExecutor.execute(this::runPostInvocation);
}
synchronized void runPostInvocation() {
if (context == 0)
return;
finalizeJavaScriptObjects();
runJobs(context);
}
private Executor jobExecutor;
synchronized public void setJobExecutor(Executor executor) {
jobExecutor = executor;
}
// hooks from js/jni to java
private Object quackGet(QuackObject quackObject, Object key) {
return quackObject.get(key);
}
private boolean quackHas(QuackObject quackObject, Object key) {
return quackObject.has(key);
}
private boolean quackSet(QuackObject quackObject, Object key, Object value) {
return quackObject.set(key, value);
}
private Object[] empty = new Object[0];
private Object quackApply(QuackObject quackObject, Object thiz, Object... args) {
return quackObject.callMethod(thiz, args == null ? empty : args);
}
private Object quackConstruct(QuackObject quackObject, Object... args) {
return quackObject.construct(args == null ? empty : args);
}
public void quackMapNative(Object key, Object value) {
nativeMappings.put(key, value);
}
public Object quackUnmapNative(Object key) {
return nativeMappings.get(key);
}
private long getNativePointer(QuackJavaScriptObject quackJavaScriptObject) {
if (quackJavaScriptObject.getNativeContext() != context)
return 0;
return quackJavaScriptObject.getNativePointer();
}
private static native long getHeapSize(long context);
private static native long createContext(QuackContext quackContext, boolean useQuickJS);
private static native void destroyContext(long context);
private static native Object evaluate(long context, String sourceCode, String fileName);
private static native Object evaluateModule(long context, String sourceCode, String fileName);
private static native JavaScriptObject compileFunction(long context, String script, String fileName);
private static native void cooperateDebugger(long context);
private static native void waitForDebugger(long context, String connectionString);
private static native boolean isDebugging(long context);
private static native void debuggerAppNotify(long context, Object... args);
private static native Object getKeyObject(long context, long object, Object key);
private static native Object getKeyString(long context, long object, String key);
private static native Object getKeyInteger(long context, long object, int index);
private static native boolean setKeyObject(long context, long object, Object key, Object value);
private static native boolean setKeyString(long context, long object, String key, Object value);
private static native boolean setKeyInteger(long context, long object, int index, Object value);
private static native Object call(long context, long object, Object... args);
private static native Object callConstructor(long context, long object, Object... args);
private static native Object callMethod(long context, long object, Object thiz, Object... args);
private static native Object callProperty(long context, long object, Object property, Object... args);
private static native JavaScriptObject getGlobalObject(long context);
private static native String stringify(long context, long object);
private static native void finalizeJavaScriptObjects(long context, long[] objects);
private static native boolean hasPendingJobs(long context);
private static native void runJobs(long context);
}