Search code examples
byte-buddy

What limitations are there to ByteBuddy's ability to "spread" an Object array over a method's parameters?


I am aware of the MethodCall#withArgumentArrayElements(int) method. Briefly, it allows you to accept an Object[] and invoke some other method, supplying its parameters in order from the Object[] identified by the int parameter. Usually you also use a dynamic assigner.

I am finding an odd limitation where this does not appear to work. I'd like to understand what I've done wrong or if there is a (rare) problem in ByteBuddy.

I have generated a static method using ByteBuddy whose Java code might look like the following:

private static final void setFrob(final T target, final Object... parameters) throws Exception {
  target.setFrob((String)parameters[0],
                 (Integer)parameters[1]); // pseudocode; happens via "spreading" mentioned above
}

The pattern, as I hope you can see, is that in my instrumented class I define a private static setter method named after the "real" setter method (for various reasons). I take in the target and the parameters, whatever they might be, and then invoke the "real" setter method on the target with the supplied parameters array supplying values for the "real" setter method's parameters. This is not complicated and works fine, but interestingly only in certain scenarios.

The method definition's ByteBuddy recipe is:

final MethodDescription staticSetterMethod =
  new MethodDescription.Latent(builder.toTypeDescription(),
                               methodToken.getName(),
                               PRIVATE_STATIC_FINAL_SYNTHETIC_VARARGS_METHOD_MODIFIERS,
                               Collections.emptyList(),
                               TypeDescription.Generic.VOID,
                               List.of(new ParameterDescription.Token(targetType,
                                                                      "target",
                                                                      ParameterManifestation.FINAL.getMask()),
                                       new ParameterDescription.Token(OBJECT_ARRAY_TYPE_DESCRIPTION_GENERIC,
                                                                      "parameters",
                                                                      ParameterManifestation.FINAL.getMask())),
                               Collections.singletonList(EXCEPTION_TYPE_GENERIC),
                               Collections.emptyList(),
                               null,
                               null);
builder = builder
  .define(staticSetterMethod)

Because things work in certain scenarios, I've been able to javap the resulting class and the method definition is as I would expect. I'm not worried about this part.

Next, the ByteBuddy implementation recipe looks like this (I've tried to keep it short and relevant):

MethodCall.invoke(new MethodDescription.Latent(targetType.asErasure(), // T's actual type
                                               methodToken)) // setFrob(String, Integer)
                     .onArgument(0) // target (of type T)
                     .withArgumentArrayElements(1) // parameters
                     .withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC));

…so: "invoke setFrob, declared by the type of target, described by methodToken, on the first argument (target) spreading the incoming Object[] array values found in the second argument (parameters) using dynamic assignment".

My understanding is that if the "real" setFrob method needs a String and an Integer supplied to it, then if the Object[] here is two elements long, and if the first element is a String, and the second is an Integer, then the withArgumentArrayElements(1) invocation will ensure that these elements are "spread" "into" the proper method parameters.

Indeed this works fine when the setFrob method being called accepts zero or one parameters (let's say it is defined to accept only a String parameter). That tells me that at least my ByteBuddy recipes are correct.

However, I was extremely surprised to note that it fails when the setFrob method being called is changed to accept two parameters. The partial stack says:

java.lang.IllegalStateException: public void com.foo.bar.TestExplorations$Foo.setFrob(java.lang.String,java.lang.Integer) does not accept 1 arguments
        at net.bytebuddy.implementation.MethodCall$Appender.toStackManipulation(MethodCall.java:3539)
        at net.bytebuddy.implementation.MethodCall$Appender.apply(MethodCall.java:3508)
        at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyCode(TypeWriter.java:708)
        at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyBody(TypeWriter.java:693)
        at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod.apply(TypeWriter.java:600)
        at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForCreation.create(TypeWriter.java:5660)
        at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:2166)
        at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:232)
        at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:204)

The error is resulting from this conditional:

ParameterList<?> parameters = invokedMethod.getParameters();
if (parameters.size() != argumentLoaders.size()) {
  throw new IllegalStateException(invokedMethod + " does not accept " + argumentLoaders.size() + " arguments");
}

The argumentLoaders in this case has a size of 1. parameters has a size of 2.

What am I doing wrong?


Solution

  • Maintainer here: It's indeed a bug, the increment was repeated inside the loop. This will be fixed in version 1.10.15.