diff options
Diffstat (limited to 'dx')
-rw-r--r-- | dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java | 536 | ||||
-rw-r--r-- | dx/src/com/android/dx/gen/Code.java | 4 | ||||
-rw-r--r-- | dx/src/com/android/dx/gen/DexCacheException.java | 28 | ||||
-rw-r--r-- | dx/src/com/android/dx/gen/ProxyBuilder.java | 667 |
4 files changed, 1234 insertions, 1 deletions
diff --git a/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java b/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java new file mode 100644 index 000000000..070031cbc --- /dev/null +++ b/dx/junit-tests/com/android/dx/gen/ProxyBuilderTest.java @@ -0,0 +1,536 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.android.dx.gen; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Random; + +public class ProxyBuilderTest extends TestCase { + private FakeInvocationHandler fakeHandler = new FakeInvocationHandler(); + + public static class SimpleClass { + public String simpleMethod() { + throw new AssertionFailedError(); + } + } + + public void testExampleOperation() throws Throwable { + fakeHandler.setFakeResult("expected"); + SimpleClass proxy = proxyFor(SimpleClass.class).build(); + assertEquals("expected", proxy.simpleMethod()); + } + + public static class ConstructorTakesArguments { + private final String argument; + + public ConstructorTakesArguments(String arg) { + argument = arg; + } + + public String method() { + throw new AssertionFailedError(); + } + } + + public void testConstruction_SucceedsIfCorrectArgumentsProvided() throws Throwable { + ConstructorTakesArguments proxy = proxyFor(ConstructorTakesArguments.class) + .constructorArgTypes(String.class) + .constructorArgValues("hello") + .build(); + assertEquals("hello", proxy.argument); + proxy.method(); + } + + public void testConstruction_FailsWithWrongNumberOfArguments() throws Throwable { + try { + proxyFor(ConstructorTakesArguments.class).build(); + fail(); + } catch (IllegalArgumentException expected) {} + } + + public void testClassIsNotAccessbile_FailsWithUnsupportedOperationException() throws Exception { + class MethodVisibilityClass { + } + try { + proxyFor(MethodVisibilityClass.class).build(); + fail(); + } catch (UnsupportedOperationException expected) {} + } + + private static class PrivateVisibilityClass { + } + + public void testPrivateClass_FailsWithUnsupportedOperationException() throws Exception { + try { + proxyFor(PrivateVisibilityClass.class).build(); + fail(); + } catch (UnsupportedOperationException expected) {} + } + + protected static class ProtectedVisibilityClass { + public String foo() { + throw new AssertionFailedError(); + } + } + + public void testProtectedVisibility_WorksFine() throws Exception { + assertEquals("fake result", proxyFor(ProtectedVisibilityClass.class).build().foo()); + } + + public static class HasFinalMethod { + public String nonFinalMethod() { + return "non-final method"; + } + + public final String finalMethod() { + return "final method"; + } + } + + public void testCanProxyClassesWithFinalMethods_WillNotCallTheFinalMethod() throws Throwable { + HasFinalMethod proxy = proxyFor(HasFinalMethod.class).build(); + assertEquals("final method", proxy.finalMethod()); + assertEquals("fake result", proxy.nonFinalMethod()); + } + + public static class HasPrivateMethod { + private String result() { + return "expected"; + } + } + + public void testProxyingPrivateMethods_NotIntercepted() throws Throwable { + assertEquals("expected", proxyFor(HasPrivateMethod.class).build().result()); + } + + public static class HasPackagePrivateMethod { + String result() { + throw new AssertionFailedError(); + } + } + + public void testProxyingPackagePrivateMethods_AreIntercepted() throws Throwable { + assertEquals("fake result", proxyFor(HasPackagePrivateMethod.class).build().result()); + } + + public static class HasProtectedMethod { + protected String result() { + throw new AssertionFailedError(); + } + } + + public void testProxyingProtectedMethods_AreIntercepted() throws Throwable { + assertEquals("fake result", proxyFor(HasProtectedMethod.class).build().result()); + } + + public static class HasVoidMethod { + public void dangerousMethod() { + fail(); + } + } + + public void testVoidMethod_ShouldNotThrowRuntimeException() throws Throwable { + proxyFor(HasVoidMethod.class).build().dangerousMethod(); + } + + public void testObjectMethodsAreAlsoProxied() throws Throwable { + Object proxy = proxyFor(Object.class).build(); + fakeHandler.setFakeResult("mystring"); + assertEquals("mystring", proxy.toString()); + fakeHandler.setFakeResult(-1); + assertEquals(-1, proxy.hashCode()); + fakeHandler.setFakeResult(false); + assertEquals(false, proxy.equals(proxy)); + } + + public static class AllPrimitiveMethods { + public boolean getBoolean() { return true; } + public int getInt() { return 1; } + public byte getByte() { return 2; } + public long getLong() { return 3L; } + public short getShort() { return 4; } + public float getFloat() { return 5f; } + public double getDouble() { return 6.0; } + public char getChar() { return 'c'; } + } + + public void testAllPrimitiveReturnTypes() throws Throwable { + AllPrimitiveMethods proxy = proxyFor(AllPrimitiveMethods.class).build(); + fakeHandler.setFakeResult(false); + assertEquals(false, proxy.getBoolean()); + fakeHandler.setFakeResult(8); + assertEquals(8, proxy.getInt()); + fakeHandler.setFakeResult((byte) 9); + assertEquals(9, proxy.getByte()); + fakeHandler.setFakeResult(10L); + assertEquals(10, proxy.getLong()); + fakeHandler.setFakeResult((short) 11); + assertEquals(11, proxy.getShort()); + fakeHandler.setFakeResult(12f); + assertEquals(12f, proxy.getFloat()); + fakeHandler.setFakeResult(13.0); + assertEquals(13.0, proxy.getDouble()); + fakeHandler.setFakeResult('z'); + assertEquals('z', proxy.getChar()); + } + + public static class PassThroughAllPrimitives { + public boolean getBoolean(boolean input) { return input; } + public int getInt(int input) { return input; } + public byte getByte(byte input) { return input; } + public long getLong(long input) { return input; } + public short getShort(short input) { return input; } + public float getFloat(float input) { return input; } + public double getDouble(double input) { return input; } + public char getChar(char input) { return input; } + public String getString(String input) { return input; } + public Object getObject(Object input) { return input; } + public void getNothing() {} + } + + public static class InvokeSuperHandler implements InvocationHandler { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return ProxyBuilder.callSuper(proxy, method, args); + } + } + + public void testPassThroughWorksForAllPrimitives() throws Exception { + PassThroughAllPrimitives proxy = proxyFor(PassThroughAllPrimitives.class) + .handler(new InvokeSuperHandler()) + .build(); + assertEquals(false, proxy.getBoolean(false)); + assertEquals(true, proxy.getBoolean(true)); + assertEquals(0, proxy.getInt(0)); + assertEquals(1, proxy.getInt(1)); + assertEquals((byte) 2, proxy.getByte((byte) 2)); + assertEquals((byte) 3, proxy.getByte((byte) 3)); + assertEquals(4L, proxy.getLong(4L)); + assertEquals(5L, proxy.getLong(5L)); + assertEquals((short) 6, proxy.getShort((short) 6)); + assertEquals((short) 7, proxy.getShort((short) 7)); + assertEquals(8f, proxy.getFloat(8f)); + assertEquals(9f, proxy.getFloat(9f)); + assertEquals(10.0, proxy.getDouble(10.0)); + assertEquals(11.0, proxy.getDouble(11.0)); + assertEquals('a', proxy.getChar('a')); + assertEquals('b', proxy.getChar('b')); + assertEquals("asdf", proxy.getString("asdf")); + assertEquals("qwer", proxy.getString("qwer")); + assertEquals(null, proxy.getString(null)); + Object a = new Object(); + assertEquals(a, proxy.getObject(a)); + assertEquals(null, proxy.getObject(null)); + proxy.getNothing(); + } + + public static class ExtendsAllPrimitiveMethods extends AllPrimitiveMethods { + public int example() { return 0; } + } + + public void testProxyWorksForSuperclassMethodsAlso() throws Throwable { + ExtendsAllPrimitiveMethods proxy = proxyFor(ExtendsAllPrimitiveMethods.class).build(); + fakeHandler.setFakeResult(99); + assertEquals(99, proxy.example()); + assertEquals(99, proxy.getInt()); + assertEquals(99, proxy.hashCode()); + } + + public static class HasOddParams { + public long method(int first, Integer second) { + throw new AssertionFailedError(); + } + } + + public void testMixingBoxedAndUnboxedParams() throws Throwable { + HasOddParams proxy = proxyFor(HasOddParams.class).build(); + fakeHandler.setFakeResult(99L); + assertEquals(99L, proxy.method(1, Integer.valueOf(2))); + } + + public static class SingleInt { + public String getString(int value) { + throw new AssertionFailedError(); + } + } + + public void testSinglePrimitiveParameter() throws Throwable { + InvocationHandler handler = new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return "asdf" + ((Integer) args[0]).intValue(); + } + }; + assertEquals("asdf1", proxyFor(SingleInt.class).handler(handler).build().getString(1)); + } + + public static class TwoConstructors { + private final String string; + + public TwoConstructors() { + string = "no-arg"; + } + + public TwoConstructors(boolean unused) { + string = "one-arg"; + } + } + + public void testNoConstructorArguments_CallsNoArgConstructor() throws Throwable { + TwoConstructors twoConstructors = proxyFor(TwoConstructors.class).build(); + assertEquals("no-arg", twoConstructors.string); + } + + public void testWithoutInvocationHandler_ThrowsIllegalArgumentException() throws Throwable { + try { + ProxyBuilder.forClass(TwoConstructors.class) + .dexCache(DexGeneratorTest.getDataDirectory()) + .build(); + fail(); + } catch (IllegalArgumentException expected) {} + } + + public static class HardToConstructCorrectly { + public HardToConstructCorrectly() { fail(); } + public HardToConstructCorrectly(Runnable ignored) { fail(); } + public HardToConstructCorrectly(Exception ignored) { fail(); } + public HardToConstructCorrectly(Boolean ignored) { /* safe */ } + public HardToConstructCorrectly(Integer ignored) { fail(); } + } + + public void testHardToConstruct_WorksIfYouSpecifyTheConstructorCorrectly() throws Throwable { + proxyFor(HardToConstructCorrectly.class) + .constructorArgTypes(Boolean.class) + .constructorArgValues(true) + .build(); + } + + public void testHardToConstruct_EvenWorksWhenArgsAreAmbiguous() throws Throwable { + proxyFor(HardToConstructCorrectly.class) + .constructorArgTypes(Boolean.class) + .constructorArgValues(new Object[] { null }) + .build(); + } + + public void testHardToConstruct_DoesNotInferTypesFromValues() throws Throwable { + try { + proxyFor(HardToConstructCorrectly.class) + .constructorArgValues(true) + .build(); + fail(); + } catch (IllegalArgumentException expected) {} + } + + public void testDefaultProxyHasSuperMethodToAccessOriginal() throws Exception { + Object objectProxy = proxyFor(Object.class).build(); + assertNotNull(objectProxy.getClass().getMethod("super_hashCode")); + } + + public static class PrintsOddAndValue { + public String method(int value) { + return "odd " + value; + } + } + + public void testSometimesDelegateToSuper() throws Exception { + InvocationHandler delegatesOddValues = new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("method")) { + int intValue = ((Integer) args[0]).intValue(); + if (intValue % 2 == 0) { + return "even " + intValue; + } + } + return ProxyBuilder.callSuper(proxy, method, args); + } + }; + PrintsOddAndValue proxy = proxyFor(PrintsOddAndValue.class) + .handler(delegatesOddValues) + .build(); + assertEquals("even 0", proxy.method(0)); + assertEquals("odd 1", proxy.method(1)); + assertEquals("even 2", proxy.method(2)); + assertEquals("odd 3", proxy.method(3)); + } + + public static class DoubleReturn { + public double getValue() { + return 2.0; + } + } + + public void testUnboxedResult() throws Exception { + fakeHandler.fakeResult = 2.0; + assertEquals(2.0, proxyFor(DoubleReturn.class).build().getValue()); + } + + public static void staticMethod() { + } + + public void testDoesNotOverrideStaticMethods() throws Exception { + // Method should exist on this test class itself. + ProxyBuilderTest.class.getDeclaredMethod("staticMethod"); + // Method should not exist on the subclass. + try { + proxyFor(ProxyBuilderTest.class).build().getClass().getDeclaredMethod("staticMethod"); + fail(); + } catch (NoSuchMethodException expected) {} + } + + public void testIllegalCacheDirectory() throws Exception { + try { + proxyFor(Object.class).dexCache(new File("//////")).build(); + fail(); + } catch (DexCacheException expected) {} + } + + public void testInvalidConstructorSpecification() throws Exception { + try { + proxyFor(Object.class) + .constructorArgTypes(String.class, Boolean.class) + .constructorArgValues("asdf", true) + .build(); + fail(); + } catch (IllegalArgumentException expected) {} + } + + public static abstract class AbstractClass { + public abstract Object getValue(); + } + + public void testAbstractClassBehaviour() throws Exception { + assertEquals("fake result", proxyFor(AbstractClass.class).build().getValue()); + } + + public static class CtorHasDeclaredException { + public CtorHasDeclaredException() throws IOException { + throw new IOException(); + } + } + + public static class CtorHasRuntimeException { + public CtorHasRuntimeException() { + throw new RuntimeException("my message"); + } + } + + public static class CtorHasError { + public CtorHasError() { + throw new Error("my message again"); + } + } + + public void testParentConstructorThrowsDeclaredException() throws Exception { + try { + proxyFor(CtorHasDeclaredException.class).build(); + fail(); + } catch (UndeclaredThrowableException expected) { + assertTrue(expected.getCause() instanceof IOException); + } + try { + proxyFor(CtorHasRuntimeException.class).build(); + fail(); + } catch (RuntimeException expected) { + assertEquals("my message", expected.getMessage()); + } + try { + proxyFor(CtorHasError.class).build(); + fail(); + } catch (Error expected) { + assertEquals("my message again", expected.getMessage()); + } + } + + public void testGetInvocationHandler_NormalOperation() throws Exception { + Object proxy = proxyFor(Object.class).build(); + assertSame(fakeHandler, ProxyBuilder.getInvocationHandler(proxy)); + } + + public void testGetInvocationHandler_NotAProxy() { + try { + ProxyBuilder.getInvocationHandler(new Object()); + fail(); + } catch (IllegalArgumentException expected) {} + } + + public static class ReturnsObject { + public Object getValue() { + return new Object(); + } + } + + public static class ReturnsString extends ReturnsObject { + @Override + public String getValue() { + return "a string"; + } + } + + public void testCovariantReturnTypes_NormalBehaviour() throws Exception { + String expected = "some string"; + fakeHandler.setFakeResult(expected); + assertSame(expected, proxyFor(ReturnsObject.class).build().getValue()); + assertSame(expected, proxyFor(ReturnsString.class).build().getValue()); + } + + public void testCovariantReturnTypes_WrongReturnType() throws Exception { + try { + fakeHandler.setFakeResult(new Object()); + proxyFor(ReturnsString.class).build().getValue(); + fail(); + } catch (ClassCastException expected) {} + } + + public void testCaching_ShouldWork() { + // TODO: We're not supporting caching yet. But we should as soon as possible. + fail(); + } + + public void testSubclassOfRandom() throws Exception { + proxyFor(Random.class) + .handler(new InvokeSuperHandler()) + .build(); + } + + /** Simple helper to add the most common args for this test to the proxy builder. */ + private <T> ProxyBuilder<T> proxyFor(Class<T> clazz) throws Exception { + return ProxyBuilder.forClass(clazz) + .handler(fakeHandler) + .dexCache(DexGeneratorTest.getDataDirectory()); + } + + private static class FakeInvocationHandler implements InvocationHandler { + private Object fakeResult = "fake result"; + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return fakeResult; + } + + public void setFakeResult(Object result) { + fakeResult = result; + } + } +} diff --git a/dx/src/com/android/dx/gen/Code.java b/dx/src/com/android/dx/gen/Code.java index b44d01cdd..3868cd3b5 100644 --- a/dx/src/com/android/dx/gen/Code.java +++ b/dx/src/com/android/dx/gen/Code.java @@ -288,7 +288,9 @@ public final class Code { // instructions: constants public <T> void loadConstant(Local<T> target, T value) { - Rop rop = Rops.opConst(target.type.ropType); + Rop rop = value == null + ? Rops.CONST_OBJECT_NOTHROW + : Rops.opConst(target.type.ropType); if (rop.getBranchingness() == BRANCH_NONE) { addInstruction(new PlainCstInsn(rop, sourcePosition, target.spec(), RegisterSpecList.EMPTY, Constants.getConstant(value))); diff --git a/dx/src/com/android/dx/gen/DexCacheException.java b/dx/src/com/android/dx/gen/DexCacheException.java new file mode 100644 index 000000000..560844098 --- /dev/null +++ b/dx/src/com/android/dx/gen/DexCacheException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.android.dx.gen; + +import java.io.IOException; + +/** Thrown when there is an IOException when writing to the dex cache directory. */ +public final class DexCacheException extends RuntimeException { + private static final long serialVersionUID = 0L; + + public DexCacheException(IOException cause) { + super(cause); + } +} diff --git a/dx/src/com/android/dx/gen/ProxyBuilder.java b/dx/src/com/android/dx/gen/ProxyBuilder.java new file mode 100644 index 000000000..852fe666f --- /dev/null +++ b/dx/src/com/android/dx/gen/ProxyBuilder.java @@ -0,0 +1,667 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.android.dx.gen; + +import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR; +import static com.android.dx.rop.code.AccessFlags.ACC_PRIVATE; +import static com.android.dx.rop.code.AccessFlags.ACC_PUBLIC; +import static com.android.dx.rop.code.AccessFlags.ACC_STATIC; + +import java.io.File; +import java.io.IOException; +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.UndeclaredThrowableException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Creates dynamic proxies of concrete classes. + * <p> + * This is similar to the {@code java.lang.reflect.Proxy} class, but works for classes instead of + * interfaces. + * <h3>Example</h3> + * The following example demonstrates the creation of a dynamic proxy for {@code java.util.Random} + * which will always return 4 when asked for integers, and which logs method calls to every method. + * <pre> + * InvocationHandler handler = new InvocationHandler() { + * @Override + * public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + * if (method.getName().equals("nextInt")) { + * // Chosen by fair dice roll, guaranteed to be random. + * return 4; + * } + * Object result = ProxyBuilder.callSuper(proxy, method, args); + * System.out.println("Method: " + method.getName() + " args: " + * + Arrays.toString(args) + " result: " + result); + * return result; + * } + * }; + * Random debugRandom = ProxyBuilder.forClass(Random.class) + * .dexCache(getInstrumentation().getTargetContext().getDir("dx", Context.MODE_PRIVATE)) + * .handler(handler) + * .build(); + * assertEquals(4, debugRandom.nextInt()); + * debugRandom.setSeed(0); + * assertTrue(debugRandom.nextBoolean()); + * </pre> + * <h3>Usage</h3> + * Call {@link #forClass(Class)} for the Class you wish to proxy. Call + * {@link #handler(InvocationHandler)} passing in an {@link InvocationHandler}, and then call + * {@link #build()}. The returned instance will be a dynamically generated subclass where all method + * calls will be delegated to the invocation handler, except as noted below. + * <p> + * The static method {@link #callSuper(Object, Method, Object...)} allows you to access the original + * super method for a given proxy. This allows the invocation handler to selectively override some + * methods but not others. + * <p> + * By default, the {@link #build()} method will call the no-arg constructor belonging to the class + * being proxied. If you wish to call a different constructor, you must provide arguments for both + * {@link #constructorArgTypes(Class[])} and {@link #constructorArgValues(Object[])}. + * <p> + * This process works only for classes with public and protected level of visibility. + * <p> + * You may proxy abstract classes. You may not proxy final classes. + * <p> + * Only non-private, non-final, non-static methods will be dispatched to the invocation handler. + * Private, static or final methods will always call through to the superclass as normal. + * <p> + * The {@link #finalize()} method on {@code Object} will not be proxied. + * <p> + * You must provide a dex cache directory via the {@link #dexCache(File)} method. You should take + * care not to make this a world-writable directory, so that third parties cannot inject code into + * your application. A suitable parameter for these output directories would be something like + * this: + * <pre>{@code + * getApplicationContext().getDir("dx", Context.MODE_PRIVATE); + * }</pre> + * <p> + * If the base class to be proxied leaks the {@code this} pointer in the constructor (bad practice), + * that is to say calls a non-private non-final method from the constructor, the invocation handler + * will not be invoked. As a simple concrete example, when proxying Random we discover that it + * inernally calls setSeed during the constructor. The proxy will not intercept this call during + * proxy construction, but will intercept as normal afterwards. This behaviour may be subject to + * change in future releases. + * <p> + * This class is <b>not thread safe</b>. + */ +public final class ProxyBuilder<T> { + private static final String FIELD_NAME_HANDLER = "$__handler"; + private static final String FIELD_NAME_METHODS = "$__methodArray"; + + private final Class<T> baseClass; + private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader(); + private InvocationHandler handler; + private File dexCache; + private Class<?>[] constructorArgTypes = new Class[0]; + private Object[] constructorArgValues = new Object[0]; + + private ProxyBuilder(Class<T> clazz) { + baseClass = clazz; + } + + public static <T> ProxyBuilder<T> forClass(Class<T> clazz) { + return new ProxyBuilder<T>(clazz); + } + + /** + * Specifies the parent ClassLoader to use when creating the proxy. + * + * <p>If null, {@code ProxyBuilder.class.getClassLoader()} will be used. + */ + public ProxyBuilder<T> parentClassLoader(ClassLoader parent) { + parentClassLoader = parent; + return this; + } + + public ProxyBuilder<T> handler(InvocationHandler handler) { + this.handler = handler; + return this; + } + + public ProxyBuilder<T> dexCache(File dexCache) { + this.dexCache = dexCache; + return this; + } + + public ProxyBuilder<T> constructorArgValues(Object... constructorArgValues) { + this.constructorArgValues = constructorArgValues; + return this; + } + + public ProxyBuilder<T> constructorArgTypes(Class<?>... constructorArgTypes) { + this.constructorArgTypes = constructorArgTypes; + return this; + } + + /** + * Create a new instance of the class to proxy. + * + * @throws UnsupportedOperationException if the class we are trying to create a proxy for is + * not accessible. + * @throws DexCacheException if an exception occurred writing to the {@code dexCache} directory. + * @throws UndeclaredThrowableException if the constructor for the base class to proxy throws + * a declared exception during construction. + * @throws IllegalArgumentException if the handler is null, if the constructor argument types + * do not match the constructor argument values, or if no such constructor exists. + */ + public T build() { + check(handler != null, "handler == null"); + check(constructorArgTypes.length == constructorArgValues.length, + "constructorArgValues.length != constructorArgTypes.length"); + DexGenerator generator = new DexGenerator(); + String generatedName = getMethodNameForProxyOf(baseClass); + Type<? extends T> generatedType = Type.get("L" + generatedName + ";"); + Type<T> superType = Type.get(baseClass); + generateConstructorsAndFields(generator, generatedType, superType, baseClass); + Method[] methodsToProxy = getMethodsToProxy(baseClass); + generateCodeForAllMethods(generator, generatedType, methodsToProxy, superType); + generator.declare(generatedType, generatedName + ".generated", ACC_PUBLIC, superType); + ClassLoader classLoader; + try { + classLoader = generator.load(parentClassLoader, dexCache, dexCache); + } catch (IOException e) { + throw new DexCacheException(e); + } + Class<? extends T> proxyClass; + try { + proxyClass = loadClass(classLoader, generatedName); + } catch (IllegalAccessError e) { + // Thrown when the base class is not accessible. + throw new UnsupportedOperationException("cannot proxy inaccessible classes", e); + } catch (ClassNotFoundException e) { + // Should not be thrown, we're sure to have generated this class. + throw new AssertionError(e); + } + setMethodsStaticField(proxyClass, methodsToProxy); + Constructor<? extends T> constructor; + try { + constructor = proxyClass.getConstructor(constructorArgTypes); + } catch (NoSuchMethodException e) { + // Thrown when the ctor to be called does not exist. + throw new IllegalArgumentException("could not find matching constructor", e); + } + T result; + try { + result = constructor.newInstance(constructorArgValues); + } catch (InstantiationException e) { + // Should not be thrown, generated class is not abstract. + throw new AssertionError(e); + } catch (IllegalAccessException e) { + // Should not be thrown, the generated constructor is accessible. + throw new AssertionError(e); + } catch (InvocationTargetException e) { + // Thrown when the base class ctor throws an exception. + throw launderCause(e); + } + setHandlerInstanceField(result, handler); + return result; + } + + // The type cast is safe: the generated type will extend the base class type. + @SuppressWarnings("unchecked") + private Class<? extends T> loadClass(ClassLoader classLoader, String generatedName) + throws ClassNotFoundException { + return (Class<? extends T>) classLoader.loadClass(generatedName); + } + + private static RuntimeException launderCause(InvocationTargetException e) { + Throwable cause = e.getCause(); + // Errors should be thrown as they are. + if (cause instanceof Error) { + throw (Error) cause; + } + // RuntimeException can be thrown as-is. + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + // Declared exceptions will have to be wrapped. + throw new UndeclaredThrowableException(cause); + } + + private static void setHandlerInstanceField(Object instance, InvocationHandler handler) { + try { + Field handlerField = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER); + handlerField.setAccessible(true); + handlerField.set(instance, handler); + } catch (NoSuchFieldException e) { + // Should not be thrown, generated proxy class has been generated with this field. + throw new AssertionError(e); + } catch (IllegalAccessException e) { + // Should not be thrown, we just set the field to accessible. + throw new AssertionError(e); + } + } + + private static void setMethodsStaticField(Class<?> proxyClass, Method[] methodsToProxy) { + try { + Field methodArrayField = proxyClass.getDeclaredField(FIELD_NAME_METHODS); + methodArrayField.setAccessible(true); + methodArrayField.set(null, methodsToProxy); + } catch (NoSuchFieldException e) { + // Should not be thrown, generated proxy class has been generated with this field. + throw new AssertionError(e); + } catch (IllegalAccessException e) { + // Should not be thrown, we just set the field to accessible. + throw new AssertionError(e); + } + } + + /** + * Returns the proxy's {@link InvocationHandler}. + * + * @throws IllegalArgumentException if the object supplied is not a proxy created by this class. + */ + public static InvocationHandler getInvocationHandler(Object instance) { + try { + Field field = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER); + field.setAccessible(true); + return (InvocationHandler) field.get(instance); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException("Not a valid proxy instance", e); + } catch (IllegalAccessException e) { + // Should not be thrown, we just set the field to accessible. + throw new AssertionError(e); + } + } + + private static <T, G extends T> void generateCodeForAllMethods(DexGenerator generator, + Type<G> generatedType, Method[] methodsToProxy, Type<T> superclassType) { + Type<InvocationHandler> handlerType = Type.get(InvocationHandler.class); + Type<Method[]> methodArrayType = Type.get(Method[].class); + FieldId<G, InvocationHandler> handlerField = + generatedType.getField(handlerType, FIELD_NAME_HANDLER); + FieldId<G, Method[]> allMethods = + generatedType.getField(methodArrayType, FIELD_NAME_METHODS); + Type<Method> methodType = Type.get(Method.class); + Type<Object[]> objectArrayType = Type.get(Object[].class); + MethodId<InvocationHandler, Object> methodInvoke = handlerType.getMethod(Type.OBJECT, + "invoke", Type.OBJECT, methodType, objectArrayType); + for (int m = 0; m < methodsToProxy.length; ++m) { + /* + * If the 5th method on the superclass Example that can be overridden were to look like + * this: + * + * public int doSomething(Bar param0, int param1) { + * ... + * } + * + * Then the following code will generate a method on the proxy that looks something + * like this: + * + * public int doSomething(Bar param0, int param1) { + * int methodIndex = 4; + * Method[] allMethods = Example_Proxy.$__methodArray; + * Method thisMethod = allMethods[methodIndex]; + * int argsLength = 2; + * Object[] args = new Object[argsLength]; + * InvocationHandler localHandler = this.$__handler; + * // for-loop begins + * int p = 0; + * Bar parameter0 = param0; + * args[p] = parameter0; + * p = 1; + * int parameter1 = param1; + * Integer boxed1 = Integer.valueOf(parameter1); + * args[p] = boxed1; + * // for-loop ends + * Object result = localHandler.invoke(this, thisMethod, args); + * Integer castResult = (Integer) result; + * int unboxedResult = castResult.intValue(); + * return unboxedResult; + * } + * + * Or, in more idiomatic Java: + * + * public int doSomething(Bar param0, int param1) { + * if ($__handler == null) { + * return super.doSomething(param0, param1); + * } + * return __handler.invoke(this, __methodArray[4], + * new Object[] { param0, Integer.valueOf(param1) }); + * } + */ + Method method = methodsToProxy[m]; + String name = method.getName(); + Class<?>[] argClasses = method.getParameterTypes(); + Type<?>[] argTypes = new Type<?>[argClasses.length]; + for (int i = 0; i < argTypes.length; ++i) { + argTypes[i] = Type.get(argClasses[i]); + } + Class<?> returnType = method.getReturnType(); + Type<?> resultType = Type.get(returnType); + MethodId<T, ?> superMethod = superclassType.getMethod(resultType, name, argTypes); + MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes); + Code code = generator.declare(methodId, ACC_PUBLIC); + Local<G> localThis = code.getThis(generatedType); + Local<InvocationHandler> localHandler = code.newLocal(handlerType); + Local<Object> invokeResult = code.newLocal(Type.OBJECT); + Local<Integer> intValue = code.newLocal(Type.INT); + Local<Object[]> args = code.newLocal(objectArrayType); + Local<Integer> argsLength = code.newLocal(Type.INT); + Local<Object> temp = code.newLocal(Type.OBJECT); + Local<?> resultHolder = code.newLocal(resultType); + Local<Method[]> methodArray = code.newLocal(methodArrayType); + Local<Method> thisMethod = code.newLocal(methodType); + Local<Integer> methodIndex = code.newLocal(Type.INT); + Class<?> aBoxedClass = PRIMITIVE_TO_BOXED.get(returnType); + Local<?> aBoxedResult = null; + if (aBoxedClass != null) { + aBoxedResult = code.newLocal(Type.get(aBoxedClass)); + } + Local<?>[] superArgs2 = new Local<?>[argClasses.length]; + Local<?> superResult2 = code.newLocal(resultType); + Local<InvocationHandler> nullHandler = code.newLocal(handlerType); + + code.loadConstant(methodIndex, m); + code.sget(allMethods, methodArray); + code.aget(methodArray, methodIndex, thisMethod); + code.loadConstant(argsLength, argTypes.length); + code.newArray(argsLength, args); + code.iget(handlerField, localThis, localHandler); + + // if (proxy == null) + code.loadConstant(nullHandler, null); + Label handlerNullCase = code.newLabel(); + code.compare(Comparison.EQ, nullHandler, localHandler, handlerNullCase); + + // This code is what we execute when we have a valid proxy: delegate to invocation + // handler. + for (int p = 0; p < argTypes.length; ++p) { + code.loadConstant(intValue, p); + Local<?> parameter = code.getParameter(p, argTypes[p]); + Local<?> unboxedIfNecessary = boxIfRequired(generator, code, parameter, temp); + code.aput(args, intValue, unboxedIfNecessary); + } + code.invokeInterface(methodInvoke, invokeResult, localHandler, + localThis, thisMethod, args); + generateCodeForReturnStatement(code, returnType, invokeResult, resultHolder, + aBoxedResult); + + // This code is executed if proxy is null: call the original super method. + // This is required to handle the case of construction of an object which leaks the + // "this" pointer. + code.mark(handlerNullCase); + for (int i = 0; i < superArgs2.length; ++i) { + superArgs2[i] = code.getParameter(i, argTypes[i]); + } + if (void.class.equals(returnType)) { + code.invokeSuper(superMethod, null, localThis, superArgs2); + code.returnVoid(); + } else { + invokeSuper(superMethod, code, localThis, superArgs2, superResult2); + code.returnValue(superResult2); + } + + /* + * And to allow calling the original super method, the following is also generated: + * + * public int super_doSomething(Bar param0, int param1) { + * int result = super.doSomething(param0, param1); + * return result; + * } + */ + String superName = "super_" + name; + MethodId<G, ?> callsSuperMethod = generatedType.getMethod( + resultType, superName, argTypes); + Code superCode = generator.declare(callsSuperMethod, ACC_PUBLIC); + Local<G> superThis = superCode.getThis(generatedType); + Local<?>[] superArgs = new Local<?>[argClasses.length]; + for (int i = 0; i < superArgs.length; ++i) { + superArgs[i] = superCode.getParameter(i, argTypes[i]); + } + if (void.class.equals(returnType)) { + superCode.invokeSuper(superMethod, null, superThis, superArgs); + superCode.returnVoid(); + } else { + Local<?> superResult = superCode.newLocal(resultType); + invokeSuper(superMethod, superCode, superThis, superArgs, superResult); + superCode.returnValue(superResult); + } + } + } + + // This one is tricky to fix, I gave up. + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static <T> void invokeSuper(MethodId superMethod, Code superCode, + Local superThis, Local[] superArgs, Local superResult) { + superCode.invokeSuper(superMethod, superResult, superThis, superArgs); + } + + private static Local<?> boxIfRequired(DexGenerator generator, Code code, Local<?> parameter, + Local<Object> temp) { + MethodId<?, ?> unboxMethod = PRIMITIVE_TYPE_TO_UNBOX_METHOD.get(parameter.getType()); + if (unboxMethod == null) { + return parameter; + } + code.invokeStatic(unboxMethod, temp, parameter); + return temp; + } + + public static Object callSuper(Object proxy, Method method, Object... args) + throws SecurityException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + return proxy.getClass() + .getMethod("super_" + method.getName(), method.getParameterTypes()) + .invoke(proxy, args); + } + + private static void check(boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException(message); + } + } + + private static <T, G extends T> void generateConstructorsAndFields(DexGenerator generator, + Type<G> generatedType, Type<T> superType, Class<T> superClass) { + Type<InvocationHandler> handlerType = Type.get(InvocationHandler.class); + Type<Method[]> methodArrayType = Type.get(Method[].class); + FieldId<G, InvocationHandler> handlerField = generatedType.getField( + handlerType, FIELD_NAME_HANDLER); + generator.declare(handlerField, ACC_PRIVATE, null); + FieldId<G, Method[]> allMethods = generatedType.getField( + methodArrayType, FIELD_NAME_METHODS); + generator.declare(allMethods, ACC_PRIVATE | ACC_STATIC, null); + for (Constructor<T> constructor : getConstructorsToOverwrite(superClass)) { + if (constructor.getModifiers() == Modifier.FINAL) { + continue; + } + Type<?>[] types = classArrayToTypeArray(constructor.getParameterTypes()); + MethodId<?, ?> method = generatedType.getConstructor(types); + Code constructorCode = generator.declare(method, ACC_PUBLIC | ACC_CONSTRUCTOR); + Local<G> thisRef = constructorCode.getThis(generatedType); + Local<?>[] params = new Local[types.length]; + for (int i = 0; i < params.length; ++i) { + params[i] = constructorCode.getParameter(i, types[i]); + } + MethodId<T, ?> superConstructor = superType.getConstructor(types); + constructorCode.invokeDirect(superConstructor, null, thisRef, params); + constructorCode.returnVoid(); + } + } + + // The type parameter on Constructor is the class in which the constructor is declared. + // The getDeclaredConstructors() method gets constructors declared only in the given class, + // hence this cast is safe. + @SuppressWarnings("unchecked") + private static <T> Constructor<T>[] getConstructorsToOverwrite(Class<T> clazz) { + return (Constructor<T>[]) clazz.getDeclaredConstructors(); + } + + /** + * Gets all {@link Method} objects we can proxy in the hierarchy of the supplied class. + */ + private static <T> Method[] getMethodsToProxy(Class<T> clazz) { + Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>(); + for (Class<?> current = clazz; current != null; current = current.getSuperclass()) { + for (Method method : current.getDeclaredMethods()) { + if ((method.getModifiers() & Modifier.FINAL) != 0) { + // Skip final methods, we can't override them. + continue; + } + if ((method.getModifiers() & Modifier.STATIC) != 0) { + // Skip static methods, overriding them has no effect. + continue; + } + if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) { + // Skip finalize method, it's likely important that it execute as normal. + continue; + } + methodsToProxy.add(new MethodSetEntry(method)); + } + } + Method[] results = new Method[methodsToProxy.size()]; + int i = 0; + for (MethodSetEntry entry : methodsToProxy) { + results[i++] = entry.originalMethod; + } + return results; + } + + private static <T> String getMethodNameForProxyOf(Class<T> clazz) { + return clazz.getSimpleName() + "_Proxy"; + } + + private static Type<?>[] classArrayToTypeArray(Class<?>[] input) { + Type<?>[] result = new Type[input.length]; + for (int i = 0; i < input.length; ++i) { + result[i] = Type.get(input[i]); + } + return result; + } + + /** + * Calculates the correct return statement code for a method. + * <p> + * A void method will not return anything. A method that returns a primitive will need to + * unbox the boxed result. Otherwise we will cast the result. + */ + // This one is tricky to fix, I gave up. + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static void generateCodeForReturnStatement(Code code, Class methodReturnType, + Local localForResultOfInvoke, Local localOfMethodReturnType, Local aBoxedResult) { + if (PRIMITIVE_TO_UNBOX_METHOD.containsKey(methodReturnType)) { + code.typeCast(localForResultOfInvoke, aBoxedResult); + MethodId unboxingMethodFor = getUnboxMethodForPrimitive(methodReturnType); + code.invokeVirtual(unboxingMethodFor, localOfMethodReturnType, aBoxedResult); + code.returnValue(localOfMethodReturnType); + } else if (void.class.equals(methodReturnType)) { + code.returnVoid(); + } else { + code.typeCast(localForResultOfInvoke, localOfMethodReturnType); + code.returnValue(localOfMethodReturnType); + } + } + + private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) { + return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType); + } + + private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_BOXED; + static { + PRIMITIVE_TO_BOXED = new HashMap<Class<?>, Class<?>>(); + PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class); + PRIMITIVE_TO_BOXED.put(int.class, Integer.class); + PRIMITIVE_TO_BOXED.put(byte.class, Byte.class); + PRIMITIVE_TO_BOXED.put(long.class, Long.class); + PRIMITIVE_TO_BOXED.put(short.class, Short.class); + PRIMITIVE_TO_BOXED.put(float.class, Float.class); + PRIMITIVE_TO_BOXED.put(double.class, Double.class); + PRIMITIVE_TO_BOXED.put(char.class, Character.class); + } + + private static final Map<Type<?>, MethodId<?, ?>> PRIMITIVE_TYPE_TO_UNBOX_METHOD; + static { + PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<Type<?>, MethodId<?, ?>>(); + for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_TO_BOXED.entrySet()) { + Type<?> primitiveType = Type.get(entry.getKey()); + Type<?> boxedType = Type.get(entry.getValue()); + MethodId<?, ?> valueOfMethod = boxedType.getMethod(boxedType, "valueOf", primitiveType); + PRIMITIVE_TYPE_TO_UNBOX_METHOD.put(primitiveType, valueOfMethod); + } + } + + /** + * Map from primitive type to method used to unbox a boxed version of the primitive. + * <p> + * This is required for methods whose return type is primitive, since the + * {@link InvocationHandler} will return us a boxed result, and we'll need to convert it back to + * primitive value. + */ + private static final Map<Class<?>, MethodId<?, ?>> PRIMITIVE_TO_UNBOX_METHOD; + static { + Map<Class<?>, MethodId<?, ?>> map = new HashMap<Class<?>, MethodId<?, ?>>(); + map.put(boolean.class, Type.get(Boolean.class).getMethod(Type.BOOLEAN, "booleanValue")); + map.put(int.class, Type.get(Integer.class).getMethod(Type.INT, "intValue")); + map.put(byte.class, Type.get(Byte.class).getMethod(Type.BYTE, "byteValue")); + map.put(long.class, Type.get(Long.class).getMethod(Type.LONG, "longValue")); + map.put(short.class, Type.get(Short.class).getMethod(Type.SHORT, "shortValue")); + map.put(float.class, Type.get(Float.class).getMethod(Type.FLOAT, "floatValue")); + map.put(double.class, Type.get(Double.class).getMethod(Type.DOUBLE, "doubleValue")); + map.put(char.class, Type.get(Character.class).getMethod(Type.CHAR, "charValue")); + PRIMITIVE_TO_UNBOX_METHOD = map; + } + + /** + * Wrapper class to let us disambiguate {@link Method} objects. + * <p> + * The purpose of this class is to override the {@link #equals(Object)} and {@link #hashCode()} + * methods so we can use a {@link Set} to remove duplicate methods that are overrides of one + * another. For these purposes, we consider two methods to be equal if they have the same + * name, return type, and parameter types. + */ + private static class MethodSetEntry { + private final String name; + private final Class<?>[] paramTypes; + private final Class<?> returnType; + private final Method originalMethod; + + public MethodSetEntry(Method method) { + originalMethod = method; + name = method.getName(); + paramTypes = method.getParameterTypes(); + returnType = method.getReturnType(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof MethodSetEntry) { + MethodSetEntry other = (MethodSetEntry) o; + return name.equals(other.name) + && returnType.equals(other.returnType) + && Arrays.equals(paramTypes, other.paramTypes); + } + return false; + } + + @Override + public int hashCode() { + int result = 17; + result += 31 * result + name.hashCode(); + result += 31 * result + returnType.hashCode(); + result += 31 * result + Arrays.hashCode(paramTypes); + return result; + } + } +} |