| /* |
| * 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.rewrite; |
| |
| import com.google.gwt.dev.shell.JavaScriptHost; |
| import com.google.gwt.dev.util.Name.InternalName; |
| |
| 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.GeneratorAdapter; |
| import org.objectweb.asm.commons.Method; |
| |
| import java.lang.reflect.Modifier; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * Turns native method declarations into normal Java functions which perform the |
| * corresponding JSNI dispatch. |
| */ |
| public class RewriteJsniMethods extends ClassVisitor { |
| |
| /** |
| * Fast way to look up boxing methods. |
| */ |
| private static class Boxing { |
| |
| /** |
| * A matching list of boxed types for each primitive type. |
| */ |
| private static final Type[] BOXED_TYPES = new Type[] { |
| VOID_TYPE, BOOLEAN_TYPE, CHARACTER_TYPE, BYTE_TYPE, SHORT_TYPE, |
| INTEGER_TYPE, FLOAT_TYPE, LONG_TYPE, DOUBLE_TYPE,}; |
| |
| /** |
| * The list of primitive types. |
| */ |
| private static final Type[] PRIMITIVE_TYPES = new Type[] { |
| Type.VOID_TYPE, Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.BYTE_TYPE, |
| Type.SHORT_TYPE, Type.INT_TYPE, Type.FLOAT_TYPE, Type.LONG_TYPE, |
| Type.DOUBLE_TYPE,}; |
| |
| /** |
| * A map of Type.sort() to valueOf Method. There are 11 possible results |
| * from Type.sort(), 0 through 10. |
| */ |
| private static final Method[] SORT_MAP = new Method[11]; |
| |
| static { |
| assert PRIMITIVE_TYPES.length == BOXED_TYPES.length; |
| for (int i = 0; i < PRIMITIVE_TYPES.length; ++i) { |
| Type primitive = PRIMITIVE_TYPES[i]; |
| Type boxed = BOXED_TYPES[i]; |
| if (boxed != null) { |
| SORT_MAP[i] = new Method("valueOf", boxed, new Type[] {primitive}); |
| } |
| } |
| } |
| |
| public static Method getBoxMethod(Type type) { |
| int sortType = type.getSort(); |
| assert (sortType >= 0 && sortType < SORT_MAP.length) : "Unexpected JavaScriptHostInfo.get index - " |
| + sortType; |
| return SORT_MAP[sortType]; |
| } |
| } |
| |
| /** |
| * Oracle for info regarding {@link JavaScriptHost}. |
| */ |
| private static class JavaScriptHostInfo { |
| |
| /** |
| * The {@link JavaScriptHost} type. |
| */ |
| public static final Type TYPE = Type.getType(JavaScriptHost.class); |
| |
| /** |
| * The parameter signature of {@link JavaScriptHost}'s invoke* methods, in |
| * classes. |
| */ |
| private static final Class<?>[] INVOKE_NATIVE_PARAM_CLASSES = new Class[] { |
| String.class, Object.class, Class[].class, Object[].class,}; |
| |
| /** |
| * The parameter signature of {@link JavaScriptHost}'s invoke* methods, in |
| * types. |
| */ |
| private static final Type[] INVOKE_NATIVE_PARAM_TYPES; |
| |
| /** |
| * A map of Type.sort() to JavaScriptHostInfo. |
| */ |
| private static final JavaScriptHostInfo[] SORT_MAP = new JavaScriptHostInfo[11]; |
| |
| static { |
| INVOKE_NATIVE_PARAM_TYPES = new Type[INVOKE_NATIVE_PARAM_CLASSES.length]; |
| for (int i = 0; i < INVOKE_NATIVE_PARAM_TYPES.length; ++i) { |
| INVOKE_NATIVE_PARAM_TYPES[i] = Type.getType(INVOKE_NATIVE_PARAM_CLASSES[i]); |
| } |
| |
| Class<?>[] primitives = { |
| void.class, boolean.class, byte.class, char.class, short.class, |
| int.class, long.class, float.class, double.class}; |
| for (Class<?> c : primitives) { |
| Type type = Type.getType(c); |
| String typeName = type.getClassName(); |
| String firstChar = typeName.substring(0, 1).toUpperCase(Locale.ROOT); |
| typeName = firstChar + typeName.substring(1); |
| SORT_MAP[type.getSort()] = new JavaScriptHostInfo(type, "invokeNative" |
| + typeName); |
| } |
| JavaScriptHostInfo objectType = new JavaScriptHostInfo( |
| Type.getType(Object.class), "invokeNativeObject", true); |
| SORT_MAP[Type.ARRAY] = objectType; |
| SORT_MAP[Type.OBJECT] = objectType; |
| assert noNulls(SORT_MAP) : "Did not fully fill in JavaScriptHostInfo.SORT_MAP"; |
| } |
| |
| public static JavaScriptHostInfo get(int sortType) { |
| assert (sortType >= 0 && sortType < SORT_MAP.length) : "Unexpected JavaScriptHostInfo.get index - " |
| + sortType; |
| return SORT_MAP[sortType]; |
| } |
| |
| /** |
| * Validate our model against the real JavaScriptHost class. |
| */ |
| private static boolean matchesRealMethod(String methodName, Type returnType) { |
| try { |
| java.lang.reflect.Method method = JavaScriptHost.class.getDeclaredMethod( |
| methodName, INVOKE_NATIVE_PARAM_CLASSES); |
| assert (method.getModifiers() & Modifier.STATIC) != 0 : "Was expecting method '" |
| + method + "' to be static"; |
| Type realReturnType = Type.getType(method.getReturnType()); |
| return realReturnType.getDescriptor().equals(returnType.getDescriptor()); |
| } catch (SecurityException e) { |
| } catch (NoSuchMethodException e) { |
| } |
| return false; |
| } |
| |
| private static boolean noNulls(Object[] array) { |
| for (Object element : array) { |
| if (element == null) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private final Method method; |
| |
| private final boolean requiresCast; |
| |
| private JavaScriptHostInfo(Type returnType, String methodName) { |
| this(returnType, methodName, false); |
| } |
| |
| private JavaScriptHostInfo(Type returnType, String methodName, |
| boolean requiresCast) { |
| this.requiresCast = requiresCast; |
| this.method = new Method(methodName, returnType, |
| INVOKE_NATIVE_PARAM_TYPES); |
| assert matchesRealMethod(methodName, returnType) : "JavaScriptHostInfo for '" |
| + this + "' does not match real method"; |
| } |
| |
| public Method getMethod() { |
| return method; |
| } |
| |
| public boolean requiresCast() { |
| return requiresCast; |
| } |
| |
| @Override |
| public String toString() { |
| return method.toString(); |
| } |
| } |
| |
| /** |
| * Rewrites native Java methods to dispatch as JSNI. |
| */ |
| private class MyMethodAdapter extends GeneratorAdapter { |
| |
| private String descriptor; |
| private boolean isStatic; |
| private String name; |
| |
| public MyMethodAdapter(MethodVisitor mv, int access, String name, |
| String desc) { |
| super(Opcodes.ASM6, mv, access, name, desc); |
| this.descriptor = desc; |
| this.name = name; |
| isStatic = (access & Opcodes.ACC_STATIC) != 0; |
| } |
| |
| /** |
| * Replacement for {@link GeneratorAdapter#box(Type)}, which always calls, |
| * for example, {@code new Boolean} instead of using |
| * {@link Boolean#valueOf(boolean)}. |
| */ |
| @Override |
| public void box(Type type) { |
| Method method = Boxing.getBoxMethod(type); |
| if (method != null) { |
| invokeStatic(method.getReturnType(), method); |
| } |
| } |
| |
| /** |
| * Does all of the work necessary to do the dispatch to the appropriate |
| * variant of {@link JavaScriptHost#invokeNativeVoid |
| * JavaScriptHost.invokeNative*}. And example output: |
| * |
| * <pre> |
| * return JavaScriptHost.invokeNativeInt( |
| * "@com.google.gwt.sample.hello.client.Hello::echo(I)", null, |
| * new Class[] {int.class,}, new Object[] {x,}); |
| * </pre> |
| */ |
| @Override |
| public void visitCode() { |
| super.visitCode(); |
| |
| /* |
| * If you modify the generated code, you must recompute the stack size in |
| * visitEnd(). |
| */ |
| |
| // First argument - JSNI signature |
| String jsniTarget = getJsniSignature(name, descriptor); |
| visitLdcInsn(jsniTarget); |
| // Stack is at 1 |
| |
| // Second argument - target; "null" if static, otherwise "this". |
| if (isStatic) { |
| visitInsn(Opcodes.ACONST_NULL); |
| } else { |
| loadThis(); |
| } |
| // Stack is at 2 |
| |
| // Third argument - a Class[] describing the types of this method |
| loadClassArray(); |
| // Stack is at 3; reaches 6 internally |
| |
| // Fourth argument - all the arguments boxed into an Object[] |
| loadArgArray(); |
| // Stack is at 4; reaches 7 or 8 internally (long/double takes 2) |
| |
| // Invoke the matching JavaScriptHost.invokeNative* method |
| Type returnType = Type.getReturnType(descriptor); |
| JavaScriptHostInfo info = JavaScriptHostInfo.get(returnType.getSort()); |
| invokeStatic(JavaScriptHostInfo.TYPE, info.getMethod()); |
| // Stack is at 1 |
| if (info.requiresCast()) { |
| checkCast(returnType); |
| } |
| returnValue(); |
| // Stack is at 0 |
| } |
| |
| @Override |
| public void visitEnd() { |
| // Force code to be visited; required since this was a native method. |
| visitCode(); |
| |
| /* |
| * For speed, we don't ask ASM to COMPUTE_MAXS. We manually calculated a |
| * max depth of 8. |
| * |
| * Also, when tobyr tried getting ASM to compute the correct stack size, |
| * ASM seemed to compute the wrong value for reasons we don't understand. |
| */ |
| int maxStack = 8; |
| int maxLocals = 0; // Computed by GeneratorAdapter superclass. |
| super.visitMaxs(maxStack, maxLocals); |
| super.visitEnd(); |
| } |
| |
| private void loadClassArray() { |
| Type[] argTypes = Type.getArgumentTypes(descriptor); |
| push(argTypes.length); |
| newArray(CLASS_TYPE); |
| // Stack is at 3 |
| for (int i = 0; i < argTypes.length; ++i) { |
| dup(); |
| push(i); |
| push(argTypes[i]); |
| arrayStore(CLASS_TYPE); |
| } |
| } |
| } |
| |
| private static final Type BOOLEAN_TYPE = Type.getObjectType("java/lang/Boolean"); |
| private static final Type BYTE_TYPE = Type.getObjectType("java/lang/Byte"); |
| private static final Type CHARACTER_TYPE = Type.getObjectType("java/lang/Character"); |
| private static final Type CLASS_TYPE = Type.getObjectType("java/lang/Class"); |
| private static final Type DOUBLE_TYPE = Type.getObjectType("java/lang/Double"); |
| private static final Type FLOAT_TYPE = Type.getObjectType("java/lang/Float"); |
| private static final Type INTEGER_TYPE = Type.getObjectType("java/lang/Integer"); |
| private static final Type LONG_TYPE = Type.getObjectType("java/lang/Long"); |
| private static final Type SHORT_TYPE = Type.getObjectType("java/lang/Short"); |
| private static final Type VOID_TYPE = Type.getObjectType("java/lang/Void"); |
| |
| /** |
| * The internal name of the class we're operating on. |
| */ |
| private String classDesc; |
| private Map<String, String> anonymousClassMap; |
| |
| public RewriteJsniMethods(ClassVisitor v, |
| Map<String, String> anonymousClassMap) { |
| super(Opcodes.ASM6, v); |
| this.anonymousClassMap = anonymousClassMap; |
| } |
| |
| @Override |
| public void visit(final int version, final int access, final String name, |
| final String signature, final String superName, final String[] interfaces) { |
| this.classDesc = name; |
| super.visit(version, access, name, signature, superName, interfaces); |
| } |
| |
| @Override |
| public MethodVisitor visitMethod(int access, String name, String desc, |
| String signature, String[] exceptions) { |
| |
| boolean isNative = (access & Opcodes.ACC_NATIVE) != 0; |
| access &= ~Opcodes.ACC_NATIVE; |
| |
| MethodVisitor mv = super.visitMethod(access, name, desc, signature, |
| exceptions); |
| |
| if (isNative) { |
| mv = new MyMethodAdapter(mv, access, name, desc); |
| } |
| |
| return mv; |
| } |
| |
| /** |
| * Returns the JSNI signature describing the method. |
| * |
| * @param name the name of the method; for example {@code "echo"} |
| * @param descriptor the descriptor for the method; for example {@code "(I)I"} |
| * @return the JSNI signature for the method; for example, {@code |
| * "@com.google.gwt.sample.hello.client.Hello::echo(I)"} |
| */ |
| private String getJsniSignature(String name, String descriptor) { |
| int argsIndexBegin = descriptor.indexOf('('); |
| int argsIndexEnd = descriptor.indexOf(')'); |
| assert argsIndexBegin != -1 && argsIndexEnd != -1 |
| && argsIndexBegin < argsIndexEnd : "Could not find the arguments in the descriptor, " |
| + descriptor; |
| String argsDescriptor = descriptor.substring(argsIndexBegin, |
| argsIndexEnd + 1); |
| String classDescriptor = InternalName.toBinaryName(classDesc); |
| String newDescriptor = anonymousClassMap.get(classDesc); |
| if (newDescriptor != null) { |
| classDescriptor = InternalName.toBinaryName(newDescriptor); |
| } |
| // Always use binary names for JSNI method names |
| return "@" + classDescriptor + "::" + name + argsDescriptor; |
| } |
| } |