blob: ff38213b8d8c78afe93c4d3b4bac29fbd810fb04 [file] [log] [blame]
/*
* Copyright 2009 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.rewrite;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.shell.rewrite.HostedModeClassRewriter.SingleJsoImplData;
import com.google.gwt.dev.util.collect.Maps;
import com.google.gwt.dev.util.collect.Sets;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Effects the renaming of {@code @SingleJsoImpl} methods from their original
* name to their mangled name. Let us call the original method an "unmangled
* method" and the new method a "mangled method". There are three steps in this
* process:
* <ol>
* <li>Within {@code @SingleJsoImpl} interfaces rename all unmangled methods to
* become mangled methods.</li>
* <li>Within non-JSO classes containing a concrete implementation of an
* unmangled method, add a mangled method which is implemented as a simple
* trampoline to the unmangled method. (We don't do this in JSO classes here
* because the one-and-only trampoline lives in JavaScriptObject$ and is emitted
* in {@link WriteJsoImpl}).
* <li>Update all call sites targeting unmangled methods to target mangled
* methods instead, provided the caller is binding to the interface rather than
* a concrete type.</li>
* </ol>
*/
public class RewriteSingleJsoImplDispatches extends ClassVisitor {
private class MyMethodVisitor extends MethodVisitor {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM7, mv);
}
/*
* Implements objective #3: updates call sites to unmangled methods.
*/
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String desc, boolean dintf) {
if (opcode == Opcodes.INVOKEINTERFACE) {
if (jsoData.getSingleJsoIntfTypes().contains(owner)) {
// Simple case; referring directly to a SingleJso interface.
name = owner.replace('/', '_') + "_" + name;
assert jsoData.getMangledNames().contains(name) : "Missing " + name;
} else {
/*
* Might be referring to a subtype of a SingleJso interface:
*
* interface IA { void foo() }
*
* interface JA extends JSO implements IA;
*
* interface IB extends IA {}
*
* void bar() { ((IB) object).foo(); }
*/
outer : for (String intf : computeAllInterfaces(owner)) {
if (jsoData.getSingleJsoIntfTypes().contains(intf)) {
/*
* Check that it really should be mangled and is not a reference
* to a method defined in a non-singleJso super-interface. If
* there are two super-interfaces that define methods with
* identical names and descriptors, the choice of implementation
* is undefined.
*/
String maybeMangled = intf.replace('/', '_') + "_" + name;
List<Method> methods = jsoData.getImplementations(maybeMangled);
if (methods != null) {
for (Method method : methods) {
/*
* Found a method with the right name, but we need to check
* the parameters and the return type. In order to do this,
* we'll look at the arguments and return type of the target
* method, removing the first argument, which is the instance.
*/
assert method.getArgumentTypes().length >= 1;
Type[] argumentTypes = new Type[method.getArgumentTypes().length - 1];
System.arraycopy(method.getArgumentTypes(), 1, argumentTypes,
0, argumentTypes.length);
String maybeDescriptor = Type.getMethodDescriptor(
method.getReturnType(), argumentTypes);
if (maybeDescriptor.equals(desc)) {
name = maybeMangled;
break outer;
}
}
}
}
}
}
}
super.visitMethodInsn(opcode, owner, name, desc, dintf);
}
}
private String currentTypeName;
private final Set<String> implementedMethods = new HashSet<String>();
private boolean inSingleJsoImplInterfaceType;
private Map<String, Set<String>> intfNamesToAllInterfaces = Maps.create();
private final SingleJsoImplData jsoData;
private final TypeOracle typeOracle;
public RewriteSingleJsoImplDispatches(ClassVisitor v, TypeOracle typeOracle,
SingleJsoImplData jsoData) {
super(Opcodes.ASM7, v);
this.typeOracle = typeOracle;
this.jsoData = jsoData;
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
assert currentTypeName == null;
super.visit(version, access, name, signature, superName, interfaces);
/*
* This visitor would mangle JSO$ since it acts as a roll-up of all
* SingleJso types and the result would be repeated method definitions due
* to the trampoline methods this visitor would create.
*/
if (name.equals(HostedModeClassRewriter.JAVASCRIPTOBJECT_IMPL_DESC)) {
return;
}
currentTypeName = name;
inSingleJsoImplInterfaceType = jsoData.getSingleJsoIntfTypes().contains(
name);
/*
* Implements objective #2: non-JSO types that implement a SingleJsoImpl
* interface don't have their original instance methods altered. Instead, we
* add trampoline methods with mangled names that simply call over to the
* original methods.
*/
if (interfaces != null && (access & Opcodes.ACC_INTERFACE) == 0) {
Set<String> toStub = computeAllInterfaces(interfaces);
toStub.retainAll(jsoData.getSingleJsoIntfTypes());
for (String stubIntr : toStub) {
writeTrampoline(stubIntr);
}
}
}
@Override
public void visitEnd() {
/*
* Add any missing methods that are defined by a super-interface, but that
* may be referenced via a more specific interface.
*/
if (inSingleJsoImplInterfaceType) {
for (String mangledName : toImplement(currentTypeName)) {
for (Method method : jsoData.getDeclarations(mangledName)) {
writeEmptyMethod(mangledName, method);
}
}
}
super.visitEnd();
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
/*
* Implements objective #2: Rename unmangled methods in a @SingleJsoImpl
* into mangled methods (except for clinit, LOL).
*/
if (inSingleJsoImplInterfaceType && !"<clinit>".equals(name)) {
name = currentTypeName.replace('/', '_') + "_" + name;
implementedMethods.add(name);
}
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
if (mv == null) {
return null;
}
return new MyMethodVisitor(mv);
}
private Set<String> computeAllInterfaces(String intfName) {
Set<String> toReturn = intfNamesToAllInterfaces.get(intfName);
if (toReturn != null) {
return toReturn;
}
toReturn = Sets.create();
List<JClassType> q = new LinkedList<JClassType>();
JClassType intf = typeOracle.findType(intfName.replace('/', '.').replace(
'$', '.'));
/*
* If the interface's compilation unit wasn't retained due to an error, then
* it won't be available in the typeOracle for us to rewrite
*/
if (intf != null) {
q.add(intf);
}
while (!q.isEmpty()) {
intf = q.remove(0);
String resourceName = getResourceName(intf);
if (!toReturn.contains(resourceName)) {
toReturn = Sets.add(toReturn, resourceName);
Collections.addAll(q, intf.getImplementedInterfaces());
}
}
intfNamesToAllInterfaces = Maps.put(intfNamesToAllInterfaces, intfName,
toReturn);
return toReturn;
}
private Set<String> computeAllInterfaces(String[] interfaces) {
Set<String> toReturn = new HashSet<String>();
for (String intfName : interfaces) {
toReturn.addAll(computeAllInterfaces(intfName));
}
return toReturn;
}
private String getResourceName(JClassType type) {
if (type.getEnclosingType() != null) {
return getResourceName(type.getEnclosingType()) + "$"
+ type.getSimpleSourceName();
}
return type.getQualifiedSourceName().replace('.', '/');
}
/**
* Given a resource name of a class, find all mangled method names that must
* be implemented.
*/
private SortedSet<String> toImplement(String typeName) {
String name = typeName.replace('/', '_');
String prefix = name + "_";
String suffix = name + "`";
SortedSet<String> toReturn = new TreeSet<String>();
for (String mangledName : jsoData.getMangledNames().subSet(prefix, suffix)) {
if (!implementedMethods.contains(mangledName)) {
toReturn.add(mangledName);
}
}
return toReturn;
}
private void writeEmptyMethod(String mangledMethodName, Method declMethod) {
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
| Opcodes.ACC_ABSTRACT, mangledMethodName, declMethod.getDescriptor(),
null, null);
mv.visitEnd();
}
/**
* For regular Java objects that implement a SingleJsoImpl interface, write
* instance trampoline dispatchers for mangled method names to the
* implementing method.
*/
private void writeTrampoline(String stubIntr) {
/*
* This is almost the same kind of trampoline as the ones generated in
* WriteJsoImpl, however there are enough small differences between the
* semantics of the dispatches that would make a common implementation far
* more awkward than the duplication of code.
*/
for (String mangledName : toImplement(stubIntr)) {
for (Method method : jsoData.getDeclarations(mangledName)) {
Method toCall = new Method(method.getName(), method.getDescriptor());
// Must not be final
MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC
| Opcodes.ACC_SYNTHETIC, mangledName, method.getDescriptor(), null, null);
if (mv != null) {
mv.visitCode();
/*
* It just so happens that the stack and local variable sizes are the
* same, but they're kept distinct to aid in clarity should the
* dispatch logic change.
*
* These start at 1 because we need to load "this" onto the stack
*/
int var = 1;
int size = 1;
// load this
mv.visitVarInsn(Opcodes.ALOAD, 0);
// then the rest of the arguments
for (Type t : toCall.getArgumentTypes()) {
size += t.getSize();
mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), var);
var += t.getSize();
}
// Make sure there's enough room for the return value
size = Math.max(size, toCall.getReturnType().getSize());
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, currentTypeName,
toCall.getName(), toCall.getDescriptor(), false);
mv.visitInsn(toCall.getReturnType().getOpcode(Opcodes.IRETURN));
mv.visitMaxs(size, var);
mv.visitEnd();
}
}
}
}
}