I'd like to see how to make invokedynamic
calls with the same dispatch logic as invokevirtual
.
I'm asking this question because the examples currently online of generating dynamic method calls with ASM are too trivial to generalise and I think this case would be a good starting point for anyone wanting to implement their own dispatch logic.
Obviously I know that just replacing invokevirtual
calls with invokedynamic
ones would be a pointless thing to do in practice.
To be clear I want to replace this:
methodVisitor.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
myClassName,
methodName,
descriptor,
false);
with this:
MethodType methodType =
MethodType.methodType(
CallSite.class,
MethodHandles.Lookup.class,
String.class,
MethodType.class);
Handle handle =
new Handle(
Opcodes.H_INVOKESTATIC,
"bytecode/generating/Class",
"bootstrap",
methodType.toMethodDescriptorString(),
false);
methodVisitor.visitInvokeDynamicInsn(
methodName,
descriptor,
handle);
// bootstrap method
public static CallSite bootstrap(
MethodHandles.Lookup caller,
String name,
MethodType type)
{
// Dispatch logic here.
}
There is not much to do in this case. The only thing you have to care for, is that invokevirtual
has an implied first argument, the receiver, which you have to insert into the descriptor of the invokedynamic
instruction as an explicit first argument:
public class ConvertToInvokeDynamic extends MethodVisitor {
public static byte[] convertInvokeVirtual(
InputStream in, String linkerClass, String linkerMethod) throws IOException {
ClassReader cr = new ClassReader(in);
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
return new ConvertToInvokeDynamic(
super.visitMethod(access, name, desc, signature, exceptions),
linkerClass, linkerMethod);
}
}, 0);
return cw.toByteArray();
}
private final Handle bsm;
public ConvertToInvokeDynamic(
MethodVisitor target, String linkerClass, String linkerMethod) {
super(Opcodes.ASM5, target);
bsm = new Handle(Opcodes.H_INVOKESTATIC, linkerClass, linkerMethod,
"(Ljava/lang/invoke/MethodHandles$Lookup;"
+ "Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;");
}
@Override
public void visitMethodInsn(
int opcode, String owner, String name, String desc, boolean itf) {
if(opcode == Opcodes.INVOKEVIRTUAL) {
desc = '('+(owner.charAt(0)!='['? 'L'+owner+';': owner)+desc.substring(1);
super.visitInvokeDynamicInsn(name, desc, bsm);
}
else super.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
As long as this is the only change, the stack state will stay the same as in the original code, hence, we don’t need to recalculate stack frames nor maximum variables/operand stack size.
The code assumes that the original’s class version is high enough to support the invokedynamic
instruction. Otherwise, the transformation would become nontrivial, as we not only may have to calculate stack maps, we may also encounter the now-forbidden jsr
and ret
instructions in older class files.
Providing a bootstrap method reestablishing the original invokevirtual
behavior, also is straight-forward. Now, the biggest (not really big) obstacle is that we now have to extract the first explicit parameter type and convert it back to the receiver type:
public class LinkLikeInvokeVirtual {
public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType type){
Class<?> receiver = type.parameterType(0);
type = type.dropParameterTypes(0, 1);
System.out.println("linking to "+name+type+" in "+receiver);
MethodHandle target;
try {
target = l.findVirtual(receiver, name, type);
} catch(NoSuchMethodException|IllegalAccessException ex) {
throw new BootstrapMethodError(ex);
}
return new ConstantCallSite(target);
}
}
Now, we can combine these two classes in a simple test case:
public class Test {
public static void main(String[] args) throws IOException,ReflectiveOperationException{
byte[] code;
try(InputStream is = Test.class.getResourceAsStream("Test.class")) {
code = ConvertToInvokeDynamic.convertInvokeVirtual(is,
LinkLikeInvokeVirtual.class.getName(), "bootstrap");
}
Class<?> transformed = new ClassLoader() {
Class<?> get() {return defineClass("Test", code, 0, code.length); }
}.get();
transformed.getMethod("example").invoke(null);
}
public static void example() {
System.out.println(Runtime.getRuntime().freeMemory()+" bytes free");
}
}
whose transformed example()
produces
linking to freeMemory()long in class java.lang.Runtime
linking to append(long)StringBuilder in class java.lang.StringBuilder
linking to append(String)StringBuilder in class java.lang.StringBuilder
linking to toString()String in class java.lang.StringBuilder
linking to println(String)void in class java.io.PrintStream
131449472 bytes free
on the first execution (as the linked call-sites remain linked, so we won’t see the bootstrap method’s output on the next invocation).
The StringBuilder
methods are an artifact of the string concatenation as compiled before Java 9, so starting with Java 9, it will only print
linking to freeMemory()long in class java.lang.Runtime
linking to println(String)void in class java.io.PrintStream
131449472 bytes free
(of course, numbers will vary)
If you want to perform an alternative dynamic dispatch based on the actual receiver, you may replace LinkLikeInvokeVirtual
with something like this:
public class LinkWithDynamicDispatch {
static final MethodHandle DISPATCHER;
static {
try {
DISPATCHER = MethodHandles.lookup().findStatic(LinkWithDynamicDispatch.class, "simpleDispatcher",
MethodType.methodType(MethodHandle.class, MethodHandle.class, String.class, Object.class));
} catch(NoSuchMethodException|IllegalAccessException ex) {
throw new ExceptionInInitializerError(ex);
}
}
public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType type){
MethodHandle target;
try {
target = l.findVirtual(type.parameterType(0), name, type.dropParameterTypes(0, 1));
} catch(NoSuchMethodException|IllegalAccessException ex) {
throw new BootstrapMethodError(ex);
}
MethodHandle d = MethodHandles.insertArguments(DISPATCHER, 0, target, name);
target = MethodHandles.foldArguments(MethodHandles.exactInvoker(type),
d.asType(d.type().changeParameterType(0, type.parameterType(0))));
return new ConstantCallSite(target);
}
public static MethodHandle simpleDispatcher(
MethodHandle invokeVirtualTarget, String methodName, Object rec) {
System.out.println("simpleDispatcher(): invoke "+methodName+" on "
+ "declared receiver type "+invokeVirtualTarget.type().parameterType(0)+", "
+ "actual receiver "+(rec==null? "null": "("+rec.getClass().getName()+"): "+rec));
return invokeVirtualTarget;
}
}
This performs a lookup like invokevirtual
based on the static types, then links to the simpleDispatcher
method which will receive the actual receiver instance additionally to the resolved target. It may then just return the target handle or a different handle, based on the actual receiver.