-
Notifications
You must be signed in to change notification settings - Fork 23
What if Java had dynamic type?
This page is a speculation on a hypothetical addition of a dynamic type to Java language backed by the invokedynamic and Dynalink; it's a thought experiment on how would such an addition work, and what support from the underlying platform would it need. It is not to be interpreted as a commitment on anybody's part to anything.
It starts simple, by introducing a new type to the language, that we'll refer to as the "dynamic type". While using a new keyword , i.e. dynamic
would be tempting, we're aware that introducing new reserved words into the language is problematic, so we can settle for an actual marker interface type, java.lang.Dynamic
that is handled specially by the compiler. (This is similar to John Rose's java.dyn.Dynamic
proposal.) In its simplest form:
Dynamic obj;
...
obj.invokeSomething();
would cause "invokeSomething" to be dynamically linked. If it followed the current Dynalink metaobject protocol, it'd end up being compiled to something like:
ALOAD 1
INVOKEDYNAMIC "dyn:callMethod:invokeSomething":void(Object)
However, for this to work, the class needs to have access to an actual dynamic linker object. In a Java language that supports dynamic linking, a dynamic linker must be an entity provided by the platform, with a significance similar that of the class loader itself. It has to be present in order for a class to be fully linked. It becomes a critical piece of platform infrastructure. A Java class compiled from a source that uses the dynamic type will need to designate a bootstrap method for linking all its dynamic call sites that delegates to the dynamic linker for that class.
How can a class obtain a dynamic linker for its bootstrap method? One obvious possibility is to add a
public DynamicLinker getDynamicLinker()
method to the java.lang.ClassLoader
class. Every class loaded through a class loader would use the dynamic linker associated with its loader. Another (non-mutually-exclusive with the previous one) possibility is to add thread context dynamic linkers, similar to how thread context class loaders work. This would mean having
public DynamicLinker getContextDynamicLinker()
public void setContextDynamicLinker(DynamicLinker)
APIs to java.lang.Thread
. Classes could then decide to delegate their linkage to the context dynamic linker, or if it is not set, to their class loader's linker. Actually, this logic could be handled by a single bootstrap method hosted in some well-known utility class that all classes would just delegate to:
public static CallSite standardJavaBootstrap(Lookup lookup, String name, MethodType signature) {
DynamicLinker linker = Thread.currentThread().getContextDynamicLinker();
if(linker == null) {
linker = lookup.getLookupClass().getClassLoader().getDynamicLinker();
}
return linker.link(new ChainedCallSite(CallSiteDescriptorFactory.create(lookup, name, signature)));
}
The definition and behavior for DynamicLinker
is pretty much what today exists in Dynalink as org.dynalang.dynalink.DynamicLinker
, except of course it would become part of the standard library, and therefore live on as java.lang.DynamicLinker
. There would always be a default instance of it, set up similarly to what today new DynamicLinkerFactory().createLinker()
would create for you in Dynalink (that is, it'd autodiscover all dynamic linkers implementations in the classpath using the JAR service mechanism, and combine them into the single dynamic linker).
Aside from method calls, other kinds of expressions in the Java language would be defined on the expressions of type Dynamic. Specifically:
- Field access expressions would be mapped to property getters and setters by compiling them into INVOKEDYNAMIC instructions calling the
dyn:getProp
anddyn:setProp
operations. Note that the built-in BeansLinker already maps these operations to field access when the target class has public fields of that name and no explicit getter/setter methods, therefore changing the declared type of an expression from a static type to Dynamic won't affect the runtime behavior of the operations. - Array access expressions could be mapped to element getters and setters by compiling them into INVOKEDYNAMIC instructions calling the
dyn:getElem
anddyn:setElem
operations. Note that the built-in BeansLinker already maps these operations to array access when it links an array, therefore changing the declared type of an expression from an array type to Dynamic won't affect the runtime behavior of the operations. However, since the BeansLinker also defines these operations onjava.util
lists and maps, the program that accesses them using the dynamic type will be able to use the array access syntax on them too. It will also require that when used with the dynamic type, the indexes used in the expressions be allowed to be of arbitrary type (so the expression works with maps), and not only those allowed to undergo unary numeric promotion (as is the case with array and list indices). -
new
operator would be somewhat more drastically changed, as it could now also be applied to an expression of type Dynamic, and it would compile into an INVOKEDYNAMIC ofdyn:new
operation.
Applying either a field access expression, method invocation expression, or array access expression to an expression of dynamic type results either in void type (for field and element setting) type, or dynamic type (in all other cases).
The static types of the parameters in the method invocation expressions and indexes in array access expressions should still be determined using ordinary Java rules, and during the compilation reflected as such in the emitted bytecode. This can allow the dynamic linker to make optimization assumptions for the parameters. For example:
Dynamic obj;
int x;
...
obj.doSomething(x);
is compiled to:
ALOAD 1
ILOAD 2
INVOKEDYNAMIC "dyn:callMethod:doSomething":void(Object, int)
that is, the type of the int argument is preserved even if it's being used as an argument in the dynamic invocation.
Of course, it should always be possible to explicitly cast an expression of type Dynamic to any other type, including a primitive type. However, in case such a type cast is applied to the return value of an INVOKEDYNAMIC instruction, the method signature at the INVOKEDYNAMIC site should specify the casted type as its expected return type, instead of an explicit CHECKCAST and possibly unboxing if it is a primitive; this should be left to the linker to decide. This can allow for optimal passing of primitive return values, without gratuitous boxing and unboxing. For example, this code:
Dynamic person;
...
int age = (int)obj.age;
should compile to:
ALOAD 1
INVOKEDYNAMIC "dyn:getProp:age" int(Object)
ISTORE 2
The way Dynalink composes method handles will ensure that if the method handle linked into this call site returns a value that isn't convertible to an int
, a ClassCastException
will be thrown, as expected.
Standard Java access control should apply at time of linking. Code belonging to a class should be allowed to invoke a private method on itself through an expression of dynamic type, if the concrete value is an instance of that class. In other cases, however, an exception must be thrown. We're, however, in a bit of a trouble here, as mandating the checked java.lang.IllegalAccessException
is unfortunate as it then mandates a try/catch block around every usage of dynamic types in the code. Using the non-checked java.lang.IllegalAccessError
is also unfortunate, as we shouldn't be throwing Error severity throwables in this case. We're in a situation here that if such an illegal access occurs, it should be considered a programmer bug, not unlike trying to address an array out of its range, and as such it would warrant a runtime exception. It seems that we'd need a new java.lang.IllegalAccessRuntimeException
for the occasion. Naturally, it has the potential to confuse a novice programmer that we now have three different Throwable types that can signify an illegal access: one for incompatible class change at "ordinary" JVM linking time, one for reflective access, and one for dynamic linking time. I'm sure it'll be a great interviewing quiz question for Java programmers.
However, we still have genuinely new error conditions that previously were caught at compile time. The code might try to invoke a method on an object through a dynamic typed expression, and the method might not exist. Again, this either calls for a new, unchecked counterpart to java.lang.NoSuchMethodException
(java.lang.NoSuchMethodRuntimeException
), or just using java.lang.NoSuchMethodError
. Alternatively, the method might be overloaded, and the actual types of the arguments might not be sufficient to unambiguously choose a single target method. This used to be caught at compile time, but now it can be a runtime concern, and it might need yet another new kind of exception - except if we overload the meaning of "no such method", which, to be honest, we could do.
It is in the very nature of dynamic invocation that a compiler can not infer whether a dynamically linked method will throw an exception or not; therefore, each dynamic call site should be treated as if it throws java.lang.Throwable
. Obviously though, we don't want to force each method containing an INVOKEDYNAMIC instruction to have to declare throws Throwable
.
What we can do instead is prescribe a standard way of handling these exceptions in the bytecode emitted by the Java compiler. We can declare a runtime exception, java.lang.UndeclaredDynamicThrowable
, similar to java.lang.reflect.UndeclaredThrowableException
, and mandate that any Java method that uses dynamic invocation must compile in a form equivalent to having the following outermost catch blocks:
void someMethodUsingDynamic() throws IOException {
try {
... whole method body having one or more INVOKEDYNAMIC instructions...
} catch(RuntimeException|Error|IOException e ) { // IOException added here as it's declared in "throws"
throw e;
} catch(Throwable t) {
throw new UndeclaredDynamicThrowable(t);
}
}
The benefit of having these catch blocks be the outermost blocks in the method body is that if the programmer writes the method body explicitly to handle some of the exceptions (i.e. it expects ParseException
), then it will be caught by his own catch blocks, and only those checked exceptions that aren't either explicitly caught or declared to be thrown by the method will propagate wrapped into an UndeclaredDynamicThrowable
. Again, this is something the compiler would provide, and not something that the programmer should explicitly add to their program. In case the method does actually declare throws Throwable
then obviously no such catch blocks are needed.
You might not have been aware of it, but this wiki is publicly editable, so as long as you're a GitHub user, you can add your comments into this page, in the venerable style of programming discussion pages on the first ever wiki, the C2 WikiWikiWeb. Feel free to go below the horizontal rule at the bottom of this paragraph, all the way past the last comment, add another horizontal rule, and leave your comment. I will preserve those comments without changes as long as they're on topic and the tone is professional. You can add comments in the main text itself too, but I will periodically perform some gardening on those and am not promising anything with regard to those - they might get incorporated, or they might fall victim to landscaping. I encourage you to sign your edits/comments like this -- Attila. (Yes, I know the autorship can be traced back through the page edit history. It's just nice to have it spelled out on the page itself.)
Comments go here
Does
int age = (int)obj.age;
also work with custom types or standard Java types, like Date or Money? (upcoming JSRs;-) Thanks, Werner
It is supposed to work with any type -- Attila.
I'll be honest: Even with it's grievous faults (generics), I like Java's type system the way it is. I'm willing to be convinced otherwise though, but let me explain my fears... Take a look at the problems with PHP: It tried to be everything to everybody, and in the end, isn't really good at anything. Java is already a large mix of styles, and the introduction of Lambdas in Java8 will add 'more ways to do things'. Wouldn't dynamic just throw more gas on a smoldering pile? Consistency and simplicity are key when working on non-trivial sized projects... What do you guys think?
-Jonathan