Wednesday, January 03, 2007

InvokeDynamic: Actually Useful?

Over time I've become less convinced that hotswappable classes would be an absolute requirement for the proposed invokedynamic bytecode to be useful, and more convinced that there's a number of ways a dynamic language like Ruby or Groovy could utilize the new bytecode. This post gives a little background on invokedynamic and attempts to summarize a few ideas off the top of my head.

Many folks, myself included, have long held that the proposed invokedynamic bytecode would only be useful if coupled with hotswappable classes. Hotswapping is the mechanism by which we could alter class structure after definition and have existing instances of the class pick up those changes. It's true this would be required if we were to compile Ruby all the way to bytecode; since Ruby classes are always open, we need the ability to add and remove methods without destroying already-created instances. The argument goes that if invokedynamic requires a dynamically-invoked method to exist on a target receiver's type, then we would only ever be able to invokedynamic against compiled Ruby code if we could continue to alter those types when classes get re-opened.

I do believe that hotswapping would be useful, but it's fraught with many really difficult problems. To begin with, there's Java's security model, whereby a class that's been loaded into the system *can not* be modified in most typical security contexts. The JVM does have the ability to replace existing method definitions at runtime, but that's generally reserved for debugging purposes, and it doesn't allow adding or removing methods. It also does not currently have the ability to wholesale remove and replace a class that has live instances, and it's an open research question to even consider the ramifications of allowing such a thing.

So what are the alternatives? Gilad Bracha proposed having the ability to attach methods dynamically to a given static class at runtime. This would perhaps be similar to the CLR's "dynamic methods". This idea perhaps has more merit...one issue not addressed by hotswappable classes is that even once we compile Ruby to bytecode, it's still dynamic and duck-typed. Would all methods accept Object and return Object? Is that useful? By specifically stating that some methods are dynamic and mutable (in the case of a Ruby class, likely all methods we've compiled), you effectively create the equivalent of hotswapping without breaking existing static types and their security semantics.

But this is all research that could and perhaps should occur outside invokedynamic, and it all may or may not be related. So then, can invokedynamic be useful with these class-structure questions unanswered? What does invokedynamic mean?

To me, invokedynamic means the ability to invoke a method without statically binding to a specific type, and perhaps additionally without specifying static types for the parameter list. For those that don't know, when generating method-call bytecodes for the JVM, you must always provide two things in addition to the method name: the class within which the method you're invoking lives and the precise parameter list of the method you want to call. And there's not much wiggle room there; if you're off on the target type or if the receiver you're calling against has not yet been cast to (or been determined to match) that type, kaboom. If your parameter list doesn't match one on the target type, kaboom. If your parameters haven't been confirmed as being compatible with that signature, kaboom. Perhaps you can see, then, why writing a compiler for the JVM is such a complicated affair.

So there's potential for invokedynamic to make even static compilation easier. Without the need to specify all those types, we can defer that compile-time magic to the VM, if we so choose. We don't have to dig around for the exact signature we want or the exact target type. Given a receiver object, a method name, and a bundle of parameter objects, invokedynamic should "do the right thing."

Now we start to see where this could be useful. Any dynamic language on the JVM is going to be most interesting in the context of the platform's available libraries. Ruby is great on its own, and there's certainly an entire (potentially large) market segment that's interested in JRuby purely as an alternative Ruby runtime. But the larger market, and the more intriguing application of JRuby, is as a language to tie the thousands of available Java libraries together. And that requires calling Java code from Ruby and Ruby code from Java with as little complexity and overhead as possible.

Enter invokedynamic.

Now I've only recently started to see how invokedynamic could really be useful even without dynamic methods or hotswappable classes, so this list is bound to grow. I'd love to have all three features, of course, but here's a few areas that invokedynamic alone would be useful:

  • Our native implementations of Ruby methods can't really be tied to a specific concrete class, since we have to be able to rewire them at runtime if they're redefined. If invokedynamic came along with a mechanism for doing a Java-based "method_missing", whereby we could intercept dynamic calls to a given object and dispatch in our own way, we could make use of the bytecode without having hot-swappable classes.
  • It would also aid compilation and code generation. In my work on the prototype compiler, one of the biggest stumbling blocks is making sure I'm binding method calls to the appropriate target type. I must make sure the receiver of a method has been casted to the type I intend to bind to or Java complains about it. If there were a way to just say invokedynamic, omitting the target type, it would make compilation far simpler; and I don't believe HotSpot would have to do any additional work to make it fast, since it already has optimizations under the covers that are fairly type-agnostic.
  • To a lesser extent, invokedynamic could push the smarts of determining appropriate method signatures onto the VM. I would supply a series of parameters and a method name, and tell the VM to invokedynamic. The VM, in turn, would look at the params and name and select an appropriate method from the receiving object. This is in essence all that's needed for real duck typing to work.
This last item calls out a perhaps surprising area that invokedynamic would be very useful: invoking Java code from a dynamic language.

When calling Java code from Ruby, for example, all we really have to work with are two details: a method name and potentially an arity. We can do some inference based on the actual types of parameters, but there's a lot of magic and a number of heuristics involved. If there were a JVM-native mechanism for calling arbitrary methods on a given object, without having to statically bind to those methods, it would eliminate much of our Java integration layer.

All told, I think invokedynamic would definitely be much more than a PR stunt, as some have claimed. It would eliminate one of the most difficult barriers to generating JVM bytecodes by allowing arbitrary method calls that aren't necessarily bound to specific types. I for one would vote yes, and I plan to throw my weight behind making invokedynamic do everything I need it to do...with or without hotswapping.

10 comments:

zproxy said...

I liked the kaboom part:)

lopex said...

Also reconsidering Fixnum optimizations, the compiler would have to emit guard code before all e.g. ... iload, iload, iadd ..., points to check whether specific Fixnum method has been changed. I'll try to check how YARV deals with that - the setting responsible for that is OPT_SPECIALISED_INSTRUCTION in vm_opts.h

John Rose said...

I think duck typing at its best uses more types than just 'any' (= Object). Also, invokedynamic and incremental class extension can be viewed as low-level building blocks for dynamic calling sequences. See my post.

John said...

Hi Charles,

it is possible to quite effectively mimic the functionality of invokedynamic; our project at java.net has been doing this for years.

Please, have a look at this very short example.

marcin szofer said...

I can't understand all that hype about java scripting, JSR292 and so on.

What makes Java greatest is the safety it guarantees as a unique environment of the language, the class library, the virtual machine and the security model. In that order, I should think. That's why Java is the platform of choice - the language makes it easy to write more robust, maintainable code than any other language, the class library allows reusing great code written by other people which provides more "batteries" than any other platform has, the VM allows it to run on more hardware/os than any other thing can, and the security model where it's needed allows controlling what the code can do to some degree - it has been a unique feature for so long.

Now allowing Ruby, Python et al. will effectively kill Java - those languages are simply asking for bugs and problems due to their "duck type" systems and lack of checked exceptions among other issues. But once it is made possible, people will start writing class libraries in those excuses for a programming language and what will it mean? Less robust, less maintainable code leading to more less robust and less maintainable code. Soon even ignoring Ruby and using Java won't guarantee that foreign class you use isn't a duck in disguise. But worse still, those languages are mostly UNIX-oriented so it may also kill portability...

Now if I take a Java library I expect it to run everywhere or has a warning sign that it uses native code so I beware. But allowing lower standards' languages into Java island will make it also pollute and blur.

I think it should not be allowed but discouraged or even forbidden (by a license stating no code but written in Java can run on what may be called Java platform).

Rick said...

Marcen, we can debate the merits of dynamic languages on the JVM until we're blue in the face, but the fastest way to kill off the Java "platform" is to forbid other languages on it.

Java (the language) is a boiler-plate ridden, barely-OO mess.

marcin szofer said...

Rick: Is it really? Yet Java platform exists over 10 years, is successful like no platform before and it's been at least unfriendly (not to say hostile) to other languages. Not only wasn't it killed but is the first platform of choice, guess why?

Maybe Java doesn't satisfy you language purists and you find it's OO model broken and it's syntax bloated, but is not of any signoficance.

Some OO purity has been given up and primitives added, yes, but that allows to implement some protocols which are very fast now and can be worked with almost on the wire since they are machine word aligned. If they were boxed you'd have 31-bit integers or some other strange size and giving no real gain but making things more difficult.

Making syntax verbose by requiring all things be declared is also a positive thing because it makes more information available, not only to the compiler but to other developers and code maintenance guys as well - which is more important. (That's why I despise static type inference like e.g. D has - maybe the compiler has all the information it needs to know exactly the type to use but the human reading code a few months later surely does not.) That means more maintainable code and thus less pains and better programs.

In fact, on a project I currently work for we decided that no method invocations may be chained, so people are not allowed to write:

Type1 obj1 = obj2.method1().method2();

but instead they have to write:

Type3 obj3 = obj2.method1();
Type1 obj1 = obj3.method2();

and of course they have to name those types, methods and variables so it makes sense in reading, no just objX :) The only exception to that is BigInteger/BigDecimal arithmetic, we do it quite much and it would be impractical. We have also disallowed using expressions for arguments of method calls, and ordered using variables or constants instead, with the same exception.

Another thing forbidden is exception chaining, every exception is to be logged at the level it is caught and serviced, and if a new exception is thrown it cannot chain the original exception.

Comments (other than Javadocs and task marks like FIXME) are also strictly forbidden, the code has to be readable without comments. So the variable names and test names and bodies look like comments :)

(And we have tools to check for violations so it is not just an unobserved wish but really enforced coding policy.)

While it was disliked at the start, and many programmers have complained that it would make their development time longer, I just told them that the company will pay for that longer so they just don't complain anymore.

And something more was done - most complaining people were assigned some maintenance work on projects that didn't have those policies and then, after they have done it, they were assigned to do other work on other people's code in their main project. And this has convinced everyone that this verbosity is positive and really needed because just gaining a line here, a line there at cost of making people who will work with this code later suffer and cry cause they don't understand the code is no good and in fact will cost everyone more and more.

Nico said...

"And that requires calling Java code from Ruby and Ruby code from Java with as little complexity and overhead as possible."

That's the crux of it. You _can_ host Ruby, or Scheme, or what have you, in the JVM, but if you want to be able to freely refer to objects from Java or the hosted language in the other, then you need JVM support. For example: no closures? no problem, just treat functions/closures as instances of a class specific to the hosted language and provide your own calling conventions -- but then the Java compiler won't know those conventions and won't recognize those objects as funcallable. Similarly for continuations: implement closures, implement CPS conversion, lambda lifting, done -- but again, there will be some severe restrictions in interfacing with Java.

Now, invokedynamic with methods that return Object and take Object as arguments plus anonymous methods would be sufficient, I think, to implement method dispatch above the JVM and have something very close to open classes (add an unknown method call feature, as you noted, and you have pretty much all you need to implement open classes). And the resulting calling convention would be understood by Java compilers, so that would be very good. Of course, the trade-off is that you'd be implementing method dispatch in JVM bytecodes, so you'd be taking some sort of perf hit there; otoh perhaps you could take advantage of this to better optimize method dispatch at runtime, since you could potentially replace the method dispatch code at runtime.

John Rose said...

Well said, Nico.

I think dynamic invoke needs (1) to look enough like other JVM invokes that the compiler can optimize it, (2) to be able to directly call Java methods when they are there to call, (3) to have a flexible MOP "hook" for cases where there's an API mismatch, (4) to support some sorts of per-call-site caching hacks, and (5) to avoid barriers to cross-language interoperability.

The following design aims at these goals:
http://blogs.sun.com/jrose/entry/autonomous_methods_for_the_jvm

It does this by partially decoupling methods from classes in the JVM.

michael said...

In reply to John (Catherino),

As I see it, the main advantage is that every language that wants to call a (Java) method given only an Object, method name, and Object[] of arguments doesn't have to implement thier own version of your "findBestMethod"

Of course, this could easily be solved by extending java.lang.Class or java.lang.Object with an appropriate getMethod