This is a playground repo to test out ideas around allowing code compiled against an older version of Gradle to run with a newer version of Gradle that have breaking changes.
There are the following subprojects:
:old-apidefines an old version of theServertype written in Java,:old-clientcontains client code compiled against the old version ofServer; there are clients written in Java, Kotlin and static and dynamic Groovy,:old-apprepresents the old version of the application, with a test to run:old-clientagainst:old-api.:new-apidefines the new version of theServertype, also written in Java,:new-clientis to demonstrate how the code manually written against the new API would look like,:new-apprepresents the new version of the application, with a test that tries to run each of the clients in:old-clientagainst the:new-api. It uses:upgraderto upgrade the old classes, by defining the actual upgrade steps to execute against the:old-api.:upgraderis the actual abstract upgrade logic.
Try with:
$ ./gradlew checkdiff --git a/javaold b/javanew
index 02f1620..0c43e4c 100644
--- a/javaold
+++ b/javanew
@@ -11,15 +11,16 @@ Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(6, label1);
methodVisitor.visitVarInsn(ALOAD, 0);
+methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "org/gradle/demo/api/evolution/Server", "getName", "()Lorg/gradle/demo/api/evolution/Property;", false);
methodVisitor.visitLdcInsn("lajos");
-methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "org/gradle/demo/api/evolution/Server", "setName", "(Ljava/lang/String;)V", false);
+methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "org/gradle/demo/api/evolution/Property", "set", "(Ljava/lang/Object;)V", false);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(7, label2);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(ALOAD, 0);
-methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "org/gradle/demo/api/evolution/Server", "getName", "()Ljava/lang/String;", false);
-methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "org/gradle/demo/api/evolution/Server", "getName", "()Lorg/gradle/demo/api/evolution/Property;", false);
+methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false);
Label label3 = new Label();
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(8, label3);- Full filtered diff: https://gist.github.com/lptr/cd7c9e149e2db5d2c92df2cb6d709728
- Full unfiltered diff: https://gist.github.com/lptr/a7a8557be684978a553ae3d56ae73006
private static void doSet(Object server) {
server.setTestProperty("lajos")
}
private static Object doGet(Object server) {
return server.getTestProperty()
} private static void doSet(Object server) {
server.getTestProperty().set("lajos")
}
private static Object doGet(Object server) {
return server.getTestProperty().get()
} private static java.lang.Object doGet(java.lang.Object);
descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
- stack=2, locals=2, args_size=1
+ stack=3, locals=2, args_size=1
: nop
: invokestatic #_ // Method $getCallSiteArray:()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
: astore_1
: aload_1
- : ldc #_ // int 5
+ : ldc #_ // int 6
+ : aaload
+ : aload_1
+ : ldc #_ // int 7
: aaload
: aload_0
: invokeinterface #_, 2 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;)Ljava/lang/Object;
+ : invokeinterface #_, 2 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;)Ljava/lang/Object;
: areturn
LineNumberTable:
line 14: 5
LocalVariableTable:
Start Length Slot Name Signature
- 0 16 0 server Ljava/lang/Object;
+ 0 25 0 server Ljava/lang/Object; private static void doSet(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
@@ -246,7 +280,11 @@
: aload_1
: ldc #_ // int 4
: aaload
+ : aload_1
+ : ldc #_ // int 5
+ : aaload
: aload_0
+ : invokeinterface #_, 2 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;)Ljava/lang/Object;
: ldc #_ // String lajos
: invokeinterface #_, 3 // InterfaceMethod org/codehaus/groovy/runtime/callsite/CallSite.call:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
: pop
@@ -255,27 +293,31 @@
line 11: 5
LocalVariableTable:
Start Length Slot Name Signature
- 0 18 0 server Ljava/lang/Object;
+ 0 27 0 server Ljava/lang/Object;Upgrading statically compiled bytecode from old API to new is relatively easy, at least when we need to replace single method calls. We can do this in two, fairly similar ways:
- remove the original bytecode calling the old API, and generate new bytecode that calls the new API, so the resulting code looks exactly as if the original code was rewritten and recompiled against the new API,
- remove the original bytecode, and replace it with a call to some compatibility class in Gradle that will call through to the new API.
For dynamic Groovy is harder, but not impossible. The problem is that in the bytecode we have no type information that would allow us to figure out which INVOKEDYNAMIC instruction corresponds to what actual API call.
During runtime, before executing the code of every dynamic method, Groovy generates an array of CallSites. The dynamic calls go through these call sites, and when they do, the necessary type information is available. The call sites are created by a generated static method called $getCallSiteArray().
We are borrowing ideas from Gradle's InstrumentingTransformer that solve our problem in two steps:
-
via bytecode transformation of the client code we decorate the call to
$getCallSiteArray()at the beginning of every dynamic method, and process the generatedCallSiteobjects via a static method. We basically wrap the code like this:/* ... */ = Instrumented.processCallSites($getCallSiteArray());
-
in the
Instrumented.processCallSites()method we wrap eachCallSitewith a wrapper that can detect calls to old APIs, and instead execute calls to the new methods.
In the case of the doSet() method, Instrumented would be aware that Server.setTestProperty() needs to be substituted with getTestProperty().set(). So the CallSite wrapper would do something like this:
@Override
public Object call(Object receiver, Object arg) throws Throwable {
if (receiver instanceof Server && getName().equals("setTestProperty()")) {
return ((Server) receiver).getTestProperty().set((String) arg);
} else {
return super.call(receiver, arg);
}
}