Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sharing Java classes across the J2V8 bridge #201

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ hs_err*.log
*.ipr
*.iws
.idea

# Build output.
node
121 changes: 121 additions & 0 deletions src/main/java/com/eclipsesource/v8/ConcurrentV8.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*******************************************************************************
* Copyright (c) 2016 Brandon Sanders
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brandon Sanders - initial API and implementation and/or initial documentation
******************************************************************************/
package com.eclipsesource.v8;

/**
* Wrapper class for an {@link com.eclipsesource.v8.V8} instance that allows
* a V8 instance to be invoked from across threads without explicitly acquiring
* or releasing locks.
*
* This class does not guarantee the safety of any objects stored in or accessed
* from the wrapped V8 instance; it only enables callers to interact with a V8
* instance from any thread. The V8 instance represented by this class should
* still be treated with thread safety in mind
*
* @author Brandon Sanders [[email protected]]
*/
public final class ConcurrentV8 {
//Private//////////////////////////////////////////////////////////////////////

// Wrapped V8 instance, initialized by the default runnable.
private V8 v8 = null;

//Protected////////////////////////////////////////////////////////////////////

// Release the V8 runtime when this class is finalized.
@Override protected void finalize() {
release();
}

//Public///////////////////////////////////////////////////////////////////////

public ConcurrentV8() {
v8 = V8.createV8Runtime();
v8.getLocker().release();
}

/**
* Calls {@link #run(ConcurrentV8Runnable)}, quietly handling any thrown
* exceptions.
*
* @see {@link #run(ConcurrentV8Runnable)}.
*/
public void runQuietly(ConcurrentV8Runnable runny) {
try {
run(runny);
} catch (Throwable t) { }
}

/**
* Runs an {@link ConcurrentV8Runnable} on the V8 thread.
*
* <b>Note: </b> This method executes synchronously, not asynchronously;
* it will not return until the passed {@link ConcurrentV8Runnable} is done
* executing.
*
* @param runny {@link ConcurrentV8Runnable} to run.
*
* @throws Exception If the passed runnable throws an exception, this
* method will throw that exact exception.
*/
public synchronized void run(ConcurrentV8Runnable runny) throws Exception {
try {
v8.getLocker().acquire();

try {
runny.run(v8);
} catch (Throwable t) {
v8.getLocker().release();
if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new Exception(t);
}
}

v8.getLocker().release();
} catch (Throwable t) {
if (v8 != null && v8.getLocker() != null && v8.getLocker().hasLock()) {
v8.getLocker().release();
}

if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new Exception(t);
}
}
}

/**
* Releases the underlying {@link V8} instance.
*
* This method should be invoked once you're done using this object,
* otherwise a large amount of garbage could be left on the JVM due to
* native resources.
*
* <b>Note:</b> If this method has already been called once, it
* will do nothing.
*/
public void release() {
if (v8 != null && !v8.isReleased()) {
// Release the V8 instance from the V8 thread context.
runQuietly(new ConcurrentV8Runnable() {
@Override
public void run(V8 v8) {
if (v8 != null && !v8.isReleased()) {
v8.release();
}
}
});
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/eclipsesource/v8/ConcurrentV8Runnable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*******************************************************************************
* Copyright (c) 2016 Brandon Sanders
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brandon Sanders - initial API and implementation and/or initial documentation
******************************************************************************/
package com.eclipsesource.v8;

/**
* Simple runnable for use with an {@link ConcurrentV8} instance.
*
* @author Brandon Sanders [[email protected]]
*/
public interface ConcurrentV8Runnable {
void run(final V8 v8) throws Exception;
}
155 changes: 155 additions & 0 deletions src/main/java/com/eclipsesource/v8/V8JavaAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*******************************************************************************
* Copyright (c) 2016 Brandon Sanders
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brandon Sanders - initial API and implementation and/or initial documentation
******************************************************************************/
package com.eclipsesource.v8;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

/**
* Utilities for adapting Java classes and objects into a V8 runtime.
*
* @author Brandon Sanders [[email protected]]
*/
public final class V8JavaAdapter {

/**
* Injects an existing Java object into V8 as a variable.
*
* If the passed object represents a primitive array (e.g., String[], Object[], int[]),
* the array will be unwrapped and injected into the V8 context as an ArrayList. Any
* modifications made to the injected list will not be passed back up to the Java runtime.
*
* This method will immediately invoke {@link #injectClass(String, Class, V8Object)}
* before injecting the object, causing the object's class to be automatically
* injected into the V8 Object if it wasn't already.
*
* <b>NOTE: </b> If you wish to use an interceptor for the class of an injected object,
* you must explicitly invoke {@link #injectClass(Class, V8JavaClassInterceptor, V8Object)} or
* {@link #injectClass(String, Class, V8JavaClassInterceptor, V8Object)}. This method will
* <b>NOT</b> specify an interceptor automatically for the injected object.
*
* @param name Name of the variable to assign the Java object to. If this value is null,
* a UUID will be automatically generated and used as the name of the variable.
* @param object Java object to inject.
* @param rootObject {@link V8Object} to inject the Java object into.
*
* @return String identifier of the injected object.
*/
public static String injectObject(String name, Object object, V8Object rootObject) {
//TODO: Add special handlers for N-dimensional and primitive arrays.
//TODO: This should inject arrays as JS arrays, not lists. Meh.
//TODO: This will bypass interceptors in some cases.
//TODO: This is terrible.
if (object.getClass().isArray()) {
Object[] rawArray = (Object[]) object;
List<Object> injectedArray = new ArrayList<Object>(rawArray.length);
for (Object obj : rawArray) {
injectedArray.add(obj);
}
return injectObject(name, injectedArray, rootObject);
} else {
injectClass("".equals(object.getClass().getSimpleName()) ?
object.getClass().getName().replaceAll("\\.+", "_") :
object.getClass().getSimpleName(),
object.getClass(),
rootObject);
}

if (name == null) {
name = "TEMP" + UUID.randomUUID().toString().replaceAll("-", "");
}

//Build an empty object instance.
V8JavaClassProxy proxy = V8JavaCache.cachedV8JavaClasses.get(object.getClass());
StringBuilder script = new StringBuilder();
script.append("var ").append(name).append(" = new function() {");
if (proxy.getInterceptor() != null) script.append(proxy.getInterceptor().getConstructorScriptBody());
script.append("\n}; ").append(name).append(";");

V8Object other = V8JavaObjectUtils.getRuntimeSarcastically(rootObject).executeObjectScript(script.toString());
String id = proxy.attachJavaObjectToJsObject(object, other);
other.release();
return id;
}

/**
* Injects a Java class into a V8 object as a prototype.
*
* The injected "class" will be equivalent to a Java Script prototype with
* a name identical to the one specified when invoking this function. For
* example, the java class {@code com.foo.Bar} could be new'd from the Java Script
* context by invoking {@code new Bar()} if {@code "Bar"} was passed as the
* name use when injecting the class.
*
* @param name Name to use when injecting the class into the V8 object.
* @param classy Java class to inject.
* @param interceptor {@link V8JavaClassInterceptor} to use with this class. Pass null if no interceptor is desired.
* @param rootObject {@link V8Object} to inject the Java class into.
*/
public static void injectClass(String name, Class<?> classy, V8JavaClassInterceptor interceptor, V8Object rootObject) {
//Calculate V8-friendly full class names.
String v8FriendlyClassname = classy.getName().replaceAll("\\.+", "_");

//Register the class proxy.
V8JavaClassProxy proxy;
if (V8JavaCache.cachedV8JavaClasses.containsKey(classy)) {
proxy = V8JavaCache.cachedV8JavaClasses.get(classy);
} else {
proxy = new V8JavaClassProxy(classy, interceptor);
V8JavaCache.cachedV8JavaClasses.put(classy, proxy);
}

//Check if the root object already has a constructor.
//TODO: Is this faster or slower than checking if a specific V8Value is "undefined"?
if (!Arrays.asList(rootObject.getKeys()).contains("v8ConstructJavaClass" + v8FriendlyClassname)) {
rootObject.registerJavaMethod(proxy, "v8ConstructJavaClass" + v8FriendlyClassname);

//Build up the constructor script.
StringBuilder script = new StringBuilder();
script.append("this.").append(name).append(" = function() {");
script.append("v8ConstructJavaClass").append(v8FriendlyClassname).append(".apply(this, arguments);");

if (proxy.getInterceptor() != null) {
script.append(proxy.getInterceptor().getConstructorScriptBody());
}

script.append("\n};");

//Evaluate the script to create a new constructor function.
V8JavaObjectUtils.getRuntimeSarcastically(rootObject).executeVoidScript(script.toString());

//Build up static methods if needed.
if (proxy.getInterceptor() == null) {
V8Object constructorFunction = (V8Object) rootObject.get(name);
for (V8JavaStaticMethodProxy method : proxy.getStaticMethods()) {
constructorFunction.registerJavaMethod(method, method.getMethodName());
}

//Clean up after ourselves.
constructorFunction.release();
}
}
}

public static void injectClass(Class<?> classy, V8JavaClassInterceptor interceptor, V8Object object) {
injectClass(classy.getSimpleName(), classy, interceptor, object);
}

public static void injectClass(String name, Class<?> classy, V8Object object) {
injectClass(name, classy, null, object);
}

public static void injectClass(Class<?> classy, V8Object object) {
injectClass(classy.getSimpleName(), classy, null, object);
}
}
53 changes: 53 additions & 0 deletions src/main/java/com/eclipsesource/v8/V8JavaCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2016 Brandon Sanders
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Brandon Sanders - initial API and implementation and/or initial documentation
******************************************************************************/
package com.eclipsesource.v8;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;

/**
* Centralized cache for resources created via the {@link V8JavaAdapter}. This class
* is not meant to be used directly by API consumers; any actions should be performed
* via the {@link V8JavaAdapter} class.
*
* @author Brandon Sanders [[email protected]]
*/
final class V8JavaCache {
/**
* Cache of Java classes injected into V8 via the {@link V8JavaAdapter}.
*/
public static final Map<Class<?>, V8JavaClassProxy> cachedV8JavaClasses = new HashMap<Class<?>, V8JavaClassProxy>();

/**
* Cache of Java objects created through V8 via a {@link V8JavaClassProxy}.
*
* TODO: This cache is shared across V8 runtimes, theoretically allowing cross-runtime sharing of Java objects.
* Is this a "feature" or a "bug"?
*/
public static final Map<String, WeakReference> identifierToV8ObjectMap = new HashMap<String, WeakReference>();
public static final Map<Object, String> v8ObjectToIdentifierMap = new WeakHashMap<Object, String>();

/**
* Removes any Java objects that have been garbage collected from the object cache.
*/
public static void removeGarbageCollectedJavaObjects() {
Iterator<Map.Entry<String, WeakReference>> it = identifierToV8ObjectMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, WeakReference> entry = it.next();
if (entry.getValue().get() == null) {
it.remove();
}
}
}
}
Loading