Few cents about my commits

aggressive treeshaker: fixing ObjectOutputStream.writeObject()

|

Aggressive tree shaker helps to reduce application footprint by removing all methods that are not referenced in code. This often breaks reflection-based code and required classes has to be explicitly specified using <forceLinkClasses> but sometimes it is not enough. While working with nitrite-database I faced case when class is referenced but some methods of it is still removed. Here is an example:

private void testSerialize() {
        try {
            new ObjectOutputStream(new ByteArrayOutputStream()).writeObject(new HashMap<>());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

fails with:

java.io.InvalidClassException: java.util.HashMap doesn't have a field loadFactor of type float
	at java.io.ObjectOutputStream.writeFieldValues(ObjectOutputStream.java:955)

This happens due internal private and unreferenced methods got removed from HashMap.

java.io.Serializable contract

As specified in documentation there is special case:

Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

HashMap is designed with for this special handling but ObjectOutputStream can’t find these methods runtime as these were aggressively removed by tree shaker and thread this object as regular one and tries to serialize it using reflection. But there is an conflict in field definition which causes exception. Anyone serialization is broken.

the fix – forceLinkMethods

Straightforward solution is to tell tree shaker to keep these methods. I’ve added following PR322 that adds this functionality and allows flexible way specifying methods to be force linked. It has to be configured using robovm.xml using <forceLinkMethods> tag.

Under <forceLinkMethods> one ore more <entry> tags follow specifying class and method pairs:

<forceLinkMethods>
    <entry>
        <!-- ... -->
    </entry>
    <entry>
        <!-- ... -->
    </entry>
</forceLinkMethods>

Entry contains following tags:

  • Class list where to look for methods:
<owners>
    <pattern extendable="true">java.io.Serializable</pattern>>
</owners>

Class name pattern is specified by <pattern> tag and its Ant pattern. If extendable=true attribute is specified then class will be checked against super classes and interfaces it (and its super) implements. Otherwise only classes that match pattern are subject for method force-linking.

  • Methods that has to be force-linked in matching classes:
  <methods>
    <method>writeObject(Ljava/io/ObjectOutputStream;)V</method>
    <method>readObject(Ljava/io/ObjectInputStream;)V</method>
 <methods>

Each method item is concatenation of method name with its full JVM signature, e.g.:

writeObject(Ljava/io/ObjectOutputStream;)V

corresponds to:

void writeObject(ObjectOutputStream stream)

packing everything together

Following configuration entry reverts makes ObjectOutputStream.writeObject() work again:

     <!--Force link methods required for ObjectOutputStream to operate-->
    <forceLinkMethods>
        <entry>
            <methods>
                <method>writeObject(Ljava/io/ObjectOutputStream;)V</method>
                <method>readObject(Ljava/io/ObjectInputStream;)V</method>
            </methods>
            <owners>
                <pattern extendable="true">java.io.Serializable</pattern>>
            </owners>
        </entry>
    </forceLinkMethods>

about type signatures

here is an snippet from java doc that explains how types are mapped to signatures:

Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L fully-qualified-class ; fully-qualified-class
[ type type[]
( arg-types ) ret-type method type

For example, the Java method:

long f (int n, String s, int[] arr);

has the following type signature:

(ILjava/lang/String;[I)J

Comments