summaryrefslogtreecommitdiffstats
path: root/mobile/android/annotations/src/main/java/org/mozilla/gecko/annotationProcessors/SDKProcessor.java
blob: c0a69de49cd743089c6a1bc16915fe0848dab3aa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.annotationProcessors;

/**
 * Generate C++ bindings for SDK classes using a config file.
 *
 * <p>java SDKProcessor <sdkjar> <max-sdk-version> <outdir> [<configfile> <fileprefix>]+
 *
 * <p><sdkjar>: jar file containing the SDK classes (e.g. android.jar) <max-sdk-version>: SDK
 * version for generated class members (bindings will not be generated for members with SDK versions
 * higher than max-sdk-version) <outdir>: output directory for generated binding files <configfile>:
 * config file for generating bindings <fileprefix>: prefix used for generated binding files
 *
 * <p>Each config file is a text file following the .ini format:
 *
 * <p>; comment [section1] property = value
 *
 * <p># comment [section2] property = value
 *
 * <p>Each section specifies a qualified SDK class. Each property specifies a member of that class.
 * The class and/or the property may specify options found in the WrapForJNI annotation. For
 * example,
 *
 * <p># Generate bindings for Bundle using default options: [android.os.Bundle]
 *
 * <p># Generate bindings for Bundle using class options: [android.os.Bundle =
 * exceptionMode:nsresult]
 *
 * <p># Generate bindings for Bundle using method options: [android.os.Bundle] putInt =
 * stubName:PutInteger
 *
 * <p># Generate bindings for Bundle using class options with method override: # (note that all
 * options are overriden at the same time.) [android.os.Bundle = exceptionMode:nsresult] # putInt
 * will have stubName "PutInteger", and exceptionMode of "abort" putInt = stubName:PutInteger #
 * putChar will have stubName "PutCharacter", and exceptionMode of "nsresult" putChar =
 * stubName:PutCharacter, exceptionMode:nsresult
 *
 * <p># Overloded methods can be specified using its signature [android.os.Bundle] # Skip the copy
 * constructor <init>(Landroid/os/Bundle;)V = skip:true
 *
 * <p># Generic member types can be specified [android.view.KeyEvent = skip:true] # Skip everything
 * except fields <field> = skip:false
 *
 * <p># Skip everything except putInt and putChar [android.os.Bundle = skip:true] putInt =
 * skip:false putChar =
 *
 * <p># Avoid conflicts in native bindings [android.os.Bundle] # Bundle(PersistableBundle) native
 * binding can conflict with Bundle(ClassLoader) <init>(Landroid/os/PersistableBundle;)V =
 * stubName:NewFromPersistableBundle
 *
 * <p># Generate a getter instead of a literal for certain runtime constants
 * [android.os.Build$VERSION = skip:true] SDK_INT = noLiteral:true
 */
import com.android.tools.lint.LintCliClient;
import com.android.tools.lint.checks.ApiLookup;
import com.android.tools.lint.client.api.LintClient;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import org.mozilla.gecko.annotationProcessors.classloader.AnnotatableEntity;
import org.mozilla.gecko.annotationProcessors.classloader.ClassWithOptions;
import org.mozilla.gecko.annotationProcessors.utils.Utils;

public class SDKProcessor {
  public static final String GENERATED_COMMENT =
      "// GENERATED CODE\n"
          + "// Generated by the Java program at /build/annotationProcessors at compile time\n"
          + "// from annotations on Java methods. To update, change the annotations on the\n"
          + "// corresponding Javamethods and rerun the build. Manually updating this file\n"
          + "// will cause your build to fail.\n"
          + "\n";

  private static ApiLookup sApiLookup;
  private static int sMaxSdkVersion;

  private static class ParseException extends Exception {
    public ParseException(final String message) {
      super(message);
    }
  }

  private static class ClassInfo {
    public final String name;

    // Map constructor/field/method signature to a set of annotation values.
    private final HashMap<String, String> mAnnotations = new HashMap<>();
    // Default set of annotation values to use.
    private final String mDefaultAnnotation;
    // List of nested classes to forward declare.
    private final ArrayList<Class<?>> mNestedClasses;

    public ClassInfo(final String text) {
      final String[] mapping = text.split("=", 2);
      name = mapping[0].trim();
      mDefaultAnnotation = mapping.length > 1 ? mapping[1].trim() : null;
      mNestedClasses = new ArrayList<>();
    }

    public void addAnnotation(final String text) throws ParseException {
      final String[] mapping = text.split("=", 2);
      final String prop = mapping[0].trim();
      if (prop.isEmpty()) {
        throw new ParseException("Missing member name: " + text);
      }
      if (mapping.length < 2) {
        throw new ParseException("Missing equal sign: " + text);
      }
      if (mAnnotations.get(prop) != null) {
        throw new ParseException("Already has member: " + prop);
      }
      mAnnotations.put(prop, mapping[1].trim());
    }

    public AnnotationInfo getAnnotationInfo(final Member member) throws ParseException {
      String stubName = Utils.getNativeName(member);
      AnnotationInfo.ExceptionMode mode = AnnotationInfo.ExceptionMode.ABORT;
      AnnotationInfo.CallingThread thread = AnnotationInfo.CallingThread.ANY;
      AnnotationInfo.DispatchTarget target = AnnotationInfo.DispatchTarget.CURRENT;
      boolean noLiteral = false;
      boolean isGeneric = false;

      final String name = Utils.getMemberName(member);
      String annotation =
          mAnnotations.get(
              name + (member instanceof Field ? ":" : "") + Utils.getSignature(member));
      if (annotation == null) {
        // Match name without signature
        annotation = mAnnotations.get(name);
      }
      if (annotation == null) {
        // Match <constructor>, <field>, <method>
        annotation =
            mAnnotations.get(
                "<" + member.getClass().getSimpleName().toLowerCase(Locale.ROOT) + '>');
        isGeneric = true;
      }
      if (annotation == null) {
        // Fallback on class options, if any.
        annotation = mDefaultAnnotation;
      }
      if (annotation == null || annotation.isEmpty()) {
        return new AnnotationInfo(stubName, mode, thread, target, noLiteral);
      }

      final String[] elements = annotation.split(",");
      for (final String element : elements) {
        final String[] pair = element.split(":", 2);
        if (pair.length < 2) {
          throw new ParseException("Missing option value: " + element);
        }
        final String pairName = pair[0].trim();
        final String pairValue = pair[1].trim();
        switch (pairName) {
          case "skip":
            if (Boolean.valueOf(pairValue)) {
              // Return null to signal skipping current method.
              return null;
            }
            break;
          case "stubName":
            if (isGeneric) {
              // Prevent specifying stubName for class options.
              throw new ParseException("stubName doesn't make sense here: " + pairValue);
            }
            stubName = pairValue;
            break;
          case "exceptionMode":
            mode = Utils.getEnumValue(AnnotationInfo.ExceptionMode.class, pairValue);
            break;
          case "calledFrom":
            thread = Utils.getEnumValue(AnnotationInfo.CallingThread.class, pairValue);
            break;
          case "dispatchTo":
            target = Utils.getEnumValue(AnnotationInfo.DispatchTarget.class, pairValue);
            break;
          case "noLiteral":
            noLiteral = Boolean.valueOf(pairValue);
            break;
          default:
            throw new ParseException("Unknown option: " + pairName);
        }
      }
      return new AnnotationInfo(stubName, mode, thread, target, noLiteral);
    }
  }

  public static void main(String[] args) throws Exception {
    // We expect a list of jars on the commandline. If missing, whinge about it.
    if (args.length < 3 || args.length % 2 != 1) {
      System.err.println(
          "Usage: java SDKProcessor sdkjar max-sdk-version outdir [configfile fileprefix]*");
      System.exit(1);
    }

    System.out.println("Processing platform bindings...");

    final File sdkJar = new File(args[0]);
    sMaxSdkVersion = Integer.parseInt(args[1]);
    final String outdir = args[2];

    final LintCliClient lintClient = new LintCliClient(LintClient.CLIENT_CLI);
    sApiLookup = ApiLookup.get(lintClient);

    for (int argIndex = 3; argIndex < args.length; argIndex += 2) {
      final String configFile = args[argIndex];
      final String generatedFilePrefix = args[argIndex + 1];
      System.out.println("Processing bindings from " + configFile);

      // Start the clock!
      long s = System.currentTimeMillis();

      // Get an iterator over the classes in the jar files given...
      // Iterator<ClassWithOptions> jarClassIterator =
      // IterableJarLoadingURLClassLoader.getIteratorOverJars(args);

      StringBuilder headerFile = new StringBuilder(GENERATED_COMMENT);
      headerFile.append(
          "#ifndef "
              + generatedFilePrefix
              + "_h__\n"
              + "#define "
              + generatedFilePrefix
              + "_h__\n"
              + "\n"
              + "#include \"mozilla/jni/Refs.h\"\n"
              + "\n"
              + "namespace mozilla {\n"
              + "namespace java {\n"
              + "namespace sdk {\n"
              + "\n");

      StringBuilder implementationFile = new StringBuilder(GENERATED_COMMENT);
      implementationFile.append(
          "#include \""
              + generatedFilePrefix
              + ".h\"\n"
              + "#include \"mozilla/jni/Accessors.h\"\n"
              + "\n"
              + "namespace mozilla {\n"
              + "namespace java {\n"
              + "namespace sdk {\n"
              + "\n");

      // Used to track the calls to the various class-specific initialisation functions.
      ClassLoader loader = null;
      try {
        loader =
            URLClassLoader.newInstance(
                new URL[] {sdkJar.toURI().toURL()}, SDKProcessor.class.getClassLoader());
      } catch (Exception e) {
        throw new RuntimeException(e.toString());
      }

      try {
        ClassInfo[] classes = getClassList(configFile);
        classes = addNestedClassForwardDeclarations(classes, loader);

        for (final ClassInfo cls : classes) {
          System.out.println("Looking up: " + cls.name);
          generateClass(Class.forName(cls.name, true, loader), cls, implementationFile, headerFile);
        }
      } catch (final IllegalStateException | IOException | ParseException e) {
        System.err.println("***");
        System.err.println("*** Error parsing config file: " + configFile);
        System.err.println("*** " + e);
        System.err.println("***");
        if (e.getCause() != null) {
          e.getCause().printStackTrace(System.err);
        }
        System.exit(1);
        return;
      }

      implementationFile.append("} /* sdk */\n" + "} /* java */\n" + "} /* mozilla */\n");

      headerFile.append("} /* sdk */\n" + "} /* java */\n" + "} /* mozilla */\n" + "#endif\n");

      writeOutputFiles(outdir, generatedFilePrefix, headerFile, implementationFile);
      long e = System.currentTimeMillis();
      System.out.println("SDK processing complete in " + (e - s) + "ms");
    }
  }

  private static int getAPIVersion(Class<?> cls, Member m) {
    if (m instanceof Method || m instanceof Constructor) {
      return sApiLookup.getMethodVersion(
          cls.getName().replace('.', '/'), Utils.getMemberName(m), Utils.getSignature(m));
    } else if (m instanceof Field) {
      return sApiLookup.getFieldVersion(
          Utils.getClassDescriptor(m.getDeclaringClass()), m.getName());
    } else {
      throw new IllegalArgumentException("expected member to be Method, Constructor, or Field");
    }
  }

  private static Member[] sortAndFilterMembers(Class<?> cls, Member[] members) {
    Arrays.sort(
        members,
        new Comparator<Member>() {
          @Override
          public int compare(Member a, Member b) {
            int result = a.getName().compareTo(b.getName());
            if (result == 0) {
              if (a instanceof Constructor && b instanceof Constructor) {
                String sa = Arrays.toString(((Constructor) a).getParameterTypes());
                String sb = Arrays.toString(((Constructor) b).getParameterTypes());
                result = sa.compareTo(sb);
              } else if (a instanceof Method && b instanceof Method) {
                String sa = Arrays.toString(((Method) a).getParameterTypes());
                String sb = Arrays.toString(((Method) b).getParameterTypes());
                result = sa.compareTo(sb);
              }
            }
            return result;
          }
        });

    ArrayList<Member> list = new ArrayList<>();
    for (final Member m : members) {
      if (m.getDeclaringClass() == Object.class) {
        // Skip methods from Object.
        continue;
      }

      // Sometimes (e.g. Bundle) has methods that moved to/from a superclass in a later SDK
      // version, so we check for both classes and see if we can find a minimum SDK version.
      int version = getAPIVersion(cls, m);
      final int version2 = getAPIVersion(m.getDeclaringClass(), m);
      if (version2 > 0 && version2 < version) {
        version = version2;
      }
      if (version > sMaxSdkVersion) {
        System.out.println(
            "Skipping "
                + m.getDeclaringClass().getName()
                + "."
                + Utils.getMemberName(m)
                + ", version "
                + version
                + " > "
                + sMaxSdkVersion);
        continue;
      }

      // Sometimes (e.g. KeyEvent) a field can appear in both a class and a superclass. In
      // that case we want to filter out the version that appears in the superclass, or
      // we'll have bindings with duplicate names.
      try {
        if (m instanceof Field && !m.equals(cls.getField(m.getName()))) {
          // m is a field in a superclass that has been hidden by
          // a field with the same name in a subclass.
          System.out.println(
              "Skipping " + Utils.getMemberName(m) + " from " + m.getDeclaringClass().getName());
          continue;
        }
      } catch (final NoSuchFieldException e) {
      }

      list.add(m);
    }

    return list.toArray(new Member[list.size()]);
  }

  private static void generateMembers(CodeGenerator generator, ClassInfo clsInfo, Member[] members)
      throws ParseException {
    for (Member m : members) {
      if (!Modifier.isPublic(m.getModifiers())) {
        continue;
      }

      // Default for SDK bindings.
      final AnnotationInfo info = clsInfo.getAnnotationInfo(m);
      if (info == null) {
        // Skip this member.
        continue;
      }
      final AnnotatableEntity entity = new AnnotatableEntity(m, info);

      if (m instanceof Constructor) {
        generator.generateConstructor(entity);
      } else if (m instanceof Method) {
        generator.generateMethod(entity);
      } else if (m instanceof Field) {
        generator.generateField(entity);
      } else {
        throw new IllegalArgumentException("expected member to be Constructor, Method, or Field");
      }
    }
  }

  private static String getGeneratedName(Class<?> clazz) {
    ArrayList<String> classes = new ArrayList<>();
    do {
      classes.add(clazz.getSimpleName());
      clazz = clazz.getDeclaringClass();
    } while (clazz != null);
    Collections.reverse(classes);
    return String.join("::", classes);
  }

  private static void generateClass(
      Class<?> clazz, ClassInfo clsInfo, StringBuilder implementationFile, StringBuilder headerFile)
      throws ParseException {
    String generatedName = getGeneratedName(clazz);

    CodeGenerator generator =
        new CodeGenerator(new ClassWithOptions(clazz, generatedName, /* ifdef */ ""));

    // Forward declaration for nested classes
    ClassWithOptions[] nestedClasses =
        clsInfo.mNestedClasses.stream()
            .map(
                nestedClass ->
                    new ClassWithOptions(nestedClass, getGeneratedName(nestedClass), null))
            .sorted((a, b) -> a.generatedName.compareTo(b.generatedName))
            .toArray(ClassWithOptions[]::new);
    generator.generateClasses(nestedClasses);

    generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getConstructors()));
    generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getMethods()));
    generateMembers(generator, clsInfo, sortAndFilterMembers(clazz, clazz.getFields()));

    headerFile.append(generator.getHeaderFileContents());
    implementationFile.append(generator.getWrapperFileContents());
  }

  private static ClassInfo[] getClassList(BufferedReader reader)
      throws ParseException, IOException {
    final ArrayList<ClassInfo> classes = new ArrayList<>();
    ClassInfo currentClass = null;
    String line;

    while ((line = reader.readLine()) != null) {
      line = line.trim();
      if (line.isEmpty()) {
        continue;
      }
      switch (line.charAt(0)) {
        case ';':
        case '#':
          // Comment
          continue;
        case '[':
          // New section
          if (line.charAt(line.length() - 1) != ']') {
            throw new ParseException("Missing trailing ']': " + line);
          }
          currentClass = new ClassInfo(line.substring(1, line.length() - 1));
          classes.add(currentClass);
          break;
        default:
          // New mapping
          if (currentClass == null) {
            throw new ParseException("Missing class: " + line);
          }
          currentClass.addAnnotation(line);
          break;
      }
    }
    if (classes.isEmpty()) {
      throw new ParseException("No class found in config file");
    }
    return classes.toArray(new ClassInfo[classes.size()]);
  }

  private static ClassInfo[] getClassList(final String path) throws ParseException, IOException {
    BufferedReader reader = null;
    try {
      reader = new BufferedReader(new FileReader(path));
      return getClassList(reader);
    } finally {
      if (reader != null) {
        reader.close();
      }
    }
  }

  /**
   * For each nested class we wish to generate bindings for, this ensures that the generated binding
   * for its outer class (recursively, until we reach a top-level class) will contain a forward
   * declaration of the nested class.
   */
  private static ClassInfo[] addNestedClassForwardDeclarations(
      final ClassInfo[] classes, final ClassLoader loader) throws ClassNotFoundException {
    final HashMap<String, ClassInfo> classMap = new HashMap<>();
    for (final ClassInfo cls : classes) {
      classMap.put(cls.name, cls);
    }

    for (final ClassInfo classInfo : classes) {
      Class<?> innerClass = Class.forName(classInfo.name, true, loader);
      while (innerClass.getDeclaringClass() != null) {
        Class<?> outerClass = innerClass.getDeclaringClass();
        ClassInfo outerClassInfo = classMap.get(outerClass.getName());
        if (outerClassInfo == null) {
          // If there isn't already a ClassInfo object for the outer class then we must insert one.
          // This ensures that we actually generate a declaration for the outer class, in which we
          // can forward-declare the inner class. "skip:true" ensures we do not generate bindings
          // for the outer class' member's, as we simply want to forward declare the inner class.
          outerClassInfo = new ClassInfo(String.format("%s = skip:true", outerClass.getName()));
          classMap.put(outerClass.getName(), outerClassInfo);
        }
        // Add the inner class to the outer class' mNestedClasses, ensuring the outer class'
        // generated code will forward-declare the inner class.
        outerClassInfo.mNestedClasses.add(innerClass);

        innerClass = outerClass;
      }
    }

    // Sort to ensure we generate the classes in a deterministic order, and that outer classes are
    // declared before nested classes.
    return classMap.values().stream()
        .sorted((a, b) -> a.name.compareTo(b.name))
        .toArray(ClassInfo[]::new);
  }

  private static void writeOutputFiles(
      String aOutputDir,
      String aPrefix,
      StringBuilder aHeaderFile,
      StringBuilder aImplementationFile) {
    FileOutputStream implStream = null;
    try {
      implStream = new FileOutputStream(new File(aOutputDir, aPrefix + ".cpp"));
      implStream.write(aImplementationFile.toString().getBytes());
    } catch (IOException e) {
      System.err.println("Unable to write " + aOutputDir + ". Perhaps a permissions issue?");
      e.printStackTrace(System.err);
    } finally {
      if (implStream != null) {
        try {
          implStream.close();
        } catch (IOException e) {
          System.err.println("Unable to close implStream due to " + e);
          e.printStackTrace(System.err);
        }
      }
    }

    FileOutputStream headerStream = null;
    try {
      headerStream = new FileOutputStream(new File(aOutputDir, aPrefix + ".h"));
      headerStream.write(aHeaderFile.toString().getBytes());
    } catch (IOException e) {
      System.err.println("Unable to write " + aOutputDir + ". Perhaps a permissions issue?");
      e.printStackTrace(System.err);
    } finally {
      if (headerStream != null) {
        try {
          headerStream.close();
        } catch (IOException e) {
          System.err.println("Unable to close headerStream due to " + e);
          e.printStackTrace(System.err);
        }
      }
    }
  }
}