Back to index

4 Method Invocations as First Class Abstractions

In this chapter we describe the framework for composable message semantics. We split this chapter into eight sections. First, we give an overview of the framework. Section 4.2 introduces the marshalling mechanism used by the stub and the skeleton code. Section 4.3 presents the different parameter passing modes and Section 4.4 describes an update mechanism for the fields of the stub object. Section 4.5 shows how to implement new semantics and Section 4.6 demonstrates how they can be used to compose more complex semantics. Finally, Section 4.7 shows how to assign semantics to existing objects and Section 4.8 presents a simple client that allows arbitrary invocation semantics for objects in the same address space. Applications of the framework specific to distributed systems are described in the second part of this thesis.

4.1 Overview

This chapter describes how a programmer can create new message semantics by either writing new message abstractions or by composing them using existing abstractions. Our invocation model distinguishes between caller-side and callee-side semantics (in a distributed environment this corresponds to the client-side and the server-side). Both sides are independent entities that can be combined in an arbitrary manner, i.e. a programmer may change the callee-side semantic without influencing the caller-side. However, an actual invocation needs both of them. Therefore, an invocation semantic consists of both a caller-side and a callee-side semantic (see Figure 4.1). This distinction is important mainly in a distributed environment, where the caller-side and callee-side semantics reside and execute on different hosts.
These added semantics are hidden from the calling application (the caller). Our semantics framework handles the invocation of the chosen semantics in a completely transparent way. Semantics are only visible to the programmer, while he/she assigns them to an object. Afterwards, our framework intercepts all invocations using a stub object. This object acts as a proxy and looks like the original object. However, methods invoked on the stub are intercepted and redirected through the assigned invocation semantics (see Figure 4.2). Finally, the callee-side uses the skeleton code in order to execute the actual method on the real receiver object.
Callee-side and caller-side semantics are built in the same way. They both consist of exactly one invocation abstraction and an arbitrary number of invocation filters (see Figure 4.3).
A complete invocation semantic consists of two such chains (see Figure 4.2), one for the caller-side and one for the callee-side. A filter decorates either another filter or the invocation abstraction. Abstractions do the actual work. However, their task is different whether they are caller-side or callee-side invocation abstractions:
  • Caller-side invocation abstractions implement the glue code between the caller-side and the callee-side. It is their task to look for the correct receiver and to invoke the correct callee-side semantic. This may be a simple look-up, but may also be much more difficult , e.g. in a distributed environment, where the invocation has to be transmitted over a network connection.
  • Callee-side invocation abstractions handle the actual invocation on the real receiver. Their only duty is to call/invoke the correct method on the correct receiver.
The composable message semantics framework offers only the invocation abstraction DirectInvocation. This abstraction may be used as a callee-side as well as a caller-side abstraction. As a callee-side abstraction it calls the appropriate skeleton code. In most applications it will be used this way. However, it may also be used as a caller-side abstraction. In this case its usage is noted by the code generator which handles this as a special case: it generates no code for procedures that use DirectInvocation as their caller-side invocation abstraction. As a result of this the specified method is executed on the stub object and not on the original object, i.e. the method is executed without any semantic additions at all.
Both invocation filters and abstractions implement the same interface:
PROCEDURE (inv: Invocation) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream) :
                                        Linearizers.Stream;
Whenever either a filter or an abstraction is invoked, it receives the actual receiver obj of the invocation, an identifier id (selector) that denotes the called method and a byte stream s that contains the marshalled parameters. After the filter/abstraction has finished its job, it returns the results of the invocation in another byte stream to its caller. As one can see, invocation filters and abstractions are not aware of their environment, i.e. one can combine them arbitrarily.
The basic framework consists of two modules. The module Invocations offers the abstract base classes, as well as a meta-programming interface that allows one to access the information relevant for invocations. Additionally, it implements the default callee-side invocation abstraction DirectInvocation. This abstraction simply calls the appropriate method with the default behaviour. The module Objects implements the actual interception mechanism. It intercepts invocations done by the caller and activates the correct caller-side semantic (see Section 4.7). The framework supplies no implementation of a caller-side invocation abstraction. Its tasks are just too domain specific in order to be implemented within the framework. Actual clients of the framework have to supply at least one caller-side invocation abstraction.
In order to define new invocation semantics, one has to access the meta information of the classes/objects that one wants to adapt. This is achieved by using the meta programming facilities of the module Invocations.
DEFINITIONS Invocations;
  TYPE
    Class = POINTER TO ClassDesc;
    ClassDesc = RECORD
      module-, name-: ARRAY 32 OF CHAR;
      methods-: Method;
      PROCEDURE (class: Class) GetMethod (name: ARRAY OF CHAR): Method;
    END;

    Method = POINTER TO MethodDesc;
    MethodDesc = RECORD
      next-: Method;
      PROCEDURE (method: Method) GetCallerInvocation (): Invocation;
      PROCEDURE (method: Method) GetCalleeInvocation (): Invocation;
      PROCEDURE (method: Method) SetCallerInvocation (): Invocation;
      PROCEDURE (method: Method) SetCalleeInvocation (): Invocation;

      PROCEDURE (method: Method) HasUpdate (): BOOLEAN;
      PROCEDURE (method: Method) SetUpdate (update: BOOLEAN);

      PROCEDURE (method: Method) IsParameterShallow (name: ARRAY OF CHAR) :
                                        BOOLEAN;
      PROCEDURE (method: Method) SetParameterShallow (name: ARRAY OF CHAR;
                                                              shallow: BOOLEAN);

      PROCEDURE (method: Method) IsReturnShallow (): BOOLEAN;
      PROCEDURE (method: Method) SetReturnShallow (shallow: BOOLEAN);
    END;

  PROCEDURE GetClass (obj: SYSTEM.PTR): Class;

  PROCEDURE Equal (c1, c2: Class): BOOLEAN;
  PROCEDURE SetDefaultClassInfo (modName, typeName: ARRAY OF CHAR; c: Class);
END.

Most methods of the above interface are introduced step by step in the following sections. However, we have to describe some of them right here. SetDefaultClassInfo allows to set the default message semantics for all objects of a class. The programmer can later query this information by calling GetClass. Finally, Equal allows comparisons of sets of semantic meta information.
In Section 4.2, we describe our marshalling mechanism that converts parameters of arbitrary types into a byte stream and back. In Sections 4.3 to 4.6, we describe how the semantics of method invocations can be changed. Section 4.7 shows how the previously prepared semantics can be assigned to an object and how they are used transparently. In order to show this we use the following class definition in all examples.
TYPE
  MyClass = POINTER TO MyClassDesc;
  MyClassDesc = RECORD
    PROCEDURE (c: MyClass) M1 (p: MyClass);
  END;

VAR
  obj: MyClass;

...
NEW(obj);

At the end of this chapter, Section 4.8 introduces the module DecObjects. DecObjects is a simple client of our framework that offers a simplified application programming interface by demanding the caller-side and callee-side semantics to reside in the same address space.

4.2 Marshalling

Marshalling is the process of taking a set of data structures and assembling them into a form suitable for transmission as a network stream. Unmarshalling is the inverse task that disassembles the stream and restores the initial data structures [CoDK94]. These tasks consists of two parts:
  • Translation of basic data items into an external format. These transformations are handled automatically by the marshalling mechanism.
  • Flattening arbitrary data structures into a sequence of basic data items. The flattened sequence must contain enough information in order to be able to restore the initial structure, i.e. it must include some kind of meta information.
Our marshalling mechanism is implemented by the module Linearizers (see the Appendix for the complete interface). It implements two abstract data types. An instance of Linearizer handles one particular flattening or restoring process. Flattening a data structure results in a byte sequence stored in an instance of type Stream.
Flattening Arbitrary Data Structures
Flattening arbitrary data structures is actually a graph traversal problem. Arbitrary graphs have to be traversed and every node has to be stored into a byte sequence. These are actually two independent problems: How to store information about existing graph edges and how to store the data items contained within the nodes of the graph. The storage of the edge information is managed transparently by the marshaller. Storing the nodes needs some support of the application programmer.
Every graph node has a type, and therefore, the system can determine the actual items of the node. One could now choose between two possible storing techniques; Either everything is done automatically (i.e. the marshalling mechanism does the storing by consulting meta-information about the node) or one lets the application programmer handle it. These possibilities can also be combined [GoJS96]. We force the programmer to define the stored layout of all objects by supplying a marshaller. A marshaller is a procedure with a predefined signature which is responsible for reading and writing objects of its associated type. For every type with marshalled objects a marshaller has to be registered (as described in [Templ94]).
TYPE
  A = POINTER TO ADesc;
  ADesc = RECORD
    x: INTEGER;
    b: BOOLEAN;
    next: A
  END;


PROCEDURE MarshallADesc (lin: Linearizers.Linearizer; obj: SYSTEM.PTR);
VAR o: A;
BEGIN
  o := SYSTEM.VAL(A, obj); (* cast to correct type *)
  lin.Integer(o.x);
  lin.Boolean(b);
  lin.Ptr(o.next, o.next)
END MarshallADesc;

Linearizers.Register ("M", "ADesc", MarshallADesc); (* register marshaller for M.ADesc *)

A marshaller receives a lineariser and the object to be marshalled. There are two subclasses of Linearizer that handle reading and writing of byte streams, respectively. Both have the same interface. The operation lin.Integer(x) denotes writing the integer x if lin is a writing lineariser, and reading if it is a reading lineariser. Due to that trick, the same procedure MarshallADesc can be registered both as a marshaller and an unmarshaller of type ADesc. If necessary, it can access the field lin.writing to find out if lin is a writing lineariser or not.
Marshallers use the methods supplied by the lineariser to marshal their individual data items. The lineariser supplies methods for all basic data types of the language Oberon. The marshalled data is stored in an instance of Stream as a simple memory-based byte sequence optimised for sequential access.
Marshalling Problems
In this sub-section we describe some problems of our marshalling mechanism. Some of them are particular to Oberon as they result from restrictions inherited from the language:
  • All lineariser methods use a reference parameter to achieve in/out behaviour. However, reference parameters of the formal type SYSTEM.PTR are pure output parameter (restriction of most Oberon implementations). This requires two parameters for those methods that write/read a reference (Ptr, ...). One is used as a pure input parameter (value parameter) and the other as an output parameter.
    lin.Ptr(o.next, o.next).
  • Some object fields cannot be transmitted to other address spaces. A font pointer, for example, must be converted to a font name by the marshaller, and the unmarshaller has to convert the font name back to a font pointer. The marshalling procedure must therefore contain the following code for handling a font pointer o.font:
    IF lin.writing THEN
      lin.String (o.font.name)
    ELSE
      lin.String (name); o.font := Fonts.Font (name)
    END;
  • Oberon has a generic pointer type, but it lacks a generic procedure type. Therefore, the corresponding input/output procedure of the lineariser has a generic pointer as its formal parameter. The calling code looks extremely ugly. However, the result is still correct.
    ...
    lin.Procedure(SYSTEM.VAL(SYSTEM.PTR, obj.procedureVariable));
    ...
  • In the context of distributed computing, it is sometimes useful not to marshal the actual object, but to marshal only naming information. The recipient of the marshalled information extracts the naming information and accesses the actual object with the help of an especially created proxy (for further information see Part 2). The marshalling mechanism supports this technique with the lineariser method ShallowPtr. It may be called instead of Ptr to indicate that only naming information should be marshalled. The lineariser uses its field GetObjectID (procedure variable) to fetch naming information about an object and GetObject to fetch an object given its name (see Figure 4.4). Actual implementations of these procedure variables have to be supplied by the client. How they work is not defined , e.g. they could use another marshalling technique or supply stubs for distributed objects (see Part 2).
  • In the context of ShallowPtr it may happen that, during marshalling, an object should be marshalled by calling Ptr that was initially generated by calling ShallowPtr. This is problematic if the previous call to ShallowPtr returned a proxy object, e.g. a stub for an object located on another host. Such objects cannot be marshalled without special precautions. Therefore, regardless of whether the user called Ptr or ShallowPtr, they have to be marshalled using ShallowPtr. This leads to another stub object on the target host (see Figure 4.5). This stub is independent of the first one and directly accesses the real object (for details see Part 2).
  • The layout of arrays and records depends on the run-time system of the implementation of the used Oberon System. Therefore, it is not possible to marshal entire arrays and records, but one has to marshal them one entry/field at a time.

4.3 Deep vs. Shallow Copy of Parameters and Return Values

The mode of pointer parameter transmission is an important choice while determining the semantic of an invocation. Our framework provides two possibilities:
  • Deep Copy: The generated stub and skeleton code uses the method Ptr supplied by the lineariser to marshal the specified parameters. The invocation generates a clone of the passed object on the callee-side. The actual method execution receives the clone as an input parameter. Changes made to the clone are not mirrored back to the original object. The original and the clone object are two independent objects. This is the default behaviour implemented by our prototype.
  • Shallow Copy: The stub and skeleton code snippets use the method ShallowPtr supplied by the lineariser to marshal the specified parameters. This results in an up-call of the marshaller in order to fetch a name/identifier for the passed object. Our basic framework for message semantics has no implementation that assigns an object its identity. Identities depend on the problem domain of the client application. Our distributed objects system offers object identities. They allow shallow copied parameters within the distributed environment (see Part 2).
We see no applications for shallow copied parameters in a purely non-distributed environment. However, in a distributed environment there are many useful applications (see Part 2).
As mentioned before, the default behaviour is to deep-copy all pointer parameters. To change this behaviour, one has to call the method SetParameterShallow with the name of the desired pointer parameter. In the following source snippet, we change the passing mode of parameter p of method M1 to shallow.
classInfo := Invocations.GetClass (obj);
method := classInfo.GetMethod ("M1");
method.SetShallowParameter ("p", TRUE);
First, we generate the default invocation information by calling GetClass. The returned meta information classInfo contains invocation information about all methods of obj. We extract the information about method M1 by calling GetMethod. Finally, we modify the returned method descriptor method to shallow copy the parameter p. Later invocations of M1 on an object that has the above defined message semantics pass the parameter p as a shallow copy.
Similar to changing the copy mode of parameters, one can also specify the way the return value is passed back to the caller with the method SetShallowReturn. A deep copied return value is transmitted as a clone. When using shallow copy, the callee transmits naming information back to the caller and the framework tries to regenerate access to the original object using the supplied naming information.
One has to remember the distinction between the meta information returned by GetClass and objects that actually use it. The meta information modified in the above source snippet is generated from the object obj. However, Invocations.GetClass uses obj only to determine the type of the desired message semantic. Changes to the returned meta information do not influence the semantics used on obj. To use the new invocation semantics on obj or on any other object, one has to explicitly assign them to these objects (see Section 4.7).

4.4 Refresh of Class Fields

Field accesses are fixed at compile-time (in Oberon). Therefore, it is not possible to intercept them in the way we do with method invocations. This may cause problems with our interception scheme, as we replicate all fields in the stub object. A field in the stub object is not linked to the corresponding field in the actual object. If one of them is changed, the other remains unchanged. This behaviour is acceptable in many circumstances. However, this is not always the case. We recommend accessor functions instead of direct field access. In this case, the above problem no longer applies.
Especially in the context of several stubs for the same object, it is desirable to mirror changes done in the actual object to all existing stub objects. This can be achieved by telling the assigned semantics to update the member fields of the stubs. This is done by calling SetUpdate.
classInfo := Invocations.GetClass (obj);
method := classInfo.GetMethod ("M1");
method.SetUpdate (TRUE);
Invoking method M1, on objects with the above message semantics classInfo, results in an automatic update of all fields of the stub object. The interception mechanism includes a copy of the real object within the resulting byte stream. This copy automatically overwrites the previous field values of the stub object.

4.5 New Abstractions and Filters

The framework as described in Section 4.1 is not ready to be used. At least one caller-side invocation abstraction has to be added (caller-side abstractions heavily depend upon the actual application domain). However, this is not restricting, as our framework offers the possibility to add arbitrary new filters and abstractions. While writing new filters and abstractions, one has to distinguish three cases. They are described in the following sub-sections.
Writing a Caller-Side Abstraction
As said before, the main task of a caller-side invocation abstraction is the transport of the marshalled invocation data to the correct callee-side semantic. This may include network-traffic, bookkeeping, delays or authentication. As all of these requirements Ð and arbitrary others Ð cannot be brought together, the task of writing an appropriate caller-side abstraction is transferred to the user of the framework. Such abstractions have to extend the class Invocations.InvocationDesc.
Invocation = POINTER TO InvocationDesc;
InvocationDesc = RECORD
  PROCEDURE (inv: Invocation) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream) :
                                          Linearizers.Stream;
END;
Writing a Callee-Side Abstraction
The basic framework already offers the default callee-side invocation abstraction Invocations.DirectInvocation. This is probably sufficient for most cases. It restores the parameters from their marshalled state, invokes the method on the real receiver without any additional semantics and marshals the results into the resulting stream. It achieves the actual invocation by calling Objects.Invoke. A new callee-side invocation abstraction has to extend the same type as caller-side abstractions, i.e. it has to extend Invocations.InvocationDesc.
Writing an Invocation Filter
This is probably the most common task. Most invocation filters are not dependent on the callee or the caller, i.e. one can use the same filters on both sides. An invocation filter is basically a decorator [GHJV95] on either another filter or on an invocation abstraction. It has to be a sub-class of Invocations.FilterDesc.
InvocationFilter = POINTER TO InvocationFilterDesc;
InvocationFilterDesc = RECORD (InvocationDesc)
  next: Invocation;
  PROCEDURE (inv: Invocation) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream) :
                                          Linearizers.Stream;
END;
InvocationFilterDesc extends InvocationDesc by adding the field next, which references the decorated object. The default implementation of InvocationFilter.Invoke delegates the incoming invocation to the decorated object. A new filter has to delegate the invocation to the next filter or has to call the default implementation (super-call) which makes the delegation as a default.
MyFilter = POINTER TO MyFilterDesc;
MyFilterDesc = RECORD (Invocations.InvocationFilterDesc)
END;

PROCEDURE (inv: MyFilter) Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream) :
                                      Linearizers.Stream;
BEGIN
  SomePreprocessing (obj, id, s);
  result := inv.Invoke^ (obj, id, s);    (* super-call: delegates to inv.next *)
  SomePostprocessing (obj, id, s, result);
  RETURN result
END Invoke;

4.6 Composing Invocation Semantics

This section discusses the extensibility of our approach. We first describe our classification model and then the way in which our invocation semantics can be composed and extended.
Combining the Decorator and Strategy Design Patterns
In a very first prototype of our system, we considered a treelike hierarchy (see Figure 4.6) to classify invocation semantics (names in brackets denote abstract classes or methods). At first glance, this structure seems appealing. A new abstraction extends the desired base class. It either implements the invocation itself; e.g. SyncInvocation, or delegates it with a super call to base class; e.g. TransactionInvocation. Unfortunately, this structure is not very flexible. Let us assume that an invocation abstraction is needed, which automatically logs all invocations, i.e. transactional as well as replicated invocations should be logged. To achieve this, we have to extend the above type hierarchy in more than one place (below TransactionInvocation and below ReplicationInvocation). The same problem occurs if we try to combine transaction semantic with asynchronous invocation semantic.
Therefore, we adopted a flatter class hierarchy, which allows us to change the behavioural combinations dynamically. To do so, we combined two design patterns: the Decorator design pattern and the Strategy design pattern [GHJV95]. The Decorator pattern is basically used for the static composition and the Strategy pattern for the dynamic aspects of our framework. The resulting architecture is shown in Figure 4.7. An invocation abstraction can be decorated with an arbitrary number of additional decorators. We distinguish between extensions of Invocation and extensions of InvocationFilter. Extensions of InvocationFilter are just decorators. We call them filters. They may be cascaded in an arbitrary order (however, some actual implementations may impose a required ordering). They extend the semantic of the invocation, i.e. they add functionality. They do not implement the invocation themselves, but they forward the invocation to their decorated object. On the other hand, extensions of Invocation (other than InvocationFilter) actually handle the invocation of the chosen method. We call them invocation abstractions. An actual composition which uses a set of filters and one abstraction, is called an invocation semantic.
This structure is flexible in two directions. First, one can add arbitrary new filters to this hierarchy, e.g. for logging, visualisation, synchronisation, authentication, etc. Second, one can add new invocation abstractions, e.g. best-effort, at-most-once, time independent, etc. The above structuring also promotes arbitrary combinations of the different invocation abstractions and filters.
Note that the combination of filters cannot always be done in an arbitrary manner. The order of decoration may be important. The following example defines two semantics that both use the same filters but in different orders: semantic1 first logs an invocation and, afterwards, replicates it to several hosts, whereas semantic2 replicates the invocation to several hosts and each of them generates a separate log. The first alternative generates only one log file, the second one log file per replica.
semantic1 := LogInvocation(ReplicationInvocation(SyncInvocation()))
versus
semantic2 := ReplicationInvocation(LogInvocation(SyncInvocation()))
Our implementation uses the Strategy design pattern. All semantics that have been defined at a certain point in time are held in an array at some known index (only semantics that were used since the latest start-up). The run-time system interprets this array as a set of strategies on how methods may be invoked (see Chapter 5 for details).
Composition
The current library of invocation abstractions and filters is relatively small. However, the principles that govern their composition stay the same when new abstractions or filters are implemented and added to the framework. Whenever one modifies the model with which a method is invoked, one has to supply two semantics: one for the caller and one for the callee. A semantic consists of exactly one invocation abstraction and of an arbitrary number of invocation filters (see Figure 4.3). A filter never handles an invocation directly, but, before/after some specific filter work, forwards it to its decorated object. Only the invocation abstraction actually executes the invocation. Callee-side abstractions actually start the execution of the method on the real object. Caller-side abstractions are responsible for transporting the invocation to the real object in order to trigger the execution of the callee-side invocation semantic.
The following code snippet assigns new semantics to the method M1. It assigns an instance of Invocations.DirectInvocation to the callee-side, i.e. we set it to the default behaviour. The caller-side semantic is a combination of MyInvocation and SyncInvocation. MyInvocation is the filter written in Section 4.5. It decorates the actual invocation abstraction SyncInvocation, which is defined by the module DObjects and executes a synchronous remote method invocation (see Part 2 for details).
VAR
  di: Invocations.DirectInvocation;
  mi: MyInvocation;
  si: DObjects.SyncInvocation;

classInfo := Invocations.GetClass (obj);
method := classInfo.GetMethod ("M1");
NEW(di); NEW(mi); NEW(si);
mi.next := si;
method.SetCallerInvocation (mi);    (* assign caller-side semantic *)
method.SetCalleeInvocation (di);    (* assign callee-side semantic *)

4.7 Applying Invocation Semantics

In order to use a previously defined set of invocation semantics, one has to assign it to specific objects. Our framework (module Objects) offers the necessary support. However, applications using the framework will probably add another layer that offers more comfortable access to this functionality, e.g. DObjects for distributed objects and DecObjects (see Section 4.8) for locally decorated objects. This section describes the basic functionality as offered by the module Objects.
DEFINITION Objects;

  IMPORT SYSTEM, Invocations, Linearizers, Types;

  PROCEDURE Receiver (obj: SYSTEM.PTR; c: Invocations.Class; VAR res: INTEGER);
  PROCEDURE Intercept (obj: SYSTEM.PTR; c: Invocations.Class; VAR res: INTEGER);
  PROCEDURE Invoke (obj: SYSTEM.PTR; id: LONGINT; s: Linearizers.Stream): Linearizers.Stream;
  PROCEDURE TypeOf (obj: SYSTEM.PTR): Types.Type;

END Objects.

Interface Description
Applying Invocation Information
  • Receiver (obj, c, res) prepares the object obj for invocations using the semantics defined in c. This call sets up all necessary callee-side data structures and generates the code necessary to handle incoming invocations.
  • Intercept (obj, c, res) prepares the stub object obj for invocations using the semantics defined in c. This call sets up all the necessary caller-side data structures and generates the necessary code snippets for the method interception. If Intercept is called in another address space that the corresponding call to Receiver, the invocation information stored in c has to be transfered accordingly.
    Miscellaneous
  • s := Invoke (obj, id, s) invokes the method defined by the selector id on object obj with the previously defined invocation semantic. It returns the linearised invocation results. This procedure is typically called by the callee-side invocation abstraction.
  • t := TypeOf (obj) returns the type descriptor of obj, regardless of whether obj is a stub object or not.
Usage
The usage of the above interface is quite complicated, as it serves as a generic framework for quite different application domains. In order to create an object with customised message semantics, one has to implement the following steps:
  • Create a stub object that serves as a placeholder for the real object. The stub object must be of the same run-time type as the real object. Methods invoked on the stub will use the newly assigned semantics. Methods invoked on the real object will be handled as before.
    Types.NewObj(stub, type of real object);
  • Create a caller-side invocation abstraction that connects the stub and the real object. Later invocations will use this abstraction. The main task of this abstraction will be to look for the real object and forward the invocation accordingly.
  • Create the necessary callee-side data structures by calling Receiver.
    Objects.Receiver(realObject, invocationInformation, res);
  • Create the necessary caller-side data structures by calling Intercept. When calling Intercept, one has to pass the stub object and not the real object.
Objects.Intercept(stub, invocationInformation, res);
As part of this thesis, we implemented two example clients that ease the use of the composable message semantics framework. DecObjects allows one to decorate local objects with arbitrary new message semantics. It is a simple layer on top of the framework and is described in Section 4.8. The second client, DObjects, implements distributed objects on top of our framework. It is much more complicated, but demonstrates the usefulness of composable message semantics in a distributed environment extremely well. It is described Ð in depth Ð in the second part of this thesis.

4.8 Intercepting Local Method Invocations using "DecObjects"

DecObjects is a simple layer on top of the interception framework. It serves as an example on how the framework may be used. In Chapter 6, we describe some applications that use composable message semantics with the help of this layer.
DEFINITION DecObjects;

  IMPORT SYSTEM, Invocations, Linearizers;

  PROCEDURE SetSemantics (o: SYSTEM.PTR; c: Invocations.Class; VAR obj: SYSTEM.PTR;
                          VAR res: INTEGER);
  PROCEDURE Invocation (): Invocations.Invocation;
  PROCEDURE SetMarshaller (modName, typeName: ARRAY OF CHAR;
                                marshaller: Linearizers.Marshaller);

END DecObjects.

Interface Description
  • SetSemantics (o, c, obj, res) decorates the object o as defined in the invocation information c. The decorated object (stub object) is returned in obj. Later invocations on obj obey the assigned semantics. Invocations on o are still handled as before.
  • inv := Invocation () returns the default caller-side invocation abstraction as defined by DecObjects. It is a simple glue code that binds the stub and the real object together.
  • SetMarshaller (modName, typeName, marshaller) sets the marshalling procedure responsible for objects of the specified type. Parameters passed during an invocation are marshalled, even though, this is not necessary in a purely non-distributed environment. However, our framework requires the marshalling process, as it is tailored to be used in a distributed environment. Therefore, we need a marshaller for every type that is passed as a parameter.
Usage
Decorating objects with the help of DecObjects is really simple. First, one has to build the desired message semantics as described in this chapter. Second, one assigns this information to the desired objects by calling SetSemantics. Later invocations transparently use the assigned semantics. For some examples see Chapter 6 of this thesis.

Back to index