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