Skip to content

Extra Chimerization Tricks

Ben Nordick edited this page May 15, 2020 · 3 revisions

Chimeras are primarily useful for swapping individual functions out of Android activities because it's somewhat more difficult to factor functions out of those classes than out of plain Java classes. However, they are more versatile than previously described.

Plain Java chimeras

It does not matter to eMPire how the chimera JAR was compiled. eMPire just operates on the class file inside it. Chimerization will work perfectly fine on a standard Java class compiled from a normal IntelliJ Java project.

Refactored methods

The method to take from the student version of the class is identified only by its name, not signature. You can therefore have students refactor a chimerized method and change its parameter types. For example, an addLine function might initially take four double coordinates but later be refactored to take two LatLngs. Your provided class, however, was compiled expecting a certain signature in your calls to the student's method, so if the signature is changed during chimerization, method call instructions—which specify the full method signature—to it from the provided methods will fail with a NoSuchMethodError. To work with either of two possible signatures, your provided code must use reflection to invoke the student method. So rather than calling the student method directly, you might have a reflective wrapper function to detect the signature present at runtime and translate parameters appropriately:

private void addLineReflective(LatLng start, LatLng end) {
    try {
        try {
            // Look for a method that takes four doubles
            Method addLine = getClass().getDeclaredMethod("addLine", double.class, double.class, double.class, double.class);
            addLine.invoke(this, start.latitude, start.longitude, end.latitude, end.longitude);
        } catch (NoSuchMethodException e1) {
            // The four-doubles version isn't there, look for a method that takes two LatLngs
            try {
                Method addLine = getClass().getDeclaredMethod("addLine", LatLng.class, LatLng.class);
                addLine.invoke(this, start, end);
            } catch (NoSuchMethodException e2) {
                // That signature wasn't there either
                throw new RuntimeException("No known addLine signature found", e2);
            }
        }
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Couldn't access addLine", e);
    } catch (InvocationTargetException e) {
        throw new RuntimeException("addLine crashed, see details in 'caused by' below", e.getCause());
    }
}

Reverse chimerization

The chimerization process will only replace the method of the one specified name with the student's version, but it will also take all student methods and fields whose signatures do not conflict with those of any provided members. You can therefore create chimeras that preserve all student methods except one you want to provide by creating a class containing only that one method and setting the method in the chimera directive to any student method's name. Watch out for default constructors—if the student's zero-argument constructor is important, give your class a private constructor with some other signature to avoid clobbering the student's. You may need to use reflection to call student methods of the chimeric class without defining them yourself.

Camouflage

You might want your chimera to do something differently based on whether the student made some change in the methods the chimera is replacing. For example, maybe the student is supposed to write a new function in an activity and call it from an existing method that they should have finished in a previous checkpoint. The chimera's provided version of that existing method should only call the new method if the student actually added the call to their version. That is, the provided method needs to simulate some part of the student version it's replacing.

To obtain information about the student class before its other methods are replaced, chimeras can be given camouflage. Like manifest editors, a camouflage component is a JAR loaded into the Gradle process that eMPire consults during a build. There should be a class outside any package that has a public static method that takes a byte[] and returns a Map<String, String>. eMPire will pass the bytecode of the student class to your function. You can process it with any tools loaded into the Gradle process (or shaded into your JAR). eMPire uses Kotlin 1.3.72 and ASM 6.0, so you can have compile-only dependencies on those. For example, you could examine the instructions of the desired method to see if there is a call to the student method. Each entry in the map you return will produce a private static final field on the chimeric class with your key as the name and your value as the constant string value. To register camouflage, pass three additional parameters to the chimera directive: the filename (including extension) of your camouflage JAR, the class name, and the name of your static method.

Your provided chimera methods can access the camouflage information at runtime with reflection (getDeclaredField). They can make decisions based on the fields' values to simulate aspects of the replaced student methods.

You can make camouflage as sophisticated as you want, but I again advise keeping student work in chimeric classes as simple as possible so that students do not encounter otherwise-inexplicable behavior.

Clone this wiki locally