• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.dexmaker;
18 
19 import com.android.dx.dex.DexFormat;
20 import com.android.dx.dex.DexOptions;
21 import com.android.dx.dex.code.DalvCode;
22 import com.android.dx.dex.code.PositionList;
23 import com.android.dx.dex.code.RopTranslator;
24 import com.android.dx.dex.file.ClassDefItem;
25 import com.android.dx.dex.file.DexFile;
26 import com.android.dx.dex.file.EncodedField;
27 import com.android.dx.dex.file.EncodedMethod;
28 import com.android.dx.rop.code.AccessFlags;
29 import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
30 import com.android.dx.rop.code.LocalVariableInfo;
31 import com.android.dx.rop.code.RopMethod;
32 import com.android.dx.rop.cst.CstString;
33 import com.android.dx.rop.cst.CstType;
34 import com.android.dx.rop.type.StdTypeList;
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.lang.reflect.InvocationTargetException;
39 import java.lang.reflect.Modifier;
40 import static java.lang.reflect.Modifier.PRIVATE;
41 import static java.lang.reflect.Modifier.STATIC;
42 
43 import java.util.Arrays;
44 import java.util.Iterator;
45 import java.util.LinkedHashMap;
46 import java.util.Map;
47 import java.util.Set;
48 import java.util.jar.JarEntry;
49 import java.util.jar.JarOutputStream;
50 
51 /**
52  * Generates a </i><strong>D</strong>alvik <strong>EX</strong>ecutable (dex)
53  * file for execution on Android. Dex files define classes and interfaces,
54  * including their member methods and fields, executable code, and debugging
55  * information. They also define annotations, though this API currently has no
56  * facility to create a dex file that contains annotations.
57  *
58  * <p>This library is intended to satisfy two use cases:
59  * <ul>
60  *   <li><strong>For runtime code generation.</strong> By embedding this library
61  *       in your Android application, you can dynamically generate and load
62  *       executable code. This approach takes advantage of the fact that the
63  *       host environment and target environment are both Android.
64  *   <li><strong>For compile time code generation.</strong> You may use this
65  *       library as a part of a compiler that targets Android. In this scenario
66  *       the generated dex file must be installed on an Android device before it
67  *       can be executed.
68  * </ul>
69  *
70  * <h3>Example: Fibonacci</h3>
71  * To illustrate how this API is used, we'll use DexMaker to generate a class
72  * equivalent to the following Java source: <pre> {@code
73  *
74  * package com.publicobject.fib;
75  *
76  * public class Fibonacci {
77  *   public static int fib(int i) {
78  *     if (i < 2) {
79  *       return i;
80  *     }
81  *     return fib(i - 1) + fib(i - 2);
82  *   }
83  * }}</pre>
84  *
85  * <p>We start by creating a {@link TypeId} to identify the generated {@code
86  * Fibonacci} class. DexMaker identifies types by their internal names like
87  * {@code Ljava/lang/Object;} rather than their Java identifiers like {@code
88  * java.lang.Object}. <pre>   {@code
89  *
90  *   TypeId<?> fibonacci = TypeId.get("Lcom/google/dexmaker/examples/Fibonacci;");
91  * }</pre>
92  *
93  * <p>Next we declare the class. It allows us to specify the type's source file
94  * for stack traces, its modifiers, its superclass, and the interfaces it
95  * implements. In this case, {@code Fibonacci} is a public class that extends
96  * from {@code Object}: <pre>   {@code
97  *
98  *   String fileName = "Fibonacci.generated";
99  *   DexMaker dexMaker = new DexMaker();
100  *   dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
101  * }</pre>
102  * It is illegal to declare members of a class without also declaring the class
103  * itself.
104  *
105  * <p>To make it easier to go from our Java method to dex instructions, we'll
106  * manually translate it to pseudocode fit for an assembler. We need to replace
107  * control flow like {@code if()} blocks and {@code for()} loops with labels and
108  * branches. We'll also avoid performing multiple operations in one statement,
109  * using local variables to hold intermediate values as necessary:
110  * <pre>   {@code
111  *
112  *   int constant1 = 1;
113  *   int constant2 = 2;
114  *   if (i < constant2) goto baseCase;
115  *   int a = i - constant1;
116  *   int b = i - constant2;
117  *   int c = fib(a);
118  *   int d = fib(b);
119  *   int result = c + d;
120  *   return result;
121  * baseCase:
122  *   return i;
123  * }</pre>
124  *
125  * <p>We look up the {@code MethodId} for the method on the declaring type. This
126  * takes the method's return type (possibly {@link TypeId#VOID}), its name and
127  * its parameters types. Next we declare the method, specifying its modifiers by
128  * bitwise ORing constants from {@link java.lang.reflect.Modifier}. The declare
129  * call returns a {@link Code} object, which we'll use to define the method's
130  * instructions. <pre>   {@code
131  *
132  *   MethodId<?, Integer> fib = fibonacci.getMethod(TypeId.INT, "fib", TypeId.INT);
133  *   Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
134  * }</pre>
135  *
136  * <p>One limitation of {@code DexMaker}'s API is that it requires all local
137  * variables to be created before any instructions are emitted. Use {@link
138  * Code#newLocal newLocal()} to create a new local variable. The method's
139  * parameters are exposed as locals using {@link Code#getParameter
140  * getParameter()}. For non-static methods the {@code this} pointer is exposed
141  * using {@link Code#getThis getThis()}. Here we declare all of the local
142  * variables that we'll need for our {@code fib()} method: <pre>   {@code
143  *
144  *   Local<Integer> i = code.getParameter(0, TypeId.INT);
145  *   Local<Integer> constant1 = code.newLocal(TypeId.INT);
146  *   Local<Integer> constant2 = code.newLocal(TypeId.INT);
147  *   Local<Integer> a = code.newLocal(TypeId.INT);
148  *   Local<Integer> b = code.newLocal(TypeId.INT);
149  *   Local<Integer> c = code.newLocal(TypeId.INT);
150  *   Local<Integer> d = code.newLocal(TypeId.INT);
151  *   Local<Integer> result = code.newLocal(TypeId.INT);
152  * }</pre>
153  *
154  * <p>Notice that {@link Local} has a type parameter of {@code Integer}. This is
155  * useful for generating code that works with existing types like {@code String}
156  * and {@code Integer}, but it can be a hindrance when generating code that
157  * involves new types. For this reason you may prefer to use raw types only and
158  * add {@code @SuppressWarnings("unsafe")} on your calling code. This will yield
159  * the same result but you won't get IDE support if you make a type error.
160  *
161  * <p>We're ready to start defining our method's instructions. The {@link Code}
162  * class catalogs the available instructions and their use. <pre>   {@code
163  *
164  *   code.loadConstant(constant1, 1);
165  *   code.loadConstant(constant2, 2);
166  *   Label baseCase = new Label();
167  *   code.compare(Comparison.LT, baseCase, i, constant2);
168  *   code.op(BinaryOp.SUBTRACT, a, i, constant1);
169  *   code.op(BinaryOp.SUBTRACT, b, i, constant2);
170  *   code.invokeStatic(fib, c, a);
171  *   code.invokeStatic(fib, d, b);
172  *   code.op(BinaryOp.ADD, result, c, d);
173  *   code.returnValue(result);
174  *   code.mark(baseCase);
175  *   code.returnValue(i);
176  * }</pre>
177  *
178  * <p>We're done defining the dex file. We just need to write it to the
179  * filesystem or load it into the current process. For this example we'll load
180  * the generated code into the current process. This only works when the current
181  * process is running on Android. We use {@link #generateAndLoad
182  * generateAndLoad()} which takes the class loader that will be used as our
183  * generated code's parent class loader. It also requires a directory where
184  * temporary files can be written. <pre>   {@code
185  *
186  *   ClassLoader loader = dexMaker.generateAndLoad(
187  *       FibonacciMaker.class.getClassLoader(), getDataDirectory());
188  * }</pre>
189  * Finally we'll use reflection to lookup our generated class on its class
190  * loader and invoke its {@code fib()} method: <pre>   {@code
191  *
192  *   Class<?> fibonacciClass = loader.loadClass("com.google.dexmaker.examples.Fibonacci");
193  *   Method fibMethod = fibonacciClass.getMethod("fib", int.class);
194  *   System.out.println(fibMethod.invoke(null, 8));
195  * }</pre>
196  */
197 public final class DexMaker {
198     private final Map<TypeId<?>, TypeDeclaration> types
199             = new LinkedHashMap<TypeId<?>, TypeDeclaration>();
200 
201     /**
202      * Creates a new {@code DexMaker} instance, which can be used to create a
203      * single dex file.
204      */
DexMaker()205     public DexMaker() {
206     }
207 
getTypeDeclaration(TypeId<?> type)208     private TypeDeclaration getTypeDeclaration(TypeId<?> type) {
209         TypeDeclaration result = types.get(type);
210         if (result == null) {
211             result = new TypeDeclaration(type);
212             types.put(type, result);
213         }
214         return result;
215     }
216 
217     /**
218      * Declares {@code type}.
219      *
220      * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
221      *     Modifier#FINAL} and {@link Modifier#ABSTRACT}.
222      */
declare(TypeId<?> type, String sourceFile, int flags, TypeId<?> supertype, TypeId<?>... interfaces)223     public void declare(TypeId<?> type, String sourceFile, int flags,
224             TypeId<?> supertype, TypeId<?>... interfaces) {
225         TypeDeclaration declaration = getTypeDeclaration(type);
226         int supportedFlags = Modifier.PUBLIC | Modifier.FINAL | Modifier.ABSTRACT;
227         if ((flags & ~supportedFlags) != 0) {
228             throw new IllegalArgumentException("Unexpected flag: "
229                     + Integer.toHexString(flags));
230         }
231         if (declaration.declared) {
232             throw new IllegalStateException("already declared: " + type);
233         }
234         declaration.declared = true;
235         declaration.flags = flags;
236         declaration.supertype = supertype;
237         declaration.sourceFile = sourceFile;
238         declaration.interfaces = new TypeList(interfaces);
239     }
240 
241     /**
242      * Declares a method or constructor.
243      *
244      * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
245      *     Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
246      *     {@link Modifier#FINAL} and {@link Modifier#SYNCHRONIZED}.
247      *     <p><strong>Warning:</strong> the {@link Modifier#SYNCHRONIZED} flag
248      *     is insufficient to generate a synchronized method. You must also use
249      *     {@link Code#monitorEnter} and {@link Code#monitorExit} to acquire
250      *     a monitor.
251      */
declare(MethodId<?, ?> method, int flags)252     public Code declare(MethodId<?, ?> method, int flags) {
253         TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
254         if (typeDeclaration.methods.containsKey(method)) {
255             throw new IllegalStateException("already declared: " + method);
256         }
257 
258         int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
259                 | Modifier.STATIC | Modifier.FINAL | Modifier.SYNCHRONIZED;
260         if ((flags & ~supportedFlags) != 0) {
261             throw new IllegalArgumentException("Unexpected flag: "
262                     + Integer.toHexString(flags));
263         }
264 
265         // replace the SYNCHRONIZED flag with the DECLARED_SYNCHRONIZED flag
266         if ((flags & Modifier.SYNCHRONIZED) != 0) {
267             flags = (flags & ~Modifier.SYNCHRONIZED) | AccessFlags.ACC_DECLARED_SYNCHRONIZED;
268         }
269 
270         if (method.isConstructor()) {
271             flags |= ACC_CONSTRUCTOR;
272         }
273 
274         MethodDeclaration methodDeclaration = new MethodDeclaration(method, flags);
275         typeDeclaration.methods.put(method, methodDeclaration);
276         return methodDeclaration.code;
277     }
278 
279     /**
280      * Declares a field.
281      *
282      * @param flags a bitwise combination of {@link Modifier#PUBLIC}, {@link
283      *     Modifier#PRIVATE}, {@link Modifier#PROTECTED}, {@link Modifier#STATIC},
284      *     {@link Modifier#FINAL}, {@link Modifier#VOLATILE}, and {@link
285      *     Modifier#TRANSIENT}.
286      * @param staticValue a constant representing the initial value for the
287      *     static field, possibly null. This must be null if this field is
288      *     non-static.
289      */
declare(FieldId<?, ?> fieldId, int flags, Object staticValue)290     public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
291         TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
292         if (typeDeclaration.fields.containsKey(fieldId)) {
293             throw new IllegalStateException("already declared: " + fieldId);
294         }
295 
296         int supportedFlags = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED
297                 | Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE | Modifier.TRANSIENT;
298         if ((flags & ~supportedFlags) != 0) {
299             throw new IllegalArgumentException("Unexpected flag: "
300                     + Integer.toHexString(flags));
301         }
302 
303         if ((flags & Modifier.STATIC) == 0 && staticValue != null) {
304             throw new IllegalArgumentException("staticValue is non-null, but field is not static");
305         }
306 
307         FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId, flags, staticValue);
308         typeDeclaration.fields.put(fieldId, fieldDeclaration);
309     }
310 
311     /**
312      * Generates a dex file and returns its bytes.
313      */
generate()314     public byte[] generate() {
315         DexOptions options = new DexOptions();
316         options.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
317         DexFile outputDex = new DexFile(options);
318 
319         for (TypeDeclaration typeDeclaration : types.values()) {
320             outputDex.add(typeDeclaration.toClassDefItem());
321         }
322 
323         try {
324             return outputDex.toDex(null, false);
325         } catch (IOException e) {
326             throw new RuntimeException(e);
327         }
328     }
329 
330     // Generate a file name for the jar by taking a checksum of MethodIds and
331     // parent class types.
generateFileName()332     private String generateFileName() {
333         int checksum = 1;
334 
335         Set<TypeId<?>> typesKeySet = types.keySet();
336         Iterator<TypeId<?>> it = typesKeySet.iterator();
337         int[] checksums = new int[typesKeySet.size()];
338         int i = 0;
339 
340         while (it.hasNext()) {
341             TypeId<?> typeId = it.next();
342             TypeDeclaration decl = getTypeDeclaration(typeId);
343             Set<MethodId> methodSet = decl.methods.keySet();
344             if (decl.supertype != null) {
345                 checksums[i++] = 31 * decl.supertype.hashCode() + methodSet.hashCode();
346             }
347         }
348         Arrays.sort(checksums);
349 
350         for (int sum : checksums) {
351             checksum *= 31;
352             checksum += sum;
353         }
354 
355         return "Generated_" + checksum +".jar";
356     }
357 
generateClassLoader(File result, File dexCache, ClassLoader parent)358     private ClassLoader generateClassLoader(File result, File dexCache, ClassLoader parent) {
359         try {
360             return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
361                     .getConstructor(String.class, String.class, String.class, ClassLoader.class)
362                     .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
363         } catch (ClassNotFoundException e) {
364             throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
365         } catch (InvocationTargetException e) {
366             throw new RuntimeException(e.getCause());
367         } catch (InstantiationException e) {
368             throw new AssertionError();
369         } catch (NoSuchMethodException e) {
370             throw new AssertionError();
371         } catch (IllegalAccessException e) {
372             throw new AssertionError();
373         }
374     }
375 
376     /**
377      * Generates a dex file and loads its types into the current process.
378      *
379      * <h3>Picking a dex cache directory</h3>
380      * The {@code dexCache} should be an application-private directory. If
381      * you pass a world-writable directory like {@code /sdcard} a malicious app
382      * could inject code into your process. Most applications should use this:
383      * <pre>   {@code
384      *
385      *     File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
386      * }</pre>
387      * If the {@code dexCache} is null, this method will consult the {@code
388      * dexmaker.dexcache} system property. If that exists, it will be used for
389      * the dex cache. If it doesn't exist, this method will attempt to guess
390      * the application's private data directory as a last resort. If that fails,
391      * this method will fail with an unchecked exception. You can avoid the
392      * exception by either providing a non-null value or setting the system
393      * property.
394      *
395      * @param parent the parent ClassLoader to be used when loading our
396      *     generated types
397      * @param dexCache the destination directory where generated and optimized
398      *     dex files will be written. If null, this class will try to guess the
399      *     application's private data dir.
400      */
generateAndLoad(ClassLoader parent, File dexCache)401     public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
402         if (dexCache == null) {
403             String property = System.getProperty("dexmaker.dexcache");
404             if (property != null) {
405                 dexCache = new File(property);
406             } else {
407                 dexCache = new AppDataDirGuesser().guess();
408                 if (dexCache == null) {
409                     throw new IllegalArgumentException("dexcache == null (and no default could be"
410                             + " found; consider setting the 'dexmaker.dexcache' system property)");
411                 }
412             }
413         }
414 
415         File result = new File(dexCache, generateFileName());
416         // Check that the file exists. If it does, return a DexClassLoader and skip all
417         // the dex bytecode generation.
418         if (result.exists()) {
419             return generateClassLoader(result, dexCache, parent);
420         }
421 
422         byte[] dex = generate();
423 
424         /*
425          * This implementation currently dumps the dex to the filesystem. It
426          * jars the emitted .dex for the benefit of Gingerbread and earlier
427          * devices, which can't load .dex files directly.
428          *
429          * TODO: load the dex from memory where supported.
430          */
431         result.createNewFile();
432         JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
433         JarEntry entry = new JarEntry(DexFormat.DEX_IN_JAR_NAME);
434         entry.setSize(dex.length);
435         jarOut.putNextEntry(entry);
436         jarOut.write(dex);
437         jarOut.closeEntry();
438         jarOut.close();
439         return generateClassLoader(result, dexCache, parent);
440     }
441 
442     private static class TypeDeclaration {
443         private final TypeId<?> type;
444 
445         /** declared state */
446         private boolean declared;
447         private int flags;
448         private TypeId<?> supertype;
449         private String sourceFile;
450         private TypeList interfaces;
451 
452         private final Map<FieldId, FieldDeclaration> fields
453                 = new LinkedHashMap<FieldId, FieldDeclaration>();
454         private final Map<MethodId, MethodDeclaration> methods
455                 = new LinkedHashMap<MethodId, MethodDeclaration>();
456 
TypeDeclaration(TypeId<?> type)457         TypeDeclaration(TypeId<?> type) {
458             this.type = type;
459         }
460 
toClassDefItem()461         ClassDefItem toClassDefItem() {
462             if (!declared) {
463                 throw new IllegalStateException("Undeclared type " + type + " declares members: "
464                         + fields.keySet() + " " + methods.keySet());
465             }
466 
467             DexOptions dexOptions = new DexOptions();
468             dexOptions.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
469 
470             CstType thisType = type.constant;
471 
472             ClassDefItem out = new ClassDefItem(thisType, flags, supertype.constant,
473                     interfaces.ropTypes, new CstString(sourceFile));
474 
475             for (MethodDeclaration method : methods.values()) {
476                 EncodedMethod encoded = method.toEncodedMethod(dexOptions);
477                 if (method.isDirect()) {
478                     out.addDirectMethod(encoded);
479                 } else {
480                     out.addVirtualMethod(encoded);
481                 }
482             }
483             for (FieldDeclaration field : fields.values()) {
484                 EncodedField encoded = field.toEncodedField();
485                 if (field.isStatic()) {
486                     out.addStaticField(encoded, Constants.getConstant(field.staticValue));
487                 } else {
488                     out.addInstanceField(encoded);
489                 }
490             }
491 
492             return out;
493         }
494     }
495 
496     static class FieldDeclaration {
497         final FieldId<?, ?> fieldId;
498         private final int accessFlags;
499         private final Object staticValue;
500 
FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue)501         FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue) {
502             if ((accessFlags & STATIC) == 0 && staticValue != null) {
503                 throw new IllegalArgumentException("instance fields may not have a value");
504             }
505             this.fieldId = fieldId;
506             this.accessFlags = accessFlags;
507             this.staticValue = staticValue;
508         }
509 
toEncodedField()510         EncodedField toEncodedField() {
511             return new EncodedField(fieldId.constant, accessFlags);
512         }
513 
isStatic()514         public boolean isStatic() {
515             return (accessFlags & STATIC) != 0;
516         }
517     }
518 
519     static class MethodDeclaration {
520         final MethodId<?, ?> method;
521         private final int flags;
522         private final Code code;
523 
MethodDeclaration(MethodId<?, ?> method, int flags)524         public MethodDeclaration(MethodId<?, ?> method, int flags) {
525             this.method = method;
526             this.flags = flags;
527             this.code = new Code(this);
528         }
529 
isStatic()530         boolean isStatic() {
531             return (flags & STATIC) != 0;
532         }
533 
isDirect()534         boolean isDirect() {
535             return (flags & (STATIC | PRIVATE | ACC_CONSTRUCTOR)) != 0;
536         }
537 
toEncodedMethod(DexOptions dexOptions)538         EncodedMethod toEncodedMethod(DexOptions dexOptions) {
539             RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
540             LocalVariableInfo locals = null;
541             DalvCode dalvCode = RopTranslator.translate(
542                     ropMethod, PositionList.NONE, locals, code.paramSize(), dexOptions);
543             return new EncodedMethod(method.constant, flags, dalvCode, StdTypeList.EMPTY);
544         }
545     }
546 }
547