DexMaker.java revision ff314e1f06b974be78de3356a71072dbf0a450cd
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
17package com.google.dexmaker;
18
19import com.android.dx.dex.DexFormat;
20import com.android.dx.dex.DexOptions;
21import com.android.dx.dex.code.DalvCode;
22import com.android.dx.dex.code.PositionList;
23import com.android.dx.dex.code.RopTranslator;
24import com.android.dx.dex.file.ClassDefItem;
25import com.android.dx.dex.file.DexFile;
26import com.android.dx.dex.file.EncodedField;
27import com.android.dx.dex.file.EncodedMethod;
28import com.android.dx.rop.code.AccessFlags;
29import static com.android.dx.rop.code.AccessFlags.ACC_CONSTRUCTOR;
30import com.android.dx.rop.code.LocalVariableInfo;
31import com.android.dx.rop.code.RopMethod;
32import com.android.dx.rop.cst.CstString;
33import com.android.dx.rop.cst.CstType;
34import com.android.dx.rop.type.StdTypeList;
35import java.io.File;
36import java.io.FileOutputStream;
37import java.io.IOException;
38import java.lang.reflect.InvocationTargetException;
39import static java.lang.reflect.Modifier.PRIVATE;
40import static java.lang.reflect.Modifier.STATIC;
41import java.util.LinkedHashMap;
42import java.util.Map;
43import java.util.jar.JarEntry;
44import java.util.jar.JarOutputStream;
45
46/**
47 * Generates a </i><strong>D</strong>alvik <strong>EX</strong>ecutable (dex)
48 * file for execution on Android. dex files defines classes and interfaces,
49 * including their member methods and fields, executable code, and debugging
50 * information. They also define annotations, though this API currently has no
51 * facility to create a dex file that contains annotations.
52 *
53 * <p>This library is intended to satisfy two use cases:
54 * <ul>
55 *   <li><strong>For runtime code generation.</strong> By embedding this library
56 *       in your Android application, you can dynamically generate and load
57 *       executable code. This approach takes advantage of the fact that the
58 *       host environment and target environment are both Android.
59 *   <li><strong>For compile time code generation.</strong> You may use this
60 *       library as a part of a compiler that targets Android. In this scenario
61 *       the generated dex file must be installed on an Android device before it
62 *       can be executed.
63 * </ul>
64 *
65 * <h3>Example: Fibonacci</h3>
66 * To illustrate how this API is used, we'll use DexMaker to generate a class
67 * equivalent to the following Java source: <pre>   {@code
68 *
69 * package com.publicobject.fib;
70 *
71 * public class Fibonacci {
72 *   public static int fib(int i) {
73 *     if (i < 2) {
74 *       return i;
75 *     }
76 *     return fib(i - 1) + fib(i - 2);
77 *   }
78 * }}</pre>
79 *
80 * <p>We start by creating a {@link TypeId} to identify the generated {@code
81 * Fibonacci} class. DexMaker identifies types by their internal names like
82 * {@code Ljava/lang/Object;} rather than their Java identifiers like {@code
83 * java.lang.Object}. <pre>   {@code
84 *
85 *   TypeId<?> fibonacci = TypeId.get("Lcom/google/dexmaker/examples/Fibonacci;");
86 * }</pre>
87 *
88 * <p>Next we declare the class. It allows us to specify the type's source file
89 * for stack traces, its modifiers, its superclass, and the interfaces it
90 * implements. In this case, {@code Fibonacci} is a public class that extends
91 * from {@code Object}: <pre>   {@code
92 *
93 *   String fileName = "Fibonacci.generated";
94 *   DexMaker dexMaker = new DexMaker();
95 *   dexMaker.declare(fibonacci, fileName, Modifier.PUBLIC, TypeId.OBJECT);
96 * }</pre>
97 * It is illegal to declare members of a class without also declaring the class
98 * itself.
99 *
100 * <p>To make it easier to go from our Java method to dex instructions, we'll
101 * manually translate it to pseudocode fit for an assembler. We need to replace
102 * control flow like {@code if()} blocks and {@code for()} loops with labels and
103 * branches. We'll also avoid performing multiple operations in one statement,
104 * using local variables to hold intermediate values as necessary:
105 * <pre>   {@code
106 *
107 *   int constant1 = 1;
108 *   int constant2 = 2;
109 *   if (i < constant2) goto baseCase;
110 *   int a = i - constant1;
111 *   int b = i - constant2;
112 *   int c = fib(a);
113 *   int d = fib(b);
114 *   int result = c + d;
115 *   return result;
116 * baseCase:
117 *   return i;
118 * }</pre>
119 *
120 * <p>We lookup the {@code MethodId} for the method on the declaring type. This
121 * takes the method's return type (possibly {@link TypeId#VOID}), its name, and
122 * its parameters. Next we declare the method, specifying its modifiers by ORing
123 * constants from {@link java.lang.reflect.Modifier}. The declare call returns a
124 * {@link Code} object, which we'll use to define the method's instructions.
125 * <pre>   {@code
126 *
127 *   MethodId<?, Integer> fib = fibonacci.getMethod(TypeId.INT, "fib", TypeId.INT);
128 *   Code code = dexMaker.declare(fib, Modifier.PUBLIC | Modifier.STATIC);
129 * }</pre>
130 *
131 * <p>One limitation of {@code DexMaker}'s API is that it requires all local
132 * variables to be created before any instructions are emitted. Use {@link
133 * Code#newLocal} to create a new local variable. The method's parameters are
134 * exposed as locals using {@link Code#getParameter}. For non-static methods the
135 * 'this' pointer is exposed using {@link Code#getThis}. Here we declare all of
136 * the local variables that we'll need for our {@code fib()} method:
137 * <pre>   {@code
138 *
139 *   Local<Integer> i = code.getParameter(0, TypeId.INT);
140 *   Local<Integer> constant1 = code.newLocal(TypeId.INT);
141 *   Local<Integer> constant2 = code.newLocal(TypeId.INT);
142 *   Local<Integer> a = code.newLocal(TypeId.INT);
143 *   Local<Integer> b = code.newLocal(TypeId.INT);
144 *   Local<Integer> c = code.newLocal(TypeId.INT);
145 *   Local<Integer> d = code.newLocal(TypeId.INT);
146 *   Local<Integer> result = code.newLocal(TypeId.INT);
147 * }</pre>
148 *
149 * <p>Notice that {@link Local} has a type parameter. This is useful for
150 * generating code that works with existing types like {@code String} and {@code
151 * Integer}, but it can be a hindrance when generating code that involves new
152 * types. For this reason you may prefer to use raw types only and add
153 * {@code @SuppressWarnings("unsafe")} on your calling code. This will yield the
154 * same result but you won't get a compiler warning if you make a type error.
155 *
156 * <p>We're ready to start defining our method's instructions: <pre>   {@code
157 *
158 *   code.loadConstant(constant1, 1);
159 *   code.loadConstant(constant2, 2);
160 *   Label baseCase = code.newLabel();
161 *   code.compare(Comparison.LT, baseCase, i, constant2);
162 *   code.op(BinaryOp.SUBTRACT, a, i, constant1);
163 *   code.op(BinaryOp.SUBTRACT, b, i, constant2);
164 *   code.invokeStatic(fib, c, a);
165 *   code.invokeStatic(fib, d, b);
166 *   code.op(BinaryOp.ADD, result, c, d);
167 *   code.returnValue(result);
168 *   code.mark(baseCase);
169 *   code.returnValue(i);
170 * }</pre>
171 *
172 * <p>We're done defining the instructions! We just need to write the dex to the
173 * file system or load it into the current process. For this example we'll load
174 * the generated code into the current process. This only works when the current
175 * process is running on Android. We use {@link #generateAndLoad} which takes
176 * the class loader that will be used as our generated code's parent class
177 * loader. It also requires a directory where temporary files can be written.
178 * <pre>   {@code
179 *
180 *   ClassLoader loader = dexMaker.generateAndLoad(
181 *       Fibonacci.class.getClassLoader(), getDataDirectory());
182 * }</pre>
183 * Finally we'll use reflection to lookup our generated class on its class
184 * loader and invoke its {@code fib()} method: <pre>   {@code
185 *
186 *   Class<?> fibonacciClass = loader.loadClass("com.google.dexmaker.examples.Fibonacci");
187 *   Method fibMethod = fibonacciClass.getMethod("fib", int.class);
188 *   System.out.println(fibMethod.invoke(null, 8));
189 * }</pre>
190 */
191public final class DexMaker {
192    private final Map<TypeId<?>, TypeDeclaration> types
193            = new LinkedHashMap<TypeId<?>, TypeDeclaration>();
194
195    private TypeDeclaration getTypeDeclaration(TypeId<?> type) {
196        TypeDeclaration result = types.get(type);
197        if (result == null) {
198            result = new TypeDeclaration(type);
199            types.put(type, result);
200        }
201        return result;
202    }
203
204    // TODO: describe the legal flags without referring to a non-public API AccessFlags
205
206    /**
207     * @param flags any flags masked by {@link com.android.dx.rop.code.AccessFlags#CLASS_FLAGS}.
208     */
209    public void declare(TypeId<?> type, String sourceFile, int flags,
210            TypeId<?> supertype, TypeId<?>... interfaces) {
211        TypeDeclaration declaration = getTypeDeclaration(type);
212        if (declaration.declared) {
213            throw new IllegalStateException("already declared: " + type);
214        }
215        declaration.declared = true;
216        declaration.flags = flags;
217        declaration.supertype = supertype;
218        declaration.sourceFile = sourceFile;
219        declaration.interfaces = new TypeList(interfaces);
220    }
221
222    /**
223     * @param flags any flags masked by {@link com.android.dx.rop.code.AccessFlags#METHOD_FLAGS}.
224     */
225    public Code declareConstructor(MethodId<?, ?> method, int flags) {
226        return declare(method, flags | ACC_CONSTRUCTOR);
227    }
228
229    /**
230     * @param flags any flags masked by {@link com.android.dx.rop.code.AccessFlags#METHOD_FLAGS}.
231     */
232    public Code declare(MethodId<?, ?> method, int flags) {
233        TypeDeclaration typeDeclaration = getTypeDeclaration(method.declaringType);
234        if (typeDeclaration.methods.containsKey(method)) {
235            throw new IllegalStateException("already declared: " + method);
236        }
237        MethodDeclaration methodDeclaration = new MethodDeclaration(method, flags);
238        typeDeclaration.methods.put(method, methodDeclaration);
239        return methodDeclaration.code;
240    }
241
242    /**
243     * @param flags any flags masked by {@link AccessFlags#FIELD_FLAGS}.
244     */
245    public void declare(FieldId<?, ?> fieldId, int flags, Object staticValue) {
246        TypeDeclaration typeDeclaration = getTypeDeclaration(fieldId.declaringType);
247        if (typeDeclaration.fields.containsKey(fieldId)) {
248            throw new IllegalStateException("already declared: " + fieldId);
249        }
250        FieldDeclaration fieldDeclaration = new FieldDeclaration(fieldId, flags, staticValue);
251        typeDeclaration.fields.put(fieldId, fieldDeclaration);
252    }
253
254    /**
255     * Returns a .dex formatted file.
256     */
257    public byte[] generate() {
258        DexOptions options = new DexOptions();
259        options.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
260        DexFile outputDex = new DexFile(options);
261
262        for (TypeDeclaration typeDeclaration : types.values()) {
263            outputDex.add(typeDeclaration.toClassDefItem());
264        }
265
266        try {
267            return outputDex.toDex(null, false);
268        } catch (IOException e) {
269            throw new RuntimeException(e);
270        }
271    }
272
273    /**
274     * Loads the generated types into the current process.
275     *
276     * <p>All parameters are optional, you may pass {@code null} and suitable
277     * defaults will be used.
278     *
279     * <p>If you opt to provide your own output directories, take care to
280     * ensure that they are not world-readable, otherwise a malicious app will
281     * be able to inject code to run.  A suitable parameter for these output
282     * directories would be something like this:
283     * {@code getApplicationContext().getDir("dx", Context.MODE_PRIVATE); }
284     *
285     * @param parent the parent ClassLoader to be used when loading
286     *     our generated types
287     * @param dexDir the destination directory where generated and
288     *     optimized dex files will be written.
289     */
290    public ClassLoader generateAndLoad(ClassLoader parent, File dexDir) throws IOException {
291        byte[] dex = generate();
292
293        /*
294         * This implementation currently dumps the dex to the filesystem. It
295         * jars the emitted .dex for the benefit of Gingerbread and earlier
296         * devices, which can't load .dex files directly.
297         *
298         * TODO: load the dex from memory where supported.
299         */
300        File result = File.createTempFile("Generated", ".jar", dexDir);
301        result.deleteOnExit();
302        JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
303        jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
304        jarOut.write(dex);
305        jarOut.closeEntry();
306        jarOut.close();
307        try {
308            return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
309                    .getConstructor(String.class, String.class, String.class, ClassLoader.class)
310                    .newInstance(result.getPath(), dexDir.getAbsolutePath(), null, parent);
311        } catch (ClassNotFoundException e) {
312            throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
313        } catch (InvocationTargetException e) {
314            throw new RuntimeException(e.getCause());
315        } catch (InstantiationException e) {
316            throw new AssertionError();
317        } catch (NoSuchMethodException e) {
318            throw new AssertionError();
319        } catch (IllegalAccessException e) {
320            throw new AssertionError();
321        }
322    }
323
324    private static class TypeDeclaration {
325        private final TypeId<?> type;
326
327        /** declared state */
328        private boolean declared;
329        private int flags;
330        private TypeId<?> supertype;
331        private String sourceFile;
332        private TypeList interfaces;
333
334        private final Map<FieldId, FieldDeclaration> fields
335                = new LinkedHashMap<FieldId, FieldDeclaration>();
336        private final Map<MethodId, MethodDeclaration> methods
337                = new LinkedHashMap<MethodId, MethodDeclaration>();
338
339        TypeDeclaration(TypeId<?> type) {
340            this.type = type;
341        }
342
343        ClassDefItem toClassDefItem() {
344            if (!declared) {
345                throw new IllegalStateException("Undeclared type " + type + " declares members: "
346                        + fields.keySet() + " " + methods.keySet());
347            }
348
349            DexOptions dexOptions = new DexOptions();
350            dexOptions.targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
351
352            CstType thisType = type.constant;
353
354            ClassDefItem out = new ClassDefItem(thisType, flags, supertype.constant,
355                    interfaces.ropTypes, new CstString(sourceFile));
356
357            for (MethodDeclaration method : methods.values()) {
358                EncodedMethod encoded = method.toEncodedMethod(dexOptions);
359                if (method.isDirect()) {
360                    out.addDirectMethod(encoded);
361                } else {
362                    out.addVirtualMethod(encoded);
363                }
364            }
365            for (FieldDeclaration field : fields.values()) {
366                EncodedField encoded = field.toEncodedField();
367                if (field.isStatic()) {
368                    out.addStaticField(encoded, Constants.getConstant(field.staticValue));
369                } else {
370                    out.addInstanceField(encoded);
371                }
372            }
373
374            return out;
375        }
376    }
377
378    static class FieldDeclaration {
379        final FieldId<?, ?> fieldId;
380        private final int accessFlags;
381        private final Object staticValue;
382
383        FieldDeclaration(FieldId<?, ?> fieldId, int accessFlags, Object staticValue) {
384            if ((accessFlags & STATIC) == 0 && staticValue != null) {
385                throw new IllegalArgumentException("instance fields may not have a value");
386            }
387            this.fieldId = fieldId;
388            this.accessFlags = accessFlags;
389            this.staticValue = staticValue;
390        }
391
392        EncodedField toEncodedField() {
393            return new EncodedField(fieldId.constant, accessFlags);
394        }
395
396        public boolean isStatic() {
397            return (accessFlags & STATIC) != 0;
398        }
399    }
400
401    static class MethodDeclaration {
402        final MethodId<?, ?> method;
403        private final int flags;
404        private final Code code;
405
406        public MethodDeclaration(MethodId<?, ?> method, int flags) {
407            this.method = method;
408            this.flags = flags;
409            this.code = new Code(this);
410        }
411
412        boolean isStatic() {
413            return (flags & STATIC) != 0;
414        }
415
416        boolean isDirect() {
417            return (flags & (STATIC | PRIVATE | ACC_CONSTRUCTOR)) != 0;
418        }
419
420        EncodedMethod toEncodedMethod(DexOptions dexOptions) {
421            RopMethod ropMethod = new RopMethod(code.toBasicBlocks(), 0);
422            LocalVariableInfo locals = null;
423            DalvCode dalvCode = RopTranslator.translate(
424                    ropMethod, PositionList.NONE, locals, code.paramSize(), dexOptions);
425            return new EncodedMethod(method.constant, flags, dalvCode, StdTypeList.EMPTY);
426        }
427    }
428}
429