Invocation Semantics as First Class Abstractions
The invocation semantics of common object oriented environments are fixed. Either the programmer is forced to use the only available semantics, or he can choose between a small fixed-sized set of semantics. Our invocation abstractions offer an open way to create arbitrary many new semantics. The library consists of two parts. Both of them are presented in this section. The first part offers a meta-programming interface. It allows to browse class interfaces, as well as to view and to change their invocation properties. The second part allows the construction of new invocation abstractions either by writing new ones, or by combining existing abstractions.
The first part offers a view on the capabilities of classes relevant for the invocation semantics (see reduced interface below). The procedure GetClass returns a Class object for the passed instance. This object contains all information on all methods (including inherited ones) with all information relevant in order to change the invocation semantics. One may now scan this information or change it according to the current necessities.
Building and Using Invocation Abstractions
Building new invocation semantics or combining existing ones is quite simple using the abstractions of Invocations. The library itself implements no concrete implementation, but only the abstract base classes. Invocation is the base class of all invocation abstractions.
Invocation = POINTER TO InvocationDesc; InvocationDesc = RECORD PROCEDURE (inv: Invocation) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream): Linearizers.Stream; END;
It defines only one method. This method Invoke has to be implemented by all concrete sub-classes. Whenever a method is invoked, which uses this invocation abstraction, the method Invoke is called either directly by the stub code, or indirectly by another abstraction. When called, Invoke receives three parameters. obj is the receiver, id identifies the called method, and s is a stream containing the marshalled parameters. An actual implementation on this level is not possible. Whatever the idea of a concrete subclass is, after the actual method has been invoked, the skeleton code returns the marshalled output (return value and output parameters) in another stream. This stream has to be returned to the caller.
PROCEDURE (inv: MyInvocation) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream): Linearizers.Stream; BEGIN DoSomething(obj, id, s); result := InvokeMethod(obj, id, s); DoSomething(obj, id, s, result); RETURN result END Invoke;
One extension of Invocation is defined. InvocationFilter allows decorating other invocation abstraction using the decorator pattern.
Just-in-Time stub generation
With the library describedabove, one can put together the desired semantics and parameter passing modes. Tu put it to work one has to generate code that intercepts ordinary method invocations. This is done by cloning the actual object. Methods invoked on the actual object are still handled as before. Methods invoked on the clone object (stub object) are handled, as specified in the meta-class-information, by the generated stub code. It has the same interface as the actual method and replaces it transparently. It marshalls the parameter and activates the appropriate invocation semantic. Simultaneously, the skeleton code is generated. The skeleton is the counterpart to the stub. It unmarshalls the parameters and calls the actual method. These code pieces are generated with help of the module Objects. It offers the necessary facilities to generate stub and skeleton code as well as the abstraction DirectInvocation, which invokes a method as done when no interception is used.
One of the main goals was - from the beginning - that the delay, introduced by the increased flexibility of arbitrary invocation abstractions, is kept as small as possible. That means, that the delay is constant regarding to the number of managed objects and the number of different semantics. This is achieved by introducing an array for the active invocation semantics. At run-time, each newly defined semantic is assigned a slot within this array, i.e. it receives a unique number. As the stub code is generated but afterwards, its possible to use this knowledge and access the correct semantics directly through an arrays access. As this value - at compilation time - is a constant its even possible for the compiler to calculate the offset. This reduces the actual overhead on the client side to a neglectable size.
What is missing in our scheme so far is the transport layer. It's the means used to transport the information from the client to the server. This part is replaceable and depends only on the chosen semantics. In a distributed environment the transport layer would actually send the data to another host. If only local decoration is used its the code piece which connects the stub object with the actual object. Whenever this layer transported another invocation to the server it has to look for the correct server-side semantics. There are two possibilities on how this lookup could be done.
The first possibility uses a more or less advanced data structure to lookup the necessary semantics. The invocation supplies the receiver object, as well as a method identifier. With the help of the meta-class information one can decided on the semantics. The complexity of this data structure decides on the amount of time spent for the lookup. The prototype implementation uses this approach. It uses simple linear lists, which imposes a linear dependency of the lookup time to the number of classes and the complexity of the called class.
A second possibility would be to supply the client with information about the location of the server semantics. The stub code would have to be modified to know both the client-side and server-side semantic locations. The server-side location would have to be forwarded to the transport layer. This layer would need no lookup for the desired semantic anymore, which makes this alternative's overhead constant. Unfortunately, this approach has two drawbacks. First, each invocation abstraction needs an additional parameter. Second, each method invocation's data increases by two bytes, which have to be transported from the client to the server.