跳到主要内容

JEP 276: Dynamic Linking of Language-Defined Object Models

Summary

Provide a facility for linking high-level operations on objects such as "read a property", "write a property", "invoke a callable object", etc., expressed as names in INVOKEDYNAMIC call sites. Provide a default linker for the usual semantics of these operations on plain Java objects, as well as a facility for installing language-specific linkers.

Goals

The primary goal is allowing compilation of high-level object operations on expressions whose type is unknown at compile time to INVOKEDYNAMIC instructions, e.g., obj.color becomes INVOKEDYNAMIC "dyn:getProp:color". The provided infrastructure will at run time dispatch a link request for these call sites to a set of linkers that have knowledge of specific object types, and can produce method handles for appropriate implementation of the operations.

These facilities provide a basis for the implementation of runtimes for programs written in languages that have a concept of object expressions whose types are not known at compile time, and need to express typical object-oriented operations on such objects.

The ability to compose multiple language runtime linkers allows a reasonable amount of interoperability between multiple language runtimes that cohabit in a single JVM process; an INVOKEDYNAMIC call site emitted by one language's compiler should be linkable by another language's linker when an object belonging to one runtime is passed to another runtime.

Non-Goals

We do not wish to provide linking semantics for operations for any single programming language or execution environment for any such language. We will not change the JVMS-described mechanism for bootstrapping a call site; we merely use it.

Motivation

INVOKEDYNAMIC provides a JVM-level foundation for the application-specific linking of methods. It does not provide a way to express higher-level operations on objects, nor methods that implement them. These operations are the usual regimen of operations in object-oriented environments: property access, access of elements of collections, invocation of constructors, invocation of named methods (potentially with multiple dispatch, e.g., link- and run-time equivalents of Java overloaded method resolution). These are all functions that are normally desired in a language on the JVM, yet every language implementation traditionally had to separately reinvent them. While there is an expected variability in the semantics of such operations between languages (they need to have some discriminator, after all, otherwise they'd all be the same language), they all have a common need: linking to Java platform objects (often referred to as "Plain Old Java Objects", i.e., instances of Java classes not belonging to or generated by the language runtime) in a manner that semantically matches their usage in the Java language.

Nashorn is a successful proof of the viability of the approach; when it encounters an expression of the form, e.g., obj.foo, the Nashorn bytecode compiler can just emit an INVOKEDYNAMIC "dyn:getProp:foo", and then defer to the dynamic linker at runtime to provide the implementation for property getters depending on whether the expression evaluates to a JavaScript object, some plain Java object, or something else.

Providing a linking facility from a dynamic language to Java also provides almost everything needed to link the language to itself, too, which Nashorn does: It has a unified linking mechanism that will dispatch to either its own linker or to the "plain Java object" linker as appropriate. Furthermore, if a runtime has a unified linking architecture to both itself and Java objects, the ability to link to objects coming from yet another language runtime in the same JVM effectively comes for free, providing a reasonable degree of cross-language interoperability.

This is a valuable common functionality for implementing any system (e.g., dynamic-language runtimes) that compiles to bytecode with some level of compile-time type uncertainty.

Description

The code in the jdk.internal.dynalink.* packages currently shipping as an internal dependency of Nashorn in JDK 8 contains a working implementation; we're looking to enhance and expose it as a set of packages named jdk.dynalink.*, hosted in a new module named jdk.dynalink.

We'll describe below the current design of it with the understanding that this section will evolve over time as the work progresses.

Operations

The operations on objects are expressed as INVOKEDYNAMIC instructions, with their name describing the operation.

This JEP defines the operations listed below. All operations defined by this JEP have the prefix "dyn:" and it is intended for this prefix to be reserved for future Dynalink extensions.

The operations for object properties are:

  • INVOKEDYNAMIC "dyn:getProp:<name>"(Object)Object is used to retrieve the value of a named property on the object. The types in the signature both here and in all other operations can be more specific than Object (they can be primitives as well).
  • INVOKEDYNAMIC "dyn:getProp"(Object,Object)Object is used to retrieve the value of a named property on the object, with the receiver being passed as the first, and the property name being passed as the second argument. In contrast with the previous operation, the name here is not fixed.
  • INVOKEDYNAMIC "dyn:setProp:<name>"(Object, Object)void is used to set the value of a named property on the object, with the first argument being the receiver and the second argument being the value to set.
  • INVOKEDYNAMIC "dyn:setProp"(Object,Object,Object)void is used to set the value of a named property on the object, with the first argument being the receiver, the second argument being the property name, and the third argument being the value to set. In contrast with the previous operation, the name here is not fixed.

The operations for collection elements are:

  • INVOKEDYNAMIC "dyn:getElem:<key>"(Object)Object is used to retrieve an element of a collection object (array, list, map, etc.) with a fixed key. In this form, the key is necessarily a string as it is expressed as part of the operation name, but runtimes are allowed to parse it as a number literal when linking to a collection using numeric indices.
  • INVOKEDYNAMIC "dyn:getElem"(Object,Object)Object is used to retrieve an element of a collection object with the receiver being passed as the first, and the element key being passed as the second argument.
  • INVOKEDYNAMIC "dyn:setElem:<key>"(Object,Object)void is used to set an element of a collection object (array, list, map, etc.) with the receiver being passed as the first, and the value being passed as the second argument. In this form, the key is necessarily a string as it is expressed as part of the operation name, but runtimes are allowed to parse it as a number literal when linking to a collection using numeric indices.
  • INVOKEDYNAMIC "dyn:setElem"(Object,Object,Object)void is used to set an element of a collection object (array, list, map, etc.) with the receiver being passed as the first, the element key being passed as the second argument, and the value being passed as the third argument.

The operations for method invocation and object creation are:

  • INVOKEDYNAMIC "dyn:getMethod:<name>"(Object)Object is used to retrieve a named method of the object.
  • INVOKEDYNAMIC "dyn:call"(Object, Object...)Object is used to call a callable object (e.g., something retrieved through an earlier dyn:getMethod invocation), with the first argument being the callable to invoke and the optional other arguments being passed to it. Depending on circumstances, the second argument to the operation will often be a "this" object.
  • INVOKEDYNAMIC "dyn:callMethod:<name>"(Object, Object...)Object is used to call a named method on an object, with the first argument being the receiver having the named method, and the optional other arguments being passed to the invocation. This operation can be implemented by folding dyn:getMethod into dyn:call. There is still a need for separate lookup and invocation operations, as some languages' semantics require them to be separate steps.
  • INVOKEDYNAMIC "dyn:new"(Object, Object...)Object is used to invoke the passed callable object as if it were a constructor, with the first argument being the constructor object and the optional other arguments being passed to it. Some languages allow callables to be invoked both as ordinary calls and as constructors, hence the need for a separate operation from dyn:call.

Linking mechanism

The entry point to the system is, as usual with INVOKEDYNAMIC, a bootstrap method. The bootstrap method needs to have access to a DYNAMIC LINKER. The bootstrap method will create an instance of a RELINKABLE CALL SITE, and tell its DYNAMIC LINKER to initialize it.

The DYNAMIC LINKER is an object that ultimately coordinates the linking. When a DYNAMIC LINKER initializes a RELINKABLE CALL SITE, it sets the site's target to a method handle of its own "relink" method that encapsulates the actual relinking algorithm that will be triggered on the immediately following, first invocation at the call site.

When the relink method is invoked, it will create a LINK REQUEST, which is an object containing the name and signature of the call site, as well as the actual arguments of the invocation that triggered the linking. By carrying actual invocation arguments it provides more information to the linker than what's available to the bootstrap method. The LINK REQUEST is passed to the GUARDING DYNAMIC LINKERs that the DYNAMIC LINKER manages.

GUARDING DYNAMIC LINKER is an object that can provide linkage for a specific class of objects (e.g., all objects instantiated for the same JVM Class). The linkage that the GUARDING DYNAMIC LINKER produces is usually conditional, meaning it is guarded by a boolean predicate that scopes its validity (e.g., "as long as the receiver is instance of List.class", or "as long as the receiver's class is an array class", etc.). A GUARDING DYNAMIC LINKER that can handle the current LINK REQUEST will produce a GUARDED INVOCATION, i.e., a triple of a method handle that implements the operation, a method handle that implements the guard, and optionally a collection of switch points for asynchronous invalidation of the linkage.

When a language runtime instantiates a DYNAMIC LINKER it will use in its classes' bootstrap methods, it will pass it an instance of its own GUARDING DYNAMIC LINKER as one of the obvious linkers it should manage. (Or pass it several linkers, since a language can have more than one; it is free to modularize its linking functionality into several classes; Nashorn currently defines eight.) The DYNAMIC LINKER will take these passed linkers, ordinarily add a "beans linker" as a fallback for linking ordinary Java objects, and also instantiate and add linkers for other languages that might be visible through the language runtime's class loader using the java.util.ServiceLoader mechanism.

The DYNAMIC LINKER will take the GUARDED INVOCATION produced by one of its consulted GUARDING DYNAMIC LINKERs, and give it to the RELINKABLE CALL SITE. Call-site classes that can participate in this high-level linking are expected to implement the interface RelinkableCallSite which define a "relink" method that allows them to receive GUARDED INVOCATIONs and incorporate them into their current linkage.

It is the duty of the DYNAMIC LINKER to act as a coordinator between GUARDING DYNAMIC LINKERs it manages and the RELINKABLE CALL SITEs that need to be (re)linked. This design separates the concerns of "what" is linked, which is determined by a linker (usually the linker of the language of the receiver) from the "how", which is the responsibility of the call site implementation, determined by the language runtime of the caller).

Relinkable call sites

The simplest such call site is a monomorphic relinkable call site, which on each relink invocation throws away its current linkage, and creates a MethodHandles.guardWithTest() combinator using the passed GUARDED INVOCATION's guard handle as the "test" handle, its invocation handle as the "target" handle, and a method handle that points back to the DYNAMIC LINKER's relink method as its "fallback" handle. That way, on every call, if the arguments don't pass the guard - meaning the currently linked invocation is not adequate for them - the call site will be relinked anew for those arguments. (Finally, if the GUARDED INVOCATION also carries a non-null Switch point the monomorphic call site would furthermore compose the resulting combinator with SwitchPoint.guardWithTest() invalidation combinator, again falling back to relinking when it is invalidated.)

Naturally, more complex call sites classes also exist, e.g., a chained call site that can contain several different methods linked into it at any time, by constructing a cascading chain of guardWithTest combinators. It could be further enhanced by containing profiling information and periodically relinking itself with a new chain containing invocations ordered from most invoked to least invoked, etc.

Value conversions

Another aspect of this infrastructure that we haven't mentioned earlier but is crucial for any dynamic language is support for value conversions. The java.lang.invoke API already supports the "method invocation conversions" as described by the Java Language Specification as part of MethodHandle.asType(), but we need to prepare for languages that allow additional implicit conversion, e.g., from int to java.lang.String. If a call site is linked to a method handle that expects a String parameter, but the call site is typed "int" for that parameter (or even more likely it's typed "Object" in bytecode, but an Integer can appear there as an actual parameter value in an invocation), the linker will have to insert a language-specific type conversion using MethodHandles.filterArguments().

Similar to a DYNAMIC LINKER, the system needs to have a TYPE CONVERTER FACTORY, which ties together a set of GUARDING TYPE CONVERTER FACTORIES. Similarly to the GUARDING DYNAMIC LINKER, a GUARDING TYPE CONVERTER FACTORY produces a guarded method handle that can convert values between specified source and target types. The method handle needs to come with a guard because often the call sites will be typed "Object" for most of their parameters, and thus the requested converters will be "Object to String", "Object to int" and so on. A language runtime will have to supply both a converter method handle as well as a guard that determines whether it is applicable, e.g., the method handle will correspond to "here's how I convert Object to int in general for the objects I recognize", and the guard will answer the "is the actual argument object of a type that I recognize?" question.

A final piece of the puzzle, normally only required for selecting among overloaded Java methods is a CONVERSION COMPARATOR. Now that we introduced language-specific conversions, when we need to link to a method on a Java class, we can actually end up with a wider set of applicable methods than what would permitted by the Java language, as the new conversions can render more methods applicable. If we tried to choose among this extended set of applicable overloads solely using JLS resolution rules, we'd often fail with ambiguity. Therefore, introduction of new type conversions also introduces the need for new conversion ranking rules. A GUARDING TYPE CONVERTER FACTORY can optionally implement an interface for CONVERSION COMPARATOR, which will be consulted by the dynamic overloaded method resolution logic, passing it the source type of a parameter at call site T and target types of parameters in the same position in the compared candidate methods U1 and U2, and it has to decide whether the conversion T-to-U1 or T-to-U2 is preferred.

The language implementers will need to implement a GUARDING DYNAMIC LINKER for their language, optionally a GUARDING TYPE CONVERTER FACTORY if their language allows more implicit conversions than what JLS allows, and finally a CONVERSION COMPARATOR in most cases where they also provide a GUARDING TYPE CONVERTER FACTORY. Actually, we should investigate whether the CONVERSION COMPARATOR functionality should just be made a mandatory part of the GUARDING TYPE CONVERTER FACTORY.

Unsuccessful linking

If no GUARDING DYNAMIC LINKER succeeds in linking an operation, the DYNAMIC LINKER will throw an exception of type NoSuchDynamicMethodException, a subclass of RuntimeException. If a GUARDING DYNAMIC LINKER can unambiguously determine that the operation will fail when invoked for the arguments it's being linked, it can either itself throw a language-specific exception immediately, or it can produce a GUARDED INVOCATION that throws an exception when it passes the guard.

Beans linker

The library contains a predefined GUARDING DYNAMIC LINKER named BeansLinker that implements all supported operations on ordinary Java objects (properties are mapped to either getter/setter methods or public fields; Java types (distinct from Class objects) are exposed as objects that can be used as constructors and holders of static fields and methods; arrays, lists, and maps act as collections).

Alternatives

This problem could be tackled with interface injection as well, but that's currently not a shipping feature in the JVM. In that case, instead of INVOKEDYNAMIC instructions the code would contain INVOKEINTERFACE instructions targeted at predefined interfaces that define methods implementing these operations. Our approach is more generic, as we don't restrict the linking behavior solely based on the type of the receiver (although that is usually the case), and our approach allows for easy loosely-coupled composition of linker behaviors. For example, a DOM node could be linked with a composition of a DOM linker and a plain Java linker, with a DOM node linker that maps the operations to the XML InfoSet semantics, but falls back to plain Java linking for non-DOM operations.

It is also interesting to consider a system that doesn't need INVOKEDYNAMIC at all, but features on-demand adaptive recompilation to reshape the code for different kinds of objects encountered at call sites. Again, we don't have such a system today in a JVM, and INVOKEDYNAMIC was specifically designed to be an alternative to such systems.

Testing

Nashorn already uses the internal version of this library, and will be expected to transition to use the version proposed by this JEP (see the Dependences section). The library is well exercised through Nashorn tests. Additional unit tests providing fairly good coverage exist in the original external (GitHub-hosted) version of the library that were not migrated with the internalization of the library into the jdk.internal.dynalink.* package. These tests could be adopted.

Risks and Assumptions

The library is designed to be useful to a wide variety of language implementations on the JVM. It is always possible some requirements of some language will not be accommodated if we don't receive sufficient feedback, on the other hand it will also be important to validate any such additional requirement against potential scope creep.

As it provides dynamic access to methods on classes, it would be a potentially attractive attack target. We are very conscious of these aspects and spent a lot of time reasoning and designing it in such a manner as to prevent any access or other privilege escalation. That is to say, to the best of our knowledge, it contains no security bugs, but has the potential of harboring them by virtue of being privileged code shipping within the JRE, accessing and then handing out method handles.

We do restrict it to operating only on public members of public classes in exported packages, though. The built-in beans linker discovers method handles using MethodHandles.publicLookup(), then internally caches and reuses them for performance reasons, with exception of invocations of methods marked as caller sensitive; those are always looked up with the MethodHandles.Lookup passed to the bootstrap method and never cached.

Dependences

The Nashorn JavaScript Engine (JEP 174) is expected to be rebased on this JEP, as the jdk.internal.dynalink package from JDK 8 will be retired in JDK 9. The change is expected to be minor as we'll strive to minimize the deviations from the existing API to the extent reasonable.