Monday, February 10, 2014

Guarding enumeration classes from ProGuard

During my application tests I received exception with the following call stack:
02-09 15:18:06.478 26166 26403 E TAG: Caused by: java.lang.NullPointerException
02-09 15:18:06.478 26166 26403 E TAG:   at java.lang.Enum$1.create(Enum.java:43)
02-09 15:18:06.478 26166 26403 E TAG:   at java.lang.Enum$1.create(Enum.java:35)
02-09 15:18:06.478 26166 26403 E TAG:   at libcore.util.BasicLruCache.get(BasicLruCache.java:54)
02-09 15:18:06.478 26166 26403 E TAG:   at java.lang.Enum.getSharedConstants(Enum.java:209)
02-09 15:18:06.478 26166 26403 E TAG:   at java.util.EnumSet.noneOf(EnumSet.java:48)
02-09 15:18:06.478 26166 26403 E TAG:   at af.a(SourceFile:115)
The exception occurred only in release version, while in debug everything worked fine. So I immediately suspected that ProGuard, whose processing is part of a release build, is somehow involved here.

From ProGuard's documentation:
If your application, applet, servlet, library, etc., contains enumeration classes, you'll have to preserve some special methods. Enumerations were introduced in Java 5. The java compiler translates enumerations into classes with a special structure. Notably, the classes contain implementations of some static methods that the run-time environment accesses by introspection (Isn't that just grand? Introspection is the self-modifying code of a new generation). You have to specify these explicitly, to make sure they aren't removed or obfuscated: 
-keepclassmembers,allowoptimization enum * {
      public static **[] values();
      public static ** valueOf(java.lang.String);
}
OK, problem found: I just need to add this 'keepclassmembers' exception in ProGuard project configuration file.

But what are values() and valueof(java.lang.String) needed for? I still was curious how exactly I received NullPointerException in java.lang.Enum entrails. Let's better understand it.

From the Enum.create method's source:

  1.     private static final BasicLruCache<Class extends Enum>, Object[]> sharedConstantsCache
  2.             = new BasicLruCache<Class extends Enum>, Object[]>(64) {
  3.         @Override protected Object[] create(Class extends Enum> enumType) {
  4.             if (!enumType.isEnum()) {
  5.                 return null;
  6.             }
  7.             Method method = (Method) Class.getDeclaredConstructorOrMethod(
  8.                     enumType, "values", EmptyArray.CLASS);
  9.             try {
  10.                 return (Object[]) method.invoke((Object[]) null);
  11.             } catch (IllegalAccessException impossible) {
  12.                 throw new AssertionError();
  13.             } catch (InvocationTargetException impossible) {
  14.                 throw new AssertionError();
  15.             }
  16.         }
  17.     };

Enum.create uses reflection to retrieve a reference to values method (Class.getDeclaredConstructorOrMethod on line 40), but I didn't wrote such method in my Enum class definition. Actually, the values method is implicitly generated by the compiler. From Oracle documentation:
The compiler automatically adds some special methods when it creates an enum. For example, they have a static values method that returns an array containing all of the values of the enum in the order they are declared.

Now I have a full picture of what happened.

During its optimization phase, ProGuard removed the compiler-generated values method from my Enum class because it didn't found any its usages in my project. All the calls to this method are done using Reflection API, of which ProGuard completely isn't aware.

Later on, when my application is trying to use this Enum, Enum.create method called, getDeclaredConstructorOrMethod returns null, which will cause null pointer dereference on line 43 - and this eventually triggers the NullPointerException throw.

5 comments: