Hi core-libs-dev,

I am maintaining a module for the popular Jackson JSON library that attempts to 
simplify code-generation code without losing performance.
Long ago, it was a huge win to code-generate custom getter / setter / field 
accessors rather than use core reflection. Now, the gap is closing a lot with 
MethodHandles, but there still seems to be some benefit.

The previous approach used for code generation relied on the CGLib + ASM 
libraries, which as I am sure you know leads to horrible-to-maintain code since 
you essentially write bytecode directly.
Feature development basically stopped because writing out long chains of 
`visitVarInsn(ASTORE, 3)` and the like scares off most contributors, myself 
included.

As an experiment, I started to port the custom class generation logic to use 
LambdaMetafactory. The idea is to use the factory to generate `Function<Bean, 
T>` getter and `BiConsumer<Bean, T>` setter implementations.
Then, use those during (de)serialization to access or set data.  Eventually 
hopefully the JVM will inline, removing all (?) reflection overhead.

The invocation looks like:
var lookup = MethodHandles.privateLookupIn(targetClass, 
MethodHandles.lookup()); // allow non-public access
var getter = lookup.unreflect(someGetterMethod);
LambdaMetafactory.metafactory(
  lookup,
  "apply",
  methodType(Function.class),
  methodType(Object.class, Object.class),
  getter,
  getter.type())

This works well for classes from the same classloader. However, once you try to 
generate lambdas with implementations loaded from a different classloader, you 
run into a check in the AbstractValidatingLambdaMetafactory constructor:

if (!caller.hasFullPrivilegeAccess()) {
  throw new LambdaConversionException(String.format(
    "Invalid caller: %s",
    caller.lookupClass().getName()));
}

The `privateLookupIn` call seems to drop MODULE privilege access when looking 
across ClassLoaders. This appears to be because the "unnamed module" differs 
between a ClassLoader and its child.
This happens without the use of modulepath at all, only classpath, where I 
would not expect module restrictions to be in play.
Through some experimentation, I discovered that while I cannot call the 
LambdaMetafactory with this less-privileged lookup, I am still allowed to call 
defineClass.

So, I compile a simple class:

package <targetclasspackage>;
class AccessCracker { static final Lookup LOOKUP = MethodHandles.lookup(); }

and inject it into the target class's existing package:

lookup = lookup.defineClass(compiledBytes).getField("LOOKUP").get(null);

and now I have a full privileged lookup into the target classloader, and the 
Metafactory then seems to generate lambdas without complaint.

This workaround seems to work well, although it's a bummer to have to generate 
and inject these dynamic accessor classes.
It feels inconsistent that on one hand my Lookup is not powerful enough to 
generate a simple call-site with the Metafactory,
but at the same time it is so powerful that I can load arbitrary bytecode into 
the target classloader, and thus indirectly do what I wanted to do in the first 
place (with a fair bit more work)

There's a bit of additional context here:
https://github.com/FasterXML/jackson-modules-base/issues/138
https://github.com/FasterXML/jackson-modules-base/pull/162/files

Any chance the Metafactory might become powerful enough to generate call sites 
even across such unnamed Modules in a future release? Or even more generally 
across arbitrary Modules, if relevant access checks pass?

I'm also curious for any feedback on the overall approach of using the 
Metafactory, perhaps I am way off in the weeds, and should just trust 
MethodHandles to perform well if you use invokeExact :) JMH does seem to show 
some benefit though especially with Graal compiler.

Thanks a bunch for any thoughts,
Steven Schlansker

Reply via email to