Hi
I saw some recent posts on Haxe’s implementation of closures on the
JVM and all I can say is yikes!
The old MethodHandle implementation was extremely broken. The new
typed functions implementation is better but could use invokedynamic
but this time properly so it works fast.
I have been working with MethodHandles on personal projects for a bit
and would love to help discuss this subject with the devs to help make
this area better.
It appears to me the old backend used MethodHandle directly to
represent functions and closures. This is a bad idea.
- MethodHandles are not faster than regular method calls only more flexible.
- NEVER call a MethodHandle directly unless they are static final values.
These limitations aren’t actually a big problem because you can make
these calls static final by dynamically spinning bytecode (note that
you can reuse the JVM’s LambdaMetafactory for this), by using the
invokedynamic call, or by attaching the MethodHandle to a
MutableCallsite.
The fastest simple way to represent a closure in Java is by using an
abstract base class like:
abstract class FunctionOfMyType {
abstract Blah apply(Foo foo, Gar gar, Bar bar);
}
Why MethodHandles are useful is because combined with invokedynamic
they are much more flexible.
In the blog post JVM Typed Functions - Haxe - The Cross-platform Toolkit you have an implementation like:
public Object invoke() {
return this.invoke((Object)null);
}
public Object invoke(Object arg0) {
return this.invoke(arg0, (Object)null);
}
public Object invoke(Object arg0, Object arg1) {
return null;
}
The typed functions approach is actually a reasonable basis to go off of.
But you can do better.
You shouldn’t manually emit such adapter bytecode. Instead you should
use invokedynamic to generate the adapter at runtime. I would
recommend reusing a framework like dynalink.
My code is a slow WIP mess but you can see how to badly implement
GuardingTypeConverterFactory to convert between Java object’s and your
own representation.
If you let indy generate such adapters automatically then you will
save on bytecode.
There are really 3 approaches here:
- The existing approach
- Using indy to generate the adapters dynamically.
public Object invoke() {
return #indy "invoke" this;
}
public Object invoke(Object arg0) {
return #indy "invoke" this, arg0;
}
public Object invoke(Object arg0, Object arg1) {
return #indy "invoke" this, arg0, arg1;
}
- Use indy for closure calls and then generate the callsite dynamicly.
This allows for your own callsite cache but is the least compatible
with Android.
Speaking about Android I head the other reason for using the new
approach was supporting older devices.
One approach you could do is dynamically spin an adapter
object. Calling a function dynamically would be something like:
void mymethod() {
// stuff...
var y = ADAPTER.invoke(f, x);
// stuff..
}
private static final Adapter ADAPTER = AdapterObject.getAdapter(AdapterObject.class);
abstract class Adapter extends AdapterObject {
static Object invoke(FunctionSig sig, Object value);
}
Note that there is tradeoff for better inlining with seperate adapters
for each callsite but more bytecode.
But I would need a deeper understanding of Haxe’s needs. And again I
would really like to talk to the team.
Thanks
Steven Stewart-Gallus