aboutsummaryrefslogtreecommitdiff
path: root/dexmaker
diff options
context:
space:
mode:
authorPaul Duffin <paulduffin@google.com>2016-07-22 11:49:48 +0100
committerPaul Duffin <paulduffin@google.com>2016-07-22 14:24:13 +0100
commita4d0f615c2921f01ce6f4486158272a8f760fed8 (patch)
treed407f452183517efe9e8a96a4ecad52f73685e15 /dexmaker
parent05f9a0eff07ee63ce4f0cfc55434a9e3dad8d6bc (diff)
downloaddexmaker-a4d0f615c2921f01ce6f4486158272a8f760fed8.tar.gz
Restructure files to match upstream structure for v1.3
This is in preparation for upgrading to v1.3 as part of the work to upgrade mockito to 1.10.19. src/main -> dexmaker/src/main src/test -> dexmaker/src/test src/dx -> dx/src/main src/mockito -> mockito/src/main The README is moved to README.google to be consistent with other open source based projects in AOSP. Also updates the patch files and instructions on how to use them. Bug: 30299479 Test: Build and run some mockito based tests. Change-Id: I2d80acc1e117a26e55dc8c4522eb9bc79f3852cc
Diffstat (limited to 'dexmaker')
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/AppDataDirGuesser.java183
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/BinaryOp.java118
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/Code.java903
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/Comparison.java71
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/Constants.java72
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/DexMaker.java546
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/FieldId.java74
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/Label.java92
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/Local.java74
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/MethodId.java120
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/TypeId.java148
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/TypeList.java64
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/UnaryOp.java42
-rw-r--r--dexmaker/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java795
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java180
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/DexMakerTest.java2020
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/TypeIdTest.java30
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/examples/FibonacciMaker.java78
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/examples/HelloWorldMaker.java103
-rw-r--r--dexmaker/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java987
20 files changed, 6700 insertions, 0 deletions
diff --git a/dexmaker/src/main/java/com/google/dexmaker/AppDataDirGuesser.java b/dexmaker/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
new file mode 100644
index 0000000..4479887
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2012 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.google.dexmaker;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Uses heuristics to guess the application's private data directory.
+ */
+class AppDataDirGuesser {
+ public File guess() {
+ try {
+ ClassLoader classLoader = guessSuitableClassLoader();
+ // Check that we have an instance of the PathClassLoader.
+ Class<?> clazz = Class.forName("dalvik.system.PathClassLoader");
+ clazz.cast(classLoader);
+ // Use the toString() method to calculate the data directory.
+ String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader, clazz);
+ File[] results = guessPath(pathFromThisClassLoader);
+ if (results.length > 0) {
+ return results[0];
+ }
+ } catch (ClassCastException ignored) {
+ } catch (ClassNotFoundException ignored) {
+ }
+ return null;
+ }
+
+ private ClassLoader guessSuitableClassLoader() {
+ return AppDataDirGuesser.class.getClassLoader();
+ }
+
+ private String getPathFromThisClassLoader(ClassLoader classLoader,
+ Class<?> pathClassLoaderClass) {
+ // Prior to ICS, we can simply read the "path" field of the
+ // PathClassLoader.
+ try {
+ Field pathField = pathClassLoaderClass.getDeclaredField("path");
+ pathField.setAccessible(true);
+ return (String) pathField.get(classLoader);
+ } catch (NoSuchFieldException ignored) {
+ } catch (IllegalAccessException ignored) {
+ } catch (ClassCastException ignored) {
+ }
+
+ // Parsing toString() method: yuck. But no other way to get the path.
+ String result = classLoader.toString();
+ return processClassLoaderString(result);
+ }
+
+ /**
+ * Given the result of a ClassLoader.toString() call, process the result so that guessPath
+ * can use it. There are currently two variants. For Android 4.3 and later, the string
+ * "DexPathList" should be recognized and the array of dex path elements is parsed. for
+ * earlier versions, the last nested array ('[' ... ']') is enclosing the string we are
+ * interested in.
+ */
+ static String processClassLoaderString(String input) {
+ if (input.contains("DexPathList")) {
+ return processClassLoaderString43OrLater(input);
+ } else {
+ return processClassLoaderString42OrEarlier(input);
+ }
+ }
+
+ private static String processClassLoaderString42OrEarlier(String input) {
+ /* The toString output looks like this:
+ * dalvik.system.PathClassLoader[dexPath=path/to/apk,libraryPath=path/to/libs]
+ */
+ int index = input.lastIndexOf('[');
+ input = (index == -1) ? input : input.substring(index + 1);
+ index = input.indexOf(']');
+ input = (index == -1) ? input : input.substring(0, index);
+ return input;
+ }
+
+ private static String processClassLoaderString43OrLater(String input) {
+ /* The toString output looks like this:
+ * dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/{NAME}", ...], nativeLibraryDirectories=[...]]]
+ */
+ int start = input.indexOf("DexPathList") + "DexPathList".length();
+ if (input.length() > start + 4) { // [[ + ]]
+ String trimmed = input.substring(start);
+ int end = trimmed.indexOf(']');
+ if (trimmed.charAt(0) == '[' && trimmed.charAt(1) == '[' && end >= 0) {
+ trimmed = trimmed.substring(2, end);
+ // Comma-separated list, Arrays.toString output.
+ String split[] = trimmed.split(",");
+
+ // Clean up parts. Each path element is the type of the element plus the path in
+ // quotes.
+ for (int i = 0; i < split.length; i++) {
+ int quoteStart = split[i].indexOf('"');
+ int quoteEnd = split[i].lastIndexOf('"');
+ if (quoteStart > 0 && quoteStart < quoteEnd) {
+ split[i] = split[i].substring(quoteStart + 1, quoteEnd);
+ }
+ }
+
+ // Need to rejoin components.
+ StringBuilder sb = new StringBuilder();
+ for (String s : split) {
+ if (sb.length() > 0) {
+ sb.append(':');
+ }
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+ }
+
+ // This is technically a parsing failure. Return the original string, maybe a later
+ // stage can still salvage this.
+ return input;
+ }
+
+ File[] guessPath(String input) {
+ List<File> results = new ArrayList<File>();
+ for (String potential : splitPathList(input)) {
+ if (!potential.startsWith("/data/app/")) {
+ continue;
+ }
+ int start = "/data/app/".length();
+ int end = potential.lastIndexOf(".apk");
+ if (end != potential.length() - 4) {
+ continue;
+ }
+ int dash = potential.indexOf("-");
+ if (dash != -1) {
+ end = dash;
+ }
+ String packageName = potential.substring(start, end);
+ File dataDir = new File("/data/data/" + packageName);
+ if (isWriteableDirectory(dataDir)) {
+ File cacheDir = new File(dataDir, "cache");
+ // The cache directory might not exist -- create if necessary
+ if (fileOrDirExists(cacheDir) || cacheDir.mkdir()) {
+ if (isWriteableDirectory(cacheDir)) {
+ results.add(cacheDir);
+ }
+ }
+ }
+ }
+ return results.toArray(new File[results.size()]);
+ }
+
+ static String[] splitPathList(String input) {
+ String trimmed = input;
+ if (input.startsWith("dexPath=")) {
+ int start = "dexPath=".length();
+ int end = input.indexOf(',');
+
+ trimmed = (end == -1) ? input.substring(start) : input.substring(start, end);
+ }
+
+ return trimmed.split(":");
+ }
+
+ boolean fileOrDirExists(File file) {
+ return file.exists();
+ }
+
+ boolean isWriteableDirectory(File file) {
+ return file.isDirectory() && file.canWrite();
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/BinaryOp.java b/dexmaker/src/main/java/com/google/dexmaker/BinaryOp.java
new file mode 100644
index 0000000..04734cf
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/BinaryOp.java
@@ -0,0 +1,118 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.code.Rop;
+import com.android.dx.rop.code.Rops;
+import com.android.dx.rop.type.TypeList;
+
+/**
+ * An operation on two values of the same type.
+ *
+ * <p>Math operations ({@link #ADD}, {@link #SUBTRACT}, {@link #MULTIPLY},
+ * {@link #DIVIDE}, and {@link #REMAINDER}) support ints, longs, floats and
+ * doubles.
+ *
+ * <p>Bit operations ({@link #AND}, {@link #OR}, {@link #XOR}, {@link
+ * #SHIFT_LEFT}, {@link #SHIFT_RIGHT}, {@link #UNSIGNED_SHIFT_RIGHT}) support
+ * ints and longs.
+ *
+ * <p>Division by zero behaves differently depending on the operand type.
+ * For int and long operands, {@link #DIVIDE} and {@link #REMAINDER} throw
+ * {@link ArithmeticException} if {@code b == 0}. For float and double operands,
+ * the operations return {@code NaN}.
+ */
+public enum BinaryOp {
+ /** {@code a + b} */
+ ADD() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opAdd(types);
+ }
+ },
+
+ /** {@code a - b} */
+ SUBTRACT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opSub(types);
+ }
+ },
+
+ /** {@code a * b} */
+ MULTIPLY() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opMul(types);
+ }
+ },
+
+ /** {@code a / b} */
+ DIVIDE() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opDiv(types);
+ }
+ },
+
+ /** {@code a % b} */
+ REMAINDER() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opRem(types);
+ }
+ },
+
+ /** {@code a & b} */
+ AND() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opAnd(types);
+ }
+ },
+
+ /** {@code a | b} */
+ OR() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opOr(types);
+ }
+ },
+
+ /** {@code a ^ b} */
+ XOR() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opXor(types);
+ }
+ },
+
+ /** {@code a << b} */
+ SHIFT_LEFT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opShl(types);
+ }
+ },
+
+ /** {@code a >> b} */
+ SHIFT_RIGHT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opShr(types);
+ }
+ },
+
+ /** {@code a >>> b} */
+ UNSIGNED_SHIFT_RIGHT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opUshr(types);
+ }
+ };
+
+ abstract Rop rop(com.android.dx.rop.type.TypeList types);
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/Code.java b/dexmaker/src/main/java/com/google/dexmaker/Code.java
new file mode 100644
index 0000000..54409a5
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/Code.java
@@ -0,0 +1,903 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.code.BasicBlockList;
+import com.android.dx.rop.code.Insn;
+import com.android.dx.rop.code.PlainCstInsn;
+import com.android.dx.rop.code.PlainInsn;
+import com.android.dx.rop.code.RegisterSpecList;
+import com.android.dx.rop.code.Rop;
+import static com.android.dx.rop.code.Rop.BRANCH_GOTO;
+import static com.android.dx.rop.code.Rop.BRANCH_NONE;
+import static com.android.dx.rop.code.Rop.BRANCH_RETURN;
+import com.android.dx.rop.code.Rops;
+import com.android.dx.rop.code.SourcePosition;
+import com.android.dx.rop.code.ThrowingCstInsn;
+import com.android.dx.rop.code.ThrowingInsn;
+import com.android.dx.rop.cst.CstInteger;
+import com.android.dx.rop.type.StdTypeList;
+import static com.android.dx.rop.type.Type.BT_BYTE;
+import static com.android.dx.rop.type.Type.BT_CHAR;
+import static com.android.dx.rop.type.Type.BT_INT;
+import static com.android.dx.rop.type.Type.BT_SHORT;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Builds a sequence of instructions.
+ *
+ * <h3>Locals</h3>
+ * All data manipulation takes place in local variables. Each parameter gets its
+ * own local by default; access these using {@link #getParameter
+ * getParameter()}. Non-static methods and constructors also have a {@code this}
+ * parameter; it's available as {@link #getThis getThis()}. Allocate a new local
+ * variable using {@link #newLocal newLocal()}, and assign a default value to it
+ * with {@link #loadConstant loadConstant()}. Copy a value from one local to
+ * another with {@link #move move()}.
+ *
+ * <p>Every local variable has a fixed type. This is either a primitive type (of
+ * any size) or a reference type. This class emits instructions appropriate to
+ * the types they operate on. Not all operations are local on all types;
+ * attempting to emit such an operation will fail with an unchecked exception.
+ *
+ * <h3>Math and Bit Operations</h3>
+ * Transform a single value into another related value using {@link
+ * #op(UnaryOp,Local,Local) op(UnaryOp, Local, Local)}. Transform two values
+ * into a third value using {@link #op(BinaryOp,Local,Local,Local) op(BinaryOp,
+ * Local, Local, Local)}. In either overload the first {@code Local} parameter
+ * is where the result will be sent; the other {@code Local} parameters are the
+ * inputs.
+ *
+ * <h3>Comparisons</h3>
+ * There are three different comparison operations each with different
+ * constraints:
+ * <ul>
+ * <li>{@link #compareLongs compareLongs()} compares two locals each
+ * containing a {@code long} primitive. This is the only operation that
+ * can compare longs. The result of the comparison is written to another
+ * {@code int} local.</li>
+ * <li>{@link #compareFloatingPoint compareFloatingPoint()} compares two
+ * locals; both {@code float} primitives or both {@code double}
+ * primitives. This is the only operation that can compare floating
+ * point values. This comparison takes an extra parameter that sets
+ * the desired result if either parameter is {@code NaN}. The result of
+ * the comparison is wrtten to another {@code int} local.
+ * <li>{@link #compare compare()} compares two locals. The {@link
+ * Comparison#EQ} and {@link Comparison#NE} options compare either
+ * {@code int} primitives or references. The other options compare only
+ * {@code int} primitives. This comparison takes a {@link Label} that
+ * will be jumped to if the comparison is true. If the comparison is
+ * false the next instruction in sequence will be executed.
+ * </ul>
+ * There's no single operation to compare longs and jump, or to compare ints and
+ * store the result in a local. Accomplish these goals by chaining multiple
+ * operations together.
+ *
+ * <h3>Branches, Labels and Returns</h3>
+ * Basic control flow is expressed using jumps and labels. Each label must be
+ * marked exactly once and may be jumped to any number of times. Create a label
+ * using its constructor: {@code new Label()}, and mark it using {@link #mark
+ * mark(Label)}. All jumps to a label will execute instructions starting from
+ * that label. You can jump to a label that hasn't yet been marked (jumping
+ * forward) or to a label that has already been marked (jumping backward). Jump
+ * unconditionally with {@link #jump jump(Label)} or conditionally based on a
+ * comparison using {@link #compare compare()}.
+ *
+ * <p>Most methods should contain a return instruction. Void methods
+ * should use {@link #returnVoid()}; non-void methods should use {@link
+ * #returnValue returnValue()} with a local whose return type matches the
+ * method's return type. Constructors are considered void methods and should
+ * call {@link #returnVoid()}. Methods may make multiple returns. Methods
+ * containing no return statements must either loop infinitely or throw
+ * unconditionally; it is not legal to end a sequence of instructions without a
+ * jump, return or throw.
+ *
+ * <h3>Throwing and Catching</h3>
+ * This API uses labels to handle thrown exceptions, errors and throwables. Call
+ * {@link #addCatchClause addCatchClause()} to register the target label and
+ * throwable class. All statements that follow will jump to that catch clause if
+ * they throw a {@link Throwable} assignable to that type. Use {@link
+ * #removeCatchClause removeCatchClause()} to unregister the throwable class.
+ *
+ * <p>Throw an throwable by first assigning it to a local and then calling
+ * {@link #throwValue throwValue()}. Control flow will jump to the nearest label
+ * assigned to a type assignable to the thrown type. In this context, "nearest"
+ * means the label requiring the fewest stack frames to be popped.
+ *
+ * <h3>Calling methods</h3>
+ * A method's caller must know its return type, name, parameters, and invoke
+ * kind. Lookup a method on a type using {@link TypeId#getMethod
+ * TypeId.getMethod()}. This is more onerous than Java language invokes, which
+ * can infer the target method using the target object and parameters. There are
+ * four invoke kinds:
+ * <ul>
+ * <li>{@link #invokeStatic invokeStatic()} is used for static methods.</li>
+ * <li>{@link #invokeDirect invokeDirect()} is used for private instance
+ * methods and for constructors to call their superclass's
+ * constructor.</li>
+ * <li>{@link #invokeInterface invokeInterface()} is used to invoke a method
+ * whose declaring type is an interface.</li>
+ * <li>{@link #invokeVirtual invokeVirtual()} is used to invoke any other
+ * method. The target must not be static, private, a constructor, or an
+ * interface method.</li>
+ * <li>{@link #invokeSuper invokeSuper()} is used to invoke the closest
+ * superclass's virtual method. The target must not be static, private,
+ * a constructor method, or an interface method.</li>
+ * <li>{@link #newInstance newInstance()} is used to invoke a
+ * constructor.</li>
+ * </ul>
+ * All invoke methods take a local for the return value. For void methods this
+ * local is unused and may be null.
+ *
+ * <h3>Field Access</h3>
+ * Read static fields using {@link #sget sget()}; write them using {@link
+ * #sput sput()}. For instance values you'll need to specify the declaring
+ * instance; use {@link #getThis getThis()} in an instance method to use {@code
+ * this}. Read instance values using {@link #iget iget()} and write them with
+ * {@link #iput iput()}.
+ *
+ * <h3>Array Access</h3>
+ * Allocate an array using {@link #newArray newArray()}. Read an array's length
+ * with {@link #arrayLength arrayLength()} and its elements with {@link #aget
+ * aget()}. Write an array's elements with {@link #aput aput()}.
+ *
+ * <h3>Types</h3>
+ * Use {@link #cast cast()} to perform either a <strong>numeric cast</strong> or
+ * a <strong>type cast</strong>. Interrogate the type of a value in a local
+ * using {@link #instanceOfType instanceOfType()}.
+ *
+ * <h3>Synchronization</h3>
+ * Acquire a monitor using {@link #monitorEnter monitorEnter()}; release it with
+ * {@link #monitorExit monitorExit()}. It is the caller's responsibility to
+ * guarantee that enter and exit calls are balanced, even in the presence of
+ * exceptions thrown.
+ *
+ * <strong>Warning:</strong> Even if a method has the {@code synchronized} flag,
+ * dex requires instructions to acquire and release monitors manually. A method
+ * declared with {@link java.lang.reflect.Modifier#SYNCHRONIZED SYNCHRONIZED}
+ * but without manual calls to {@code monitorEnter()} and {@code monitorExit()}
+ * will not be synchronized when executed.
+ */
+public final class Code {
+ private final MethodId<?, ?> method;
+ /**
+ * All allocated labels. Although the order of the labels in this list
+ * shouldn't impact behavior, it is used to determine basic block indices.
+ */
+ private final List<Label> labels = new ArrayList<Label>();
+
+ /**
+ * The label currently receiving instructions. This is null if the most
+ * recent instruction was a return or goto.
+ */
+ private Label currentLabel;
+
+ /** true once we've fixed the positions of the parameter registers */
+ private boolean localsInitialized;
+
+ private final Local<?> thisLocal;
+
+ /**
+ * The parameters on this method. If this is non-static, the first parameter
+ * is 'thisLocal' and we have to offset the user's indices by one.
+ */
+ private final List<Local<?>> parameters = new ArrayList<Local<?>>();
+ private final List<Local<?>> locals = new ArrayList<Local<?>>();
+ private SourcePosition sourcePosition = SourcePosition.NO_INFO;
+ private final List<TypeId<?>> catchTypes = new ArrayList<TypeId<?>>();
+ private final List<Label> catchLabels = new ArrayList<Label>();
+ private StdTypeList catches = StdTypeList.EMPTY;
+
+ Code(DexMaker.MethodDeclaration methodDeclaration) {
+ this.method = methodDeclaration.method;
+ if (methodDeclaration.isStatic()) {
+ thisLocal = null;
+ } else {
+ thisLocal = Local.get(this, method.declaringType);
+ parameters.add(thisLocal);
+ }
+ for (TypeId<?> parameter : method.parameters.types) {
+ parameters.add(Local.get(this, parameter));
+ }
+ this.currentLabel = new Label();
+ adopt(this.currentLabel);
+ this.currentLabel.marked = true;
+ }
+
+ /**
+ * Allocates a new local variable of type {@code type}. It is an error to
+ * allocate a local after instructions have been emitted.
+ */
+ public <T> Local<T> newLocal(TypeId<T> type) {
+ if (localsInitialized) {
+ throw new IllegalStateException("Cannot allocate locals after adding instructions");
+ }
+ Local<T> result = Local.get(this, type);
+ locals.add(result);
+ return result;
+ }
+
+ /**
+ * Returns the local for the parameter at index {@code index} and of type
+ * {@code type}.
+ */
+ public <T> Local<T> getParameter(int index, TypeId<T> type) {
+ if (thisLocal != null) {
+ index++; // adjust for the hidden 'this' parameter
+ }
+ return coerce(parameters.get(index), type);
+ }
+
+ /**
+ * Returns the local for {@code this} of type {@code type}. It is an error
+ * to call {@code getThis()} if this is a static method.
+ */
+ public <T> Local<T> getThis(TypeId<T> type) {
+ if (thisLocal == null) {
+ throw new IllegalStateException("static methods cannot access 'this'");
+ }
+ return coerce(thisLocal, type);
+ }
+
+ @SuppressWarnings("unchecked") // guarded by an equals check
+ private <T> Local<T> coerce(Local<?> local, TypeId<T> expectedType) {
+ if (!local.type.equals(expectedType)) {
+ throw new IllegalArgumentException(
+ "requested " + expectedType + " but was " + local.type);
+ }
+ return (Local<T>) local;
+ }
+
+ /**
+ * Assigns registers to locals. From the spec:
+ * "the N arguments to a method land in the last N registers of the
+ * method's invocation frame, in order. Wide arguments consume two
+ * registers. Instance methods are passed a this reference as their
+ * first argument."
+ *
+ * In addition to assigning registers to each of the locals, this creates
+ * instructions to move parameters into their initial registers. These
+ * instructions are inserted before the code's first real instruction.
+ */
+ void initializeLocals() {
+ if (localsInitialized) {
+ throw new AssertionError();
+ }
+ localsInitialized = true;
+
+ int reg = 0;
+ for (Local<?> local : locals) {
+ reg += local.initialize(reg);
+ }
+ int firstParamReg = reg;
+ List<Insn> moveParameterInstructions = new ArrayList<Insn>();
+ for (Local<?> local : parameters) {
+ CstInteger paramConstant = CstInteger.make(reg - firstParamReg);
+ reg += local.initialize(reg);
+ moveParameterInstructions.add(new PlainCstInsn(Rops.opMoveParam(local.type.ropType),
+ sourcePosition, local.spec(), RegisterSpecList.EMPTY, paramConstant));
+ }
+ labels.get(0).instructions.addAll(0, moveParameterInstructions);
+ }
+
+ /**
+ * Returns the number of registers to hold the parameters. This includes the
+ * 'this' parameter if it exists.
+ */
+ int paramSize() {
+ int result = 0;
+ for (Local<?> local : parameters) {
+ result += local.size();
+ }
+ return result;
+ }
+
+ // labels
+
+ /**
+ * Assigns {@code target} to this code.
+ */
+ private void adopt(Label target) {
+ if (target.code == this) {
+ return; // already adopted
+ }
+ if (target.code != null) {
+ throw new IllegalArgumentException("Cannot adopt label; it belongs to another Code");
+ }
+ target.code = this;
+ labels.add(target);
+ }
+
+ /**
+ * Start defining instructions for the named label.
+ */
+ public void mark(Label label) {
+ adopt(label);
+ if (label.marked) {
+ throw new IllegalStateException("already marked");
+ }
+ label.marked = true;
+ if (currentLabel != null) {
+ jump(label); // blocks must end with a branch, return or throw
+ }
+ currentLabel = label;
+ }
+
+ /**
+ * Transfers flow control to the instructions at {@code target}. It is an
+ * error to jump to a label not marked on this {@code Code}.
+ */
+ public void jump(Label target) {
+ adopt(target);
+ addInstruction(new PlainInsn(Rops.GOTO, sourcePosition, null, RegisterSpecList.EMPTY),
+ target);
+ }
+
+ /**
+ * Registers {@code catchClause} as a branch target for all instructions
+ * in this frame that throw a class assignable to {@code toCatch}. This
+ * includes methods invoked from this frame. Deregister the clause using
+ * {@link #removeCatchClause removeCatchClause()}. It is an error to
+ * register a catch clause without also {@link #mark marking it} in the same
+ * {@code Code} instance.
+ */
+ public void addCatchClause(TypeId<? extends Throwable> toCatch, Label catchClause) {
+ if (catchTypes.contains(toCatch)) {
+ throw new IllegalArgumentException("Already caught: " + toCatch);
+ }
+ adopt(catchClause);
+ catchTypes.add(toCatch);
+ catches = toTypeList(catchTypes);
+ catchLabels.add(catchClause);
+ }
+
+ /**
+ * Deregisters the catch clause label for {@code toCatch} and returns it.
+ */
+ public Label removeCatchClause(TypeId<? extends Throwable> toCatch) {
+ int index = catchTypes.indexOf(toCatch);
+ if (index == -1) {
+ throw new IllegalArgumentException("No catch clause: " + toCatch);
+ }
+ catchTypes.remove(index);
+ catches = toTypeList(catchTypes);
+ return catchLabels.remove(index);
+ }
+
+ /**
+ * Throws the throwable in {@code toThrow}.
+ */
+ public void throwValue(Local<? extends Throwable> toThrow) {
+ addInstruction(new ThrowingInsn(Rops.THROW, sourcePosition,
+ RegisterSpecList.make(toThrow.spec()), catches));
+ }
+
+ private StdTypeList toTypeList(List<TypeId<?>> types) {
+ StdTypeList result = new StdTypeList(types.size());
+ for (int i = 0; i < types.size(); i++) {
+ result.set(i, types.get(i).ropType);
+ }
+ return result;
+ }
+
+ private void addInstruction(Insn insn) {
+ addInstruction(insn, null);
+ }
+
+ /**
+ * @param branch the branches to follow; interpretation depends on the
+ * instruction's branchingness.
+ */
+ private void addInstruction(Insn insn, Label branch) {
+ if (currentLabel == null || !currentLabel.marked) {
+ throw new IllegalStateException("no current label");
+ }
+ currentLabel.instructions.add(insn);
+
+ switch (insn.getOpcode().getBranchingness()) {
+ case BRANCH_NONE:
+ if (branch != null) {
+ throw new IllegalArgumentException("unexpected branch: " + branch);
+ }
+ return;
+
+ case BRANCH_RETURN:
+ if (branch != null) {
+ throw new IllegalArgumentException("unexpected branch: " + branch);
+ }
+ currentLabel = null;
+ break;
+
+ case BRANCH_GOTO:
+ if (branch == null) {
+ throw new IllegalArgumentException("branch == null");
+ }
+ currentLabel.primarySuccessor = branch;
+ currentLabel = null;
+ break;
+
+ case Rop.BRANCH_IF:
+ if (branch == null) {
+ throw new IllegalArgumentException("branch == null");
+ }
+ splitCurrentLabel(branch, Collections.<Label>emptyList());
+ break;
+
+ case Rop.BRANCH_THROW:
+ if (branch != null) {
+ throw new IllegalArgumentException("unexpected branch: " + branch);
+ }
+ splitCurrentLabel(null, new ArrayList<Label>(catchLabels));
+ break;
+
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Closes the current label and starts a new one.
+ *
+ * @param catchLabels an immutable list of catch labels
+ */
+ private void splitCurrentLabel(Label alternateSuccessor, List<Label> catchLabels) {
+ Label newLabel = new Label();
+ adopt(newLabel);
+ currentLabel.primarySuccessor = newLabel;
+ currentLabel.alternateSuccessor = alternateSuccessor;
+ currentLabel.catchLabels = catchLabels;
+ currentLabel = newLabel;
+ currentLabel.marked = true;
+ }
+
+ // instructions: locals
+
+ /**
+ * Copies the constant value {@code value} to {@code target}. The constant
+ * must be a primitive, String, Class, TypeId, or null.
+ */
+ public <T> void loadConstant(Local<T> target, T value) {
+ 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)));
+ } else {
+ addInstruction(new ThrowingCstInsn(rop, sourcePosition,
+ RegisterSpecList.EMPTY, catches, Constants.getConstant(value)));
+ moveResult(target, true);
+ }
+ }
+
+ /**
+ * Copies the value in {@code source} to {@code target}.
+ */
+ public <T> void move(Local<T> target, Local<T> source) {
+ addInstruction(new PlainInsn(Rops.opMove(source.type.ropType),
+ sourcePosition, target.spec(), source.spec()));
+ }
+
+ // instructions: unary and binary
+
+ /**
+ * Executes {@code op} and sets {@code target} to the result.
+ */
+ public <T> void op(UnaryOp op, Local<T> target, Local<T> source) {
+ addInstruction(new PlainInsn(op.rop(source.type), sourcePosition,
+ target.spec(), source.spec()));
+ }
+
+ /**
+ * Executes {@code op} and sets {@code target} to the result. For most
+ * binary operations, the types of {@code a} and {@code b} must be the same.
+ * Shift operations (like {@link BinaryOp#SHIFT_LEFT}) require {@code b} to
+ * be an {@code int}, even when {@code a} is a {@code long}.
+ */
+ public <T1, T2> void op(BinaryOp op, Local<T1> target, Local<T1> a, Local<T2> b) {
+ Rop rop = op.rop(StdTypeList.make(a.type.ropType, b.type.ropType));
+ RegisterSpecList sources = RegisterSpecList.make(a.spec(), b.spec());
+
+ if (rop.getBranchingness() == BRANCH_NONE) {
+ addInstruction(new PlainInsn(rop, sourcePosition, target.spec(), sources));
+ } else {
+ addInstruction(new ThrowingInsn(rop, sourcePosition, sources, catches));
+ moveResult(target, true);
+ }
+ }
+
+ // instructions: branches
+
+ /**
+ * Compare ints or references. If the comparison is true, execution jumps to
+ * {@code trueLabel}. If it is false, execution continues to the next
+ * instruction.
+ */
+ public <T> void compare(Comparison comparison, Label trueLabel, Local<T> a, Local<T> b) {
+ adopt(trueLabel);
+ // TODO: ops to compare with zero/null: just omit the 2nd local in StdTypeList.make()
+ Rop rop = comparison.rop(StdTypeList.make(a.type.ropType, b.type.ropType));
+ addInstruction(new PlainInsn(rop, sourcePosition, null,
+ RegisterSpecList.make(a.spec(), b.spec())), trueLabel);
+ }
+
+ /**
+ * Compare floats or doubles. This stores -1 in {@code target} if {@code
+ * a < b}, 0 in {@code target} if {@code a == b} and 1 in target if {@code
+ * a > b}. This stores {@code nanValue} in {@code target} if either value
+ * is {@code NaN}.
+ */
+ public <T extends Number> void compareFloatingPoint(
+ Local<Integer> target, Local<T> a, Local<T> b, int nanValue) {
+ Rop rop;
+ if (nanValue == 1) {
+ rop = Rops.opCmpg(a.type.ropType);
+ } else if (nanValue == -1) {
+ rop = Rops.opCmpl(a.type.ropType);
+ } else {
+ throw new IllegalArgumentException("expected 1 or -1 but was " + nanValue);
+ }
+ addInstruction(new PlainInsn(rop, sourcePosition, target.spec(),
+ RegisterSpecList.make(a.spec(), b.spec())));
+ }
+
+ /**
+ * Compare longs. This stores -1 in {@code target} if {@code
+ * a < b}, 0 in {@code target} if {@code a == b} and 1 in target if {@code
+ * a > b}.
+ */
+ public void compareLongs(Local<Integer> target, Local<Long> a, Local<Long> b) {
+ addInstruction(new PlainInsn(Rops.CMPL_LONG, sourcePosition, target.spec(),
+ RegisterSpecList.make(a.spec(), b.spec())));
+ }
+
+ // instructions: fields
+
+ /**
+ * Copies the value in instance field {@code fieldId} of {@code instance} to
+ * {@code target}.
+ */
+ public <D, V> void iget(FieldId<D, V> fieldId, Local<V> target, Local<D> instance) {
+ addInstruction(new ThrowingCstInsn(Rops.opGetField(target.type.ropType), sourcePosition,
+ RegisterSpecList.make(instance.spec()), catches, fieldId.constant));
+ moveResult(target, true);
+ }
+
+ /**
+ * Copies the value in {@code source} to the instance field {@code fieldId}
+ * of {@code instance}.
+ */
+ public <D, V> void iput(FieldId<D, V> fieldId, Local<D> instance, Local<V> source) {
+ addInstruction(new ThrowingCstInsn(Rops.opPutField(source.type.ropType), sourcePosition,
+ RegisterSpecList.make(source.spec(), instance.spec()), catches, fieldId.constant));
+ }
+
+ /**
+ * Copies the value in the static field {@code fieldId} to {@code target}.
+ */
+ public <V> void sget(FieldId<?, V> fieldId, Local<V> target) {
+ addInstruction(new ThrowingCstInsn(Rops.opGetStatic(target.type.ropType), sourcePosition,
+ RegisterSpecList.EMPTY, catches, fieldId.constant));
+ moveResult(target, true);
+ }
+
+ /**
+ * Copies the value in {@code source} to the static field {@code fieldId}.
+ */
+ public <V> void sput(FieldId<?, V> fieldId, Local<V> source) {
+ addInstruction(new ThrowingCstInsn(Rops.opPutStatic(source.type.ropType), sourcePosition,
+ RegisterSpecList.make(source.spec()), catches, fieldId.constant));
+ }
+
+ // instructions: invoke
+
+ /**
+ * Calls the constructor {@code constructor} using {@code args} and assigns
+ * the new instance to {@code target}.
+ */
+ public <T> void newInstance(Local<T> target, MethodId<T, Void> constructor, Local<?>... args) {
+ if (target == null) {
+ throw new IllegalArgumentException();
+ }
+ addInstruction(new ThrowingCstInsn(Rops.NEW_INSTANCE, sourcePosition,
+ RegisterSpecList.EMPTY, catches, constructor.declaringType.constant));
+ moveResult(target, true);
+ invokeDirect(constructor, null, target, args);
+ }
+
+ /**
+ * Calls the static method {@code method} using {@code args} and assigns the
+ * result to {@code target}.
+ *
+ * @param target the local to receive the method's return value, or {@code
+ * null} if the return type is {@code void} or if its value not needed.
+ */
+ public <R> void invokeStatic(MethodId<?, R> method, Local<? super R> target, Local<?>... args) {
+ invoke(Rops.opInvokeStatic(method.prototype(true)), method, target, null, args);
+ }
+
+ /**
+ * Calls the non-private instance method {@code method} of {@code instance}
+ * using {@code args} and assigns the result to {@code target}.
+ *
+ * @param method a non-private, non-static, method declared on a class. May
+ * not be an interface method or a constructor.
+ * @param target the local to receive the method's return value, or {@code
+ * null} if the return type is {@code void} or if its value not needed.
+ */
+ public <D, R> void invokeVirtual(MethodId<D, R> method, Local<? super R> target,
+ Local<? extends D> instance, Local<?>... args) {
+ invoke(Rops.opInvokeVirtual(method.prototype(true)), method, target, instance, args);
+ }
+
+ /**
+ * Calls {@code method} of {@code instance} using {@code args} and assigns
+ * the result to {@code target}.
+ *
+ * @param method either a private method or the superclass's constructor in
+ * a constructor's call to {@code super()}.
+ * @param target the local to receive the method's return value, or {@code
+ * null} if the return type is {@code void} or if its value not needed.
+ */
+ public <D, R> void invokeDirect(MethodId<D, R> method, Local<? super R> target,
+ Local<? extends D> instance, Local<?>... args) {
+ invoke(Rops.opInvokeDirect(method.prototype(true)), method, target, instance, args);
+ }
+
+ /**
+ * Calls the closest superclass's virtual method {@code method} of {@code
+ * instance} using {@code args} and assigns the result to {@code target}.
+ *
+ * @param target the local to receive the method's return value, or {@code
+ * null} if the return type is {@code void} or if its value not needed.
+ */
+ public <D, R> void invokeSuper(MethodId<D, R> method, Local<? super R> target,
+ Local<? extends D> instance, Local<?>... args) {
+ invoke(Rops.opInvokeSuper(method.prototype(true)), method, target, instance, args);
+ }
+
+ /**
+ * Calls the interface method {@code method} of {@code instance} using
+ * {@code args} and assigns the result to {@code target}.
+ *
+ * @param method a method declared on an interface.
+ * @param target the local to receive the method's return value, or {@code
+ * null} if the return type is {@code void} or if its value not needed.
+ */
+ public <D, R> void invokeInterface(MethodId<D, R> method, Local<? super R> target,
+ Local<? extends D> instance, Local<?>... args) {
+ invoke(Rops.opInvokeInterface(method.prototype(true)), method, target, instance, args);
+ }
+
+ private <D, R> void invoke(Rop rop, MethodId<D, R> method, Local<? super R> target,
+ Local<? extends D> object, Local<?>... args) {
+ addInstruction(new ThrowingCstInsn(rop, sourcePosition, concatenate(object, args),
+ catches, method.constant));
+ if (target != null) {
+ moveResult(target, false);
+ }
+ }
+
+ // instructions: types
+
+ /**
+ * Tests if the value in {@code source} is assignable to {@code type}. If it
+ * is, {@code target} is assigned to 1; otherwise {@code target} is assigned
+ * to 0.
+ */
+ public void instanceOfType(Local<?> target, Local<?> source, TypeId<?> type) {
+ addInstruction(new ThrowingCstInsn(Rops.INSTANCE_OF, sourcePosition,
+ RegisterSpecList.make(source.spec()), catches, type.constant));
+ moveResult(target, true);
+ }
+
+ /**
+ * Performs either a numeric cast or a type cast.
+ *
+ * <h3>Numeric Casts</h3>
+ * Converts a primitive to a different representation. Numeric casts may
+ * be lossy. For example, converting the double {@code 1.8d} to an integer
+ * yields {@code 1}, losing the fractional part. Converting the integer
+ * {@code 0x12345678} to a short yields {@code 0x5678}, losing the high
+ * bytes. The following numeric casts are supported:
+ *
+ * <p><table border="1">
+ * <tr><th>From</th><th>To</th></tr>
+ * <tr><td>int</td><td>byte, char, short, long, float, double</td></tr>
+ * <tr><td>long</td><td>int, float, double</td></tr>
+ * <tr><td>float</td><td>int, long, double</td></tr>
+ * <tr><td>double</td><td>int, long, float</td></tr>
+ * </table>
+ *
+ * <p>For some primitive conversions it will be necessary to chain multiple
+ * cast operations. For example, to go from float to short one would first
+ * cast float to int and then int to short.
+ *
+ * <p>Numeric casts never throw {@link ClassCastException}.
+ *
+ * <h3>Type Casts</h3>
+ * Checks that a reference value is assignable to the target type. If it is
+ * assignable it is copied to the target local. If it is not assignable a
+ * {@link ClassCastException} is thrown.
+ */
+ public void cast(Local<?> target, Local<?> source) {
+ if (source.getType().ropType.isReference()) {
+ addInstruction(new ThrowingCstInsn(Rops.CHECK_CAST, sourcePosition,
+ RegisterSpecList.make(source.spec()), catches, target.type.constant));
+ moveResult(target, true);
+ } else {
+ addInstruction(new PlainInsn(getCastRop(source.type.ropType, target.type.ropType),
+ sourcePosition, target.spec(), source.spec()));
+ }
+ }
+
+ private Rop getCastRop(com.android.dx.rop.type.Type sourceType,
+ com.android.dx.rop.type.Type targetType) {
+ if (sourceType.getBasicType() == BT_INT) {
+ switch (targetType.getBasicType()) {
+ case BT_SHORT:
+ return Rops.TO_SHORT;
+ case BT_CHAR:
+ return Rops.TO_CHAR;
+ case BT_BYTE:
+ return Rops.TO_BYTE;
+ }
+ }
+ return Rops.opConv(targetType, sourceType);
+ }
+
+ // instructions: arrays
+
+ /**
+ * Sets {@code target} to the length of the array in {@code array}.
+ */
+ public <T> void arrayLength(Local<Integer> target, Local<T> array) {
+ addInstruction(new ThrowingInsn(Rops.ARRAY_LENGTH, sourcePosition,
+ RegisterSpecList.make(array.spec()), catches));
+ moveResult(target, true);
+ }
+
+ /**
+ * Assigns {@code target} to a newly allocated array of length {@code
+ * length}. The array's type is the same as {@code target}'s type.
+ */
+ public <T> void newArray(Local<T> target, Local<Integer> length) {
+ addInstruction(new ThrowingCstInsn(Rops.opNewArray(target.type.ropType), sourcePosition,
+ RegisterSpecList.make(length.spec()), catches, target.type.constant));
+ moveResult(target, true);
+ }
+
+ /**
+ * Assigns the element at {@code index} in {@code array} to {@code target}.
+ */
+ public void aget(Local<?> target, Local<?> array, Local<Integer> index) {
+ addInstruction(new ThrowingInsn(Rops.opAget(target.type.ropType), sourcePosition,
+ RegisterSpecList.make(array.spec(), index.spec()), catches));
+ moveResult(target, true);
+ }
+
+ /**
+ * Assigns {@code source} to the element at {@code index} in {@code array}.
+ */
+ public void aput(Local<?> array, Local<Integer> index, Local<?> source) {
+ addInstruction(new ThrowingInsn(Rops.opAput(source.type.ropType), sourcePosition,
+ RegisterSpecList.make(source.spec(), array.spec(), index.spec()), catches));
+ }
+
+ // instructions: return
+
+ /**
+ * Returns from a {@code void} method. After a return it is an error to
+ * define further instructions after a return without first {@link #mark
+ * marking} an existing unmarked label.
+ */
+ public void returnVoid() {
+ if (!method.returnType.equals(TypeId.VOID)) {
+ throw new IllegalArgumentException("declared " + method.returnType
+ + " but returned void");
+ }
+ addInstruction(new PlainInsn(Rops.RETURN_VOID, sourcePosition, null,
+ RegisterSpecList.EMPTY));
+ }
+
+ /**
+ * Returns the value in {@code result} to the calling method. After a return
+ * it is an error to define further instructions after a return without
+ * first {@link #mark marking} an existing unmarked label.
+ */
+ public void returnValue(Local<?> result) {
+ if (!result.type.equals(method.returnType)) {
+ // TODO: this is probably too strict.
+ throw new IllegalArgumentException("declared " + method.returnType
+ + " but returned " + result.type);
+ }
+ addInstruction(new PlainInsn(Rops.opReturn(result.type.ropType), sourcePosition,
+ null, RegisterSpecList.make(result.spec())));
+ }
+
+ private void moveResult(Local<?> target, boolean afterNonInvokeThrowingInsn) {
+ Rop rop = afterNonInvokeThrowingInsn
+ ? Rops.opMoveResultPseudo(target.type.ropType)
+ : Rops.opMoveResult(target.type.ropType);
+ addInstruction(new PlainInsn(rop, sourcePosition, target.spec(), RegisterSpecList.EMPTY));
+ }
+
+ // instructions; synchronized
+
+ /**
+ * Awaits the lock on {@code monitor}, and acquires it.
+ */
+ public void monitorEnter(Local<?> monitor) {
+ addInstruction(new ThrowingInsn(Rops.MONITOR_ENTER, sourcePosition,
+ RegisterSpecList.make(monitor.spec()), catches));
+ }
+
+ /**
+ * Releases the held lock on {@code monitor}.
+ */
+ public void monitorExit(Local<?> monitor) {
+ addInstruction(new ThrowingInsn(Rops.MONITOR_ENTER, sourcePosition,
+ RegisterSpecList.make(monitor.spec()), catches));
+ }
+
+ // produce BasicBlocks for dex
+
+ BasicBlockList toBasicBlocks() {
+ if (!localsInitialized) {
+ initializeLocals();
+ }
+
+ cleanUpLabels();
+
+ BasicBlockList result = new BasicBlockList(labels.size());
+ for (int i = 0; i < labels.size(); i++) {
+ result.set(i, labels.get(i).toBasicBlock());
+ }
+ return result;
+ }
+
+ /**
+ * Removes empty labels and assigns IDs to non-empty labels.
+ */
+ private void cleanUpLabels() {
+ int id = 0;
+ for (Iterator<Label> i = labels.iterator(); i.hasNext();) {
+ Label label = i.next();
+ if (label.isEmpty()) {
+ i.remove();
+ } else {
+ label.compact();
+ label.id = id++;
+ }
+ }
+ }
+
+ private static RegisterSpecList concatenate(Local<?> first, Local<?>[] rest) {
+ int offset = (first != null) ? 1 : 0;
+ RegisterSpecList result = new RegisterSpecList(offset + rest.length);
+ if (first != null) {
+ result.set(0, first.spec());
+ }
+ for (int i = 0; i < rest.length; i++) {
+ result.set(i + offset, rest[i].spec());
+ }
+ return result;
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/Comparison.java b/dexmaker/src/main/java/com/google/dexmaker/Comparison.java
new file mode 100644
index 0000000..06e3ca3
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/Comparison.java
@@ -0,0 +1,71 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.code.Rop;
+import com.android.dx.rop.code.Rops;
+import com.android.dx.rop.type.TypeList;
+
+/**
+ * A comparison between two values of the same type.
+ */
+public enum Comparison {
+
+ /** {@code a < b}. Supports int only. */
+ LT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfLt(types);
+ }
+ },
+
+ /** {@code a <= b}. Supports int only. */
+ LE() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfLe(types);
+ }
+ },
+
+ /** {@code a == b}. Supports int and reference types. */
+ EQ() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfEq(types);
+ }
+ },
+
+ /** {@code a >= b}. Supports int only. */
+ GE() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfGe(types);
+ }
+ },
+
+ /** {@code a > b}. Supports int only. */
+ GT() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfGt(types);
+ }
+ },
+
+ /** {@code a != b}. Supports int and reference types. */
+ NE() {
+ @Override Rop rop(TypeList types) {
+ return Rops.opIfNe(types);
+ }
+ };
+
+ abstract Rop rop(TypeList types);
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/Constants.java b/dexmaker/src/main/java/com/google/dexmaker/Constants.java
new file mode 100644
index 0000000..ec21cb8
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/Constants.java
@@ -0,0 +1,72 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.cst.CstBoolean;
+import com.android.dx.rop.cst.CstByte;
+import com.android.dx.rop.cst.CstChar;
+import com.android.dx.rop.cst.CstDouble;
+import com.android.dx.rop.cst.CstFloat;
+import com.android.dx.rop.cst.CstInteger;
+import com.android.dx.rop.cst.CstKnownNull;
+import com.android.dx.rop.cst.CstLong;
+import com.android.dx.rop.cst.CstShort;
+import com.android.dx.rop.cst.CstString;
+import com.android.dx.rop.cst.CstType;
+import com.android.dx.rop.cst.TypedConstant;
+
+/**
+ * Factory for rop constants.
+ */
+final class Constants {
+ private Constants() {}
+
+ /**
+ * Returns a rop constant for the specified value.
+ *
+ * @param value null, a boxed primitive, String, Class, or TypeId.
+ */
+ static TypedConstant getConstant(Object value) {
+ if (value == null) {
+ return CstKnownNull.THE_ONE;
+ } else if (value instanceof Boolean) {
+ return CstBoolean.make((Boolean) value);
+ } else if (value instanceof Byte) {
+ return CstByte.make((Byte) value);
+ } else if (value instanceof Character) {
+ return CstChar.make((Character) value);
+ } else if (value instanceof Double) {
+ return CstDouble.make(Double.doubleToLongBits((Double) value));
+ } else if (value instanceof Float) {
+ return CstFloat.make(Float.floatToIntBits((Float) value));
+ } else if (value instanceof Integer) {
+ return CstInteger.make((Integer) value);
+ } else if (value instanceof Long) {
+ return CstLong.make((Long) value);
+ } else if (value instanceof Short) {
+ return CstShort.make((Short) value);
+ } else if (value instanceof String) {
+ return new CstString((String) value);
+ } else if (value instanceof Class) {
+ return new CstType(TypeId.get((Class<?>) value).ropType);
+ } else if (value instanceof TypeId) {
+ return new CstType(((TypeId) value).ropType);
+ } else {
+ throw new UnsupportedOperationException("Not a constant: " + value);
+ }
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/DexMaker.java b/dexmaker/src/main/java/com/google/dexmaker/DexMaker.java
new file mode 100644
index 0000000..4e67433
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/DexMaker.java
@@ -0,0 +1,546 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.dex.DexFormat;
+import com.android.dx.dex.DexOptions;
+import com.android.dx.dex.code.DalvCode;
+import com.android.dx.dex.code.PositionList;
+import com.android.dx.dex.code.RopTranslator;
+import com.android.dx.dex.file.ClassDefItem;
+import com.android.dx.dex.file.DexFile;
+import com.android.dx.dex.file.EncodedField;
+import com.android.dx.dex.file.EncodedMethod;
+import com.android.dx.rop.code.AccessFlags;
+import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
+import com.android.dx.rop.code.LocalVariableInfo;
+import com.android.dx.rop.code.RopMethod;
+import com.android.dx.rop.cst.CstString;
+import com.android.dx.rop.cst.CstType;
+import com.android.dx.rop.type.StdTypeList;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import static java.lang.reflect.Modifier.PRIVATE;
+import static java.lang.reflect.Modifier.STATIC;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+/**
+ * Generates a </i><strong>D</strong>alvik <strong>EX</strong>ecutable (dex)
+ * file for execution on Android. Dex files define classes and interfaces,
+ * including their member methods and fields, executable code, and debugging
+ * information. They also define annotations, though this API currently has no
+ * facility to create a dex file that contains annotations.
+ *
+ * <p>This library is intended to satisfy two use cases:
+ * <ul>
+ * <li><strong>For runtime code generation.</strong> By embedding this library
+ * in your Android application, you can dynamically generate and load
+ * executable code. This approach takes advantage of the fact that the
+ * host environment and target environment are both Android.
+ * <li><strong>For compile time code generation.</strong> You may use this
+ * library as a part of a compiler that targets Android. In this scenario
+ * the generated dex file must be installed on an Android device before it
+ * can be executed.
+ * </ul>
+ *
+ * <h3>Example: Fibonacci</h3>
+ * To illustrate how this API is used, we'll use DexMaker to generate a class
+ * equivalent to the following Java source: <pre> {@code
+ *
+ * package com.publicobject.fib;
+ *
+ * public class Fibonacci {
+ * public static int fib(int i) {
+ * if (i < 2) {
+ * return i;
+ * }
+ * return fib(i - 1) + fib(i - 2);
+ * }
+ * }}</pre>
+ *
+ * <p>We start by creating a {@link TypeId} to identify the generated {@code
+ * Fibonacci} class. DexMaker identifies types by their internal names like
+ * {@code Ljava/lang/Object;} rather than their Java identifiers like {@code
+ * java.lang.Object}. <pre> {@code
+ *
+ * TypeId<?> fibonacci = TypeId.get("Lcom/google/dexmaker/examples/Fibonacci;");
+ * }</pre>
+ *
+ * <p>Next we declare the class. It allows us to specify the type's source file
+ * for stack traces, its modifiers, its superclass, and the interfaces it
+ * implements. In this case, {@code Fibonacci} is a public class that extends
+ * from {@code Object}: <pre> {@code
+ *
+ * String fileName = "Fibonacci.generated";
+ * DexMaker dexMaker = new DexMaker();
+ * dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
+ * }</pre>
+ * It is illegal to declare members of a class without also declaring the class
+ * itself.
+ *
+ * <p>To make it easier to go from our Java method to dex instructions, we'll
+ * manually translate it to pseudocode fit for an assembler. We need to replace
+ * control flow like {@code if()} blocks and {@code for()} loops with labels and
+ * branches. We'll also avoid performing multiple operations in one statement,
+ * using local variables to hold intermediate values as necessary:
+ * <pre> {@code
+ *
+ * int constant1 = 1;
+ * int constant2 = 2;
+ * if (i < constant2) goto baseCase;
+ * int a = i - constant1;
+ * int b = i - constant2;
+ * int c = fib(a);
+ * int d = fib(b);
+ * int result = c + d;
+ * return result;
+ * baseCase:
+ * return i;
+ * }</pre>
+ *
+ * <p>We look up the {@code MethodId} for the method on the declaring type. This
+ * takes the method's return type (possibly {@link TypeId#VOID}), its name and
+ * its parameters types. Next we declare the method, specifying its modifiers by
+ * bitwise ORing constants from {@link java.lang.reflect.Modifier}. The declare
+ * call returns a {@link Code} object, which we'll use to define the method's
+ * instructions. <pre> {@code
+ *
+ * MethodId<?, Integer> fib = fibonacci.getMethod(TypeId.INT, "fib", TypeId.INT);
+ * Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
+ * }</pre>
+ *
+ * <p>One limitation of {@code DexMaker}'s API is that it requires all local
+ * variables to be created before any instructions are emitted. Use {@link
+ * Code#newLocal newLocal()} to create a new local variable. The method's
+ * parameters are exposed as locals using {@link Code#getParameter
+ * getParameter()}. For non-static methods the {@code this} pointer is exposed
+ * using {@link Code#getThis getThis()}. Here we declare all of the local
+ * variables that we'll need for our {@code fib()} method: <pre> {@code
+ *
+ * Local<Integer> i = code.getParameter(0, TypeId.INT);
+ * Local<Integer> constant1 = code.newLocal(TypeId.INT);
+ * Local<Integer> constant2 = code.newLocal(TypeId.INT);
+ * Local<Integer> a = code.newLocal(TypeId.INT);
+ * Local<Integer> b = code.newLocal(TypeId.INT);
+ * Local<Integer> c = code.newLocal(TypeId.INT);
+ * Local<Integer> d = code.newLocal(TypeId.INT);
+ * Local<Integer> result = code.newLocal(TypeId.INT);
+ * }</pre>
+ *
+ * <p>Notice that {@link Local} has a type parameter of {@code Integer}. This is
+ * useful for generating code that works with existing types like {@code String}
+ * and {@code Integer}, but it can be a hindrance when generating code that
+ * involves new types. For this reason you may prefer to use raw types only and
+ * add {@code @SuppressWarnings("unsafe")} on your calling code. This will yield
+ * the same result but you won't get IDE support if you make a type error.
+ *
+ * <p>We're ready to start defining our method's instructions. The {@link Code}
+ * class catalogs the available instructions and their use. <pre> {@code
+ *
+ * code.loadConstant(constant1, 1);
+ * code.loadConstant(constant2, 2);
+ * Label baseCase = new Label();
+ * code.compare(Comparison.LT, baseCase, i, constant2);
+ * code.op(BinaryOp.SUBTRACT, a, i, constant1);
+ * code.op(BinaryOp.SUBTRACT, b, i, constant2);
+ * code.invokeStatic(fib, c, a);
+ * code.invokeStatic(fib, d, b);
+ * code.op(BinaryOp.ADD, result, c, d);
+ * code.returnValue(result);
+ * code.mark(baseCase);
+ * code.returnValue(i);
+ * }</pre>
+ *
+ * <p>We're done defining the dex file. We just need to write it to the
+ * filesystem or load it into the current process. For this example we'll load
+ * the generated code into the current process. This only works when the current
+ * process is running on Android. We use {@link #generateAndLoad
+ * generateAndLoad()} which takes the class loader that will be used as our
+ * generated code's parent class loader. It also requires a directory where
+ * temporary files can be written. <pre> {@code
+ *
+ * ClassLoader loader = dexMaker.generateAndLoad(
+ * FibonacciMaker.class.getClassLoader(), getDataDirectory());
+ * }</pre>
+ * Finally we'll use reflection to lookup our generated class on its class
+ * loader and invoke its {@code fib()} method: <pre> {@code
+ *
+ * Class<?> fibonacciClass = loader.loadClass("com.google.dexmaker.examples.Fibonacci");
+ * Method fibMethod = fibonacciClass.getMethod("fib", int.class);
+ * System.out.println(fibMethod.invoke(null, 8));
+ * }</pre>
+ */
+public final class DexMaker {
+ private final Map<TypeId<?>, TypeDeclaration> types
+ = new LinkedHashMap<TypeId<?>, TypeDeclaration>();
+
+ /**
+ * Creates a new {@code DexMaker} instance, which can be used to create a
+ * single dex file.
+ */
+ public DexMaker() {
+ }
+
+ private TypeDeclaration getTypeDeclaration(TypeId<?> type) {
+ TypeDeclaration result = types.get(type);
+ if (result == null) {
+ result = new TypeDeclaration(type);
+ types.put(type, result);
+ }
+ return result;
+ }
+
+ /**
+ * Declares {@code type}.
+ *
+ * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
+ * Modifier#FINAL} and {@link Modifier#ABSTRACT}.
+ */
+ public void declare(TypeId<?> type, String sourceFile, int flags,
+ TypeId<?> supertype, TypeId<?>... interfaces) {
+ TypeDeclaration declaration = getTypeDeclaration(type);
+ int supportedFlags = Modifier.PUBLIC | Modifier.FINAL | Modifier.ABSTRACT;
+ if ((flags & ~supportedFlags) != 0) {
+ throw new IllegalArgumentException("Unexpected flag: "
+ + Integer.toHexString(flags));
+ }
+ if (declaration.declared) {
+ throw new IllegalStateException("already declared: " + type);
+ }
+ declaration.declared = true;
+ declaration.flags = flags;
+ declaration.supertype = supertype;
+ declaration.sourceFile = sourceFile;
+ declaration.interfaces = new TypeList(interfaces);
+ }
+
+ /**
+ * Declares a method or constructor.
+ *
+ * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
+ * Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
+ * {@link Modifier#FINAL} and {@link Modifier#SYNCHRONIZED}.
+ * <p><strong>Warning:</strong> the {@link Modifier#SYNCHRONIZED} flag
+ * is insufficient to generate a synchronized method. You must also use
+ * {@link Code#monitorEnter} and {@link Code#monitorExit} to acquire
+ * a monitor.
+ */
+ public Code declare(MethodId<?, ?> method, int flags) {
+ TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
+ if (typeDeclaration.methods.containsKey(method)) {
+ throw new IllegalStateException("already declared: " + method);
+ }
+
+ int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
+ | Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED;
+ if ((flags & ~supportedFlags) != 0) {
+ throw new IllegalArgumentException("Unexpected flag: "
+ + Integer.toHexString(flags));
+ }
+
+ // replace the SYNCHRONIZED flag with the DECLARED_SYNCHRONIZED flag
+ if ((flags & Modifier.SYNCHRONIZED) != 0) {
+ flags = (flags & ~Modifier.SYNCHRONIZED) | AccessFlags.ACC_DECLARED_SYNCHRONIZED;
+ }
+
+ if (method.isConstructor()) {
+ flags |= ACC_CONSTRUCTOR;
+ }
+
+ MethodDeclaration methodDeclaration = new MethodDeclaration(method, flags);
+ typeDeclaration.methods.put(method, methodDeclaration);
+ return methodDeclaration.code;
+ }
+
+ /**
+ * Declares a field.
+ *
+ * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
+ * Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
+ * {@link Modifier#FINAL}, {@link Modifier#VOLATILE}, and {@link
+ * Modifier#TRANSIENT}.
+ * @param staticValue a constant representing the initial value for the
+ * static field, possibly null. This must be null if this field is
+ * non-static.
+ */
+ public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
+ TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
+ if (typeDeclaration.fields.containsKey(fieldId)) {
+ throw new IllegalStateException("already declared: " + fieldId);
+ }
+
+ int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
+ | Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | Modifier.TRANSIENT;
+ if ((flags & ~supportedFlags) != 0) {
+ throw new IllegalArgumentException("Unexpected flag: "
+ + Integer.toHexString(flags));
+ }
+
+ if ((flags & Modifier.STATIC) == 0 && staticValue != null) {
+ throw new IllegalArgumentException("staticValue is non-null, but field is not static");
+ }
+
+ FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId, flags, staticValue);
+ typeDeclaration.fields.put(fieldId, fieldDeclaration);
+ }
+
+ /**
+ * Generates a dex file and returns its bytes.
+ */
+ public byte[] generate() {
+ DexOptions options = new DexOptions();
+ options.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
+ DexFile outputDex = new DexFile(options);
+
+ for (TypeDeclaration typeDeclaration : types.values()) {
+ outputDex.add(typeDeclaration.toClassDefItem());
+ }
+
+ try {
+ return outputDex.toDex(null, false);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Generate a file name for the jar by taking a checksum of MethodIds and
+ // parent class types.
+ private String generateFileName() {
+ int checksum = 1;
+
+ Set<TypeId<?>> typesKeySet = types.keySet();
+ Iterator<TypeId<?>> it = typesKeySet.iterator();
+ int[] checksums = new int[typesKeySet.size()];
+ int i = 0;
+
+ while (it.hasNext()) {
+ TypeId<?> typeId = it.next();
+ TypeDeclaration decl = getTypeDeclaration(typeId);
+ Set<MethodId> methodSet = decl.methods.keySet();
+ if (decl.supertype != null) {
+ checksums[i++] = 31 * decl.supertype.hashCode() + methodSet.hashCode();
+ }
+ }
+ Arrays.sort(checksums);
+
+ for (int sum : checksums) {
+ checksum *= 31;
+ checksum += sum;
+ }
+
+ return "Generated_" + checksum +".jar";
+ }
+
+ private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) {
+ try {
+ return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
+ .getConstructor(String.class, String.class, String.class, ClassLoader.class)
+ .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
+ } catch (ClassNotFoundException e) {
+ throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ } catch (InstantiationException e) {
+ throw new AssertionError();
+ } catch (NoSuchMethodException e) {
+ throw new AssertionError();
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Generates a dex file and loads its types into the current process.
+ *
+ * <h3>Picking a dex cache directory</h3>
+ * The {@code dexCache} should be an application-private directory. If
+ * you pass a world-writable directory like {@code /sdcard} a malicious app
+ * could inject code into your process. Most applications should use this:
+ * <pre> {@code
+ *
+ * File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
+ * }</pre>
+ * If the {@code dexCache} is null, this method will consult the {@code
+ * dexmaker.dexcache} system property. If that exists, it will be used for
+ * the dex cache. If it doesn't exist, this method will attempt to guess
+ * the application's private data directory as a last resort. If that fails,
+ * this method will fail with an unchecked exception. You can avoid the
+ * exception by either providing a non-null value or setting the system
+ * property.
+ *
+ * @param parent the parent ClassLoader to be used when loading our
+ * generated types
+ * @param dexCache the destination directory where generated and optimized
+ * dex files will be written. If null, this class will try to guess the
+ * application's private data dir.
+ */
+ public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
+ if (dexCache == null) {
+ String property = System.getProperty("dexmaker.dexcache");
+ if (property != null) {
+ dexCache = new File(property);
+ } else {
+ dexCache = new AppDataDirGuesser().guess();
+ if (dexCache == null) {
+ throw new IllegalArgumentException("dexcache == null (and no default could be"
+ + " found; consider setting the 'dexmaker.dexcache' system property)");
+ }
+ }
+ }
+
+ File result = new File(dexCache, generateFileName());
+ // Check that the file exists. If it does, return a DexClassLoader and skip all
+ // the dex bytecode generation.
+ if (result.exists()) {
+ return generateClassLoader(result, dexCache, parent);
+ }
+
+ byte[] dex = generate();
+
+ /*
+ * This implementation currently dumps the dex to the filesystem. It
+ * jars the emitted .dex for the benefit of Gingerbread and earlier
+ * devices, which can't load .dex files directly.
+ *
+ * TODO: load the dex from memory where supported.
+ */
+ result.createNewFile();
+ JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
+ JarEntry entry = new JarEntry(DexFormat.DEX_IN_JAR_NAME);
+ entry.setSize(dex.length);
+ jarOut.putNextEntry(entry);
+ jarOut.write(dex);
+ jarOut.closeEntry();
+ jarOut.close();
+ return generateClassLoader(result, dexCache, parent);
+ }
+
+ private static class TypeDeclaration {
+ private final TypeId<?> type;
+
+ /** declared state */
+ private boolean declared;
+ private int flags;
+ private TypeId<?> supertype;
+ private String sourceFile;
+ private TypeList interfaces;
+
+ private final Map<FieldId, FieldDeclaration> fields
+ = new LinkedHashMap<FieldId, FieldDeclaration>();
+ private final Map<MethodId, MethodDeclaration> methods
+ = new LinkedHashMap<MethodId, MethodDeclaration>();
+
+ TypeDeclaration(TypeId<?> type) {
+ this.type = type;
+ }
+
+ ClassDefItem toClassDefItem() {
+ if (!declared) {
+ throw new IllegalStateException("Undeclared type " + type + " declares members: "
+ + fields.keySet() + " " + methods.keySet());
+ }
+
+ DexOptions dexOptions = new DexOptions();
+ dexOptions.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
+
+ CstType thisType = type.constant;
+
+ ClassDefItem out = new ClassDefItem(thisType, flags, supertype.constant,
+ interfaces.ropTypes, new CstString(sourceFile));
+
+ for (MethodDeclaration method : methods.values()) {
+ EncodedMethod encoded = method.toEncodedMethod(dexOptions);
+ if (method.isDirect()) {
+ out.addDirectMethod(encoded);
+ } else {
+ out.addVirtualMethod(encoded);
+ }
+ }
+ for (FieldDeclaration field : fields.values()) {
+ EncodedField encoded = field.toEncodedField();
+ if (field.isStatic()) {
+ out.addStaticField(encoded, Constants.getConstant(field.staticValue));
+ } else {
+ out.addInstanceField(encoded);
+ }
+ }
+
+ return out;
+ }
+ }
+
+ static class FieldDeclaration {
+ final FieldId<?, ?> fieldId;
+ private final int accessFlags;
+ private final Object staticValue;
+
+ FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue) {
+ if ((accessFlags & STATIC) == 0 && staticValue != null) {
+ throw new IllegalArgumentException("instance fields may not have a value");
+ }
+ this.fieldId = fieldId;
+ this.accessFlags = accessFlags;
+ this.staticValue = staticValue;
+ }
+
+ EncodedField toEncodedField() {
+ return new EncodedField(fieldId.constant, accessFlags);
+ }
+
+ public boolean isStatic() {
+ return (accessFlags & STATIC) != 0;
+ }
+ }
+
+ static class MethodDeclaration {
+ final MethodId<?, ?> method;
+ private final int flags;
+ private final Code code;
+
+ public MethodDeclaration(MethodId<?, ?> method, int flags) {
+ this.method = method;
+ this.flags = flags;
+ this.code = new Code(this);
+ }
+
+ boolean isStatic() {
+ return (flags & STATIC) != 0;
+ }
+
+ boolean isDirect() {
+ return (flags & (STATIC | PRIVATE | ACC_CONSTRUCTOR)) != 0;
+ }
+
+ EncodedMethod toEncodedMethod(DexOptions dexOptions) {
+ RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
+ LocalVariableInfo locals = null;
+ DalvCode dalvCode = RopTranslator.translate(
+ ropMethod, PositionList.NONE, locals, code.paramSize(), dexOptions);
+ return new EncodedMethod(method.constant, flags, dalvCode, StdTypeList.EMPTY);
+ }
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/FieldId.java b/dexmaker/src/main/java/com/google/dexmaker/FieldId.java
new file mode 100644
index 0000000..c03e1d1
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/FieldId.java
@@ -0,0 +1,74 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.cst.CstFieldRef;
+import com.android.dx.rop.cst.CstNat;
+import com.android.dx.rop.cst.CstString;
+
+/**
+ * Identifies a field.
+ *
+ * @param <D> the type declaring this field
+ * @param <V> the type of value this field holds
+ */
+public final class FieldId<D, V> {
+ final TypeId<D> declaringType;
+ final TypeId<V> type;
+ final String name;
+
+ /** cached converted state */
+ final CstNat nat;
+ final CstFieldRef constant;
+
+ FieldId(TypeId<D> declaringType, TypeId<V> type, String name) {
+ if (declaringType == null || type == null || name == null) {
+ throw new NullPointerException();
+ }
+ this.declaringType = declaringType;
+ this.type = type;
+ this.name = name;
+ this.nat = new CstNat(new CstString(name), new CstString(type.name));
+ this.constant = new CstFieldRef(declaringType.constant, nat);
+ }
+
+ public TypeId<D> getDeclaringType() {
+ return declaringType;
+ }
+
+ public TypeId<V> getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof FieldId
+ && ((FieldId<?, ?>) o).declaringType.equals(declaringType)
+ && ((FieldId<?, ?>) o).name.equals(name);
+ }
+
+ @Override public int hashCode() {
+ return declaringType.hashCode() + 37 * name.hashCode();
+ }
+
+ @Override public String toString() {
+ return declaringType + "." + name;
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/Label.java b/dexmaker/src/main/java/com/google/dexmaker/Label.java
new file mode 100644
index 0000000..b37b18c
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/Label.java
@@ -0,0 +1,92 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.code.BasicBlock;
+import com.android.dx.rop.code.Insn;
+import com.android.dx.rop.code.InsnList;
+import com.android.dx.util.IntList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A branch target in a list of instructions.
+ */
+public final class Label {
+
+ final List<Insn> instructions = new ArrayList<Insn>();
+
+ Code code;
+
+ boolean marked = false;
+
+ /** an immutable list of labels corresponding to the types in the catch list */
+ List<Label> catchLabels = Collections.emptyList();
+
+ /** contains the next instruction if no branch occurs */
+ Label primarySuccessor;
+
+ /** contains the instruction to jump to if the if is true */
+ Label alternateSuccessor;
+
+ int id = -1;
+
+ public Label() {}
+
+ boolean isEmpty() {
+ return instructions.isEmpty();
+ }
+
+ void compact() {
+ for (int i = 0; i < catchLabels.size(); i++) {
+ while (catchLabels.get(i).isEmpty()) {
+ catchLabels.set(i, catchLabels.get(i).primarySuccessor);
+ }
+ }
+ while (primarySuccessor != null && primarySuccessor.isEmpty()) {
+ primarySuccessor = primarySuccessor.primarySuccessor;
+ }
+ while (alternateSuccessor != null && alternateSuccessor.isEmpty()) {
+ alternateSuccessor = alternateSuccessor.primarySuccessor;
+ }
+ }
+
+ BasicBlock toBasicBlock() {
+ InsnList result = new InsnList(instructions.size());
+ for (int i = 0; i < instructions.size(); i++) {
+ result.set(i, instructions.get(i));
+ }
+ result.setImmutable();
+
+ int primarySuccessorIndex = -1;
+ IntList successors = new IntList();
+ for (Label catchLabel : catchLabels) {
+ successors.add(catchLabel.id);
+ }
+ if (primarySuccessor != null) {
+ primarySuccessorIndex = primarySuccessor.id;
+ successors.add(primarySuccessorIndex);
+ }
+ if (alternateSuccessor != null) {
+ successors.add(alternateSuccessor.id);
+ }
+ successors.setImmutable();
+
+ return new BasicBlock(id, result, successors, primarySuccessorIndex);
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/Local.java b/dexmaker/src/main/java/com/google/dexmaker/Local.java
new file mode 100644
index 0000000..c89436d
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/Local.java
@@ -0,0 +1,74 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.code.RegisterSpec;
+
+/**
+ * A temporary variable that holds a single value of a known type.
+ */
+public final class Local<T> {
+ private final Code code;
+ final TypeId<T> type;
+ private int reg = -1;
+ private RegisterSpec spec;
+
+ private Local(Code code, TypeId<T> type) {
+ this.code = code;
+ this.type = type;
+ }
+
+ static <T> Local<T> get(Code code, TypeId<T> type) {
+ return new Local<T>(code, type);
+ }
+
+ /**
+ * Assigns registers to this local.
+ *
+ * @return the number of registers required.
+ */
+ int initialize(int nextAvailableRegister) {
+ this.reg = nextAvailableRegister;
+ this.spec = RegisterSpec.make(nextAvailableRegister, type.ropType);
+ return size();
+ }
+
+ /**
+ * Returns the number of registered required to hold this local.
+ */
+ int size() {
+ return type.ropType.getCategory();
+ }
+
+ RegisterSpec spec() {
+ if (spec == null) {
+ code.initializeLocals();
+ if (spec == null) {
+ throw new AssertionError();
+ }
+ }
+ return spec;
+ }
+
+ public TypeId getType() {
+ return type;
+ }
+
+ @Override public String toString() {
+ return "v" + reg + "(" + type + ")";
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/MethodId.java b/dexmaker/src/main/java/com/google/dexmaker/MethodId.java
new file mode 100644
index 0000000..a43060c
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/MethodId.java
@@ -0,0 +1,120 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.cst.CstMethodRef;
+import com.android.dx.rop.cst.CstNat;
+import com.android.dx.rop.cst.CstString;
+import com.android.dx.rop.type.Prototype;
+import java.util.List;
+
+/**
+ * Identifies a method or constructor.
+ *
+ * @param <D> the type declaring this field
+ * @param <R> the return type of this method
+ */
+public final class MethodId<D, R> {
+ final TypeId<D> declaringType;
+ final TypeId<R> returnType;
+ final String name;
+ final TypeList parameters;
+
+ /** cached converted state */
+ final CstNat nat;
+ final CstMethodRef constant;
+
+ MethodId(TypeId<D> declaringType, TypeId<R> returnType, String name, TypeList parameters) {
+ if (declaringType == null || returnType == null || name == null || parameters == null) {
+ throw new NullPointerException();
+ }
+ this.declaringType = declaringType;
+ this.returnType = returnType;
+ this.name = name;
+ this.parameters = parameters;
+ this.nat = new CstNat(new CstString(name), new CstString(descriptor(false)));
+ this.constant = new CstMethodRef(declaringType.constant, nat);
+ }
+
+ public TypeId<D> getDeclaringType() {
+ return declaringType;
+ }
+
+ public TypeId<R> getReturnType() {
+ return returnType;
+ }
+
+ /**
+ * Returns true if this method is a constructor for its declaring class.
+ */
+ public boolean isConstructor() {
+ return name.equals("<init>");
+ }
+
+ /**
+ * Returns the method's name. This is "<init>" if this is a constructor.
+ */
+ public String getName() {
+ return name;
+ }
+
+ public List<TypeId<?>> getParameters() {
+ return parameters.asList();
+ }
+
+ /**
+ * Returns a descriptor like "(Ljava/lang/Class;[I)Ljava/lang/Object;".
+ */
+ String descriptor(boolean includeThis) {
+ StringBuilder result = new StringBuilder();
+ result.append("(");
+ if (includeThis) {
+ result.append(declaringType.name);
+ }
+ for (TypeId t : parameters.types) {
+ result.append(t.name);
+ }
+ result.append(")");
+ result.append(returnType.name);
+ return result.toString();
+ }
+
+ Prototype prototype(boolean includeThis) {
+ return Prototype.intern(descriptor(includeThis));
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof MethodId
+ && ((MethodId<?, ?>) o).declaringType.equals(declaringType)
+ && ((MethodId<?, ?>) o).name.equals(name)
+ && ((MethodId<?, ?>) o).parameters.equals(parameters)
+ && ((MethodId<?, ?>) o).returnType.equals(returnType);
+ }
+
+ @Override public int hashCode() {
+ int result = 17;
+ result = 31 * result + declaringType.hashCode();
+ result = 31 * result + name.hashCode();
+ result = 31 * result + parameters.hashCode();
+ result = 31 * result + returnType.hashCode();
+ return result;
+ }
+
+ @Override public String toString() {
+ return declaringType + "." + name + "(" + parameters + ")";
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/TypeId.java b/dexmaker/src/main/java/com/google/dexmaker/TypeId.java
new file mode 100644
index 0000000..36e0b5a
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/TypeId.java
@@ -0,0 +1,148 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.cst.CstType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A primitive type, interface or class.
+ *
+ * <p><strong>Warning:</strong> Use care when dealing with boxed primitive
+ * types. Java's lack of support for parameterized primitive types means that
+ * a primitive type like {@code int} and its boxed type {@code
+ * java.lang.Integer} have the same type parameter: {@code TypeId<Integer>}.
+ * These types are different and it will be a runtime error if the boxed type
+ * {@code java.lang.Integer} is used where the primitive type {@code int} is
+ * expected.
+ */
+public final class TypeId<T> {
+ /** The {@code boolean} primitive type. */
+ public static final TypeId<Boolean> BOOLEAN
+ = new TypeId<Boolean>(com.android.dx.rop.type.Type.BOOLEAN);
+
+ /** The {@code byte} primitive type. */
+ public static final TypeId<Byte> BYTE = new TypeId<Byte>(com.android.dx.rop.type.Type.BYTE);
+
+ /** The {@code char} primitive type. */
+ public static final TypeId<Character> CHAR
+ = new TypeId<Character>(com.android.dx.rop.type.Type.CHAR);
+
+ /** The {@code double} primitive type. */
+ public static final TypeId<Double> DOUBLE = new TypeId<Double>(com.android.dx.rop.type.Type.DOUBLE);
+
+ /** The {@code float} primitive type. */
+ public static final TypeId<Float> FLOAT = new TypeId<Float>(com.android.dx.rop.type.Type.FLOAT);
+
+ /** The {@code int} primitive type. */
+ public static final TypeId<Integer> INT = new TypeId<Integer>(com.android.dx.rop.type.Type.INT);
+
+ /** The {@code long} primitive type. */
+ public static final TypeId<Long> LONG = new TypeId<Long>(com.android.dx.rop.type.Type.LONG);
+
+ /** The {@code short} primitive type. */
+ public static final TypeId<Short> SHORT = new TypeId<Short>(com.android.dx.rop.type.Type.SHORT);
+
+ /** The {@code void} primitive type. Only used as a return type. */
+ public static final TypeId<Void> VOID = new TypeId<Void>(com.android.dx.rop.type.Type.VOID);
+
+ /** The {@code Object} type. */
+ public static final TypeId<Object> OBJECT = new TypeId<Object>(com.android.dx.rop.type.Type.OBJECT);
+
+ /** The {@code String} type. */
+ public static final TypeId<String> STRING = new TypeId<String>(com.android.dx.rop.type.Type.STRING);
+
+ private static final Map<Class<?>, TypeId<?>> PRIMITIVE_TO_TYPE
+ = new HashMap<Class<?>, TypeId<?>>();
+ static {
+ PRIMITIVE_TO_TYPE.put(boolean.class, BOOLEAN);
+ PRIMITIVE_TO_TYPE.put(byte.class, BYTE);
+ PRIMITIVE_TO_TYPE.put(char.class, CHAR);
+ PRIMITIVE_TO_TYPE.put(double.class, DOUBLE);
+ PRIMITIVE_TO_TYPE.put(float.class, FLOAT);
+ PRIMITIVE_TO_TYPE.put(int.class, INT);
+ PRIMITIVE_TO_TYPE.put(long.class, LONG);
+ PRIMITIVE_TO_TYPE.put(short.class, SHORT);
+ PRIMITIVE_TO_TYPE.put(void.class, VOID);
+ }
+
+ final String name;
+
+ /** cached converted values */
+ final com.android.dx.rop.type.Type ropType;
+ final CstType constant;
+
+ TypeId(com.android.dx.rop.type.Type ropType) {
+ this(ropType.getDescriptor(), ropType);
+ }
+
+ TypeId(String name, com.android.dx.rop.type.Type ropType) {
+ if (name == null || ropType == null) {
+ throw new NullPointerException();
+ }
+ this.name = name;
+ this.ropType = ropType;
+ this.constant = CstType.intern(ropType);
+ }
+
+ /**
+ * @param name a descriptor like "Ljava/lang/Class;".
+ */
+ public static <T> TypeId<T> get(String name) {
+ return new TypeId<T>(name, com.android.dx.rop.type.Type.internReturnType(name));
+ }
+
+ public static <T> TypeId<T> get(Class<T> type) {
+ if (type.isPrimitive()) {
+ @SuppressWarnings("unchecked") // guarded by equals
+ TypeId<T> result = (TypeId<T>) PRIMITIVE_TO_TYPE.get(type);
+ return result;
+ }
+ String name = type.getName().replace('.', '/');
+ return get(type.isArray() ? name : 'L' + name + ';');
+ }
+
+ public <V> FieldId<T, V> getField(TypeId<V> type, String name) {
+ return new FieldId<T, V>(this, type, name);
+ }
+
+ public MethodId<T, Void> getConstructor(TypeId<?>... parameters) {
+ return new MethodId<T, Void>(this, VOID, "<init>", new TypeList(parameters));
+ }
+
+ public <R> MethodId<T, R> getMethod(TypeId<R> returnType, String name, TypeId<?>... parameters) {
+ return new MethodId<T, R>(this, returnType, name, new TypeList(parameters));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof TypeId
+ && ((TypeId) o).name.equals(name);
+ }
+
+ @Override public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override public String toString() {
+ return name;
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/TypeList.java b/dexmaker/src/main/java/com/google/dexmaker/TypeList.java
new file mode 100644
index 0000000..688157f
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/TypeList.java
@@ -0,0 +1,64 @@
+/*
+ * 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.google.dexmaker;
+
+import com.android.dx.rop.type.StdTypeList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An immutable of types.
+ */
+final class TypeList {
+ final TypeId<?>[] types;
+ final StdTypeList ropTypes;
+
+ TypeList(TypeId<?>[] types) {
+ this.types = types.clone();
+ this.ropTypes = new StdTypeList(types.length);
+ for (int i = 0; i < types.length; i++) {
+ ropTypes.set(i, types[i].ropType);
+ }
+ }
+
+ /**
+ * Returns an immutable list.
+ */
+ public List<TypeId<?>> asList() {
+ return Collections.unmodifiableList(Arrays.asList(types));
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof TypeList && Arrays.equals(((TypeList) o).types, types);
+ }
+
+ @Override public int hashCode() {
+ return Arrays.hashCode(types);
+ }
+
+ @Override public String toString() {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < types.length; i++) {
+ if (i > 0) {
+ result.append(", ");
+ }
+ result.append(types[i]);
+ }
+ return result.toString();
+ }
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/UnaryOp.java b/dexmaker/src/main/java/com/google/dexmaker/UnaryOp.java
new file mode 100644
index 0000000..cf565bb
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/UnaryOp.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 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.google.dexmaker;
+
+import com.android.dx.rop.code.Rop;
+import com.android.dx.rop.code.Rops;
+
+/**
+ * An operation on one value.
+ */
+public enum UnaryOp {
+
+ /** {@code ~a}. Supports int and long. */
+ NOT() {
+ @Override Rop rop(TypeId<?> type) {
+ return Rops.opNot(type.ropType);
+ }
+ },
+
+ /** {@code -a}. Supports int, long, float and double. */
+ NEGATE() {
+ @Override Rop rop(TypeId<?> type) {
+ return Rops.opNeg(type.ropType);
+ }
+ };
+
+ abstract Rop rop(TypeId<?> type);
+}
diff --git a/dexmaker/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java b/dexmaker/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
new file mode 100644
index 0000000..aa59886
--- /dev/null
+++ b/dexmaker/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
@@ -0,0 +1,795 @@
+/*
+ * 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.google.dexmaker.stock;
+
+import com.google.dexmaker.Code;
+import com.google.dexmaker.Comparison;
+import com.google.dexmaker.DexMaker;
+import com.google.dexmaker.FieldId;
+import com.google.dexmaker.Label;
+import com.google.dexmaker.Local;
+import com.google.dexmaker.MethodId;
+import com.google.dexmaker.TypeId;
+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 static java.lang.reflect.Modifier.PRIVATE;
+import static java.lang.reflect.Modifier.PUBLIC;
+import static java.lang.reflect.Modifier.STATIC;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * 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() {
+ * &#64;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> {
+ // Version of ProxyBuilder. It should be updated if the implementation
+ // of the generated proxy class changes.
+ public static final int VERSION = 1;
+
+ private static final String FIELD_NAME_HANDLER = "$__handler";
+ private static final String FIELD_NAME_METHODS = "$__methodArray";
+
+ /**
+ * A cache of all proxy classes ever generated. At the time of writing,
+ * Android's runtime doesn't support class unloading so there's little
+ * value in using weak references.
+ */
+ private static final Map<Class<?>, Class<?>> generatedProxyClasses
+ = Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
+
+ 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 Set<Class<?>> interfaces = new HashSet<Class<?>>();
+
+ 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;
+ }
+
+ /**
+ * Sets the directory where executable code is stored. See {@link
+ * DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
+ * choosing a secure location for the dex cache.
+ */
+ public ProxyBuilder<T> dexCache(File dexCacheParent) {
+ dexCache = new File(dexCacheParent, "v" + Integer.toString(VERSION));
+ dexCache.mkdir();
+ return this;
+ }
+
+ public ProxyBuilder<T> implementing(Class<?>... interfaces) {
+ for (Class<?> i : interfaces) {
+ if (!i.isInterface()) {
+ throw new IllegalArgumentException("Not an interface: " + i.getName());
+ }
+ this.interfaces.add(i);
+ }
+ 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 IOException 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() throws IOException {
+ check(handler != null, "handler == null");
+ check(constructorArgTypes.length == constructorArgValues.length,
+ "constructorArgValues.length != constructorArgTypes.length");
+ Class<? extends T> proxyClass = buildProxyClass();
+ Constructor<? extends T> constructor;
+ try {
+ constructor = proxyClass.getConstructor(constructorArgTypes);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("No constructor for " + baseClass.getName()
+ + " with parameter types " + Arrays.toString(constructorArgTypes));
+ }
+ 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 constructor throws an exception.
+ throw launderCause(e);
+ }
+ setHandlerInstanceField(result, handler);
+ return result;
+ }
+
+ // TODO: test coverage for this
+ // TODO: documentation for this
+ public Class<? extends T> buildProxyClass() throws IOException {
+ // try the cache to see if we've generated this one before
+ @SuppressWarnings("unchecked") // we only populate the map with matching types
+ Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass);
+ if (proxyClass != null
+ && proxyClass.getClassLoader().getParent() == parentClassLoader
+ && interfaces.equals(asSet(proxyClass.getInterfaces()))) {
+ return proxyClass; // cache hit!
+ }
+
+ // the cache missed; generate the class
+ DexMaker dexMaker = new DexMaker();
+ String generatedName = getMethodNameForProxyOf(baseClass);
+ TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";");
+ TypeId<T> superType = TypeId.get(baseClass);
+ generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
+ Method[] methodsToProxy = getMethodsToProxyRecursive();
+ generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
+ dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType,
+ getInterfacesAsTypeIds());
+ ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
+ try {
+ proxyClass = loadClass(classLoader, generatedName);
+ } catch (IllegalAccessError e) {
+ // Thrown when the base class is not accessible.
+ throw new UnsupportedOperationException(
+ "cannot proxy inaccessible class " + baseClass, e);
+ } catch (ClassNotFoundException e) {
+ // Should not be thrown, we're sure to have generated this class.
+ throw new AssertionError(e);
+ }
+ setMethodsStaticField(proxyClass, methodsToProxy);
+ generatedProxyClasses.put(baseClass, proxyClass);
+ return proxyClass;
+ }
+
+ // 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);
+ }
+ }
+
+ // TODO: test coverage for isProxyClass
+
+ /**
+ * Returns true if {@code c} is a proxy class created by this builder.
+ */
+ public static boolean isProxyClass(Class<?> c) {
+ // TODO: use a marker interface instead?
+ try {
+ c.getDeclaredField(FIELD_NAME_HANDLER);
+ return true;
+ } catch (NoSuchFieldException e) {
+ return false;
+ }
+ }
+
+ private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
+ TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
+ TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
+ TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
+ FieldId<G, InvocationHandler> handlerField =
+ generatedType.getField(handlerType, FIELD_NAME_HANDLER);
+ FieldId<G, Method[]> allMethods =
+ generatedType.getField(methodArrayType, FIELD_NAME_METHODS);
+ TypeId<Method> methodType = TypeId.get(Method.class);
+ TypeId<Object[]> objectArrayType = TypeId.get(Object[].class);
+ MethodId<InvocationHandler, Object> methodInvoke = handlerType.getMethod(TypeId.OBJECT,
+ "invoke", TypeId.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();
+ TypeId<?>[] argTypes = new TypeId<?>[argClasses.length];
+ for (int i = 0; i < argTypes.length; ++i) {
+ argTypes[i] = TypeId.get(argClasses[i]);
+ }
+ Class<?> returnType = method.getReturnType();
+ TypeId<?> resultType = TypeId.get(returnType);
+ MethodId<T, ?> superMethod = superclassType.getMethod(resultType, name, argTypes);
+ MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes);
+ Code code = dexMaker.declare(methodId, PUBLIC);
+ Local<G> localThis = code.getThis(generatedType);
+ Local<InvocationHandler> localHandler = code.newLocal(handlerType);
+ Local<Object> invokeResult = code.newLocal(TypeId.OBJECT);
+ Local<Integer> intValue = code.newLocal(TypeId.INT);
+ Local<Object[]> args = code.newLocal(objectArrayType);
+ Local<Integer> argsLength = code.newLocal(TypeId.INT);
+ Local<Object> temp = code.newLocal(TypeId.OBJECT);
+ Local<?> resultHolder = code.newLocal(resultType);
+ Local<Method[]> methodArray = code.newLocal(methodArrayType);
+ Local<Method> thisMethod = code.newLocal(methodType);
+ Local<Integer> methodIndex = code.newLocal(TypeId.INT);
+ Class<?> aBoxedClass = PRIMITIVE_TO_BOXED.get(returnType);
+ Local<?> aBoxedResult = null;
+ if (aBoxedClass != null) {
+ aBoxedResult = code.newLocal(TypeId.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(thisMethod, methodArray, methodIndex);
+ code.loadConstant(argsLength, argTypes.length);
+ code.newArray(args, argsLength);
+ code.iget(handlerField, localHandler, localThis);
+
+ // if (proxy == null)
+ code.loadConstant(nullHandler, null);
+ Label handlerNullCase = new Label();
+ code.compare(Comparison.EQ, handlerNullCase, nullHandler, localHandler);
+
+ // 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(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 String super$doSomething$java_lang_String(Bar param0, int param1) {
+ * int result = super.doSomething(param0, param1);
+ * return result;
+ * }
+ */
+ // TODO: don't include a super_ method if the target is abstract!
+ MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
+ resultType, superMethodName(method), argTypes);
+ Code superCode = dexMaker.declare(callsSuperMethod, 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);
+ }
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static void invokeSuper(MethodId superMethod, Code superCode,
+ Local superThis, Local[] superArgs, Local superResult) {
+ superCode.invokeSuper(superMethod, superResult, superThis, superArgs);
+ }
+
+ private static Local<?> boxIfRequired(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 Throwable {
+ try {
+ return proxy.getClass()
+ .getMethod(superMethodName(method), method.getParameterTypes())
+ .invoke(proxy, args);
+ } catch (InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ /**
+ * The super method must include the return type, otherwise its ambiguous
+ * for methods with covariant return types.
+ */
+ private static String superMethodName(Method method) {
+ String returnType = method.getReturnType().getName();
+ return "super$" + method.getName() + "$"
+ + returnType.replace('.', '_').replace('[', '_').replace(';', '_');
+ }
+
+ private static void check(boolean condition, String message) {
+ if (!condition) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ private static <T, G extends T> void generateConstructorsAndFields(DexMaker dexMaker,
+ TypeId<G> generatedType, TypeId<T> superType, Class<T> superClass) {
+ TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
+ TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
+ FieldId<G, InvocationHandler> handlerField = generatedType.getField(
+ handlerType, FIELD_NAME_HANDLER);
+ dexMaker.declare(handlerField, PRIVATE, null);
+ FieldId<G, Method[]> allMethods = generatedType.getField(
+ methodArrayType, FIELD_NAME_METHODS);
+ dexMaker.declare(allMethods, PRIVATE | STATIC, null);
+ for (Constructor<T> constructor : getConstructorsToOverwrite(superClass)) {
+ if (constructor.getModifiers() == Modifier.FINAL) {
+ continue;
+ }
+ TypeId<?>[] types = classArrayToTypeArray(constructor.getParameterTypes());
+ MethodId<?, ?> method = generatedType.getConstructor(types);
+ Code constructorCode = dexMaker.declare(method, PUBLIC);
+ 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();
+ }
+
+ private TypeId<?>[] getInterfacesAsTypeIds() {
+ TypeId<?>[] result = new TypeId<?>[interfaces.size()];
+ int i = 0;
+ for (Class<?> implemented : interfaces) {
+ result[i++] = TypeId.get(implemented);
+ }
+ return result;
+ }
+
+ /**
+ * Gets all {@link Method} objects we can proxy in the hierarchy of the
+ * supplied class.
+ */
+ private Method[] getMethodsToProxyRecursive() {
+ Set<MethodSetEntry> methodsToProxy = new HashSet<MethodSetEntry>();
+ Set<MethodSetEntry> seenFinalMethods = new HashSet<MethodSetEntry>();
+ for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
+ getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
+ }
+ for (Class<?> c : interfaces) {
+ getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
+ }
+
+ Method[] results = new Method[methodsToProxy.size()];
+ int i = 0;
+ for (MethodSetEntry entry : methodsToProxy) {
+ results[i++] = entry.originalMethod;
+ }
+
+ // Sort the results array so that they are returned by this method
+ // in a deterministic fashion.
+ Arrays.sort(results, new Comparator<Method>() {
+ @Override
+ public int compare(Method method1, Method method2) {
+ return method1.toString().compareTo(method2.toString());
+ }
+ });
+
+ return results;
+ }
+
+ private void getMethodsToProxy(Set<MethodSetEntry> sink, Set<MethodSetEntry> seenFinalMethods,
+ Class<?> c) {
+ for (Method method : c.getDeclaredMethods()) {
+ if ((method.getModifiers() & Modifier.FINAL) != 0) {
+ // Skip final methods, we can't override them. We
+ // also need to remember them, in case the same
+ // method exists in a parent class.
+ MethodSetEntry entry = new MethodSetEntry(method);
+ seenFinalMethods.add(entry);
+ // We may have seen this method already, from an interface
+ // implemented by a child class. We need to remove it here.
+ sink.remove(entry);
+ continue;
+ }
+ if ((method.getModifiers() & 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;
+ }
+ MethodSetEntry entry = new MethodSetEntry(method);
+ if (seenFinalMethods.contains(entry)) {
+ // This method is final in a child class.
+ // We can't override it.
+ continue;
+ }
+ sink.add(entry);
+ }
+
+ for (Class<?> i : c.getInterfaces()) {
+ getMethodsToProxy(sink, seenFinalMethods, i);
+ }
+ }
+
+ private static <T> String getMethodNameForProxyOf(Class<T> clazz) {
+ return clazz.getSimpleName() + "_Proxy";
+ }
+
+ private static TypeId<?>[] classArrayToTypeArray(Class<?>[] input) {
+ TypeId<?>[] result = new TypeId[input.length];
+ for (int i = 0; i < input.length; ++i) {
+ result[i] = TypeId.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.cast(aBoxedResult, localForResultOfInvoke);
+ MethodId unboxingMethodFor = getUnboxMethodForPrimitive(methodReturnType);
+ code.invokeVirtual(unboxingMethodFor, localOfMethodReturnType, aBoxedResult);
+ code.returnValue(localOfMethodReturnType);
+ } else if (void.class.equals(methodReturnType)) {
+ code.returnVoid();
+ } else {
+ code.cast(localOfMethodReturnType, localForResultOfInvoke);
+ code.returnValue(localOfMethodReturnType);
+ }
+ }
+
+ private static <T> Set<T> asSet(T... array) {
+ return new CopyOnWriteArraySet<T>(Arrays.asList(array));
+ }
+
+ 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<TypeId<?>, MethodId<?, ?>> PRIMITIVE_TYPE_TO_UNBOX_METHOD;
+ static {
+ PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<TypeId<?>, MethodId<?, ?>>();
+ for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_TO_BOXED.entrySet()) {
+ TypeId<?> primitiveType = TypeId.get(entry.getKey());
+ TypeId<?> boxedType = TypeId.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, TypeId.get(Boolean.class).getMethod(TypeId.BOOLEAN, "booleanValue"));
+ map.put(int.class, TypeId.get(Integer.class).getMethod(TypeId.INT, "intValue"));
+ map.put(byte.class, TypeId.get(Byte.class).getMethod(TypeId.BYTE, "byteValue"));
+ map.put(long.class, TypeId.get(Long.class).getMethod(TypeId.LONG, "longValue"));
+ map.put(short.class, TypeId.get(Short.class).getMethod(TypeId.SHORT, "shortValue"));
+ map.put(float.class, TypeId.get(Float.class).getMethod(TypeId.FLOAT, "floatValue"));
+ map.put(double.class, TypeId.get(Double.class).getMethod(TypeId.DOUBLE, "doubleValue"));
+ map.put(char.class, TypeId.get(Character.class).getMethod(TypeId.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;
+ }
+ }
+}
diff --git a/dexmaker/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java b/dexmaker/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
new file mode 100644
index 0000000..b638509
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2012 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.google.dexmaker;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+public final class AppDataDirGuesserTest extends TestCase {
+ public void testGuessCacheDir_SimpleExample() {
+ guessCacheDirFor("/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+ guessCacheDirFor("/data/app/a.b.c.tests.apk").shouldGive("/data/data/a.b.c.tests/cache");
+ }
+
+ public void testGuessCacheDir_MultipleResultsSeparatedByColon() {
+ guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+ .shouldGive("/data/data/a.b.c/cache", "/data/data/d.e.f/cache");
+ }
+
+ public void testGuessCacheDir_NotWriteableSkipped() {
+ guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+ .withNonWriteable("/data/data/a.b.c/cache")
+ .shouldGive("/data/data/d.e.f/cache");
+ }
+
+ public void testGuessCacheDir_StripHyphenatedSuffixes() {
+ guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache");
+ }
+
+ public void testGuessCacheDir_LeadingAndTrailingColonsIgnored() {
+ guessCacheDirFor("/data/app/a.b.c.apk:asdf:").shouldGive("/data/data/a.b.c/cache");
+ guessCacheDirFor(":asdf:/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+ }
+
+ public void testGuessCacheDir_InvalidInputsGiveEmptyArray() {
+ guessCacheDirFor("").shouldGive();
+ }
+
+ public void testGuessCacheDir_JarsIgnored() {
+ guessCacheDirFor("/data/app/a.b.c.jar").shouldGive();
+ guessCacheDirFor("/system/framework/android.test.runner.jar").shouldGive();
+ }
+
+ public void testGuessCacheDir_RealWorldExample() {
+ String realPath = "/system/framework/android.test.runner.jar:" +
+ "/data/app/com.google.android.voicesearch.tests-2.apk:" +
+ "/data/app/com.google.android.voicesearch-1.apk";
+ guessCacheDirFor(realPath)
+ .withNonWriteable("/data/data/com.google.android.voicesearch.tests/cache")
+ .shouldGive("/data/data/com.google.android.voicesearch/cache");
+ }
+
+ public void testSplitPathList() {
+ final String[] expected = { "foo", "bar" };
+ assertTrue(Arrays.equals(expected, AppDataDirGuesser.splitPathList("foo:bar")));
+ assertTrue(Arrays.equals(expected,
+ AppDataDirGuesser.splitPathList("dexPath=foo:bar")));
+ assertTrue(Arrays.equals(expected,
+ AppDataDirGuesser.splitPathList("dexPath=foo:bar,bazPath=bar:bar2")));
+ }
+
+ public void testPre43PathProcessing() {
+ String input = "dalvik.system.PathClassLoader[dexPath=/data/app/abc-1.apk," +
+ "libraryPath=/data/app-lib/abc-1]";
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ assertTrue("dexPath=/data/app/abc-1.apk,libraryPath=/data/app-lib/abc-1".equals(processed));
+ }
+
+ public void test43PathProcessing() {
+ String input = "dalvik.system.PathClassLoader[DexPathList[[zip file " +
+ "\"/data/app/abc-1/base.apk\", zip file \"/data/app/def-1/base.apk\"], " +
+ "nativeLibraryDirectories=[/data/app-lib/abc-1]]]";
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ assertTrue("/data/app/abc-1/base.apk:/data/app/def-1/base.apk".equals(processed));
+ }
+
+ // Try to find the SDK level of the device.
+ private int getSDKLevel() {
+ // Maybe the version is reflected into the system properties correctly.
+ String level = System.getProperty("ro.build.version.sdk");
+ try {
+ return Integer.parseInt(level);
+ } catch (Exception ignored) {
+ }
+
+ // Run getprop and parse the result.
+ try {
+ Process p = Runtime.getRuntime().exec("/system/bin/getprop ro.build.version.sdk");
+ int exitValue = p.waitFor();
+ if (exitValue == 0) {
+ String line =
+ new BufferedReader(new InputStreamReader(p.getInputStream())).readLine();
+ if (line != null) {
+ return Integer.parseInt(line);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+
+ // It would be nice to access android.os.Build.SDK_INT. However, that bottoms out in some
+ // native code reading system properties. Try to load the library and *hope* that the
+ // methods don't need registration code. Note: this will likely fail.
+ try {
+ // Need to load android_runtime.
+ System.loadLibrary("android_runtime");
+ Class<?> buildClass = Class.forName("android.os.Build");
+ java.lang.reflect.Field field = buildClass.getDeclaredField("SDK_INT");
+ return field.getInt(null);
+ } catch (Throwable exc) {
+ // This is already the fallback of the fallback, so throw an unchecked exception.
+ throw new RuntimeException(exc);
+ }
+ }
+
+ public void testApiLevel17PlusPathProcessing() {
+ int level = getSDKLevel();
+ if (level >= 17) {
+ // Our processing should work for anything >= Android 4.2.
+ String input = getClass().getClassLoader().toString();
+ String processed = AppDataDirGuesser.processClassLoaderString(input);
+ // A tighter check would be interesting. But vogar doesn't run the tests in a directory
+ // recognized by the guesser (usually under /data/local/tmp), so we cannot use the
+ // processed result as input to guessPath.
+ assertTrue(!input.equals(processed));
+ }
+ }
+
+ private interface TestCondition {
+ TestCondition withNonWriteable(String... files);
+ void shouldGive(String... files);
+ }
+
+ private TestCondition guessCacheDirFor(final String path) {
+ final Set<String> notWriteable = new HashSet<String>();
+ return new TestCondition() {
+ public void shouldGive(String... files) {
+ AppDataDirGuesser guesser = new AppDataDirGuesser() {
+ @Override
+ public boolean isWriteableDirectory(File file) {
+ return !notWriteable.contains(file.getAbsolutePath());
+ }
+ @Override
+ boolean fileOrDirExists(File file) {
+ return true;
+ }
+ };
+ File[] results = guesser.guessPath(path);
+ assertNotNull("Null results for " + path, results);
+ assertEquals("Bad lengths for " + path, files.length, results.length);
+ for (int i = 0; i < files.length; ++i) {
+ assertEquals("Element " + i, new File(files[i]), results[i]);
+ }
+ }
+
+ public TestCondition withNonWriteable(String... files) {
+ notWriteable.addAll(Arrays.asList(files));
+ return this;
+ }
+ };
+ }
+}
diff --git a/dexmaker/src/test/java/com/google/dexmaker/DexMakerTest.java b/dexmaker/src/test/java/com/google/dexmaker/DexMakerTest.java
new file mode 100644
index 0000000..2b7a27b
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/DexMakerTest.java
@@ -0,0 +1,2020 @@
+/*
+ * 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.google.dexmaker;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import static java.lang.reflect.Modifier.ABSTRACT;
+import static java.lang.reflect.Modifier.FINAL;
+import static java.lang.reflect.Modifier.NATIVE;
+import static java.lang.reflect.Modifier.PRIVATE;
+import static java.lang.reflect.Modifier.PROTECTED;
+import static java.lang.reflect.Modifier.PUBLIC;
+import static java.lang.reflect.Modifier.STATIC;
+import static java.lang.reflect.Modifier.SYNCHRONIZED;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import junit.framework.TestCase;
+
+/**
+ * This generates a class named 'Generated' with one or more generated methods
+ * and fields. In loads the generated class into the current VM and uses
+ * reflection to invoke its methods.
+ *
+ * <p>This test must run on a Dalvik VM.
+ */
+public final class DexMakerTest extends TestCase {
+ private DexMaker dexMaker;
+ private static TypeId<DexMakerTest> TEST_TYPE = TypeId.get(DexMakerTest.class);
+ private static TypeId<?> INT_ARRAY = TypeId.get(int[].class);
+ private static TypeId<boolean[]> BOOLEAN_ARRAY = TypeId.get(boolean[].class);
+ private static TypeId<long[]> LONG_ARRAY = TypeId.get(long[].class);
+ private static TypeId<Object[]> OBJECT_ARRAY = TypeId.get(Object[].class);
+ private static TypeId<long[][]> LONG_2D_ARRAY = TypeId.get(long[][].class);
+ private static TypeId<?> GENERATED = TypeId.get("LGenerated;");
+ private static TypeId<Callable> CALLABLE = TypeId.get(Callable.class);
+ private static MethodId<Callable, Object> CALL = CALLABLE.getMethod(TypeId.OBJECT, "call");
+
+ @Override protected void setUp() throws Exception {
+ super.setUp();
+ reset();
+ }
+
+ /**
+ * The generator is mutable. Calling reset creates a new empty generator.
+ * This is necessary to generate multiple classes in the same test method.
+ */
+ private void reset() {
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ clearDataDirectory();
+ }
+
+ private void clearDataDirectory() {
+ for (File f : getDataDirectory().listFiles()) {
+ if (f.getName().endsWith(".jar") || f.getName().endsWith(".dex")) {
+ f.delete();
+ }
+ }
+ }
+
+ public void testNewInstance() throws Exception {
+ /*
+ * public static Constructable call(long a, boolean b) {
+ * Constructable result = new Constructable(a, b);
+ * return result;
+ * }
+ */
+ TypeId<Constructable> constructable = TypeId.get(Constructable.class);
+ MethodId<?, Constructable> methodId = GENERATED.getMethod(
+ constructable, "call", TypeId.LONG, TypeId.BOOLEAN);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Long> localA = code.getParameter(0, TypeId.LONG);
+ Local<Boolean> localB = code.getParameter(1, TypeId.BOOLEAN);
+ MethodId<Constructable, Void> constructor
+ = constructable.getConstructor(TypeId.LONG, TypeId.BOOLEAN);
+ Local<Constructable> localResult = code.newLocal(constructable);
+ code.newInstance(localResult, constructor, localA, localB);
+ code.returnValue(localResult);
+
+ Constructable constructed = (Constructable) getMethod().invoke(null, 5L, false);
+ assertEquals(5L, constructed.a);
+ assertEquals(false, constructed.b);
+ }
+
+ public static class Constructable {
+ private final long a;
+ private final boolean b;
+ public Constructable(long a, boolean b) {
+ this.a = a;
+ this.b = b;
+ }
+ }
+
+ public void testVoidNoArgMemberMethod() throws Exception {
+ /*
+ * public void call() {
+ * }
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ Code code = dexMaker.declare(methodId, PUBLIC);
+ code.returnVoid();
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("call");
+ method.invoke(instance);
+ }
+
+ public void testInvokeStatic() throws Exception {
+ /*
+ * public static int call(int a) {
+ * int result = DexMakerTest.staticMethod(a);
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localA = code.getParameter(0, TypeId.INT);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ MethodId<?, Integer> staticMethod
+ = TEST_TYPE.getMethod(TypeId.INT, "staticMethod", TypeId.INT);
+ code.invokeStatic(staticMethod, localResult, localA);
+ code.returnValue(localResult);
+
+ assertEquals(10, getMethod().invoke(null, 4));
+ }
+
+ public void testCreateLocalMethodAsNull() throws Exception {
+ /*
+ * public void call(int value) {
+ * Method method = null;
+ * }
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call", TypeId.INT);
+ TypeId<Method> methodType = TypeId.get(Method.class);
+ Code code = dexMaker.declare(methodId, PUBLIC);
+ Local<Method> localMethod = code.newLocal(methodType);
+ code.loadConstant(localMethod, null);
+ code.returnVoid();
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("call", int.class);
+ method.invoke(instance, 0);
+ }
+
+ @SuppressWarnings("unused") // called by generated code
+ public static int staticMethod(int a) {
+ return a + 6;
+ }
+
+ public void testInvokeVirtual() throws Exception {
+ /*
+ * public static int call(DexMakerTest test, int a) {
+ * int result = test.virtualMethod(a);
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TEST_TYPE, TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<DexMakerTest> localInstance = code.getParameter(0, TEST_TYPE);
+ Local<Integer> localA = code.getParameter(1, TypeId.INT);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ MethodId<DexMakerTest, Integer> virtualMethod
+ = TEST_TYPE.getMethod(TypeId.INT, "virtualMethod", TypeId.INT);
+ code.invokeVirtual(virtualMethod, localResult, localInstance, localA);
+ code.returnValue(localResult);
+
+ assertEquals(9, getMethod().invoke(null, this, 4));
+ }
+
+ @SuppressWarnings("unused") // called by generated code
+ public int virtualMethod(int a) {
+ return a + 5;
+ }
+
+ public <G> void testInvokeDirect() throws Exception {
+ /*
+ * private int directMethod() {
+ * int a = 5;
+ * return a;
+ * }
+ *
+ * public static int call(Generated g) {
+ * int b = g.directMethod();
+ * return b;
+ * }
+ */
+ TypeId<G> generated = TypeId.get("LGenerated;");
+ MethodId<G, Integer> directMethodId = generated.getMethod(TypeId.INT, "directMethod");
+ Code directCode = dexMaker.declare(directMethodId, PRIVATE);
+ directCode.getThis(generated); // 'this' is unused
+ Local<Integer> localA = directCode.newLocal(TypeId.INT);
+ directCode.loadConstant(localA, 5);
+ directCode.returnValue(localA);
+
+ MethodId<G, Integer> methodId = generated.getMethod(TypeId.INT, "call", generated);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localB = code.newLocal(TypeId.INT);
+ Local<G> localG = code.getParameter(0, generated);
+ code.invokeDirect(directMethodId, localB, localG);
+ code.returnValue(localB);
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("call", generatedClass);
+ assertEquals(5, method.invoke(null, instance));
+ }
+
+ public <G> void testInvokeSuper() throws Exception {
+ /*
+ * public int superHashCode() {
+ * int result = super.hashCode();
+ * return result;
+ * }
+ * public int hashCode() {
+ * return 0;
+ * }
+ */
+ TypeId<G> generated = TypeId.get("LGenerated;");
+ MethodId<Object, Integer> objectHashCode = TypeId.OBJECT.getMethod(TypeId.INT, "hashCode");
+ Code superHashCode = dexMaker.declare(
+ GENERATED.getMethod(TypeId.INT, "superHashCode"), PUBLIC);
+ Local<Integer> localResult = superHashCode.newLocal(TypeId.INT);
+ Local<G> localThis = superHashCode.getThis(generated);
+ superHashCode.invokeSuper(objectHashCode, localResult, localThis);
+ superHashCode.returnValue(localResult);
+
+ Code generatedHashCode = dexMaker.declare(
+ GENERATED.getMethod(TypeId.INT, "hashCode"), PUBLIC);
+ Local<Integer> localZero = generatedHashCode.newLocal(TypeId.INT);
+ generatedHashCode.loadConstant(localZero, 0);
+ generatedHashCode.returnValue(localZero);
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("superHashCode");
+ assertEquals(System.identityHashCode(instance), method.invoke(instance));
+ }
+
+ public void testInvokeInterface() throws Exception {
+ /*
+ * public static Object call(Callable c) {
+ * Object result = c.call();
+ * return result;
+ * }
+ */
+ MethodId<?, Object> methodId = GENERATED.getMethod(TypeId.OBJECT, "call", CALLABLE);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Callable> localC = code.getParameter(0, CALLABLE);
+ Local<Object> localResult = code.newLocal(TypeId.OBJECT);
+ code.invokeInterface(CALL, localResult, localC);
+ code.returnValue(localResult);
+
+ Callable<Object> callable = new Callable<Object>() {
+ public Object call() throws Exception {
+ return "abc";
+ }
+ };
+ assertEquals("abc", getMethod().invoke(null, callable));
+ }
+
+ public void testInvokeVoidMethodIgnoresTargetLocal() throws Exception {
+ /*
+ * public static int call() {
+ * int result = 5;
+ * DexMakerTest.voidMethod();
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call");
+ MethodId<?, Void> voidMethod = TEST_TYPE.getMethod(TypeId.VOID, "voidMethod");
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> result = code.newLocal(TypeId.INT);
+ code.loadConstant(result, 5);
+ code.invokeStatic(voidMethod, null);
+ code.returnValue(result);
+
+ assertEquals(5, getMethod().invoke(null));
+ }
+
+ @SuppressWarnings("unused") // called by generated code
+ public static void voidMethod() {
+ }
+
+ public void testParameterMismatch() throws Exception {
+ TypeId<?>[] argTypes = {
+ TypeId.get(Integer.class), // should fail because the code specifies int
+ TypeId.OBJECT,
+ };
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", argTypes);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ try {
+ code.getParameter(0, TypeId.INT);
+ } catch (IllegalArgumentException e) {
+ }
+ try {
+ code.getParameter(2, TypeId.INT);
+ } catch (IndexOutOfBoundsException e) {
+ }
+ }
+
+ public void testInvokeTypeSafety() throws Exception {
+ /*
+ * public static boolean call(DexMakerTest test) {
+ * CharSequence cs = test.toString();
+ * boolean result = cs.equals(test);
+ * return result;
+ * }
+ */
+ MethodId<?, Boolean> methodId = GENERATED.getMethod(TypeId.BOOLEAN, "call", TEST_TYPE);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<DexMakerTest> localTest = code.getParameter(0, TEST_TYPE);
+ TypeId<CharSequence> charSequenceType = TypeId.get(CharSequence.class);
+ MethodId<Object, String> objectToString
+ = TypeId.OBJECT.getMethod(TypeId.STRING, "toString");
+ MethodId<Object, Boolean> objectEquals
+ = TypeId.OBJECT.getMethod(TypeId.BOOLEAN, "equals", TypeId.OBJECT);
+ Local<CharSequence> localCs = code.newLocal(charSequenceType);
+ Local<Boolean> localResult = code.newLocal(TypeId.BOOLEAN);
+ code.invokeVirtual(objectToString, localCs, localTest);
+ code.invokeVirtual(objectEquals, localResult, localCs, localTest);
+ code.returnValue(localResult);
+
+ assertEquals(false, getMethod().invoke(null, this));
+ }
+
+ public void testReturnTypeMismatch() {
+ MethodId<?, String> methodId = GENERATED.getMethod(TypeId.STRING, "call");
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ try {
+ code.returnValue(code.newLocal(TypeId.BOOLEAN));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ code.returnVoid();
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testDeclareStaticFields() throws Exception {
+ /*
+ * class Generated {
+ * public static int a;
+ * protected static Object b;
+ * }
+ */
+ dexMaker.declare(GENERATED.getField(TypeId.INT, "a"), PUBLIC | STATIC, 3);
+ dexMaker.declare(GENERATED.getField(TypeId.OBJECT, "b"), PROTECTED | STATIC, null);
+ Class<?> generatedClass = generateAndLoad();
+
+ Field a = generatedClass.getField("a");
+ assertEquals(int.class, a.getType());
+ assertEquals(3, a.get(null));
+
+ Field b = generatedClass.getDeclaredField("b");
+ assertEquals(Object.class, b.getType());
+ b.setAccessible(true);
+ assertEquals(null, b.get(null));
+ }
+
+ public void testDeclareInstanceFields() throws Exception {
+ /*
+ * class Generated {
+ * public int a;
+ * protected Object b;
+ * }
+ */
+ dexMaker.declare(GENERATED.getField(TypeId.INT, "a"), PUBLIC, null);
+ dexMaker.declare(GENERATED.getField(TypeId.OBJECT, "b"), PROTECTED, null);
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+
+ Field a = generatedClass.getField("a");
+ assertEquals(int.class, a.getType());
+ assertEquals(0, a.get(instance));
+
+ Field b = generatedClass.getDeclaredField("b");
+ assertEquals(Object.class, b.getType());
+ b.setAccessible(true);
+ assertEquals(null, b.get(instance));
+ }
+
+ /**
+ * Declare a constructor that takes an int parameter and assigns it to a
+ * field.
+ */
+ public <G> void testDeclareConstructor() throws Exception {
+ /*
+ * class Generated {
+ * public final int a;
+ * public Generated(int a) {
+ * this.a = a;
+ * }
+ * }
+ */
+ TypeId<G> generated = TypeId.get("LGenerated;");
+ FieldId<G, Integer> fieldId = generated.getField(TypeId.INT, "a");
+ dexMaker.declare(fieldId, PUBLIC | FINAL, null);
+ MethodId<?, Void> constructor = GENERATED.getConstructor(TypeId.INT);
+ Code code = dexMaker.declare(constructor, PUBLIC);
+ Local<G> thisRef = code.getThis(generated);
+ Local<Integer> parameter = code.getParameter(0, TypeId.INT);
+ code.invokeDirect(TypeId.OBJECT.getConstructor(), null, thisRef);
+ code.iput(fieldId, thisRef, parameter);
+ code.returnVoid();
+
+ Class<?> generatedClass = generateAndLoad();
+ Field a = generatedClass.getField("a");
+ Object instance = generatedClass.getConstructor(int.class).newInstance(0xabcd);
+ assertEquals(0xabcd, a.get(instance));
+ }
+
+ public void testReturnType() throws Exception {
+ testReturnType(boolean.class, true);
+ testReturnType(byte.class, (byte) 5);
+ testReturnType(char.class, 'E');
+ testReturnType(double.class, 5.0);
+ testReturnType(float.class, 5.0f);
+ testReturnType(int.class, 5);
+ testReturnType(long.class, 5L);
+ testReturnType(short.class, (short) 5);
+ testReturnType(void.class, null);
+ testReturnType(String.class, "foo");
+ testReturnType(Class.class, List.class);
+ }
+
+ private <T> void testReturnType(Class<T> javaType, T value) throws Exception {
+ /*
+ * public int call() {
+ * int a = 5;
+ * return a;
+ * }
+ */
+ reset();
+ TypeId<T> returnType = TypeId.get(javaType);
+ Code code = dexMaker.declare(GENERATED.getMethod(returnType, "call"), PUBLIC | STATIC);
+ if (value != null) {
+ Local<T> i = code.newLocal(returnType);
+ code.loadConstant(i, value);
+ code.returnValue(i);
+ } else {
+ code.returnVoid();
+ }
+
+ Class<?> generatedClass = generateAndLoad();
+ Method method = generatedClass.getMethod("call");
+ assertEquals(javaType, method.getReturnType());
+ assertEquals(value, method.invoke(null));
+ }
+
+ public void testBranching() throws Exception {
+ Method lt = branchingMethod(Comparison.LT);
+ assertEquals(Boolean.TRUE, lt.invoke(null, 1, 2));
+ assertEquals(Boolean.FALSE, lt.invoke(null, 1, 1));
+ assertEquals(Boolean.FALSE, lt.invoke(null, 2, 1));
+
+ Method le = branchingMethod(Comparison.LE);
+ assertEquals(Boolean.TRUE, le.invoke(null, 1, 2));
+ assertEquals(Boolean.TRUE, le.invoke(null, 1, 1));
+ assertEquals(Boolean.FALSE, le.invoke(null, 2, 1));
+
+ Method eq = branchingMethod(Comparison.EQ);
+ assertEquals(Boolean.FALSE, eq.invoke(null, 1, 2));
+ assertEquals(Boolean.TRUE, eq.invoke(null, 1, 1));
+ assertEquals(Boolean.FALSE, eq.invoke(null, 2, 1));
+
+ Method ge = branchingMethod(Comparison.GE);
+ assertEquals(Boolean.FALSE, ge.invoke(null, 1, 2));
+ assertEquals(Boolean.TRUE, ge.invoke(null, 1, 1));
+ assertEquals(Boolean.TRUE, ge.invoke(null, 2, 1));
+
+ Method gt = branchingMethod(Comparison.GT);
+ assertEquals(Boolean.FALSE, gt.invoke(null, 1, 2));
+ assertEquals(Boolean.FALSE, gt.invoke(null, 1, 1));
+ assertEquals(Boolean.TRUE, gt.invoke(null, 2, 1));
+
+ Method ne = branchingMethod(Comparison.NE);
+ assertEquals(Boolean.TRUE, ne.invoke(null, 1, 2));
+ assertEquals(Boolean.FALSE, ne.invoke(null, 1, 1));
+ assertEquals(Boolean.TRUE, ne.invoke(null, 2, 1));
+ }
+
+ private Method branchingMethod(Comparison comparison) throws Exception {
+ /*
+ * public static boolean call(int localA, int localB) {
+ * if (a comparison b) {
+ * return true;
+ * }
+ * return false;
+ * }
+ */
+ reset();
+ MethodId<?, Boolean> methodId = GENERATED.getMethod(
+ TypeId.BOOLEAN, "call", TypeId.INT, TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localA = code.getParameter(0, TypeId.INT);
+ Local<Integer> localB = code.getParameter(1, TypeId.INT);
+ Local<Boolean> result = code.newLocal(TypeId.get(boolean.class));
+ Label afterIf = new Label();
+ Label ifBody = new Label();
+ code.compare(comparison, ifBody, localA, localB);
+ code.jump(afterIf);
+
+ code.mark(ifBody);
+ code.loadConstant(result, true);
+ code.returnValue(result);
+
+ code.mark(afterIf);
+ code.loadConstant(result, false);
+ code.returnValue(result);
+ return getMethod();
+ }
+
+ public void testCastIntegerToInteger() throws Exception {
+ Method intToLong = numericCastingMethod(int.class, long.class);
+ assertEquals(0x0000000000000000L, intToLong.invoke(null, 0x00000000));
+ assertEquals(0x000000007fffffffL, intToLong.invoke(null, 0x7fffffff));
+ assertEquals(0xffffffff80000000L, intToLong.invoke(null, 0x80000000));
+ assertEquals(0xffffffffffffffffL, intToLong.invoke(null, 0xffffffff));
+
+ Method longToInt = numericCastingMethod(long.class, int.class);
+ assertEquals(0x1234abcd, longToInt.invoke(null, 0x000000001234abcdL));
+ assertEquals(0x1234abcd, longToInt.invoke(null, 0x123456781234abcdL));
+ assertEquals(0x1234abcd, longToInt.invoke(null, 0xffffffff1234abcdL));
+
+ Method intToShort = numericCastingMethod(int.class, short.class);
+ assertEquals((short) 0x1234, intToShort.invoke(null, 0x00001234));
+ assertEquals((short) 0x1234, intToShort.invoke(null, 0xabcd1234));
+ assertEquals((short) 0x1234, intToShort.invoke(null, 0xffff1234));
+
+ Method intToChar = numericCastingMethod(int.class, char.class);
+ assertEquals((char) 0x1234, intToChar.invoke(null, 0x00001234));
+ assertEquals((char) 0x1234, intToChar.invoke(null, 0xabcd1234));
+ assertEquals((char) 0x1234, intToChar.invoke(null, 0xffff1234));
+
+ Method intToByte = numericCastingMethod(int.class, byte.class);
+ assertEquals((byte) 0x34, intToByte.invoke(null, 0x00000034));
+ assertEquals((byte) 0x34, intToByte.invoke(null, 0xabcd1234));
+ assertEquals((byte) 0x34, intToByte.invoke(null, 0xffffff34));
+ }
+
+ public void testCastIntegerToFloatingPoint() throws Exception {
+ Method intToFloat = numericCastingMethod(int.class, float.class);
+ assertEquals(0.0f, intToFloat.invoke(null, 0));
+ assertEquals(-1.0f, intToFloat.invoke(null, -1));
+ assertEquals(16777216f, intToFloat.invoke(null, 16777216));
+ assertEquals(16777216f, intToFloat.invoke(null, 16777217)); // precision
+
+ Method intToDouble = numericCastingMethod(int.class, double.class);
+ assertEquals(0.0, intToDouble.invoke(null, 0));
+ assertEquals(-1.0, intToDouble.invoke(null, -1));
+ assertEquals(16777216.0, intToDouble.invoke(null, 16777216));
+ assertEquals(16777217.0, intToDouble.invoke(null, 16777217));
+
+ Method longToFloat = numericCastingMethod(long.class, float.class);
+ assertEquals(0.0f, longToFloat.invoke(null, 0L));
+ assertEquals(-1.0f, longToFloat.invoke(null, -1L));
+ assertEquals(16777216f, longToFloat.invoke(null, 16777216L));
+ assertEquals(16777216f, longToFloat.invoke(null, 16777217L));
+
+ Method longToDouble = numericCastingMethod(long.class, double.class);
+ assertEquals(0.0, longToDouble.invoke(null, 0L));
+ assertEquals(-1.0, longToDouble.invoke(null, -1L));
+ assertEquals(9007199254740992.0, longToDouble.invoke(null, 9007199254740992L));
+ assertEquals(9007199254740992.0, longToDouble.invoke(null, 9007199254740993L)); // precision
+ }
+
+ public void testCastFloatingPointToInteger() throws Exception {
+ Method floatToInt = numericCastingMethod(float.class, int.class);
+ assertEquals(0, floatToInt.invoke(null, 0.0f));
+ assertEquals(-1, floatToInt.invoke(null, -1.0f));
+ assertEquals(Integer.MAX_VALUE, floatToInt.invoke(null, 10e15f));
+ assertEquals(0, floatToInt.invoke(null, 0.5f));
+ assertEquals(Integer.MIN_VALUE, floatToInt.invoke(null, Float.NEGATIVE_INFINITY));
+ assertEquals(0, floatToInt.invoke(null, Float.NaN));
+
+ Method floatToLong = numericCastingMethod(float.class, long.class);
+ assertEquals(0L, floatToLong.invoke(null, 0.0f));
+ assertEquals(-1L, floatToLong.invoke(null, -1.0f));
+ assertEquals(10000000272564224L, floatToLong.invoke(null, 10e15f));
+ assertEquals(0L, floatToLong.invoke(null, 0.5f));
+ assertEquals(Long.MIN_VALUE, floatToLong.invoke(null, Float.NEGATIVE_INFINITY));
+ assertEquals(0L, floatToLong.invoke(null, Float.NaN));
+
+ Method doubleToInt = numericCastingMethod(double.class, int.class);
+ assertEquals(0, doubleToInt.invoke(null, 0.0));
+ assertEquals(-1, doubleToInt.invoke(null, -1.0));
+ assertEquals(Integer.MAX_VALUE, doubleToInt.invoke(null, 10e15));
+ assertEquals(0, doubleToInt.invoke(null, 0.5));
+ assertEquals(Integer.MIN_VALUE, doubleToInt.invoke(null, Double.NEGATIVE_INFINITY));
+ assertEquals(0, doubleToInt.invoke(null, Double.NaN));
+
+ Method doubleToLong = numericCastingMethod(double.class, long.class);
+ assertEquals(0L, doubleToLong.invoke(null, 0.0));
+ assertEquals(-1L, doubleToLong.invoke(null, -1.0));
+ assertEquals(10000000000000000L, doubleToLong.invoke(null, 10e15));
+ assertEquals(0L, doubleToLong.invoke(null, 0.5));
+ assertEquals(Long.MIN_VALUE, doubleToLong.invoke(null, Double.NEGATIVE_INFINITY));
+ assertEquals(0L, doubleToLong.invoke(null, Double.NaN));
+ }
+
+ public void testCastFloatingPointToFloatingPoint() throws Exception {
+ Method floatToDouble = numericCastingMethod(float.class, double.class);
+ assertEquals(0.0, floatToDouble.invoke(null, 0.0f));
+ assertEquals(-1.0, floatToDouble.invoke(null, -1.0f));
+ assertEquals(0.5, floatToDouble.invoke(null, 0.5f));
+ assertEquals(Double.NEGATIVE_INFINITY, floatToDouble.invoke(null, Float.NEGATIVE_INFINITY));
+ assertEquals(Double.NaN, floatToDouble.invoke(null, Float.NaN));
+
+ Method doubleToFloat = numericCastingMethod(double.class, float.class);
+ assertEquals(0.0f, doubleToFloat.invoke(null, 0.0));
+ assertEquals(-1.0f, doubleToFloat.invoke(null, -1.0));
+ assertEquals(0.5f, doubleToFloat.invoke(null, 0.5));
+ assertEquals(Float.NEGATIVE_INFINITY, doubleToFloat.invoke(null, Double.NEGATIVE_INFINITY));
+ assertEquals(Float.NaN, doubleToFloat.invoke(null, Double.NaN));
+ }
+
+ private Method numericCastingMethod(Class<?> source, Class<?> target)
+ throws Exception {
+ /*
+ * public static short call(int source) {
+ * short casted = (short) source;
+ * return casted;
+ * }
+ */
+ reset();
+ TypeId<?> sourceType = TypeId.get(source);
+ TypeId<?> targetType = TypeId.get(target);
+ MethodId<?, ?> methodId = GENERATED.getMethod(targetType, "call", sourceType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<?> localSource = code.getParameter(0, sourceType);
+ Local<?> localCasted = code.newLocal(targetType);
+ code.cast(localCasted, localSource);
+ code.returnValue(localCasted);
+ return getMethod();
+ }
+
+ public void testNot() throws Exception {
+ Method notInteger = notMethod(int.class);
+ assertEquals(0xffffffff, notInteger.invoke(null, 0x00000000));
+ assertEquals(0x00000000, notInteger.invoke(null, 0xffffffff));
+ assertEquals(0xedcba987, notInteger.invoke(null, 0x12345678));
+
+ Method notLong = notMethod(long.class);
+ assertEquals(0xffffffffffffffffL, notLong.invoke(null, 0x0000000000000000L));
+ assertEquals(0x0000000000000000L, notLong.invoke(null, 0xffffffffffffffffL));
+ assertEquals(0x98765432edcba987L, notLong.invoke(null, 0x6789abcd12345678L));
+ }
+
+ private <T> Method notMethod(Class<T> source) throws Exception {
+ /*
+ * public static short call(int source) {
+ * source = ~source;
+ * return not;
+ * }
+ */
+ reset();
+ TypeId<T> valueType = TypeId.get(source);
+ MethodId<?, T> methodId = GENERATED.getMethod(valueType, "call", valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<T> localSource = code.getParameter(0, valueType);
+ code.op(UnaryOp.NOT, localSource, localSource);
+ code.returnValue(localSource);
+ return getMethod();
+ }
+
+ public void testNegate() throws Exception {
+ Method negateInteger = negateMethod(int.class);
+ assertEquals(0, negateInteger.invoke(null, 0));
+ assertEquals(-1, negateInteger.invoke(null, 1));
+ assertEquals(Integer.MIN_VALUE, negateInteger.invoke(null, Integer.MIN_VALUE));
+
+ Method negateLong = negateMethod(long.class);
+ assertEquals(0L, negateLong.invoke(null, 0));
+ assertEquals(-1L, negateLong.invoke(null, 1));
+ assertEquals(Long.MIN_VALUE, negateLong.invoke(null, Long.MIN_VALUE));
+
+ Method negateFloat = negateMethod(float.class);
+ assertEquals(-0.0f, negateFloat.invoke(null, 0.0f));
+ assertEquals(-1.0f, negateFloat.invoke(null, 1.0f));
+ assertEquals(Float.NaN, negateFloat.invoke(null, Float.NaN));
+ assertEquals(Float.POSITIVE_INFINITY, negateFloat.invoke(null, Float.NEGATIVE_INFINITY));
+
+ Method negateDouble = negateMethod(double.class);
+ assertEquals(-0.0, negateDouble.invoke(null, 0.0));
+ assertEquals(-1.0, negateDouble.invoke(null, 1.0));
+ assertEquals(Double.NaN, negateDouble.invoke(null, Double.NaN));
+ assertEquals(Double.POSITIVE_INFINITY, negateDouble.invoke(null, Double.NEGATIVE_INFINITY));
+ }
+
+ private <T> Method negateMethod(Class<T> source) throws Exception {
+ /*
+ * public static short call(int source) {
+ * source = -source;
+ * return not;
+ * }
+ */
+ reset();
+ TypeId<T> valueType = TypeId.get(source);
+ MethodId<?, T> methodId = GENERATED.getMethod(valueType, "call", valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<T> localSource = code.getParameter(0, valueType);
+ code.op(UnaryOp.NEGATE, localSource, localSource);
+ code.returnValue(localSource);
+ return getMethod();
+ }
+
+ public void testIntBinaryOps() throws Exception {
+ Method add = binaryOpMethod(int.class, int.class, BinaryOp.ADD);
+ assertEquals(79, add.invoke(null, 75, 4));
+
+ Method subtract = binaryOpMethod(int.class, int.class, BinaryOp.SUBTRACT);
+ assertEquals(71, subtract.invoke(null, 75, 4));
+
+ Method multiply = binaryOpMethod(int.class, int.class, BinaryOp.MULTIPLY);
+ assertEquals(300, multiply.invoke(null, 75, 4));
+
+ Method divide = binaryOpMethod(int.class, int.class, BinaryOp.DIVIDE);
+ assertEquals(18, divide.invoke(null, 75, 4));
+ try {
+ divide.invoke(null, 75, 0);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(ArithmeticException.class, expected.getCause().getClass());
+ }
+
+ Method remainder = binaryOpMethod(int.class, int.class, BinaryOp.REMAINDER);
+ assertEquals(3, remainder.invoke(null, 75, 4));
+ try {
+ remainder.invoke(null, 75, 0);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(ArithmeticException.class, expected.getCause().getClass());
+ }
+
+ Method and = binaryOpMethod(int.class, int.class, BinaryOp.AND);
+ assertEquals(0xff000000, and.invoke(null, 0xff00ff00, 0xffff0000));
+
+ Method or = binaryOpMethod(int.class, int.class, BinaryOp.OR);
+ assertEquals(0xffffff00, or.invoke(null, 0xff00ff00, 0xffff0000));
+
+ Method xor = binaryOpMethod(int.class, int.class, BinaryOp.XOR);
+ assertEquals(0x00ffff00, xor.invoke(null, 0xff00ff00, 0xffff0000));
+
+ Method shiftLeft = binaryOpMethod(int.class, int.class, BinaryOp.SHIFT_LEFT);
+ assertEquals(0xcd123400, shiftLeft.invoke(null, 0xabcd1234, 8));
+
+ Method shiftRight = binaryOpMethod(int.class, int.class, BinaryOp.SHIFT_RIGHT);
+ assertEquals(0xffabcd12, shiftRight.invoke(null, 0xabcd1234, 8));
+
+ Method unsignedShiftRight = binaryOpMethod(int.class,
+ int.class, BinaryOp.UNSIGNED_SHIFT_RIGHT);
+ assertEquals(0x00abcd12, unsignedShiftRight.invoke(null, 0xabcd1234, 8));
+ }
+
+ public void testLongBinaryOps() throws Exception {
+ Method add = binaryOpMethod(long.class, long.class, BinaryOp.ADD);
+ assertEquals(30000000079L, add.invoke(null, 10000000075L, 20000000004L));
+
+ Method subtract = binaryOpMethod(long.class, long.class, BinaryOp.SUBTRACT);
+ assertEquals(20000000071L, subtract.invoke(null, 30000000075L, 10000000004L));
+
+ Method multiply = binaryOpMethod(long.class, long.class, BinaryOp.MULTIPLY);
+ assertEquals(-8742552812415203028L, multiply.invoke(null, 30000000075L, 20000000004L));
+
+ Method divide = binaryOpMethod(long.class, long.class, BinaryOp.DIVIDE);
+ assertEquals(-2L, divide.invoke(null, -8742552812415203028L, 4142552812415203028L));
+ try {
+ divide.invoke(null, -8742552812415203028L, 0L);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(ArithmeticException.class, expected.getCause().getClass());
+ }
+
+ Method remainder = binaryOpMethod(long.class, long.class, BinaryOp.REMAINDER);
+ assertEquals(10000000004L, remainder.invoke(null, 30000000079L, 20000000075L));
+ try {
+ remainder.invoke(null, 30000000079L, 0L);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(ArithmeticException.class, expected.getCause().getClass());
+ }
+
+ Method and = binaryOpMethod(long.class, long.class, BinaryOp.AND);
+ assertEquals(0xff00ff0000000000L,
+ and.invoke(null, 0xff00ff00ff00ff00L, 0xffffffff00000000L));
+
+ Method or = binaryOpMethod(long.class, long.class, BinaryOp.OR);
+ assertEquals(0xffffffffff00ff00L,
+ or.invoke(null, 0xff00ff00ff00ff00L, 0xffffffff00000000L));
+
+ Method xor = binaryOpMethod(long.class, long.class, BinaryOp.XOR);
+ assertEquals(0x00ff00ffff00ff00L,
+ xor.invoke(null, 0xff00ff00ff00ff00L, 0xffffffff00000000L));
+
+ Method shiftLeft = binaryOpMethod(long.class, int.class, BinaryOp.SHIFT_LEFT);
+ assertEquals(0xcdef012345678900L, shiftLeft.invoke(null, 0xabcdef0123456789L, 8));
+
+ Method shiftRight = binaryOpMethod(long.class, int.class, BinaryOp.SHIFT_RIGHT);
+ assertEquals(0xffabcdef01234567L, shiftRight.invoke(null, 0xabcdef0123456789L, 8));
+
+ Method unsignedShiftRight = binaryOpMethod(
+ long.class, int.class, BinaryOp.UNSIGNED_SHIFT_RIGHT);
+ assertEquals(0x00abcdef01234567L, unsignedShiftRight.invoke(null, 0xabcdef0123456789L, 8));
+ }
+
+ public void testFloatBinaryOps() throws Exception {
+ Method add = binaryOpMethod(float.class, float.class, BinaryOp.ADD);
+ assertEquals(6.75f, add.invoke(null, 5.5f, 1.25f));
+
+ Method subtract = binaryOpMethod(float.class, float.class, BinaryOp.SUBTRACT);
+ assertEquals(4.25f, subtract.invoke(null, 5.5f, 1.25f));
+
+ Method multiply = binaryOpMethod(float.class, float.class, BinaryOp.MULTIPLY);
+ assertEquals(6.875f, multiply.invoke(null, 5.5f, 1.25f));
+
+ Method divide = binaryOpMethod(float.class, float.class, BinaryOp.DIVIDE);
+ assertEquals(4.4f, divide.invoke(null, 5.5f, 1.25f));
+ assertEquals(Float.POSITIVE_INFINITY, divide.invoke(null, 5.5f, 0.0f));
+
+ Method remainder = binaryOpMethod(float.class, float.class, BinaryOp.REMAINDER);
+ assertEquals(0.5f, remainder.invoke(null, 5.5f, 1.25f));
+ assertEquals(Float.NaN, remainder.invoke(null, 5.5f, 0.0f));
+ }
+
+ public void testDoubleBinaryOps() throws Exception {
+ Method add = binaryOpMethod(double.class, double.class, BinaryOp.ADD);
+ assertEquals(6.75, add.invoke(null, 5.5, 1.25));
+
+ Method subtract = binaryOpMethod(double.class, double.class, BinaryOp.SUBTRACT);
+ assertEquals(4.25, subtract.invoke(null, 5.5, 1.25));
+
+ Method multiply = binaryOpMethod(double.class, double.class, BinaryOp.MULTIPLY);
+ assertEquals(6.875, multiply.invoke(null, 5.5, 1.25));
+
+ Method divide = binaryOpMethod(double.class, double.class, BinaryOp.DIVIDE);
+ assertEquals(4.4, divide.invoke(null, 5.5, 1.25));
+ assertEquals(Double.POSITIVE_INFINITY, divide.invoke(null, 5.5, 0.0));
+
+ Method remainder = binaryOpMethod(double.class, double.class, BinaryOp.REMAINDER);
+ assertEquals(0.5, remainder.invoke(null, 5.5, 1.25));
+ assertEquals(Double.NaN, remainder.invoke(null, 5.5, 0.0));
+ }
+
+ private <T1, T2> Method binaryOpMethod(
+ Class<T1> valueAClass, Class<T2> valueBClass, BinaryOp op) throws Exception {
+ /*
+ * public static int binaryOp(int a, int b) {
+ * int result = a + b;
+ * return result;
+ * }
+ */
+ reset();
+ TypeId<T1> valueAType = TypeId.get(valueAClass);
+ TypeId<T2> valueBType = TypeId.get(valueBClass);
+ MethodId<?, T1> methodId = GENERATED.getMethod(valueAType, "call", valueAType, valueBType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<T1> localA = code.getParameter(0, valueAType);
+ Local<T2> localB = code.getParameter(1, valueBType);
+ Local<T1> localResult = code.newLocal(valueAType);
+ code.op(op, localResult, localA, localB);
+ code.returnValue(localResult);
+ return getMethod();
+ }
+
+ public void testReadAndWriteInstanceFields() throws Exception {
+ Instance instance = new Instance();
+
+ Method intSwap = instanceSwapMethod(int.class, "intValue");
+ instance.intValue = 5;
+ assertEquals(5, intSwap.invoke(null, instance, 10));
+ assertEquals(10, instance.intValue);
+
+ Method longSwap = instanceSwapMethod(long.class, "longValue");
+ instance.longValue = 500L;
+ assertEquals(500L, longSwap.invoke(null, instance, 1234L));
+ assertEquals(1234L, instance.longValue);
+
+ Method booleanSwap = instanceSwapMethod(boolean.class, "booleanValue");
+ instance.booleanValue = false;
+ assertEquals(false, booleanSwap.invoke(null, instance, true));
+ assertEquals(true, instance.booleanValue);
+
+ Method floatSwap = instanceSwapMethod(float.class, "floatValue");
+ instance.floatValue = 1.5f;
+ assertEquals(1.5f, floatSwap.invoke(null, instance, 0.5f));
+ assertEquals(0.5f, instance.floatValue);
+
+ Method doubleSwap = instanceSwapMethod(double.class, "doubleValue");
+ instance.doubleValue = 155.5;
+ assertEquals(155.5, doubleSwap.invoke(null, instance, 266.6));
+ assertEquals(266.6, instance.doubleValue);
+
+ Method objectSwap = instanceSwapMethod(Object.class, "objectValue");
+ instance.objectValue = "before";
+ assertEquals("before", objectSwap.invoke(null, instance, "after"));
+ assertEquals("after", instance.objectValue);
+
+ Method byteSwap = instanceSwapMethod(byte.class, "byteValue");
+ instance.byteValue = 0x35;
+ assertEquals((byte) 0x35, byteSwap.invoke(null, instance, (byte) 0x64));
+ assertEquals((byte) 0x64, instance.byteValue);
+
+ Method charSwap = instanceSwapMethod(char.class, "charValue");
+ instance.charValue = 'A';
+ assertEquals('A', charSwap.invoke(null, instance, 'B'));
+ assertEquals('B', instance.charValue);
+
+ Method shortSwap = instanceSwapMethod(short.class, "shortValue");
+ instance.shortValue = (short) 0xabcd;
+ assertEquals((short) 0xabcd, shortSwap.invoke(null, instance, (short) 0x1234));
+ assertEquals((short) 0x1234, instance.shortValue);
+ }
+
+ public class Instance {
+ public int intValue;
+ public long longValue;
+ public float floatValue;
+ public double doubleValue;
+ public Object objectValue;
+ public boolean booleanValue;
+ public byte byteValue;
+ public char charValue;
+ public short shortValue;
+ }
+
+ private <V> Method instanceSwapMethod(
+ Class<V> valueClass, String fieldName) throws Exception {
+ /*
+ * public static int call(Instance instance, int newValue) {
+ * int oldValue = instance.intValue;
+ * instance.intValue = newValue;
+ * return oldValue;
+ * }
+ */
+ reset();
+ TypeId<V> valueType = TypeId.get(valueClass);
+ TypeId<Instance> objectType = TypeId.get(Instance.class);
+ FieldId<Instance, V> fieldId = objectType.getField(valueType, fieldName);
+ MethodId<?, V> methodId = GENERATED.getMethod(valueType, "call", objectType, valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Instance> localInstance = code.getParameter(0, objectType);
+ Local<V> localNewValue = code.getParameter(1, valueType);
+ Local<V> localOldValue = code.newLocal(valueType);
+ code.iget(fieldId, localOldValue, localInstance);
+ code.iput(fieldId, localInstance, localNewValue);
+ code.returnValue(localOldValue);
+ return getMethod();
+ }
+
+ public void testReadAndWriteStaticFields() throws Exception {
+ Method intSwap = staticSwapMethod(int.class, "intValue");
+ Static.intValue = 5;
+ assertEquals(5, intSwap.invoke(null, 10));
+ assertEquals(10, Static.intValue);
+
+ Method longSwap = staticSwapMethod(long.class, "longValue");
+ Static.longValue = 500L;
+ assertEquals(500L, longSwap.invoke(null, 1234L));
+ assertEquals(1234L, Static.longValue);
+
+ Method booleanSwap = staticSwapMethod(boolean.class, "booleanValue");
+ Static.booleanValue = false;
+ assertEquals(false, booleanSwap.invoke(null, true));
+ assertEquals(true, Static.booleanValue);
+
+ Method floatSwap = staticSwapMethod(float.class, "floatValue");
+ Static.floatValue = 1.5f;
+ assertEquals(1.5f, floatSwap.invoke(null, 0.5f));
+ assertEquals(0.5f, Static.floatValue);
+
+ Method doubleSwap = staticSwapMethod(double.class, "doubleValue");
+ Static.doubleValue = 155.5;
+ assertEquals(155.5, doubleSwap.invoke(null, 266.6));
+ assertEquals(266.6, Static.doubleValue);
+
+ Method objectSwap = staticSwapMethod(Object.class, "objectValue");
+ Static.objectValue = "before";
+ assertEquals("before", objectSwap.invoke(null, "after"));
+ assertEquals("after", Static.objectValue);
+
+ Method byteSwap = staticSwapMethod(byte.class, "byteValue");
+ Static.byteValue = 0x35;
+ assertEquals((byte) 0x35, byteSwap.invoke(null, (byte) 0x64));
+ assertEquals((byte) 0x64, Static.byteValue);
+
+ Method charSwap = staticSwapMethod(char.class, "charValue");
+ Static.charValue = 'A';
+ assertEquals('A', charSwap.invoke(null, 'B'));
+ assertEquals('B', Static.charValue);
+
+ Method shortSwap = staticSwapMethod(short.class, "shortValue");
+ Static.shortValue = (short) 0xabcd;
+ assertEquals((short) 0xabcd, shortSwap.invoke(null, (short) 0x1234));
+ assertEquals((short) 0x1234, Static.shortValue);
+ }
+
+ public static class Static {
+ public static int intValue;
+ public static long longValue;
+ public static float floatValue;
+ public static double doubleValue;
+ public static Object objectValue;
+ public static boolean booleanValue;
+ public static byte byteValue;
+ public static char charValue;
+ public static short shortValue;
+ }
+
+ private <V> Method staticSwapMethod(Class<V> valueClass, String fieldName)
+ throws Exception {
+ /*
+ * public static int call(int newValue) {
+ * int oldValue = Static.intValue;
+ * Static.intValue = newValue;
+ * return oldValue;
+ * }
+ */
+ reset();
+ TypeId<V> valueType = TypeId.get(valueClass);
+ TypeId<Static> objectType = TypeId.get(Static.class);
+ FieldId<Static, V> fieldId = objectType.getField(valueType, fieldName);
+ MethodId<?, V> methodId = GENERATED.getMethod(valueType, "call", valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<V> localNewValue = code.getParameter(0, valueType);
+ Local<V> localOldValue = code.newLocal(valueType);
+ code.sget(fieldId, localOldValue);
+ code.sput(fieldId, localNewValue);
+ code.returnValue(localOldValue);
+ return getMethod();
+ }
+
+ public void testTypeCast() throws Exception {
+ /*
+ * public static String call(Object o) {
+ * String s = (String) o;
+ * }
+ */
+ MethodId<?, String> methodId = GENERATED.getMethod(TypeId.STRING, "call", TypeId.OBJECT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Object> localObject = code.getParameter(0, TypeId.OBJECT);
+ Local<String> localString = code.newLocal(TypeId.STRING);
+ code.cast(localString, localObject);
+ code.returnValue(localString);
+
+ Method method = getMethod();
+ assertEquals("s", method.invoke(null, "s"));
+ assertEquals(null, method.invoke(null, (String) null));
+ try {
+ method.invoke(null, 5);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(ClassCastException.class, expected.getCause().getClass());
+ }
+ }
+
+ public void testInstanceOf() throws Exception {
+ /*
+ * public static boolean call(Object o) {
+ * boolean result = o instanceof String;
+ * return result;
+ * }
+ */
+ MethodId<?, Boolean> methodId = GENERATED.getMethod(TypeId.BOOLEAN, "call", TypeId.OBJECT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Object> localObject = code.getParameter(0, TypeId.OBJECT);
+ Local<Boolean> localResult = code.newLocal(TypeId.BOOLEAN);
+ code.instanceOfType(localResult, localObject, TypeId.STRING);
+ code.returnValue(localResult);
+
+ Method method = getMethod();
+ assertEquals(true, method.invoke(null, "s"));
+ assertEquals(false, method.invoke(null, (String) null));
+ assertEquals(false, method.invoke(null, 5));
+ }
+
+ /**
+ * Tests that we can construct a for loop.
+ */
+ public void testForLoop() throws Exception {
+ /*
+ * public static int call(int count) {
+ * int result = 1;
+ * for (int i = 0; i < count; i += 1) {
+ * result = result * 2;
+ * }
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localCount = code.getParameter(0, TypeId.INT);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ Local<Integer> localI = code.newLocal(TypeId.INT);
+ Local<Integer> local1 = code.newLocal(TypeId.INT);
+ Local<Integer> local2 = code.newLocal(TypeId.INT);
+ code.loadConstant(local1, 1);
+ code.loadConstant(local2, 2);
+ code.loadConstant(localResult, 1);
+ code.loadConstant(localI, 0);
+ Label loopCondition = new Label();
+ Label loopBody = new Label();
+ Label afterLoop = new Label();
+ code.mark(loopCondition);
+ code.compare(Comparison.LT, loopBody, localI, localCount);
+ code.jump(afterLoop);
+ code.mark(loopBody);
+ code.op(BinaryOp.MULTIPLY, localResult, localResult, local2);
+ code.op(BinaryOp.ADD, localI, localI, local1);
+ code.jump(loopCondition);
+ code.mark(afterLoop);
+ code.returnValue(localResult);
+
+ Method pow2 = getMethod();
+ assertEquals(1, pow2.invoke(null, 0));
+ assertEquals(2, pow2.invoke(null, 1));
+ assertEquals(4, pow2.invoke(null, 2));
+ assertEquals(8, pow2.invoke(null, 3));
+ assertEquals(16, pow2.invoke(null, 4));
+ }
+
+ /**
+ * Tests that we can construct a while loop.
+ */
+ public void testWhileLoop() throws Exception {
+ /*
+ * public static int call(int max) {
+ * int result = 1;
+ * while (result < max) {
+ * result = result * 2;
+ * }
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localMax = code.getParameter(0, TypeId.INT);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ Local<Integer> local2 = code.newLocal(TypeId.INT);
+ code.loadConstant(localResult, 1);
+ code.loadConstant(local2, 2);
+ Label loopCondition = new Label();
+ Label loopBody = new Label();
+ Label afterLoop = new Label();
+ code.mark(loopCondition);
+ code.compare(Comparison.LT, loopBody, localResult, localMax);
+ code.jump(afterLoop);
+ code.mark(loopBody);
+ code.op(BinaryOp.MULTIPLY, localResult, localResult, local2);
+ code.jump(loopCondition);
+ code.mark(afterLoop);
+ code.returnValue(localResult);
+
+ Method ceilPow2 = getMethod();
+ assertEquals(1, ceilPow2.invoke(null, 1));
+ assertEquals(2, ceilPow2.invoke(null, 2));
+ assertEquals(4, ceilPow2.invoke(null, 3));
+ assertEquals(16, ceilPow2.invoke(null, 10));
+ assertEquals(128, ceilPow2.invoke(null, 100));
+ assertEquals(1024, ceilPow2.invoke(null, 1000));
+ }
+
+ public void testIfElseBlock() throws Exception {
+ /*
+ * public static int call(int a, int b, int c) {
+ * if (a < b) {
+ * if (a < c) {
+ * return a;
+ * } else {
+ * return c;
+ * }
+ * } else if (b < c) {
+ * return b;
+ * } else {
+ * return c;
+ * }
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(
+ TypeId.INT, "call", TypeId.INT, TypeId.INT, TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localA = code.getParameter(0, TypeId.INT);
+ Local<Integer> localB = code.getParameter(1, TypeId.INT);
+ Local<Integer> localC = code.getParameter(2, TypeId.INT);
+ Label aLessThanB = new Label();
+ Label aLessThanC = new Label();
+ Label bLessThanC = new Label();
+ code.compare(Comparison.LT, aLessThanB, localA, localB);
+ code.compare(Comparison.LT, bLessThanC, localB, localC);
+ code.returnValue(localC);
+ // (a < b)
+ code.mark(aLessThanB);
+ code.compare(Comparison.LT, aLessThanC, localA, localC);
+ code.returnValue(localC);
+ // (a < c)
+ code.mark(aLessThanC);
+ code.returnValue(localA);
+ // (b < c)
+ code.mark(bLessThanC);
+ code.returnValue(localB);
+
+ Method min = getMethod();
+ assertEquals(1, min.invoke(null, 1, 2, 3));
+ assertEquals(1, min.invoke(null, 2, 3, 1));
+ assertEquals(1, min.invoke(null, 2, 1, 3));
+ assertEquals(1, min.invoke(null, 3, 2, 1));
+ }
+
+ public void testRecursion() throws Exception {
+ /*
+ * public static int call(int a) {
+ * if (a < 2) {
+ * return a;
+ * }
+ * a -= 1;
+ * int x = call(a)
+ * a -= 1;
+ * int y = call(a);
+ * int result = x + y;
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localA = code.getParameter(0, TypeId.INT);
+ Local<Integer> local1 = code.newLocal(TypeId.INT);
+ Local<Integer> local2 = code.newLocal(TypeId.INT);
+ Local<Integer> localX = code.newLocal(TypeId.INT);
+ Local<Integer> localY = code.newLocal(TypeId.INT);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ Label baseCase = new Label();
+ code.loadConstant(local1, 1);
+ code.loadConstant(local2, 2);
+ code.compare(Comparison.LT, baseCase, localA, local2);
+ code.op(BinaryOp.SUBTRACT, localA, localA, local1);
+ code.invokeStatic(methodId, localX, localA);
+ code.op(BinaryOp.SUBTRACT, localA, localA, local1);
+ code.invokeStatic(methodId, localY, localA);
+ code.op(BinaryOp.ADD, localResult, localX, localY);
+ code.returnValue(localResult);
+ code.mark(baseCase);
+ code.returnValue(localA);
+
+ Method fib = getMethod();
+ assertEquals(0, fib.invoke(null, 0));
+ assertEquals(1, fib.invoke(null, 1));
+ assertEquals(1, fib.invoke(null, 2));
+ assertEquals(2, fib.invoke(null, 3));
+ assertEquals(3, fib.invoke(null, 4));
+ assertEquals(5, fib.invoke(null, 5));
+ assertEquals(8, fib.invoke(null, 6));
+ }
+
+ public void testCatchExceptions() throws Exception {
+ /*
+ * public static String call(int i) {
+ * try {
+ * DexMakerTest.thrower(i);
+ * return "NONE";
+ * } catch (IllegalArgumentException e) {
+ * return "IAE";
+ * } catch (IllegalStateException e) {
+ * return "ISE";
+ * } catch (RuntimeException e) {
+ * return "RE";
+ * }
+ */
+ MethodId<?, String> methodId = GENERATED.getMethod(TypeId.STRING, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localI = code.getParameter(0, TypeId.INT);
+ Local<String> result = code.newLocal(TypeId.STRING);
+ Label catchIae = new Label();
+ Label catchIse = new Label();
+ Label catchRe = new Label();
+
+ code.addCatchClause(TypeId.get(IllegalArgumentException.class), catchIae);
+ code.addCatchClause(TypeId.get(IllegalStateException.class), catchIse);
+ code.addCatchClause(TypeId.get(RuntimeException.class), catchRe);
+ MethodId<?, ?> thrower = TEST_TYPE.getMethod(TypeId.VOID, "thrower", TypeId.INT);
+ code.invokeStatic(thrower, null, localI);
+ code.loadConstant(result, "NONE");
+ code.returnValue(result);
+
+ code.mark(catchIae);
+ code.loadConstant(result, "IAE");
+ code.returnValue(result);
+
+ code.mark(catchIse);
+ code.loadConstant(result, "ISE");
+ code.returnValue(result);
+
+ code.mark(catchRe);
+ code.loadConstant(result, "RE");
+ code.returnValue(result);
+
+ Method method = getMethod();
+ assertEquals("NONE", method.invoke(null, 0));
+ assertEquals("IAE", method.invoke(null, 1));
+ assertEquals("ISE", method.invoke(null, 2));
+ assertEquals("RE", method.invoke(null, 3));
+ try {
+ method.invoke(null, 4);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(IOException.class, expected.getCause().getClass());
+ }
+ }
+
+ @SuppressWarnings("unused") // called by generated code
+ public static void thrower(int a) throws Exception {
+ switch (a) {
+ case 0:
+ return;
+ case 1:
+ throw new IllegalArgumentException();
+ case 2:
+ throw new IllegalStateException();
+ case 3:
+ throw new UnsupportedOperationException();
+ case 4:
+ throw new IOException();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public void testNestedCatchClauses() throws Exception {
+ /*
+ * public static String call(int a, int b, int c) {
+ * try {
+ * DexMakerTest.thrower(a);
+ * try {
+ * DexMakerTest.thrower(b);
+ * } catch (IllegalArgumentException) {
+ * return "INNER";
+ * }
+ * DexMakerTest.thrower(c);
+ * return "NONE";
+ * } catch (IllegalArgumentException e) {
+ * return "OUTER";
+ * }
+ */
+ MethodId<?, String> methodId = GENERATED.getMethod(
+ TypeId.STRING, "call", TypeId.INT, TypeId.INT, TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localA = code.getParameter(0, TypeId.INT);
+ Local<Integer> localB = code.getParameter(1, TypeId.INT);
+ Local<Integer> localC = code.getParameter(2, TypeId.INT);
+ Local<String> localResult = code.newLocal(TypeId.STRING);
+ Label catchInner = new Label();
+ Label catchOuter = new Label();
+
+ TypeId<IllegalArgumentException> iaeType = TypeId.get(IllegalArgumentException.class);
+ code.addCatchClause(iaeType, catchOuter);
+
+ MethodId<?, ?> thrower = TEST_TYPE.getMethod(TypeId.VOID, "thrower", TypeId.INT);
+ code.invokeStatic(thrower, null, localA);
+
+ // for the inner catch clause, we stash the old label and put it back afterwards.
+ Label previousLabel = code.removeCatchClause(iaeType);
+ code.addCatchClause(iaeType, catchInner);
+ code.invokeStatic(thrower, null, localB);
+ code.removeCatchClause(iaeType);
+ code.addCatchClause(iaeType, previousLabel);
+ code.invokeStatic(thrower, null, localC);
+ code.loadConstant(localResult, "NONE");
+ code.returnValue(localResult);
+
+ code.mark(catchInner);
+ code.loadConstant(localResult, "INNER");
+ code.returnValue(localResult);
+
+ code.mark(catchOuter);
+ code.loadConstant(localResult, "OUTER");
+ code.returnValue(localResult);
+
+ Method method = getMethod();
+ assertEquals("OUTER", method.invoke(null, 1, 0, 0));
+ assertEquals("INNER", method.invoke(null, 0, 1, 0));
+ assertEquals("OUTER", method.invoke(null, 0, 0, 1));
+ assertEquals("NONE", method.invoke(null, 0, 0, 0));
+ }
+
+ public void testThrow() throws Exception {
+ /*
+ * public static void call() {
+ * throw new IllegalStateException();
+ * }
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ TypeId<IllegalStateException> iseType = TypeId.get(IllegalStateException.class);
+ MethodId<IllegalStateException, Void> iseConstructor = iseType.getConstructor();
+ Local<IllegalStateException> localIse = code.newLocal(iseType);
+ code.newInstance(localIse, iseConstructor);
+ code.throwValue(localIse);
+
+ try {
+ getMethod().invoke(null);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertEquals(IllegalStateException.class, expected.getCause().getClass());
+ }
+ }
+
+ public void testUnusedParameters() throws Exception {
+ /*
+ * public static void call(int unused1, long unused2, long unused3) {}
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(
+ TypeId.VOID, "call", TypeId.INT, TypeId.LONG, TypeId.LONG);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ code.returnVoid();
+ getMethod().invoke(null, 1, 2, 3);
+ }
+
+ public void testFloatingPointCompare() throws Exception {
+ Method floatG = floatingPointCompareMethod(TypeId.FLOAT, 1);
+ assertEquals(-1, floatG.invoke(null, 1.0f, Float.POSITIVE_INFINITY));
+ assertEquals(-1, floatG.invoke(null, 1.0f, 2.0f));
+ assertEquals(0, floatG.invoke(null, 1.0f, 1.0f));
+ assertEquals(1, floatG.invoke(null, 2.0f, 1.0f));
+ assertEquals(1, floatG.invoke(null, 1.0f, Float.NaN));
+ assertEquals(1, floatG.invoke(null, Float.NaN, 1.0f));
+ assertEquals(1, floatG.invoke(null, Float.NaN, Float.NaN));
+ assertEquals(1, floatG.invoke(null, Float.NaN, Float.POSITIVE_INFINITY));
+
+ Method floatL = floatingPointCompareMethod(TypeId.FLOAT, -1);
+ assertEquals(-1, floatG.invoke(null, 1.0f, Float.POSITIVE_INFINITY));
+ assertEquals(-1, floatL.invoke(null, 1.0f, 2.0f));
+ assertEquals(0, floatL.invoke(null, 1.0f, 1.0f));
+ assertEquals(1, floatL.invoke(null, 2.0f, 1.0f));
+ assertEquals(-1, floatL.invoke(null, 1.0f, Float.NaN));
+ assertEquals(-1, floatL.invoke(null, Float.NaN, 1.0f));
+ assertEquals(-1, floatL.invoke(null, Float.NaN, Float.NaN));
+ assertEquals(-1, floatL.invoke(null, Float.NaN, Float.POSITIVE_INFINITY));
+
+ Method doubleG = floatingPointCompareMethod(TypeId.DOUBLE, 1);
+ assertEquals(-1, doubleG.invoke(null, 1.0, Double.POSITIVE_INFINITY));
+ assertEquals(-1, doubleG.invoke(null, 1.0, 2.0));
+ assertEquals(0, doubleG.invoke(null, 1.0, 1.0));
+ assertEquals(1, doubleG.invoke(null, 2.0, 1.0));
+ assertEquals(1, doubleG.invoke(null, 1.0, Double.NaN));
+ assertEquals(1, doubleG.invoke(null, Double.NaN, 1.0));
+ assertEquals(1, doubleG.invoke(null, Double.NaN, Double.NaN));
+ assertEquals(1, doubleG.invoke(null, Double.NaN, Double.POSITIVE_INFINITY));
+
+ Method doubleL = floatingPointCompareMethod(TypeId.DOUBLE, -1);
+ assertEquals(-1, doubleL.invoke(null, 1.0, Double.POSITIVE_INFINITY));
+ assertEquals(-1, doubleL.invoke(null, 1.0, 2.0));
+ assertEquals(0, doubleL.invoke(null, 1.0, 1.0));
+ assertEquals(1, doubleL.invoke(null, 2.0, 1.0));
+ assertEquals(-1, doubleL.invoke(null, 1.0, Double.NaN));
+ assertEquals(-1, doubleL.invoke(null, Double.NaN, 1.0));
+ assertEquals(-1, doubleL.invoke(null, Double.NaN, Double.NaN));
+ assertEquals(-1, doubleL.invoke(null, Double.NaN, Double.POSITIVE_INFINITY));
+ }
+
+ private <T extends Number> Method floatingPointCompareMethod(
+ TypeId<T> valueType, int nanValue) throws Exception {
+ /*
+ * public static int call(float a, float b) {
+ * int result = a <=> b;
+ * return result;
+ * }
+ */
+ reset();
+ MethodId<?, Integer> methodId = GENERATED.getMethod(
+ TypeId.INT, "call", valueType, valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<T> localA = code.getParameter(0, valueType);
+ Local<T> localB = code.getParameter(1, valueType);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ code.compareFloatingPoint(localResult, localA, localB, nanValue);
+ code.returnValue(localResult);
+ return getMethod();
+ }
+
+ public void testLongCompare() throws Exception {
+ /*
+ * public static int call(long a, long b) {
+ * int result = a <=> b;
+ * return result;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.LONG, TypeId.LONG);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Long> localA = code.getParameter(0, TypeId.LONG);
+ Local<Long> localB = code.getParameter(1, TypeId.LONG);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ code.compareLongs(localResult, localA, localB);
+ code.returnValue(localResult);
+
+ Method method = getMethod();
+ assertEquals(0, method.invoke(null, Long.MIN_VALUE, Long.MIN_VALUE));
+ assertEquals(-1, method.invoke(null, Long.MIN_VALUE, 0));
+ assertEquals(-1, method.invoke(null, Long.MIN_VALUE, Long.MAX_VALUE));
+ assertEquals(1, method.invoke(null, 0, Long.MIN_VALUE));
+ assertEquals(0, method.invoke(null, 0, 0));
+ assertEquals(-1, method.invoke(null, 0, Long.MAX_VALUE));
+ assertEquals(1, method.invoke(null, Long.MAX_VALUE, Long.MIN_VALUE));
+ assertEquals(1, method.invoke(null, Long.MAX_VALUE, 0));
+ assertEquals(0, method.invoke(null, Long.MAX_VALUE, Long.MAX_VALUE));
+ }
+
+ public void testArrayLength() throws Exception {
+ Method booleanArrayLength = arrayLengthMethod(BOOLEAN_ARRAY);
+ assertEquals(0, booleanArrayLength.invoke(null, new Object[] { new boolean[0] }));
+ assertEquals(5, booleanArrayLength.invoke(null, new Object[] { new boolean[5] }));
+
+ Method intArrayLength = arrayLengthMethod(INT_ARRAY);
+ assertEquals(0, intArrayLength.invoke(null, new Object[] { new int[0] }));
+ assertEquals(5, intArrayLength.invoke(null, new Object[] { new int[5] }));
+
+ Method longArrayLength = arrayLengthMethod(LONG_ARRAY);
+ assertEquals(0, longArrayLength.invoke(null, new Object[] { new long[0] }));
+ assertEquals(5, longArrayLength.invoke(null, new Object[] { new long[5] }));
+
+ Method objectArrayLength = arrayLengthMethod(OBJECT_ARRAY);
+ assertEquals(0, objectArrayLength.invoke(null, new Object[] { new Object[0] }));
+ assertEquals(5, objectArrayLength.invoke(null, new Object[] { new Object[5] }));
+
+ Method long2dArrayLength = arrayLengthMethod(LONG_2D_ARRAY);
+ assertEquals(0, long2dArrayLength.invoke(null, new Object[] { new long[0][0] }));
+ assertEquals(5, long2dArrayLength.invoke(null, new Object[] { new long[5][10] }));
+ }
+
+ private <T> Method arrayLengthMethod(TypeId<T> valueType) throws Exception {
+ /*
+ * public static int call(long[] array) {
+ * int result = array.length;
+ * return result;
+ * }
+ */
+ reset();
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", valueType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<T> localArray = code.getParameter(0, valueType);
+ Local<Integer> localResult = code.newLocal(TypeId.INT);
+ code.arrayLength(localResult, localArray);
+ code.returnValue(localResult);
+ return getMethod();
+ }
+
+ public void testNewArray() throws Exception {
+ Method newBooleanArray = newArrayMethod(BOOLEAN_ARRAY);
+ assertEquals("[]", Arrays.toString((boolean[]) newBooleanArray.invoke(null, 0)));
+ assertEquals("[false, false, false]",
+ Arrays.toString((boolean[]) newBooleanArray.invoke(null, 3)));
+
+ Method newIntArray = newArrayMethod(INT_ARRAY);
+ assertEquals("[]", Arrays.toString((int[]) newIntArray.invoke(null, 0)));
+ assertEquals("[0, 0, 0]", Arrays.toString((int[]) newIntArray.invoke(null, 3)));
+
+ Method newLongArray = newArrayMethod(LONG_ARRAY);
+ assertEquals("[]", Arrays.toString((long[]) newLongArray.invoke(null, 0)));
+ assertEquals("[0, 0, 0]", Arrays.toString((long[]) newLongArray.invoke(null, 3)));
+
+ Method newObjectArray = newArrayMethod(OBJECT_ARRAY);
+ assertEquals("[]", Arrays.toString((Object[]) newObjectArray.invoke(null, 0)));
+ assertEquals("[null, null, null]",
+ Arrays.toString((Object[]) newObjectArray.invoke(null, 3)));
+
+ Method new2dLongArray = newArrayMethod(LONG_2D_ARRAY);
+ assertEquals("[]", Arrays.deepToString((long[][]) new2dLongArray.invoke(null, 0)));
+ assertEquals("[null, null, null]",
+ Arrays.deepToString((long[][]) new2dLongArray.invoke(null, 3)));
+ }
+
+ private <T> Method newArrayMethod(TypeId<T> valueType) throws Exception {
+ /*
+ * public static long[] call(int length) {
+ * long[] result = new long[length];
+ * return result;
+ * }
+ */
+ reset();
+ MethodId<?, T> methodId = GENERATED.getMethod(valueType, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> localLength = code.getParameter(0, TypeId.INT);
+ Local<T> localResult = code.newLocal(valueType);
+ code.newArray(localResult, localLength);
+ code.returnValue(localResult);
+ return getMethod();
+ }
+
+ public void testReadAndWriteArray() throws Exception {
+ Method swapBooleanArray = arraySwapMethod(BOOLEAN_ARRAY, TypeId.BOOLEAN);
+ boolean[] booleans = new boolean[3];
+ assertEquals(false, swapBooleanArray.invoke(null, booleans, 1, true));
+ assertEquals("[false, true, false]", Arrays.toString(booleans));
+
+ Method swapIntArray = arraySwapMethod(INT_ARRAY, TypeId.INT);
+ int[] ints = new int[3];
+ assertEquals(0, swapIntArray.invoke(null, ints, 1, 5));
+ assertEquals("[0, 5, 0]", Arrays.toString(ints));
+
+ Method swapLongArray = arraySwapMethod(LONG_ARRAY, TypeId.LONG);
+ long[] longs = new long[3];
+ assertEquals(0L, swapLongArray.invoke(null, longs, 1, 6L));
+ assertEquals("[0, 6, 0]", Arrays.toString(longs));
+
+ Method swapObjectArray = arraySwapMethod(OBJECT_ARRAY, TypeId.OBJECT);
+ Object[] objects = new Object[3];
+ assertEquals(null, swapObjectArray.invoke(null, objects, 1, "X"));
+ assertEquals("[null, X, null]", Arrays.toString(objects));
+
+ Method swapLong2dArray = arraySwapMethod(LONG_2D_ARRAY, LONG_ARRAY);
+ long[][] longs2d = new long[3][];
+ assertEquals(null, swapLong2dArray.invoke(null, longs2d, 1, new long[] { 7 }));
+ assertEquals("[null, [7], null]", Arrays.deepToString(longs2d));
+ }
+
+ private <A, T> Method arraySwapMethod(TypeId<A> arrayType, TypeId<T> singleType)
+ throws Exception {
+ /*
+ * public static long swap(long[] array, int index, long newValue) {
+ * long result = array[index];
+ * array[index] = newValue;
+ * return result;
+ * }
+ */
+ reset();
+ MethodId<?, T> methodId = GENERATED.getMethod(
+ singleType, "call", arrayType, TypeId.INT, singleType);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<A> localArray = code.getParameter(0, arrayType);
+ Local<Integer> localIndex = code.getParameter(1, TypeId.INT);
+ Local<T> localNewValue = code.getParameter(2, singleType);
+ Local<T> localResult = code.newLocal(singleType);
+ code.aget(localResult, localArray, localIndex);
+ code.aput(localArray, localIndex, localNewValue);
+ code.returnValue(localResult);
+ return getMethod();
+ }
+
+ public void testSynchronizedFlagImpactsDeclarationOnly() throws Exception {
+ /*
+ * public synchronized void call() {
+ * wait(100L);
+ * }
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ MethodId<Object, Void> wait = TypeId.OBJECT.getMethod(TypeId.VOID, "wait", TypeId.LONG);
+ Code code = dexMaker.declare(methodId, PUBLIC | SYNCHRONIZED);
+ Local<?> thisLocal = code.getThis(GENERATED);
+ Local<Long> timeout = code.newLocal(TypeId.LONG);
+ code.loadConstant(timeout, 100L);
+ code.invokeVirtual(wait, null, thisLocal, timeout);
+ code.returnVoid();
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("call");
+ assertTrue(Modifier.isSynchronized(method.getModifiers()));
+ try {
+ method.invoke(instance);
+ fail();
+ } catch (InvocationTargetException expected) {
+ assertTrue(expected.getCause() instanceof IllegalMonitorStateException);
+ }
+ }
+
+ public void testMonitorEnterMonitorExit() throws Exception {
+ /*
+ * public synchronized void call() {
+ * synchronized (this) {
+ * wait(100L);
+ * }
+ * }
+ */
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ MethodId<Object, Void> wait = TypeId.OBJECT.getMethod(TypeId.VOID, "wait", TypeId.LONG);
+ Code code = dexMaker.declare(methodId, PUBLIC);
+ Local<?> thisLocal = code.getThis(GENERATED);
+ Local<Long> timeout = code.newLocal(TypeId.LONG);
+ code.monitorEnter(thisLocal);
+ code.loadConstant(timeout, 100L);
+ code.invokeVirtual(wait, null, thisLocal, timeout);
+ code.monitorExit(thisLocal);
+ code.returnVoid();
+
+ addDefaultConstructor();
+
+ Class<?> generatedClass = generateAndLoad();
+ Object instance = generatedClass.newInstance();
+ Method method = generatedClass.getMethod("call");
+ assertFalse(Modifier.isSynchronized(method.getModifiers()));
+ method.invoke(instance); // will take 100ms
+ }
+
+ public void testMoveInt() throws Exception {
+ /*
+ * public static int call(int a) {
+ * int b = a;
+ * int c = a + b;
+ * return c;
+ * }
+ */
+ MethodId<?, Integer> methodId = GENERATED.getMethod(TypeId.INT, "call", TypeId.INT);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ Local<Integer> a = code.getParameter(0, TypeId.INT);
+ Local<Integer> b = code.newLocal(TypeId.INT);
+ Local<Integer> c = code.newLocal(TypeId.INT);
+ code.move(b, a);
+ code.op(BinaryOp.ADD, c, a, b);
+ code.returnValue(c);
+
+ assertEquals(6, getMethod().invoke(null, 3));
+ }
+
+ public void testPrivateClassesAreUnsupported() {
+ try {
+ dexMaker.declare(TypeId.get("LPrivateClass;"), "PrivateClass.generated", PRIVATE,
+ TypeId.OBJECT);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testAbstractMethodsAreUnsupported() {
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ try {
+ dexMaker.declare(methodId, ABSTRACT);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testNativeMethodsAreUnsupported() {
+ MethodId<?, Void> methodId = GENERATED.getMethod(TypeId.VOID, "call");
+ try {
+ dexMaker.declare(methodId, NATIVE);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testSynchronizedFieldsAreUnsupported() {
+ try {
+ FieldId<?, ?> fieldId = GENERATED.getField(TypeId.OBJECT, "synchronizedField");
+ dexMaker.declare(fieldId, SYNCHRONIZED, null);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testInitialValueWithNonStaticField() {
+ try {
+ FieldId<?, ?> fieldId = GENERATED.getField(TypeId.OBJECT, "nonStaticField");
+ dexMaker.declare(fieldId, 0, 1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ // TODO: cast primitive to non-primitive
+ // TODO: cast non-primitive to primitive
+ // TODO: cast byte to integer
+ // TODO: cast byte to long
+ // TODO: cast long to byte
+ // TODO: fail if a label is unreachable (never navigated to)
+ // TODO: more strict type parameters: Integer on methods
+ // TODO: don't generate multiple times (?)
+ // TODO: test array types
+ // TODO: test generating an interface
+ // TODO: declare native method or abstract method
+ // TODO: get a thrown exception 'e' into a local
+ // TODO: move a primitive or reference
+
+ private void addDefaultConstructor() {
+ Code code = dexMaker.declare(GENERATED.getConstructor(), PUBLIC);
+ Local<?> thisRef = code.getThis(GENERATED);
+ code.invokeDirect(TypeId.OBJECT.getConstructor(), null, thisRef);
+ code.returnVoid();
+ }
+
+ public void testCaching_Methods() throws Exception {
+ int origSize = getDataDirectory().listFiles().length;
+ final String defaultMethodName = "call";
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex.
+ assertEquals(origSize + 2, getDataDirectory().listFiles().length);
+
+ long lastModified = getJarFiles()[0].lastModified();
+
+ // Create new dexmaker generator with same method signature.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 2, getDataDirectory().listFiles().length);
+ assertEquals(lastModified, getJarFiles()[0].lastModified());
+
+ // Create new dexmaker generators with different params.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.DOUBLE);
+ generateAndLoad();
+ assertEquals(origSize + 4, getDataDirectory().listFiles().length);
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.DOUBLE);
+ generateAndLoad();
+ assertEquals(origSize + 6, getDataDirectory().listFiles().length);
+
+ // Create new dexmaker generator with different return types.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.DOUBLE, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 8, getDataDirectory().listFiles().length);
+
+ // Create new dexmaker generators with multiple methods.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN); // new method
+ generateAndLoad();
+ assertEquals(origSize + 10, getDataDirectory().listFiles().length);
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 10, getDataDirectory().listFiles().length); // should already be cached.
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.INT, TypeId.BOOLEAN); // new method
+ generateAndLoad();
+ assertEquals(origSize + 12, getDataDirectory().listFiles().length);
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.INT); // new method
+ generateAndLoad();
+ assertEquals(origSize + 14, getDataDirectory().listFiles().length);
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addMethodToDexMakerGenerator(TypeId.INT, "differentName", TypeId.INT); // new method
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT, TypeId.BOOLEAN);
+ generateAndLoad();
+ assertEquals(origSize + 16, getDataDirectory().listFiles().length);
+ }
+
+ public static class BlankClassA {
+
+ }
+
+ public static class BlankClassB {
+
+ }
+
+ public void testCaching_DifferentParentClasses() throws Exception {
+ int origSize = getDataDirectory().listFiles().length;
+ final String defaultMethodName = "call";
+
+ // Create new dexmaker generator with BlankClassA as supertype.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.get(BlankClassA.class));
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex.
+ assertEquals(origSize + 2, getDataDirectory().listFiles().length);
+
+ // Create new dexmaker generator with BlankClassB as supertype.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.get(BlankClassB.class));
+ addMethodToDexMakerGenerator(TypeId.INT, defaultMethodName, TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 4, getDataDirectory().listFiles().length);
+
+ }
+
+ private void addMethodToDexMakerGenerator(TypeId<?> typeId, String methodName, TypeId<?>... params) throws Exception {
+ MethodId<?, ?> methodId = GENERATED.getMethod(typeId, methodName, params);
+ Code code = dexMaker.declare(methodId, PUBLIC | STATIC);
+ TypeId<IllegalStateException> iseType = TypeId.get(IllegalStateException.class);
+ Local<IllegalStateException> localIse = code.newLocal(iseType);
+ if (params.length > 0) {
+ if (params[0] == typeId) {
+ Local<?> localResult = code.getParameter(0, TypeId.INT);
+ code.returnValue(localResult);
+ } else {
+ code.throwValue(localIse);
+ }
+ } else {
+ code.throwValue(localIse);
+ }
+ }
+
+ public void testCaching_Constructors() throws Exception {
+ int origSize = getDataDirectory().listFiles().length;
+
+ // Create new dexmaker generator with Generated(int) constructor.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addConstructorToDexMakerGenerator(TypeId.INT);
+ generateAndLoad();
+ // DexMaker writes two files to disk at a time: Generated_XXXX.jar and Generated_XXXX.dex.
+ assertEquals(origSize + 2, getDataDirectory().listFiles().length);
+
+ long lastModified = getJarFiles()[0].lastModified();
+
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addConstructorToDexMakerGenerator(TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 2, getDataDirectory().listFiles().length);
+ assertEquals(lastModified, getJarFiles()[0].lastModified());
+
+ // Create new dexmaker generator with Generated(boolean) constructor.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addConstructorToDexMakerGenerator(TypeId.BOOLEAN);
+ generateAndLoad();
+ assertEquals(origSize + 4, getDataDirectory().listFiles().length);
+
+ // Create new dexmaker generator with multiple constructors.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addConstructorToDexMakerGenerator(TypeId.INT);
+ addConstructorToDexMakerGenerator(TypeId.BOOLEAN);
+ generateAndLoad();
+ assertEquals(origSize + 6, getDataDirectory().listFiles().length);
+
+ // Ensure that order of constructors does not affect caching decision.
+ dexMaker = new DexMaker();
+ dexMaker.declare(GENERATED, "Generated.java", PUBLIC, TypeId.OBJECT);
+ addConstructorToDexMakerGenerator(TypeId.BOOLEAN);
+ addConstructorToDexMakerGenerator(TypeId.INT);
+ generateAndLoad();
+ assertEquals(origSize + 6, getDataDirectory().listFiles().length);
+ }
+
+ private void addConstructorToDexMakerGenerator(TypeId<?>... params) throws Exception {
+ MethodId<?, Void> constructor = GENERATED.getConstructor(params);
+ Code code = dexMaker.declare(constructor, PUBLIC);
+ code.returnVoid();
+ }
+
+ private File[] getJarFiles() {
+ return getDataDirectory().listFiles(new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".jar");
+ }
+ });
+ }
+
+ /**
+ * Returns the generated method.
+ */
+ private Method getMethod() throws Exception {
+ Class<?> generated = generateAndLoad();
+ for (Method method : generated.getMethods()) {
+ if (method.getName().equals("call")) {
+ return method;
+ }
+ }
+ throw new IllegalStateException("no call() method");
+ }
+
+ public static File getDataDirectory() {
+ String envVariable = "ANDROID_DATA";
+ String defaultLoc = "/data";
+ String path = System.getenv(envVariable);
+ return path == null ? new File(defaultLoc) : new File(path);
+ }
+
+ private Class<?> generateAndLoad() throws Exception {
+ return dexMaker.generateAndLoad(getClass().getClassLoader(), getDataDirectory())
+ .loadClass("Generated");
+ }
+}
diff --git a/dexmaker/src/test/java/com/google/dexmaker/TypeIdTest.java b/dexmaker/src/test/java/com/google/dexmaker/TypeIdTest.java
new file mode 100644
index 0000000..26e8413
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/TypeIdTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.google.dexmaker;
+
+import junit.framework.TestCase;
+
+public final class TypeIdTest extends TestCase {
+ public void testGetType() {
+ assertEquals("Ljava/lang/String;", TypeId.get(String.class).getName());
+ assertEquals("[Ljava/lang/String;", TypeId.get(String[].class).getName());
+ assertEquals("[[Ljava/lang/String;", TypeId.get(String[][].class).getName());
+ assertEquals("I", TypeId.get(int.class).getName());
+ assertEquals("[I", TypeId.get(int[].class).getName());
+ assertEquals("[[I", TypeId.get(int[][].class).getName());
+ }
+}
diff --git a/dexmaker/src/test/java/com/google/dexmaker/examples/FibonacciMaker.java b/dexmaker/src/test/java/com/google/dexmaker/examples/FibonacciMaker.java
new file mode 100644
index 0000000..ced3d53
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/examples/FibonacciMaker.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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.google.dexmaker.examples;
+
+import com.google.dexmaker.BinaryOp;
+import com.google.dexmaker.Code;
+import com.google.dexmaker.Comparison;
+import com.google.dexmaker.DexMaker;
+import com.google.dexmaker.Label;
+import com.google.dexmaker.Local;
+import com.google.dexmaker.MethodId;
+import com.google.dexmaker.TypeId;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+public final class FibonacciMaker {
+ public static void main(String[] args) throws Exception {
+ TypeId<?> fibonacci = TypeId.get("Lcom/google/dexmaker/examples/Fibonacci;");
+
+ String fileName = "Fibonacci.generated";
+ DexMaker dexMaker = new DexMaker();
+ dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
+
+ MethodId<?, Integer> fib = fibonacci.getMethod(TypeId.INT, "fib", TypeId.INT);
+ Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
+
+ Local<Integer> i = code.getParameter(0, TypeId.INT);
+ Local<Integer> constant1 = code.newLocal(TypeId.INT);
+ Local<Integer> constant2 = code.newLocal(TypeId.INT);
+ Local<Integer> a = code.newLocal(TypeId.INT);
+ Local<Integer> b = code.newLocal(TypeId.INT);
+ Local<Integer> c = code.newLocal(TypeId.INT);
+ Local<Integer> d = code.newLocal(TypeId.INT);
+ Local<Integer> result = code.newLocal(TypeId.INT);
+
+ code.loadConstant(constant1, 1);
+ code.loadConstant(constant2, 2);
+ Label baseCase = new Label();
+ code.compare(Comparison.LT, baseCase, i, constant2);
+ code.op(BinaryOp.SUBTRACT, a, i, constant1);
+ code.op(BinaryOp.SUBTRACT, b, i, constant2);
+ code.invokeStatic(fib, c, a);
+ code.invokeStatic(fib, d, b);
+ code.op(BinaryOp.ADD, result, c, d);
+ code.returnValue(result);
+ code.mark(baseCase);
+ code.returnValue(i);
+
+ ClassLoader loader = dexMaker.generateAndLoad(
+ FibonacciMaker.class.getClassLoader(), getDataDirectory());
+
+ Class<?> fibonacciClass = loader.loadClass("com.google.dexmaker.examples.Fibonacci");
+ Method fibMethod = fibonacciClass.getMethod("fib", int.class);
+ System.out.println(fibMethod.invoke(null, 8));
+ }
+
+ public static File getDataDirectory() {
+ String envVariable = "ANDROID_DATA";
+ String defaultLoc = "/data";
+ String path = System.getenv(envVariable);
+ return path == null ? new File(defaultLoc) : new File(path);
+ }
+}
diff --git a/dexmaker/src/test/java/com/google/dexmaker/examples/HelloWorldMaker.java b/dexmaker/src/test/java/com/google/dexmaker/examples/HelloWorldMaker.java
new file mode 100644
index 0000000..40dd1aa
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/examples/HelloWorldMaker.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 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.google.dexmaker.examples;
+
+import com.google.dexmaker.BinaryOp;
+import com.google.dexmaker.Code;
+import com.google.dexmaker.DexMaker;
+import com.google.dexmaker.FieldId;
+import com.google.dexmaker.Local;
+import com.google.dexmaker.MethodId;
+import com.google.dexmaker.TypeId;
+import java.io.File;
+import java.io.PrintStream;
+import java.lang.reflect.Modifier;
+
+public final class HelloWorldMaker {
+ public static void main(String[] args) throws Exception {
+ DexMaker dexMaker = new DexMaker();
+
+ // Generate a HelloWorld class.
+ TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
+ dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
+ generateHelloMethod(dexMaker, helloWorld);
+
+ // Create the dex file and load it.
+ File outputDir = new File(".");
+ ClassLoader loader = dexMaker.generateAndLoad(HelloWorldMaker.class.getClassLoader(),
+ outputDir);
+ Class<?> helloWorldClass = loader.loadClass("HelloWorld");
+
+ // Execute our newly-generated code in-process.
+ helloWorldClass.getMethod("hello").invoke(null);
+ }
+
+ /**
+ * Generates Dalvik bytecode equivalent to the following method.
+ * public static void hello() {
+ * int a = 0xabcd;
+ * int b = 0xaaaa;
+ * int c = a - b;
+ * String s = Integer.toHexString(c);
+ * System.out.println(s);
+ * return;
+ * }
+ */
+ private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
+ // Lookup some types we'll need along the way.
+ TypeId<System> systemType = TypeId.get(System.class);
+ TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);
+
+ // Identify the 'hello()' method on declaringType.
+ MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");
+
+ // Declare that method on the dexMaker. Use the returned Code instance
+ // as a builder that we can append instructions to.
+ Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);
+
+ // Declare all the locals we'll need up front. The API requires this.
+ Local<Integer> a = code.newLocal(TypeId.INT);
+ Local<Integer> b = code.newLocal(TypeId.INT);
+ Local<Integer> c = code.newLocal(TypeId.INT);
+ Local<String> s = code.newLocal(TypeId.STRING);
+ Local<PrintStream> localSystemOut = code.newLocal(printStreamType);
+
+ // int a = 0xabcd;
+ code.loadConstant(a, 0xabcd);
+
+ // int b = 0xaaaa;
+ code.loadConstant(b, 0xaaaa);
+
+ // int c = a - b;
+ code.op(BinaryOp.SUBTRACT, c, a, b);
+
+ // String s = Integer.toHexString(c);
+ MethodId<Integer, String> toHexString
+ = TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
+ code.invokeStatic(toHexString, s, c);
+
+ // System.out.println(s);
+ FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
+ code.sget(systemOutField, localSystemOut);
+ MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
+ TypeId.VOID, "println", TypeId.STRING);
+ code.invokeVirtual(printlnMethod, null, localSystemOut, s);
+
+ // return;
+ code.returnVoid();
+ }
+} \ No newline at end of file
diff --git a/dexmaker/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java b/dexmaker/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
new file mode 100644
index 0000000..763d740
--- /dev/null
+++ b/dexmaker/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
@@ -0,0 +1,987 @@
+/*
+ * 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.google.dexmaker.stock;
+
+import com.google.dexmaker.DexMakerTest;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicInteger;
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+public class ProxyBuilderTest extends TestCase {
+ private FakeInvocationHandler fakeHandler = new FakeInvocationHandler();
+ private File versionedDxDir = new File(DexMakerTest.getDataDirectory(), "v1");
+
+ public void setUp() throws Exception {
+ super.setUp();
+ versionedDxDir.mkdirs();
+ clearVersionedDxDir();
+ getGeneratedProxyClasses().clear();
+ }
+
+ public void tearDown() throws Exception {
+ getGeneratedProxyClasses().clear();
+ clearVersionedDxDir();
+ super.tearDown();
+ }
+
+ private void clearVersionedDxDir() {
+ for (File f : versionedDxDir.listFiles()) {
+ f.delete();
+ }
+ }
+
+ 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());
+ assertEquals(2, versionedDxDir.listFiles().length);
+ }
+
+ public void testExampleOperation_DexMakerCaching() throws Throwable {
+ fakeHandler.setFakeResult("expected");
+ SimpleClass proxy = proxyFor(SimpleClass.class).build();
+ assertEquals(2, versionedDxDir.listFiles().length);
+ assertEquals("expected", proxy.simpleMethod());
+
+ // Force ProxyBuilder to create a DexMaker generator and call DexMaker.generateAndLoad().
+ getGeneratedProxyClasses().clear();
+
+ proxy = proxyFor(SimpleClass.class).build();
+ assertEquals(2, versionedDxDir.listFiles().length);
+ 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 AllReturnTypes {
+ 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 int[] getIntArray() { return new int[] { 8, 9 }; }
+ public String[] getStringArray() { return new String[] { "d", "e" }; }
+ }
+
+ public void testAllReturnTypes() throws Throwable {
+ AllReturnTypes proxy = proxyFor(AllReturnTypes.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());
+ fakeHandler.setFakeResult(new int[] { -1, -2 });
+ assertEquals("[-1, -2]", Arrays.toString(proxy.getIntArray()));
+ fakeHandler.setFakeResult(new String[] { "x", "y" });
+ assertEquals("[x, y]", Arrays.toString(proxy.getStringArray()));
+ }
+
+ public static class PassThroughAllTypes {
+ 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 {
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return ProxyBuilder.callSuper(proxy, method, args);
+ }
+ }
+
+ public void testPassThroughWorksForAllTypes() throws Exception {
+ PassThroughAllTypes proxy = proxyFor(PassThroughAllTypes.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 ExtendsAllReturnTypes extends AllReturnTypes {
+ public int example() { return 0; }
+ }
+
+ public void testProxyWorksForSuperclassMethodsAlso() throws Throwable {
+ ExtendsAllReturnTypes proxy = proxyFor(ExtendsAllReturnTypes.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() {
+ 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(DexMakerTest.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$int"));
+ }
+
+ public static class PrintsOddAndValue {
+ public String method(int value) {
+ return "odd " + value;
+ }
+ }
+
+ public void testSometimesDelegateToSuper() throws Exception {
+ InvocationHandler delegatesOddValues = new InvocationHandler() {
+ 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 void testCallSuperThrows() throws Exception {
+ InvocationHandler handler = new InvocationHandler() {
+ public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+ return ProxyBuilder.callSuper(o, method, objects);
+ }
+ };
+
+ FooThrows fooThrows = proxyFor(FooThrows.class)
+ .handler(handler)
+ .build();
+
+ try {
+ fooThrows.foo();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("boom!", expected.getMessage());
+ }
+ }
+
+ public static class FooThrows {
+ public void foo() {
+ throw new IllegalStateException("boom!");
+ }
+ }
+
+ 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(ProxyForIllegalCacheDirectory.class)
+ .dexCache(new File("/poop/"))
+ .build();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ public static class ProxyForIllegalCacheDirectory {
+ }
+
+ 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() throws Exception {
+ SimpleClass a = proxyFor(SimpleClass.class).build();
+ SimpleClass b = proxyFor(SimpleClass.class).build();
+ assertSame(a.getClass(), b.getClass());
+ }
+
+ public void testCachingWithMultipleConstructors() throws Exception {
+ HasMultipleConstructors a = ProxyBuilder.forClass(HasMultipleConstructors.class)
+ .constructorArgTypes()
+ .constructorArgValues()
+ .handler(fakeHandler)
+ .dexCache(DexMakerTest.getDataDirectory()).build();
+ assertEquals("no args", a.calledConstructor);
+ HasMultipleConstructors b = ProxyBuilder.forClass(HasMultipleConstructors.class)
+ .constructorArgTypes(int.class)
+ .constructorArgValues(2)
+ .handler(fakeHandler)
+ .dexCache(DexMakerTest.getDataDirectory()).build();
+ assertEquals("int 2", b.calledConstructor);
+ assertEquals(a.getClass(), b.getClass());
+
+ HasMultipleConstructors c = ProxyBuilder.forClass(HasMultipleConstructors.class)
+ .constructorArgTypes(Integer.class)
+ .constructorArgValues(3)
+ .handler(fakeHandler)
+ .dexCache(DexMakerTest.getDataDirectory()).build();
+ assertEquals("Integer 3", c.calledConstructor);
+ assertEquals(a.getClass(), c.getClass());
+ }
+
+ public static class HasMultipleConstructors {
+ private final String calledConstructor;
+ public HasMultipleConstructors() {
+ calledConstructor = "no args";
+ }
+ public HasMultipleConstructors(int b) {
+ calledConstructor = "int " + b;
+ }
+ public HasMultipleConstructors(Integer c) {
+ calledConstructor = "Integer " + c;
+ }
+ }
+
+ public void testClassNotCachedWithDifferentParentClassLoaders() throws Exception {
+ ClassLoader classLoaderA = newPathClassLoader();
+ SimpleClass a = proxyFor(SimpleClass.class)
+ .parentClassLoader(classLoaderA)
+ .build();
+ assertEquals(classLoaderA, a.getClass().getClassLoader().getParent());
+
+ ClassLoader classLoaderB = newPathClassLoader();
+ SimpleClass b = proxyFor(SimpleClass.class)
+ .parentClassLoader(classLoaderB)
+ .build();
+ assertEquals(classLoaderB, b.getClass().getClassLoader().getParent());
+
+ assertTrue(a.getClass() != b.getClass());
+ }
+
+ public void testAbstractClassWithUndeclaredInterfaceMethod() throws Throwable {
+ DeclaresInterface declaresInterface = proxyFor(DeclaresInterface.class)
+ .build();
+ assertEquals("fake result", declaresInterface.call());
+ try {
+ ProxyBuilder.callSuper(declaresInterface, Callable.class.getMethod("call"));
+ fail();
+ } catch (AbstractMethodError expected) {
+ }
+ }
+
+ public static abstract class DeclaresInterface implements Callable<String> {
+ }
+
+ public void testImplementingInterfaces() throws Throwable {
+ SimpleClass simpleClass = proxyFor(SimpleClass.class)
+ .implementing(Callable.class)
+ .implementing(Comparable.class)
+ .build();
+ assertEquals("fake result", simpleClass.simpleMethod());
+
+ Callable<?> asCallable = (Callable<?>) simpleClass;
+ assertEquals("fake result", asCallable.call());
+
+ Comparable<?> asComparable = (Comparable<?>) simpleClass;
+ fakeHandler.fakeResult = 3;
+ assertEquals(3, asComparable.compareTo(null));
+ }
+
+ public void testCallSuperWithInterfaceMethod() throws Throwable {
+ SimpleClass simpleClass = proxyFor(SimpleClass.class)
+ .implementing(Callable.class)
+ .build();
+ try {
+ ProxyBuilder.callSuper(simpleClass, Callable.class.getMethod("call"));
+ fail();
+ } catch (AbstractMethodError expected) {
+ } catch (NoSuchMethodError expected) {
+ }
+ }
+
+ public void testImplementInterfaceCallingThroughConcreteClass() throws Throwable {
+ InvocationHandler invocationHandler = new InvocationHandler() {
+ public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+ assertEquals("a", ProxyBuilder.callSuper(o, method, objects));
+ return "b";
+ }
+ };
+ ImplementsCallable proxy = proxyFor(ImplementsCallable.class)
+ .implementing(Callable.class)
+ .handler(invocationHandler)
+ .build();
+ assertEquals("b", proxy.call());
+ assertEquals("a", ProxyBuilder.callSuper(
+ proxy, ImplementsCallable.class.getMethod("call")));
+ }
+
+ /**
+ * This test is a bit unintuitive because it exercises the synthetic methods
+ * that support covariant return types. Calling 'Object call()' on the
+ * interface bridges to 'String call()', and so the super method appears to
+ * also be proxied.
+ */
+ public void testImplementInterfaceCallingThroughInterface() throws Throwable {
+ final AtomicInteger count = new AtomicInteger();
+
+ InvocationHandler invocationHandler = new InvocationHandler() {
+ public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+ count.incrementAndGet();
+ return ProxyBuilder.callSuper(o, method, objects);
+ }
+ };
+
+ Callable<?> proxy = proxyFor(ImplementsCallable.class)
+ .implementing(Callable.class)
+ .handler(invocationHandler)
+ .build();
+
+ // the invocation handler is called twice!
+ assertEquals("a", proxy.call());
+ assertEquals(2, count.get());
+
+ // the invocation handler is called, even though this is a callSuper() call!
+ assertEquals("a", ProxyBuilder.callSuper(proxy, Callable.class.getMethod("call")));
+ assertEquals(3, count.get());
+ }
+
+ public static class ImplementsCallable implements Callable<String> {
+ public String call() throws Exception {
+ return "a";
+ }
+ }
+
+ /**
+ * This test shows that our generated proxies follow the bytecode convention
+ * where methods can have the same name but unrelated return types. This is
+ * different from javac's convention where return types must be assignable
+ * in one direction or the other.
+ */
+ public void testInterfacesSameNamesDifferentReturnTypes() throws Throwable {
+ InvocationHandler handler = new InvocationHandler() {
+ public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
+ if (method.getReturnType() == void.class) {
+ return null;
+ } else if (method.getReturnType() == String.class) {
+ return "X";
+ } else if (method.getReturnType() == int.class) {
+ return 3;
+ } else {
+ throw new AssertionFailedError();
+ }
+ }
+ };
+
+ Object o = proxyFor(Object.class)
+ .implementing(FooReturnsVoid.class, FooReturnsString.class, FooReturnsInt.class)
+ .handler(handler)
+ .build();
+
+ FooReturnsVoid a = (FooReturnsVoid) o;
+ a.foo();
+
+ FooReturnsString b = (FooReturnsString) o;
+ assertEquals("X", b.foo());
+
+ FooReturnsInt c = (FooReturnsInt) o;
+ assertEquals(3, c.foo());
+ }
+
+ public void testInterfacesSameNamesSameReturnType() throws Throwable {
+ Object o = proxyFor(Object.class)
+ .implementing(FooReturnsInt.class, FooReturnsInt2.class)
+ .build();
+
+ fakeHandler.setFakeResult(3);
+
+ FooReturnsInt a = (FooReturnsInt) o;
+ assertEquals(3, a.foo());
+
+ FooReturnsInt2 b = (FooReturnsInt2) o;
+ assertEquals(3, b.foo());
+ }
+
+ public interface FooReturnsVoid {
+ void foo();
+ }
+
+ public interface FooReturnsString {
+ String foo();
+ }
+
+ public interface FooReturnsInt {
+ int foo();
+ }
+
+ public interface FooReturnsInt2 {
+ int foo();
+ }
+
+ private ClassLoader newPathClassLoader() throws Exception {
+ return (ClassLoader) Class.forName("dalvik.system.PathClassLoader")
+ .getConstructor(String.class, ClassLoader.class)
+ .newInstance("", getClass().getClassLoader());
+
+ }
+
+ public void testSubclassOfRandom() throws Exception {
+ proxyFor(Random.class)
+ .handler(new InvokeSuperHandler())
+ .build();
+ }
+
+ public static class FinalToString {
+ @Override public final String toString() {
+ return "no proxy";
+ }
+ }
+
+ // https://code.google.com/p/dexmaker/issues/detail?id=12
+ public void testFinalToString() throws Throwable {
+ assertEquals("no proxy", proxyFor(FinalToString.class).build().toString());
+ }
+
+ public static class FinalInterfaceImpl implements FooReturnsString {
+ @Override public final String foo() {
+ return "no proxy";
+ }
+ }
+
+ public static class ExtenstionOfFinalInterfaceImpl extends FinalInterfaceImpl
+ implements FooReturnsString {
+ }
+
+ public void testFinalInterfaceImpl() throws Throwable {
+ assertEquals("no proxy", proxyFor(ExtenstionOfFinalInterfaceImpl.class).build().foo());
+ }
+
+ /** 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(DexMakerTest.getDataDirectory());
+ }
+
+ private static class FakeInvocationHandler implements InvocationHandler {
+ private Object fakeResult = "fake result";
+
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ return fakeResult;
+ }
+
+ public void setFakeResult(Object result) {
+ fakeResult = result;
+ }
+ }
+
+ public static class TestOrderingClass {
+ public int returnsInt() {
+ return 0;
+ }
+
+ public int returnsInt(int param1, int param2) {
+ return 1;
+ }
+
+ public String returnsString() {
+ return "string";
+ }
+
+ public boolean returnsBoolean() {
+ return false;
+ }
+
+ public double returnsDouble() {
+ return 1.0;
+ }
+
+ public Object returnsObject() {
+ return new Object();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testMethodsGeneratedInDeterministicOrder() throws Exception {
+ // Grab the static methods array from the original class.
+ Method[] methods1 = getMethodsForProxyClass(TestOrderingClass.class);
+ assertNotNull(methods1);
+
+ // Clear ProxyBuilder's in-memory cache of classes. This will force
+ // it to rebuild the class and reset the static methods field.
+ Map<Class<?>, Class<?>> map = getGeneratedProxyClasses();
+ assertNotNull(map);
+ map.clear();
+
+ // Grab the static methods array from the rebuilt class.
+ Method[] methods2 = getMethodsForProxyClass(TestOrderingClass.class);
+ assertNotNull(methods2);
+
+ // Ensure that the two method arrays are equal.
+ assertTrue(Arrays.equals(methods1, methods2));
+ }
+
+ public void testOrderingClassWithDexMakerCaching() throws Exception {
+ doTestOrderClassWithDexMakerCaching();
+
+ // Force ProxyBuilder to call DexMaker.generateAndLoad()
+ getGeneratedProxyClasses().clear();
+
+ doTestOrderClassWithDexMakerCaching();
+ }
+
+ private void doTestOrderClassWithDexMakerCaching() throws Exception {
+ TestOrderingClass proxy = ProxyBuilder.forClass(TestOrderingClass.class)
+ .handler(new InvokeSuperHandler())
+ .dexCache(DexMakerTest.getDataDirectory())
+ .build();
+ assertEquals(0, proxy.returnsInt());
+ assertEquals(1, proxy.returnsInt(1, 1));
+ assertEquals("string", proxy.returnsString());
+ assertFalse(proxy.returnsBoolean());
+ assertEquals(1.0, proxy.returnsDouble());
+ assertNotNull(proxy.returnsObject());
+ assertEquals(2, versionedDxDir.listFiles().length);
+ }
+
+ // Returns static methods array from a proxy class.
+ private Method[] getMethodsForProxyClass(Class<?> parentClass) throws Exception {
+ Class<?> proxyClass = proxyFor(parentClass).buildProxyClass();
+ Method[] methods = null;
+ for (Field f : proxyClass.getDeclaredFields()) {
+ if (Method[].class.isAssignableFrom(f.getType())) {
+ f.setAccessible(true);
+ methods = (Method[]) f.get(null);
+ break;
+ }
+ }
+
+ return methods;
+ }
+
+ private Map<Class<?>, Class<?>> getGeneratedProxyClasses() throws Exception {
+ Field mapField = ProxyBuilder.class
+ .getDeclaredField("generatedProxyClasses");
+ mapField.setAccessible(true);
+ return (Map<Class<?>, Class<?>>) mapField.get(null);
+ }
+
+ public static class ConcreteClassA implements FooReturnsInt {
+ // from FooReturnsInt
+ public int foo() {
+ return 1;
+ }
+
+ // not from FooReturnsInt
+ public String bar() {
+ return "bar";
+ }
+ }
+
+ public static class ConcreteClassB implements FooReturnsInt {
+ // from FooReturnsInt
+ public int foo() {
+ return 0;
+ }
+
+ // not from FooReturnsInt
+ public String bar() {
+ return "bahhr";
+ }
+ }
+
+ public void testTwoClassesWithIdenticalMethodSignatures_DexMakerCaching() throws Exception {
+ ConcreteClassA proxyA = ProxyBuilder.forClass(ConcreteClassA.class)
+ .handler(new InvokeSuperHandler())
+ .dexCache(DexMakerTest.getDataDirectory())
+ .build();
+ assertEquals(1, proxyA.foo());
+ assertEquals("bar", proxyA.bar());
+ assertEquals(2, versionedDxDir.listFiles().length);
+
+ ConcreteClassB proxyB = ProxyBuilder.forClass(ConcreteClassB.class)
+ .handler(new InvokeSuperHandler())
+ .dexCache(DexMakerTest.getDataDirectory())
+ .build();
+ assertEquals(0, proxyB.foo());
+ assertEquals("bahhr", proxyB.bar());
+ assertEquals(4, versionedDxDir.listFiles().length);
+ }
+}