In-process cross-language object interaction: adapters or navigators?

Friday, 4th May 2007

Introduction

A runtime for a programming language is usually itself a piece of software, and as such it is too implemented in a programming language1. This runtime will rarely operate as a completely isolated layer atop of the runtime layer of the language used for its own implementation. More often, this runtime will provide a way to interact with the runtime structures of the layer below it, as that provides useful integration capabilities. If both the implemented language and the implementation language subscribe to the paradigm of object-orientation, the "runtime structures" in question are usually objects, and the "way to interaction" is basically a mechanism by which code from one language can observe, manipulate, and create objects from the other language. The levels of integrations may differ - in the ideal integration, the native objects from one language are indistinguishable from native objects in the other language.

Creating a no-rough-edges injection of the implementation language's objects into the implemented language can be challenging, as cases of impedance mismatch invariably arise as we try to reconcile conceptual differences between the languages2. Similarly challenging can be defining an injection the other way round - from the set of objects in the implemented language into the set of object of the underlying implementation language.

But it can get even more fun than that! Imagine for a moment that you have runtime implementations for two independent languages, R1, and R2, both runtimes implemented atop of a third, base runtime B. Let's suppose both these implementations provide intuitive object marshaling capabilities between R1 and B as well as between R2 and B. Do you think it'd be nice if those capabilities could be combined to provide an equally intuitive marshaling between R1 and R2? And by "combined" I actually mean "combined without R1 knowing anything about R2 and vice versa". I do. Therefore, we're not talking about only upstream marshaling from implementation language to the implemented language, nor only about downstream marshaling from implemented language to implementation language, but also about automatic cross-marshaling between various implemented languages running on the same underlying runtime.

Notes on this article

The ideas discussed in this article aren't particular to any programming language or runtime system. When I use examples, I will however use examples of language runtimes implemented atop of the Java Virtual Machine (JVM). I'm personally involved in two language projects atop of the JVM: FreeMarker and Mozilla Rhino. At times I'll use them as references of various techniques discussed here. However (reinforcing the point), the ideas themselves are independent of languages and runtime systems.

Implementation of higher-level objects

Most of the implemented language runtimes (from now on referred to as "higher-level runtime", or simply HLR) use 1-to-1 mapping of their own objects to underlying runtime's (from now on referred to as "lower-level runtime", or simply LLR) objects. In a typical example, every single JavaScript or Python object in the runtime written for JVM will map to a single Java object. The class of the LLR object is usually something specific to the implementation of the HLR, i.e. it'll live in package org.mozilla.javascript, or org.python.core or something similar. They usually implement one or more interfaces expected by the HLR. This is assuming the LLR has a concept of classes and interfaces of course. (If it doesn't then these LLR objects will probably just provide methods with expected names and rely on duck-typing.) One way or the other, the HLR property-retrieval expression (usually present in most languages)

someObject.someProperty

typically ends up being implemented in LLR as

somObjectImpl.getProperty("someProperty")

where getProperty is a method either declared in an interface that is implemented by the class of someObjectImpl, or just simply expected by the HLR (if its LLR implementation uses duck typing).

Adapters: internalizing higher-level knowledge in the object

Regardless of whether someObjectImpl simply exposes a getProperty method, or implements some hypothetical HigherLevelLanguageObject interface, it is still the object itself that has to know how to conform to the expectations of the HLR. For the classes that implement the HLR's native objects, this is not really a problem, after all, they're implementation-specific by definition. But what happens if we want to lift an unaware LLR-native object into the HLR? We typically wrap it into a HLR aware object that is often called wrapper, although the technically correct term would be adapter.

The adapter exposes the interface(s) expected by the HLR, so as far as the HLR is concerned, it is indistinguishable from its native objects (in fact, for most HLR's "implements my expected interfaces" actually is the definition of its native objects).

Drawbacks of the adapter approach

The approach however has several drawbacks, namely:

For each adapted object, we need one adapter, which imposes a memory overhead (as well as a creation overhead, although those tend to be not significant in modern LLRs, i.e. in JVM or a CLR)

If our HLR programs are traversing LLR object hierarchies a lot, we either create and discard lots of adapters, or have to write caches so a particular LLR object is always mapped to exactly the same (cached) HLR adapter. You can not really escape it if there's a requirement that a LLR-to-HLR mapping must preserve object identity invariants - in that case you must ensure that if a single LLR object can be observed twice in the HLR program, the HLR program must see the same adapter instance. However, such adapter caching also must take care to not interfere with automated garbage collector of adapted objects in the LLR, therefore it must be weak. All these aspects add significant complexity to any fully-developed adapter caching solution.

A corner-case problem arises if the HLR runtime defines not one, but several interfaces for its native objects. I.e. consider it has interfaces HigherLevelDictionary and HigherLevelList. For sake of the argument, the dictionary elements are addressed using string keys, and the list's using integer keys. Any adapter factory (we'll get to factories in a moment) must decide up front which interfaces will a particular adapter implement. If there are lots of interfaces that can be implemented, it is hard to come up with all meaningful combinations up front, which tends to lure people to look into dynamic code generators3. If, on the other hand, there's a single überinterface, it can be tedious to have to provide implementing all the methods we don't really need in a particular case. Of course, if your LLR encourages duck typing instead of static interfaces, you wouldn't care one way or the other, but most typical LLRs (JVM and CLR) would encourage you use explicit interfaces.

Then there's the effect I refer to as "factory incomposability". To create adapters, we typically employ a factory object4. Usually, adapters are aware of the factory that created them, and if they too need to produce further adapters (for return values from property getters or method invocations on the LLR object), they'll hand the LLR object to their factory. The problem arises when you want to compose factories. Imagine that you have a factory for XML DOM objects and factory for generic POJOs, and even you write a composing factory that delegates to either the DOM or the POJO factory, based on whether the object to be adapted implements a DOM interface or not. It works for top-level wrapping invocations. However, if a POJO returns a DOM object from one of its properties, its adapter will likely wrap it as a POJO, using its own factory. Except if you explicitly design all your adapters to have a notion of a "factory outer identity", that is, to be able to discover the outermost composing factory in which their direct factory might be participating. This is however again an additional complexity you don't really want in your code; it makes thing more complex than you intuitively feel they should be, and therefore less elegant than they could be. If you think the DOM/POJO discrepancy is not too significant (and you really shouldn't think that), I'd like to remind you that we'd also want to be able to do full cross-language integrations, i.e. using Python objects in JavaScript, or Ruby objects in Groovy.

Navigators: externalizing the higher-level knowledge

Instead of declaring interfaces that need to be implemented for an object to be usable in HLR, and then implementing these interfaces in adapters and creating factories, there is another approach that yields much simpler implementations. I call the objects we use in this other approach "navigators", after the naming used in the first software I saw that uses this approach, the Jaxen XPath engine. I could just as well call them "metaobject protocol implementations", as that's what they really are, but "navigator" is a simpler term to use in a longish article. Put simply, a navigator is an object that is able to evaluate HLR expressions on an arbitrary LLR object. To present an example, Rhino (a JavaScript HLR implemented atop of JVM as LLR) currently expects objects to implement the Scriptable interface, and then invokes the get method on them, as:

((Scriptable)someObject).get(someProperty, ...)

if it used the navigator approach, it'd rather ask its configured navigator to do it:

navigator.getProperty(someObject, someProperty, ...)

It's easy to see that this approach doesn't need adapter objects, so the whole chapter of problems with efficient creation and/or caching of adapters becomes a nonissue. We keep operating on the LLR objects directly. Of course, we might need to use special object classes in certain cases - a JavaScript object can dynamically acquire properties and that behavior can't be modeled with a Java or CLR class, so a JavaScript implementation might keep a separate NativeObject class, and have its navigator recognize it as such.

With support in the navigator's contract, navigators can be composed into chains. A navigator can have a method to declare whether it can operate on a particular object, or even better, a special return value from its methods saying it's unable to perform the requested operation on the object. This "special return value" could be an exception as well, although I have performance concerns about that, despite word being out that recent JVMs have a rather good performance optimizations for exceptions. And anyway, if it's a situation you expect - and we expect it with chained navigators that not all navigators will be able to handle all objects - you shouldn't model it with exceptions. In the end, this approach allows a very fine grained arbitration on a per object and per operation basis, going through the chain of navigators until one can handle the operation on the object. If neither can, HLR raises an error. Consider again the DOM/POJO example. Let's assume we have a composing navigator that composes a DOM and a POJO navigator. If we ask it to evaluate the following expression:

someObject.namespaceURI

Then the navigator chain of two navigators might actually execute the equivalent of the following:

Let's assume the DOM navigator works by descending child elements, and since it won't find a child element named "namespaceURI", it'll return the special NOT_FOUND5 value and thus allow the next navigator in the composition to give it a try. The POJO navigator will then map the property to the invocation of the getNamespaceURI method.

In order for the approach to be really useful for cross-language in-process interchange of objects, all languages on the particular LLR would need to use the same Navigator interface as their foundation. It should probably be released as a public-domain library to ensure its universal applicability and ubiquity, similarly to the (universally applied and ubiquitous) SAX. Each language would provide its own native navigator implementation, but there should also be some sort of a way to chain navigators. In JVM at least, the JAR service discovery mechanism is appropriate for this task, although a directly implemented ChainingNavigator class is also possible - with that at least you could build your chain of navigators explicitly, thus prioritizing them. I'd expect that each HLR would have a mechanism for injecting an arbitrary navigator into it, as well as a convenience "use all discoverable navigators in addition to your native one" mode. It is easy to see that you could natively use, say, JRuby objects in Rhino if they both used navigators, and you added the Ruby navigator to the Rhino's chain of navigators. A chain of navigators a specific language uses would typically contain a navigator for the language's native object implementations at the top of the chain, a generic fallback navigator for generic LLR objects (in Java, that'd be a generic JavaBeans navigator for POJOs) at the bottom of the chain, and any number of extensions in between for intelligent integration with other object models, thus delivering on the promise of cross-language object interaction.

Exposing HLR objects in LLR

Sometimes it makes sense to automatically marshal HLR objects back in LLR. A typical example is passing HLR objects as arguments in method calls on LLR objects. In case our LLR is statically typed (again, both JVM and CLR fit the description), we can even use the expected type of the argument as a hint as to how to represent the HLR object in LLR. I.e. if the expected type is java.util.Map, we'll likely create a Map representation that exposes properties of the HLR object. If the expected type is an array type, we'll create a native array of matching type, and marshal the HLR array/list/sequence elements to the expected component type of the array and so forth. A navigator can support a method of the form Object cast(Object object, Class hint) to support marshaling of the objects it knows about to a LLR representation.

(Of course, the approach with adapters and adapter factories can also provide such a facility, it's not exclusive to navigators.)

Cross-language object interaction with adapters

It would be unfair to say that cross-language object interaction is not possible with the approach of adapters and adapter factories. In the most trivial case, the aforementioned cast() method can be implemented on the adapters or the adapter factories as well. Passing an object from HLR language H1 to HLR language H2 can be implemented by first marshaling the object to its LLR representation using the cast() method (using either the root class of the LLR class hierarchy as the hint, or alternatively, if known, the root class for objects of the HLR2 native representation and hope HLR1 is aware of it), then wrapping that representation in a H2 adapter. Of course, since we can't know for certain how will the program in HLR 2 want to interact with the object, we can't be smart about marshaling. Will it want to use it as a map? Or as an array? We can't know in advance.

That's why I believe the navigator approach is more powerful, as it doesn't force you into such premature decisions. If HLR 2 program wants to treat the object as an array, it might invoke a navigator.getProperty(Object, int) on it to retrieve a property with an integer index. Or, it might ask for an iterator over all its values using navigator.values(Object) when it wants to run a "forall" loop over it. The chain of navigators hidden behind the facade object "navigator" will then try to evaluate the requested operation on the object, eventually finding a navigator able to deal with it (either the HLR 1 navigator or the generic navigator).

When you compare the expressive power of the adapter and navigator approach, they appear fairly similar. Actually, they are identical as long as you're only concerned with object interaction between a single HLR and the LLR (i.e. Rhino/Jython/JRuby and JVM). As soon as you want to implement a decently intuitively behaving interaction between two HLRs (i.e. use JRuby objects natively from within Rhino) on the same LLR though, the navigator (or, let's say again at the end of the article, pluggable metaobject protocol implementation) approach's advantage becomes obvious.

This level of interoperability - differing languages being able to use each others' objects natively - opens up new possibilities for people implementing software systems, as they become free to implement different subsystems in languages that fit the job most.

Discussion

If you feel like discussing anything related to this article, you can post a comment to my related blog entry.

1 Usually a different one, but you can actually come across exotic examples of interpreters implemented in the language they interpret.

2 Well, except when the implemented language was specifically designed with a concrete implementation language in mind, in which case it can just adopt the implementation language's object model, but in these case it really is more of a dialect of the implementation language, see either Groovy or Scala for examples.

3 FreeMarker is an example of a multi-interface approach. It is probably not a widely known fact that the nowadays quite ubiquitous Java CGLIB library originally came to life when Chris Nockleberg figured out he'd need to dynamically generate classes implementing arbitrary combinations of FreeMarker interfaces at runtime.

4 We call factories too "wrappers" often, as it's them who do the wrapping - in that nomenclature the adapters themselves would then probably need to be called "wraps". That's why I stick to the more precise "factory" and "adapter" terms.

5 I'm intentionally differentiating "not found" and "can't handle" return values. The first says "I have an interpretation of this object and can authoritatively say that this property doesn't exist here according to my interpretation". The other says "I don't know how to interpret this object, thus can't say anything about the existence of the property".