blob: dd130084f44653c001fb8c5953be76a9c3c0d343 [file] [log] [blame]
/*
* Copyright 2008 Google 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.google.gwt.dev.shell;
import com.google.gwt.core.client.GWTBridge;
import com.google.gwt.core.client.GwtScriptOnly;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JArrayType;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.javac.CompilationProblemReporter;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
import com.google.gwt.dev.javac.CompiledClass;
import com.google.gwt.dev.javac.JsniMethod;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.shell.rewrite.HasAnnotation;
import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter;
import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.InstanceMethodOracle;
import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.SingleJsoImplData;
import com.google.gwt.dev.util.JsniRef;
import com.google.gwt.dev.util.Name;
import com.google.gwt.dev.util.Name.InternalName;
import com.google.gwt.dev.util.Name.SourceOrBinaryName;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.collect.Lists;
import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
import com.google.gwt.thirdparty.guava.common.collect.MapMaker;
import com.google.gwt.util.tools.Utility;
import java.beans.Beans;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
/**
* An isolated {@link ClassLoader} for running all user code. All user files are
* compiled from source code byte a {@link ByteCodeCompiler}. After compilation,
* some byte code rewriting is performed to support
* <code>JavaScriptObject</code> and its subtypes.
*
* TODO: we should refactor this class to move the getClassInfoByDispId,
* getDispId, getMethodDispatch and putMethodDispatch into a separate entity
* since they really do not interact with the CompilingClassLoader
* functionality.
*/
public final class CompilingClassLoader extends ClassLoader implements
DispatchIdOracle {
/**
* Oracle that can answer questions about {@link DispatchClassInfo
* DispatchClassInfos}.
*/
private final class DispatchClassInfoOracle {
/**
* Class identifier to DispatchClassInfo mapping.
*/
private final ArrayList<DispatchClassInfo> classIdToClassInfo = new ArrayList<DispatchClassInfo>();
/**
* Binary or source class name to DispatchClassInfo map.
*/
private final Map<String, DispatchClassInfo> classNameToClassInfo = new HashMap<String, DispatchClassInfo>();
/**
* Clears out the contents of this oracle.
*/
public synchronized void clear() {
classIdToClassInfo.clear();
classNameToClassInfo.clear();
}
/**
* Returns the {@link DispatchClassInfo} for a given dispatch id.
*
* @param dispId dispatch id
* @return DispatchClassInfo for the requested dispatch id
*/
public synchronized DispatchClassInfo getClassInfoByDispId(int dispId) {
int classId = extractClassIdFromDispId(dispId);
return classIdToClassInfo.get(classId);
}
/**
* Returns the dispatch id for a given member reference. Member references
* can be encoded as: "@class::field" or "@class::method(typesigs)".
*
* @param jsniMemberRef a string encoding a JSNI member to use
* @return integer encoded as ((classId << 16) | memberId)
*/
public synchronized int getDispId(String jsniMemberRef) {
/*
* Map JS toString() onto the Java toString() method.
*/
if (jsniMemberRef.equals("toString")) {
jsniMemberRef = "@java.lang.Object::toString()";
}
JsniRef parsed = JsniRef.parse(jsniMemberRef);
if (parsed == null) {
logger.log(TreeLogger.ERROR, "Malformed JSNI reference '"
+ jsniMemberRef + "'; expect subsequent failures",
new NoSuchFieldError(jsniMemberRef));
return -1;
}
// Do the lookup by class name.
String className = parsed.className();
DispatchClassInfo dispClassInfo = getClassInfoFromClassName(className);
if (dispClassInfo != null) {
String memberName = parsed.memberSignature();
/*
* Disallow the use of JSNI references to SingleJsoImpl interface
* methods. This policy is due to web-mode dispatch implementation
* details; resolving the JSNI reference wouldn't be just be a name
* replacement, instead it would be necessary to significantly alter the
* semantics of the hand-written JS.
*/
if (singleJsoImplTypes.contains(canonicalizeClassName(className))) {
logger.log(TreeLogger.ERROR,
"Invalid JSNI reference to SingleJsoImpl interface (" + className
+ "); consider using a trampoline. "
+ "Expect subsequent failures.", new NoSuchFieldError(
jsniMemberRef));
return -1;
}
int memberId = dispClassInfo.getMemberId(memberName);
if (memberId < 0) {
if (!className.startsWith("java.")) {
logger.log(TreeLogger.ERROR, "Member '" + memberName
+ "' in JSNI reference '" + jsniMemberRef
+ "' could not be found; expect subsequent failures",
new NoSuchFieldError(memberName));
}
}
return synthesizeDispId(dispClassInfo.getClassId(), memberId);
}
logger.log(TreeLogger.ERROR, "Class '" + className
+ "' in JSNI reference '" + jsniMemberRef
+ "' could not be found; expect subsequent failures",
new ClassNotFoundException(className));
return -1;
}
/**
* Extracts the class id from the dispatch id.
*
* @param dispId
* @return the classId encoded into this dispatch id
*/
private int extractClassIdFromDispId(int dispId) {
return (dispId >> 16) & 0xffff;
}
/**
* Returns the {@link java.lang.Class} instance for a given binary class
* name. It is important to avoid initializing the class because this would
* potentially cause initializers to be run in a different order than in web
* mode. Moreover, we may not have injected all of the JSNI code required to
* initialize the class.
*
* @param binaryClassName the binary name of a class
* @return {@link java.lang.Class} instance or null if the given binary
* class name could not be found
*/
private Class<?> getClassFromBinaryName(String binaryClassName) {
int dims = 0;
while (binaryClassName.endsWith("[]")) {
dims++;
binaryClassName = binaryClassName.substring(0,
binaryClassName.length() - 2);
}
Class<?> clazz = primitiveTypes.get(binaryClassName);
if (clazz == null) {
try {
clazz = Class.forName(binaryClassName, false,
CompilingClassLoader.this);
} catch (ClassNotFoundException e) {
}
}
// TODO(deprecation): remove this support eventually.
if (clazz == null && binaryClassName.length() == 1
&& "ZBCDFIJSV".indexOf(binaryClassName.charAt(0)) >= 0) {
clazz = getDeprecatedPrimitiveType(binaryClassName.charAt(0));
assert clazz != null;
}
if (dims > 0) {
return Array.newInstance(clazz, new int[dims]).getClass();
} else {
return clazz;
}
}
/**
* Returns the {@link java.lang.Class} object for a class that matches the
* source or binary name given.
*
* @param className binary or source name
* @return {@link java.lang.Class} instance, if found, or null
*/
private Class<?> getClassFromBinaryOrSourceName(String className) {
// Try the type oracle first
JClassType type = typeOracle.findType(SourceOrBinaryName.toSourceName(className));
if (type != null) {
// Use the type oracle to compute the exact binary name
String jniSig = type.getJNISignature();
jniSig = jniSig.substring(1, jniSig.length() - 1);
className = InternalName.toBinaryName(jniSig);
}
return getClassFromBinaryName(className);
}
/**
* Returns the {@link DispatchClassInfo} associated with the class name.
* Since we allow both binary and source names to be used in JSNI class
* references, we need to be able to deal with the fact that multiple
* permutations of the class name with regards to source or binary forms map
* on the same {@link DispatchClassInfo}.
*
* @param className binary or source name for a class
* @return {@link DispatchClassInfo} associated with the binary or source
* class name; null if there is none
*/
private DispatchClassInfo getClassInfoFromClassName(String className) {
DispatchClassInfo dispClassInfo = classNameToClassInfo.get(className);
if (dispClassInfo != null) {
// return the cached value
return dispClassInfo;
}
Class<?> cls = getClassFromBinaryOrSourceName(className);
if (cls == null) {
/*
* default to return null; mask the specific error and let the caller
* handle it
*/
return null;
}
// Map JSO type references to the appropriate impl class.
if (classRewriter.isJsoIntf(cls.getName())) {
cls = getClassFromBinaryName(cls.getName() + "$");
}
/*
* we need to create a new DispatchClassInfo since we have never seen this
* class before under any source or binary class name
*/
int classId = classIdToClassInfo.size();
dispClassInfo = new DispatchClassInfo(cls, classId);
classIdToClassInfo.add(dispClassInfo);
/*
* Whether we created a new DispatchClassInfo or not, we need to add a
* mapping for this name
*/
classNameToClassInfo.put(className, dispClassInfo);
return dispClassInfo;
}
@Deprecated
private Class<?> getDeprecatedPrimitiveType(char c) {
switch (c) {
case 'Z':
return boolean.class;
case 'B':
return byte.class;
case 'C':
return char.class;
case 'D':
return double.class;
case 'F':
return float.class;
case 'I':
return int.class;
case 'J':
return long.class;
case 'S':
return short.class;
case 'V':
return void.class;
default:
return null;
}
}
/**
* Synthesizes a dispatch identifier for the given class and member ids.
*
* @param classId class index
* @param memberId member index
* @return dispatch identifier for the given class and member ids
*/
private int synthesizeDispId(int classId, int memberId) {
return (classId << 16) | memberId;
}
}
/**
* A ClassLoader that will delegate to a parent ClassLoader and fall back to
* loading bytecode as resources from an alternate parent ClassLoader.
*/
private static class MultiParentClassLoader extends ClassLoader {
private final ClassLoader resources;
public MultiParentClassLoader(ClassLoader parent, ClassLoader resources) {
super(parent);
assert parent != null;
this.resources = resources;
}
@Override
protected synchronized Class<?> findClass(String name)
throws ClassNotFoundException {
String resourceName = name.replace('.', '/') + ".class";
URL url = resources.getResource(resourceName);
if (url == null) {
throw new ClassNotFoundException();
}
byte[] bytes = Util.readURLAsBytes(url);
return defineClass(name, bytes, 0, bytes.length);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
Class c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
return getParent().loadClass(name);
} catch (Throwable t) {
// Make a second attempt not only on ClassNotFoundExceptions, but also errors like
// ClassCircularityError
Class c = findClass(name);
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
/**
* Implements {@link InstanceMethodOracle} on behalf of the
* {@link HostedModeClassRewriter}. Implemented using {@link TypeOracle}.
*/
private class MyInstanceMethodOracle implements InstanceMethodOracle {
private final Map<String, Set<JClassType>> signatureToDeclaringClasses = new HashMap<String, Set<JClassType>>();
public MyInstanceMethodOracle(Set<JClassType> jsoTypes,
JClassType javaLangObject, SingleJsoImplData jsoData) {
// Record that the JSO implements its own methods
for (JClassType type : jsoTypes) {
for (JMethod method : type.getMethods()) {
if (!method.isStatic()) {
assert !method.isAbstract() : "Abstract method in JSO type "
+ method;
add(type, method);
}
}
}
/*
* Record the implementing types for methods defined in SingleJsoImpl
* interfaces. We have to make this pass because of possible variance in
* the return types between the abstract method declaration in the
* interface and the concrete method.
*/
for (String intfName : jsoData.getSingleJsoIntfTypes()) {
// We only store the name in the data block to keep it lightweight
JClassType intf = typeOracle.findType(Name.InternalName.toSourceName(intfName));
JClassType jso = typeOracle.getSingleJsoImpl(intf);
for (JMethod method : intf.getMethods()) {
JClassType implementingJso = findImplementingTypeForMethod(jso,
method);
assert implementingJso != null : "Jso should contain method: "
+ method.getJsniSignature();
add(implementingJso, method);
}
}
// Object clobbers everything.
for (JMethod method : javaLangObject.getMethods()) {
if (!method.isStatic()) {
String signature = createSignature(method);
Set<JClassType> declaringClasses = new HashSet<JClassType>();
signatureToDeclaringClasses.put(signature, declaringClasses);
declaringClasses.add(javaLangObject);
}
}
}
public String findOriginalDeclaringClass(String desc, String signature) {
// Lookup the method.
Set<JClassType> declaringClasses = signatureToDeclaringClasses.get(signature);
assert declaringClasses != null : "No classes for " + signature;
if (declaringClasses.size() == 1) {
// Shortcut: if there's only one answer, it must be right.
return createDescriptor(declaringClasses.iterator().next());
}
// Must check for assignability.
String sourceName = desc.replace('/', '.');
sourceName = sourceName.replace('$', '.');
JClassType declaredType = typeOracle.findType(sourceName);
// Check if I declare this directly.
if (declaringClasses.contains(declaredType)) {
return desc;
}
// Check to see what type I am assignable to.
for (JClassType possibleSupertype : declaringClasses) {
if (declaredType.isAssignableTo(possibleSupertype)) {
return createDescriptor(possibleSupertype);
}
}
throw new IllegalArgumentException("Could not resolve signature '"
+ signature + "' from class '" + desc + "'");
}
/**
* Record that a given JSO type contains the concrete implementation of a
* (possibly abstract) method.
*/
private void add(JClassType type, JMethod method) {
String signature = createSignature(method);
Set<JClassType> declaringClasses = signatureToDeclaringClasses.get(signature);
if (declaringClasses == null) {
declaringClasses = new HashSet<JClassType>();
signatureToDeclaringClasses.put(signature, declaringClasses);
}
declaringClasses.add(type);
}
private String createDescriptor(JClassType type) {
String jniSignature = type.getJNISignature();
return jniSignature.substring(1, jniSignature.length() - 1);
}
private String createSignature(JMethod method) {
StringBuffer sb = new StringBuffer(method.getName());
sb.append('(');
for (JParameter param : method.getParameters()) {
sb.append(param.getType().getJNISignature());
}
sb.append(')');
sb.append(method.getReturnType().getJNISignature());
String signature = sb.toString();
return signature;
}
}
/**
* Cook up the data we need to support JSO subtypes that implement interfaces
* with methods. This includes the set of SingleJsoImpl interfaces actually
* implemented by a JSO type, the mangled method names, and the names of the
* Methods that should actually implement the virtual functions.
*
* Given the current implementation of JSO$ and incremental execution of
* rebinds, it's not possible for Generators to produce additional
* JavaScriptObject subtypes, so this data can remain static.
*/
private class MySingleJsoImplData implements SingleJsoImplData {
private final SortedSet<String> mangledNames = new TreeSet<String>();
private final Map<String, List<com.google.gwt.dev.asm.commons.Method>> mangledNamesToDeclarations = new HashMap<String, List<com.google.gwt.dev.asm.commons.Method>>();
private final Map<String, List<com.google.gwt.dev.asm.commons.Method>> mangledNamesToImplementations = new HashMap<String, List<com.google.gwt.dev.asm.commons.Method>>();
private final Set<String> unmodifiableIntfNames = Collections.unmodifiableSet(singleJsoImplTypes);
private final SortedSet<String> unmodifiableNames = Collections.unmodifiableSortedSet(mangledNames);
public MySingleJsoImplData() {
// Loop over all interfaces with JSO implementations
typeLoop : for (JClassType type : typeOracle.getSingleJsoImplInterfaces()) {
assert type.isInterface() == type : "Expecting interfaces only";
/*
* By preemptively adding all possible mangled names by which a method
* could be called, we greatly simplify the logic necessary to rewrite
* the call-site.
*
* interface A {void m();}
*
* interface B extends A {void z();}
*
* becomes
*
* c_g_p_A_m() -> JsoA$.m$()
*
* c_g_p_B_m() -> JsoA$.m$()
*
* c_g_p_B_z() -> JsoB$.z$()
*/
for (JMethod intfMethod : type.getOverridableMethods()) {
assert intfMethod.isAbstract() : "Expecting only abstract methods";
/*
* It is necessary to locate the implementing type on a per-method
* basis. Consider the case of
*
* @SingleJsoImpl interface C extends A, B {}
*
* Methods inherited from interfaces A and B must be dispatched to
* their respective JSO implementations.
*/
JClassType implementingType = typeOracle.getSingleJsoImpl(intfMethod.getEnclosingType());
if (implementingType == null || implementingType.isAnnotationPresent(GwtScriptOnly.class)) {
/*
* This means that there is no concrete implementation of the
* interface by a JSO. Any implementation that might be created by a
* Generator won't be a JSO subtype, so we'll just ignore it as an
* actionable type. Were Generators ever able to create new JSO
* subtypes, we'd have to speculatively rewrite the callsite.
*/
continue typeLoop;
}
/*
* Record the type as being actionable.
*/
singleJsoImplTypes.add(canonicalizeClassName(getBinaryName(type)));
/*
* The mangled name adds the current interface like
*
* com_foo_Bar_methodName
*/
String mangledName = getBinaryName(type).replace('.', '_') + "_"
+ intfMethod.getName();
mangledNames.add(mangledName);
/*
* Handle virtual overrides by finding the method that we would
* normally invoke and using its declaring class as the dispatch
* target.
*/
JMethod implementingMethod;
while ((implementingMethod = findOverloadUsingErasure(
implementingType, intfMethod)) == null) {
implementingType = implementingType.getSuperclass();
}
// implementingmethod and implementingType cannot be null here
/*
* Create a pseudo-method declaration for the interface method. This
* should look something like
*
* ReturnType method$ (ParamType, ParamType)
*
* This must be kept in sync with the WriteJsoImpl class.
*/
{
String decl = getBinaryOrPrimitiveName(intfMethod.getReturnType().getErasedType())
+ " " + intfMethod.getName() + "(";
for (JParameter param : intfMethod.getParameters()) {
decl += ",";
decl += getBinaryOrPrimitiveName(param.getType().getErasedType());
}
decl += ")";
com.google.gwt.dev.asm.commons.Method declaration = com.google.gwt.dev.asm.commons.Method.getMethod(decl);
addToMap(mangledNamesToDeclarations, mangledName, declaration);
}
/*
* Cook up the a pseudo-method declaration for the concrete type. This
* should look something like
*
* ReturnType method$ (JsoType, ParamType, ParamType)
*
* This must be kept in sync with the WriteJsoImpl class.
*/
{
String returnName = getBinaryOrPrimitiveName(implementingMethod.getReturnType().getErasedType());
String jsoName = getBinaryOrPrimitiveName(implementingType);
String decl = returnName + " " + intfMethod.getName() + "$ ("
+ jsoName;
for (JParameter param : implementingMethod.getParameters()) {
decl += ",";
decl += getBinaryOrPrimitiveName(param.getType().getErasedType());
}
decl += ")";
com.google.gwt.dev.asm.commons.Method toImplement = com.google.gwt.dev.asm.commons.Method.getMethod(decl);
addToMap(mangledNamesToImplementations, mangledName, toImplement);
}
}
}
if (logger.isLoggable(TreeLogger.SPAM)) {
TreeLogger dumpLogger = logger.branch(TreeLogger.SPAM,
"SingleJsoImpl method mappings");
for (Map.Entry<String, List<com.google.gwt.dev.asm.commons.Method>> entry : mangledNamesToImplementations.entrySet()) {
dumpLogger.log(TreeLogger.SPAM, entry.getKey() + " -> " + entry.getValue());
}
}
}
public List<com.google.gwt.dev.asm.commons.Method> getDeclarations(
String mangledName) {
List<com.google.gwt.dev.asm.commons.Method> toReturn = mangledNamesToDeclarations.get(mangledName);
return toReturn == null ? null : Collections.unmodifiableList(toReturn);
}
public List<com.google.gwt.dev.asm.commons.Method> getImplementations(
String mangledName) {
List<com.google.gwt.dev.asm.commons.Method> toReturn = mangledNamesToImplementations.get(mangledName);
return toReturn == null ? toReturn
: Collections.unmodifiableList(toReturn);
}
public SortedSet<String> getMangledNames() {
return unmodifiableNames;
}
public Set<String> getSingleJsoIntfTypes() {
return unmodifiableIntfNames;
}
/**
* Assumes that the usual case is a 1:1 mapping.
*/
private <K, V> void addToMap(Map<K, List<V>> map, K key, V value) {
List<V> list = map.get(key);
if (list == null) {
map.put(key, Lists.create(value));
} else {
List<V> maybeOther = Lists.add(list, value);
if (maybeOther != list) {
map.put(key, maybeOther);
}
}
}
/**
* Looks for a concrete implementation of <code>intfMethod</code> in
* <code>implementingType</code>.
*/
private JMethod findOverloadUsingErasure(JClassType implementingType,
JMethod intfMethod) {
int numParams = intfMethod.getParameters().length;
JType[] erasedTypes = new JType[numParams];
for (int i = 0; i < numParams; i++) {
erasedTypes[i] = intfMethod.getParameters()[i].getType().getErasedType();
}
outer : for (JMethod method : implementingType.getOverloads(intfMethod.getName())) {
JParameter[] params = method.getParameters();
if (params.length != numParams) {
continue;
}
for (int i = 0; i < numParams; i++) {
if (params[i].getType().getErasedType() != erasedTypes[i]) {
continue outer;
}
}
return method;
}
return null;
}
}
/**
* Only loads bootstrap classes, specifically excluding classes from the classpath.
*/
private static final ClassLoader bootstrapClassLoader = new ClassLoader(null) { };
/**
* The names of the bridge classes.
*/
private static final Map<String, Class<?>> BRIDGE_CLASS_NAMES = new HashMap<String, Class<?>>();
/**
* The set of classes exposed into user space that actually live in hosted
* space (thus, they bridge across the spaces).
*/
private static final Class<?>[] BRIDGE_CLASSES = new Class<?>[]{
// Have to include the shared GWTBridge class since the client one
// inherits from it, otherwise we get verify errors
ShellJavaScriptHost.class, GWTBridge.class, com.google.gwt.core.shared.GWTBridge.class};
private static final boolean CLASS_DUMP = Boolean.getBoolean("gwt.dev.classDump");
private static final String CLASS_DUMP_PATH = System.getProperty(
"gwt.dev.classDumpPath", "rewritten-classes");
private static boolean emmaAvailable = false;
private static EmmaStrategy emmaStrategy;
/**
* Caches the byte code for {@link JavaScriptHost}.
*/
private static byte[] javaScriptHostBytes;
private static final Map<String, Class<?>> primitiveTypes = new HashMap<String, Class<?>>();
static {
primitiveTypes.put(boolean.class.getSimpleName(), boolean.class);
primitiveTypes.put(byte.class.getSimpleName(), boolean.class);
primitiveTypes.put(char.class.getSimpleName(), boolean.class);
primitiveTypes.put(double.class.getSimpleName(), boolean.class);
primitiveTypes.put(float.class.getSimpleName(), boolean.class);
primitiveTypes.put(int.class.getSimpleName(), boolean.class);
primitiveTypes.put(long.class.getSimpleName(), boolean.class);
primitiveTypes.put(short.class.getSimpleName(), boolean.class);
primitiveTypes.put(void.class.getSimpleName(), boolean.class);
}
static {
for (Class<?> c : BRIDGE_CLASSES) {
BRIDGE_CLASS_NAMES.put(c.getName(), c);
}
/*
* Specific support for bridging to Emma since the user classloader is
* generally completely isolated.
*
* We are looking for a specific emma class "com.vladium.emma.rt.RT". If
* that changes in the future, this code would need to be updated as well.
*/
try {
Class<?> emmaBridge = Class.forName(EmmaStrategy.EMMA_RT_CLASSNAME,
false, Thread.currentThread().getContextClassLoader());
BRIDGE_CLASS_NAMES.put(EmmaStrategy.EMMA_RT_CLASSNAME, emmaBridge);
emmaAvailable = true;
} catch (ClassNotFoundException ignored) {
}
emmaStrategy = EmmaStrategy.get(emmaAvailable);
}
private static void classDump(String name, byte[] bytes) {
String packageName, className;
int pos = name.lastIndexOf('.');
if (pos < 0) {
packageName = "";
className = name;
} else {
packageName = name.substring(0, pos);
className = name.substring(pos + 1);
}
File dir = new File(CLASS_DUMP_PATH + File.separator
+ packageName.replace('.', File.separatorChar));
if (!dir.exists()) {
// No need to check mkdirs result because an IOException will occur anyway
dir.mkdirs();
}
File file = new File(dir, className + ".class");
FileOutputStream fileOutput = null;
try {
fileOutput = new FileOutputStream(file);
fileOutput.write(bytes);
fileOutput.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileOutput != null) {
try {
fileOutput.close();
} catch (IOException e) {
// oh well, we tried
}
}
}
}
/**
* Magic: {@link JavaScriptHost} was never compiled because it's a part of the
* hosted mode infrastructure. However, unlike {@link #BRIDGE_CLASSES},
* {@code JavaScriptHost} needs a separate copy per inside the ClassLoader for
* each module.
*/
private static void ensureJavaScriptHostBytes(TreeLogger logger)
throws UnableToCompleteException {
if (javaScriptHostBytes != null) {
return;
}
String className = JavaScriptHost.class.getName();
try {
String path = className.replace('.', '/') + ".class";
ClassLoader cl = Thread.currentThread().getContextClassLoader();
URL url = cl.getResource(path);
if (url != null) {
javaScriptHostBytes = getClassBytesFromStream(url.openStream());
} else {
logger.log(TreeLogger.ERROR,
"Could not find required bootstrap class '" + className
+ "' in the classpath", null);
throw new UnableToCompleteException();
}
} catch (IOException e) {
logger.log(TreeLogger.ERROR,
"Error reading class bytes for " + className, e);
throw new UnableToCompleteException();
}
}
private static JClassType findImplementingTypeForMethod(JClassType type,
JMethod method) {
JType[] methodParamTypes = method.getErasedParameterTypes();
while (type != null) {
for (JMethod candidate : type.getMethods()) {
if (hasMatchingErasedSignature(method, methodParamTypes, candidate)) {
return type;
}
}
type = type.getSuperclass();
}
return null;
}
private static byte[] getClassBytesFromStream(InputStream is)
throws IOException {
try {
byte classBytes[] = new byte[is.available()];
int read = 0;
while (read < classBytes.length) {
read += is.read(classBytes, read, classBytes.length - read);
}
return classBytes;
} finally {
Utility.close(is);
}
}
private static boolean hasMatchingErasedSignature(JMethod a,
JType[] aParamTypes, JMethod b) {
if (!a.getName().equals(b.getName())) {
return false;
}
JType[] bParamTypes = b.getErasedParameterTypes();
if (aParamTypes.length != bParamTypes.length) {
return false;
}
for (int i = 0; i < aParamTypes.length; ++i) {
if (aParamTypes[i] != bParamTypes[i]) {
return false;
}
}
return true;
}
/**
* The set of units whose JSNI has already been injected.
*/
private Set<CompilationUnit> alreadyInjected = new HashSet<CompilationUnit>();
private final HostedModeClassRewriter classRewriter;
private CompilationState compilationState;
private final DispatchClassInfoOracle dispClassInfoOracle = new DispatchClassInfoOracle();
private Class<?> gwtClass, javaScriptHostClass;
/**
* Used by {@link #findClass(String)} to prevent reentrant JSNI injection.
*/
private boolean isInjectingClass = false;
private final ReentrantLock loadLock = new ReentrantLock();
private final TreeLogger logger;
private final Set<String> scriptOnlyClasses = new HashSet<String>();
private ClassLoader scriptOnlyClassLoader;
private ShellJavaScriptHost shellJavaScriptHost;
private final Set<String> singleJsoImplTypes = new HashSet<String>();
/**
* Used by {@link #findClass(String)} to prevent reentrant JSNI injection.
*/
private Stack<CompilationUnit> toInject = new Stack<CompilationUnit>();
private final TypeOracle typeOracle;
private final Map<Object, Object> weakJavaWrapperCache = new MapMaker().weakKeys().weakValues().makeMap();
private final Map<Integer, Object> weakJsoCache = new MapMaker().weakValues().makeMap();
public CompilingClassLoader(TreeLogger logger,
CompilationState compilationState, ShellJavaScriptHost javaScriptHost)
throws UnableToCompleteException {
super(null);
this.logger = logger;
this.compilationState = compilationState;
this.shellJavaScriptHost = javaScriptHost;
this.typeOracle = compilationState.getTypeOracle();
// Assertions are always on in hosted mode.
setDefaultAssertionStatus(true);
ensureJavaScriptHostBytes(logger);
// Create a class rewriter based on all the subtypes of the JSO class.
JClassType jsoType = typeOracle.findType(JsValueGlue.JSO_CLASS);
if (jsoType != null) {
// Create a set of binary names.
Set<JClassType> jsoTypes = new HashSet<JClassType>();
JClassType[] jsoSubtypes = jsoType.getSubtypes();
Collections.addAll(jsoTypes, jsoSubtypes);
jsoTypes.add(jsoType);
Set<String> jsoTypeNames = new HashSet<String>();
Map<String, List<String>> jsoSuperTypes = new HashMap<String, List<String>>();
for (JClassType type : jsoTypes) {
List<String> types = new ArrayList<String>();
types.add(getBinaryName(type.getSuperclass()));
for (JClassType impl : type.getImplementedInterfaces()) {
types.add(getBinaryName(impl));
}
String binaryName = getBinaryName(type);
jsoTypeNames.add(binaryName);
jsoSuperTypes.put(binaryName, types);
}
SingleJsoImplData singleJsoImplData = new MySingleJsoImplData();
MyInstanceMethodOracle mapper = new MyInstanceMethodOracle(jsoTypes,
typeOracle.getJavaLangObject(), singleJsoImplData);
classRewriter = new HostedModeClassRewriter(jsoTypeNames, jsoSuperTypes,
singleJsoImplData, mapper);
} else {
// If we couldn't find the JSO class, we don't need to do any rewrites.
classRewriter = null;
}
}
/**
* Retrieves the mapped JSO for a given unique id, provided the id was
* previously cached and the JSO has not been garbage collected.
*
* @param uniqueId the previously stored unique id
* @return the mapped JSO, or <code>null</code> if the id was not previously
* mapped or if the JSO has been garbage collected
*/
public Object getCachedJso(int uniqueId) {
return weakJsoCache.get(uniqueId);
}
/**
* Returns the {@link DispatchClassInfo} for a given dispatch id.
*
* @param dispId dispatch identifier
* @return {@link DispatchClassInfo} for a given dispatch id or null if one
* does not exist
*/
public DispatchClassInfo getClassInfoByDispId(int dispId) {
return dispClassInfoOracle.getClassInfoByDispId(dispId);
}
/**
* Returns the dispatch id for a JSNI member reference.
*
* @param jsniMemberRef a JSNI member reference
* @return dispatch id or -1 if the JSNI member reference could not be found
*/
public int getDispId(String jsniMemberRef) {
return dispClassInfoOracle.getDispId(jsniMemberRef);
}
/**
* Retrieves the mapped wrapper for a given Java Object, provided the wrapper
* was previously cached and has not been garbage collected.
*
* @param javaObject the Object being wrapped
* @return the mapped wrapper, or <code>null</code> if the Java object mapped
* or if the wrapper has been garbage collected
*/
public Object getWrapperForObject(Object javaObject) {
return weakJavaWrapperCache.get(javaObject);
}
/**
* Weakly caches a given JSO by unique id. A cached JSO can be looked up by
* unique id until it is garbage collected.
*
* @param uniqueId a unique id associated with the JSO
* @param jso the value to cache
*/
public void putCachedJso(int uniqueId, Object jso) {
weakJsoCache.put(uniqueId, jso);
}
/**
* Weakly caches a wrapper for a given Java Object.
*
* @param javaObject the Object being wrapped
* @param wrapper the mapped wrapper
*/
public void putWrapperForObject(Object javaObject, Object wrapper) {
weakJavaWrapperCache.put(javaObject, wrapper);
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
if (className == null) {
throw new ClassNotFoundException("null class name",
new NullPointerException());
}
if (className.equals("com.google.gwt.core.ext.debug.JsoEval")) {
// In addition to the system ClassLoader, we let JsoEval be available
// from this CompilingClassLoader in case that's where the debugger
// happens to look.
return ClassLoader.getSystemClassLoader().loadClass(className);
}
loadLock.lock();
try {
if (scriptOnlyClasses.contains(className)) {
// Allow the child ClassLoader to handle this
throw new ClassNotFoundException();
}
// Check for a bridge class that spans hosted and user space.
if (BRIDGE_CLASS_NAMES.containsKey(className)) {
return BRIDGE_CLASS_NAMES.get(className);
}
// Get the bytes, compiling if necessary.
byte[] classBytes = findClassBytes(className);
if (classBytes == null) {
throw new ClassNotFoundException(className);
}
if (HasAnnotation.hasAnnotation(classBytes, GwtScriptOnly.class)) {
scriptOnlyClasses.add(className);
maybeInitializeScriptOnlyClassLoader();
/*
* Release the lock before side-loading from scriptOnlyClassLoader. This prevents deadlock
* conditions when a class from scriptOnlyClassLoader ends up trying to call back into this
* classloader from another thread.
*/
loadLock.unlock();
// Also don't run the static initializer to lower the risk of deadlock.
return Class.forName(className, false, scriptOnlyClassLoader);
}
/*
* Prevent reentrant problems where classes that need to be injected have
* circular dependencies on one another via JSNI and inheritance. This check
* ensures that a class's supertype can refer to the subtype (static
* members, etc) via JSNI references by ensuring that the Class for the
* subtype will have been defined before injecting the JSNI for the
* supertype.
*/
boolean localInjection;
if (!isInjectingClass) {
localInjection = isInjectingClass = true;
} else {
localInjection = false;
}
Class<?> newClass = defineClass(className, classBytes, 0, classBytes.length);
if (className.equals(JavaScriptHost.class.getName())) {
javaScriptHostClass = newClass;
updateJavaScriptHost();
}
/*
* We have to inject the JSNI code after defining the class, since dispId
* assignment is based around reflection on Class objects. Don't inject JSNI
* when loading a JSO interface class; just wait until the implementation
* class is loaded.
*/
if (!classRewriter.isJsoIntf(className)) {
CompilationUnit unit = getUnitForClassName(canonicalizeClassName(className));
if (unit != null) {
toInject.push(unit);
}
}
if (localInjection) {
try {
/*
* Can't use an iterator here because calling injectJsniFor may cause
* additional entries to be added.
*/
while (toInject.size() > 0) {
CompilationUnit unit = toInject.remove(0);
if (!alreadyInjected.contains(unit)) {
injectJsniMethods(unit);
alreadyInjected.add(unit);
}
}
} finally {
isInjectingClass = false;
}
}
if (className.equals("com.google.gwt.core.client.GWT")) {
gwtClass = newClass;
updateGwtClass();
}
return newClass;
} finally {
if (loadLock.isLocked()) {
loadLock.unlock();
}
}
}
/**
* Remove some of the excess locking that we'd normally inherit from loadClass.
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// at design time we want to provide parent ClassLoader, so keep default implementation
if (Beans.isDesignTime()) {
return super.loadClass(name, resolve);
}
Class c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
assert getParent() == null;
try {
c = bootstrapClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
void clear() {
// Release our references to the shell.
shellJavaScriptHost = null;
scriptOnlyClasses.clear();
scriptOnlyClassLoader = null;
updateJavaScriptHost();
weakJsoCache.clear();
weakJavaWrapperCache.clear();
dispClassInfoOracle.clear();
}
/**
* Convert a binary class name into a resource-like name.
*/
private String canonicalizeClassName(String className) {
String lookupClassName = className.replace('.', '/');
// A JSO impl class ends with $, strip it
if (classRewriter != null && classRewriter.isJsoImpl(className)) {
lookupClassName = lookupClassName.substring(0,
lookupClassName.length() - 1);
}
return lookupClassName;
}
@SuppressWarnings("deprecation")
private byte[] findClassBytes(String className) {
if (JavaScriptHost.class.getName().equals(className)) {
// No need to rewrite.
return javaScriptHostBytes;
}
// A JSO impl class needs the class bytes for the original class.
String lookupClassName = canonicalizeClassName(className);
CompiledClass compiledClass = compilationState.getClassFileMap().get(
lookupClassName);
if (classRewriter != null && classRewriter.isJsoIntf(className)) {
// Generate a synthetic JSO interface class.
byte[] newBytes = classRewriter.writeJsoIntf(className, compiledClass != null ?
compiledClass.getBytes() : null);
if (CLASS_DUMP) {
classDump(className, newBytes);
}
return newBytes;
}
CompilationUnit unit = (compiledClass == null)
? getUnitForClassName(lookupClassName) : compiledClass.getUnit();
if (emmaAvailable) {
/*
* build the map for anonymous classes. Do so only if unit has anonymous
* classes, jsni methods, is not super-source and the map has not been
* built before.
*/
List<JsniMethod> jsniMethods = (unit == null) ? null
: unit.getJsniMethods();
if (unit != null && !unit.isSuperSource() && !unit.isGenerated()
&& unit.hasAnonymousClasses() && jsniMethods != null
&& jsniMethods.size() > 0 && !unit.createdClassMapping()) {
if (!unit.constructAnonymousClassMappings(logger)) {
logger.log(TreeLogger.ERROR,
"Our heuristic for mapping anonymous classes between compilers "
+ "failed. Unsafe to continue because the wrong jsni code "
+ "could end up running. className = " + className);
return null;
}
}
}
byte classBytes[] = null;
if (compiledClass != null) {
classBytes = compiledClass.getBytes();
if (!compiledClass.getUnit().isSuperSource()) {
classBytes = emmaStrategy.getEmmaClassBytes(classBytes,
lookupClassName, compiledClass.getUnit().getLastModified());
} else {
if (logger.isLoggable(TreeLogger.SPAM)) {
logger.log(TreeLogger.SPAM, "no emma instrumentation for "
+ lookupClassName + " because it is from super-source");
}
}
} else if (emmaAvailable) {
/*
* TypeOracle does not know about this class. Most probably, this class
* was referenced in one of the classes loaded from disk. Check if we can
* find it on disk. Typically this is a synthetic class added by the
* compiler.
*/
if (typeHasCompilationUnit(lookupClassName)
&& CompilationUnit.isClassnameGenerated(className)) {
/*
* modification time = 0 ensures that whatever is on the disk is always
* loaded.
*/
if (logger.isLoggable(TreeLogger.DEBUG)) {
logger.log(TreeLogger.DEBUG, "EmmaStrategy: loading " + lookupClassName
+ " from disk even though TypeOracle does not know about it");
}
classBytes = emmaStrategy.getEmmaClassBytes(null, lookupClassName, 0);
}
}
if (classBytes != null && classRewriter != null) {
Map<String, String> anonymousClassMap = Collections.emptyMap();
if (unit != null) {
anonymousClassMap = unit.getAnonymousClassMap();
}
byte[] newBytes = classRewriter.rewrite(typeOracle, className,
classBytes, anonymousClassMap);
if (CLASS_DUMP) {
if (!Arrays.equals(classBytes, newBytes)) {
classDump(className, newBytes);
}
}
classBytes = newBytes;
}
if (unit != null && unit.isError()) {
// Compile worked, but the unit had some kind of error (JSNI?)
CompilationProblemReporter.reportErrors(logger, unit, false);
}
return classBytes;
}
private String getBinaryName(JClassType type) {
String name = type.getPackage().getName() + '.';
name += type.getName().replace('.', '$');
return name;
}
private String getBinaryOrPrimitiveName(JType type) {
JArrayType asArray = type.isArray();
JClassType asClass = type.isClassOrInterface();
JPrimitiveType asPrimitive = type.isPrimitive();
if (asClass != null) {
return getBinaryName(asClass);
} else if (asPrimitive != null) {
return asPrimitive.getQualifiedSourceName();
} else if (asArray != null) {
JType componentType = asArray.getComponentType();
return getBinaryOrPrimitiveName(componentType) + "[]";
} else {
throw new InternalCompilerException("Cannot create binary name for "
+ type.getQualifiedSourceName());
}
}
/**
* Returns the compilationUnit corresponding to the className. For nested
* classes, the unit corresponding to the top level type is returned.
*
* Since a file might have several top-level types, search using classFileMap.
*/
private CompilationUnit getUnitForClassName(String className) {
String mainTypeName = className;
int index = mainTypeName.length();
CompiledClass cc = null;
while (cc == null && index != -1) {
mainTypeName = mainTypeName.substring(0, index);
cc = compilationState.getClassFileMap().get(mainTypeName);
index = mainTypeName.lastIndexOf('$');
}
return cc == null ? null : cc.getUnit();
}
private void injectJsniMethods(CompilationUnit unit) {
if (unit == null || unit.getJsniMethods() == null) {
return;
}
Event event = SpeedTracerLogger.start(DevModeEventType.LOAD_JSNI, "unit",
unit.getTypeName());
try {
shellJavaScriptHost.createNativeMethods(logger, unit.getJsniMethods(),
this);
} finally {
event.end();
}
}
private void maybeInitializeScriptOnlyClassLoader() {
if (scriptOnlyClassLoader == null) {
scriptOnlyClassLoader = new MultiParentClassLoader(this,
Thread.currentThread().getContextClassLoader());
}
}
private boolean typeHasCompilationUnit(String className) {
return getUnitForClassName(className) != null;
}
/**
* Tricky one, this. Reaches over into this modules's JavaScriptHost class and
* sets its static 'host' field to our module space.
*
* @see JavaScriptHost
*/
private void updateGwtClass() {
if (gwtClass == null) {
return;
}
Throwable caught;
try {
GWTBridgeImpl bridge;
if (shellJavaScriptHost == null) {
bridge = null;
} else {
bridge = new GWTBridgeImpl(shellJavaScriptHost);
}
final Class<?>[] paramTypes = new Class[]{GWTBridge.class};
Method setBridgeMethod = gwtClass.getDeclaredMethod("setBridge",
paramTypes);
setBridgeMethod.setAccessible(true);
setBridgeMethod.invoke(gwtClass, new Object[]{bridge});
return;
} catch (SecurityException e) {
caught = e;
} catch (NoSuchMethodException e) {
caught = e;
} catch (IllegalArgumentException e) {
caught = e;
} catch (IllegalAccessException e) {
caught = e;
} catch (InvocationTargetException e) {
caught = e.getTargetException();
}
throw new RuntimeException("Error initializing GWT bridge", caught);
}
/**
* Tricky one, this. Reaches over into this modules's JavaScriptHost class and
* sets its static 'host' field to our module space.
*
* @see JavaScriptHost
*/
private void updateJavaScriptHost() {
if (javaScriptHostClass == null) {
return;
}
Throwable caught;
try {
final Class<?>[] paramTypes = new Class[]{ShellJavaScriptHost.class};
Method setHostMethod = javaScriptHostClass.getMethod("setHost",
paramTypes);
setHostMethod.invoke(javaScriptHostClass,
new Object[]{shellJavaScriptHost});
return;
} catch (SecurityException e) {
caught = e;
} catch (NoSuchMethodException e) {
caught = e;
} catch (IllegalArgumentException e) {
caught = e;
} catch (IllegalAccessException e) {
caught = e;
} catch (InvocationTargetException e) {
caught = e.getTargetException();
}
throw new RuntimeException("Error initializing JavaScriptHost", caught);
}
}