The Mapping to the Common Type System


Recapitulation of the .NET Interoperability Framework

From an interoperability perspective, the main constituents of the .NET language framework are as follows :

  • A component packaging model

  • A compile-time naming hierarchy

    • A namespace is a qualified set of names .

  • A runtime deployment hierarchy

    • An application is a logically connected set of assemblies described by a configuration script (in XML).

    • An assembly is a deployment unit consisting of a collection of modules described by a manifest.

    • A module is either a portable executable (DLL or EXE file) or a container for additional resources such as fonts and pictures.

  • A virtual object system called CTS (Common Type System)

    • The CTS supports the well-known object concepts of class, subclass, interface, static class member, instance class member, abstract method, virtual method , sealed (final) method, virtual call, static call, and interface call.

  • A stack engine model

    • The stack engine model operates on an abstract execution stack. MSIL is a comprehensive intermediate language covering all three .NET models so that, in the end, the task of implementing a language for .NET simply amounts to compiling the language to MSIL.

      In the case of Active Oberon, the compiler uses a simple recursive descent strategy [6], with the benefit of a built-in mapping to the stack engine model. No substantial problems have arisen from this choice, and MSIL has proved to be a very suitable target. However, our additional constraints of single-pass compilation and, in particular, the "declare before use" obligation made our task more difficult. Obviously, this obligation is awkward in combination with a scenario of active object types mutually referring to one another. Also, a fine point worth mentioning in connection with expression compilation is the problem of constant folding. It can be caricatured as follows. When compiling an expression of the form X op Y (where X and Y are subexpressions and op is an operator), the recursive descent essentially proceeds as

       Operand(X); Get(op); Operand(Y); Operator(op) 

      under assumption of the following semantics: Procedure Operand parses an operand and emits code for its evaluation on the stack, procedure Get reads and saves the operator, and procedure Operator emits the operator to the stack. Obviously, this may lead to premature code emission if X and Y are constant expressions. An improved version of Operand would fold constants instead of emitting code until it encountered a nonconstant element. However, in the case of a constant X and a nonconstant Y , this strategy leads to emitting Y first, which, of course, is not a problem if either op is symmetric or a swap-stack-top instruction is available in MSIL. Unfortunately, no such instruction exists, and we must leave constant folding to the JIT.

Returning now to a more conceptual level, we first observe that languages within the .NET Framework typically take dual roles as consumers and producers of components . Correspondingly, the primary tasks language implementers for .NET are confronted with can be summarized as follows:

  • Mapping their language to the .NET model (preferably to the official .NET Common Language Specification)

  • Mapping the .NET model (at least the CLS) to their language

In the following sections, we shall discuss these tasks in the special case of Active Oberon. For this purpose, we first recall the main constructs supported by Active Oberon and their connecting relations:

  • Constructs: Module, object type, definition

  • Relations: IMPORTS , IMPLEMENTS , and REFINES

Table H.1 summarizes these ideas.

Modules and definitions are the only compilation units in Active Oberon. Object types are not compiled separately, so their declarations need to be wrapped in some module scope.

Table H.1. Constructs and Relations Supported by Active Oberon

Construct

Relation

Construct

Definition

REFINES

Definition

Object type

IMPLEMENTS

Definition

Module

IMPLEMENTS

Definition

Module

IMPORTS

Module

Mapping Modules

Let us first concentrate on the compilation of modules ”in particular, on the compilation of our exemplary module M . The result is a cascade as shown in Figure H.6: an assembly M containing a portable executable M , containing (1) a collection of sealed classes A , X , and Y corresponding to the object types declared in M and (2) a sealed class M corresponding to the static object associated with M .

Figure H.6. Compilation result of an Active Oberon for .NET module

graphics/hfig06.gif

The sealed class M is sometimes called the module class. It is static in the sense that all its methods are static. Note in particular how the module's initialization code is compiled into the constructor of the module class. In detail, the activities given in Listing H.5 are performed exactly once at instantiation time of the module class.

Listing H.5
 FOR each module N imported by M DO   Check version of N;   IF compatible with version expected by M THEN     Load and initialize static object N   ELSE throw loading exception   END END; Initialize static object M 

When compiling an Active Oberon module, the compiler implicitly\animtext4 generates a .NET namespace for later use by any .NET language. Concretely, in the case of our exemplary module M , a namespace M is generated containing members M.A , M.X , M.Y , and M.M (the static object associated with M ). The elements of the module object can be accessed by qualification as usual. For example, method F of M is referred to in general as M.M.F . Within Active Oberon, the shortcut notation M.F is considered equivalent to M.M.F .

Let us now turn to definitions, the second kind of Active Oberon compilation units. Remembering that definitions are abstractions, it is natural to compile each definition into an abstract .NET class, with the immediate advantage that refinement (at least a simple form of it) harmoniously maps to class extension in .NET.

However, this approach is infeasible with the IMPLEMENTS relation, at least in the case of multiple definitions implemented by the same object type. The mapping of the IMPLEMENTS relation to .NET is, in fact, a most intricate problem. It essentially amounts to finding a mapping from a restricted form of "multiple inheritance" to single inheritance plus interfaces.

Mapping Definitions

For the sake of concreteness, let now A be an object type in Active Oberon, and let D be any definition implemented by A . We already know that A is mapped to a sealed .NET class. For the mapping of the IMPLEMENTS relation, we can identify three modeling options:

  • Declare D as the base class of A

  • Aggregate D with A

  • Integrate D with A

We already mentioned the first option at the end of the previous section. It is simple to implement but is restricted to one definition per object type. The second and third options are replicable arbitrarily.

In detail, several possibilities exist for aggregating D with A . Our favorite one is subsumed by a transformation of D into a pair consisting of an interface I D and a static class C D , constructed according to the following rules:

  1. Transform each state variable of D into a property signature ”that is, a pair (get, set) of abstract methods.

  2. Define I D as the set of property signatures constructed in 1 plus the method signatures of D .

  3. Transform each method of D into a static method by (a) adding a reference parameter of type A to its signature and (b) consistently replacing accesses to D 's state variables in the implementation by accesses to the corresponding properties of the object passed as a reference parameter.

  4. Define C D as the set of the static methods constructed in 3.

  5. Extend A 's state space by D 's state variables.

  6. Have A implement I D ”that is, implement the property signatures constructed in 1 and D 's methods, either by explicit implementation or by delegation to C D .

It is probably best to demonstrate this procedure with a concrete example. Let T be any data type, and let D and A be declared as shown in Listing H.6.

Listing H.6
 DEFINITION D;   VAR x: T;   PROCEDURE f (y: T);     VAR z: T;   BEGIN x := y ; z := x   END f;   PROCEDURE { ABSTRACT } g(): T; END D; TYPE   A = OBJECT IMPLEMENTS D;     PROCEDURE g (y: T) IMPLEMENTS D.g;       VAR z: T;     BEGIN z := x; x := y     END g;   END A; 

Then, the result of the transformation formulated in C#, .NET's canonical language, is as shown in Listing H.7.

Listing H.7
 public interface ID {   int x { get; set; }   void f(int y); /* implemented by SD.f */   void g(int y); } public class SD {   public static void f(D me, int y)   { int z; me.x = y; z = me.x; } } sealed class A: ID {   int D_x;   public void g(int y) { int z; z = x; x = y; }   public void f(int y) { SD.f(this, y); } /* delegation */   public int x { get { return D_x; } set { D_x = value; } } } 

It is worth emphasizing that within the Active Oberon language domain:

  • The exact identity of each method called is known when A is compiled.

  • D is never instantiated and does not have to be precompiled.

Consequently, our third modeling option relies on lazy code generation and takes D as a source code template to be integrated with A at compile time. The obvious advantage of this method is direct access to D 's state variables without virtual calls; the disadvantage is unavoidable multiplication of runtime code.

Our current solution of preference is "optimized aggregation" or, more precisely, aggregation plus

  • Mapping of one of the definitions implemented by A to A 's base class; and

  • Trusting the JIT optimizer to draw benefit from A 's seal.

Interoperability

Language interoperability is .NET's highlight and major strength, and language implementers on .NET are well advised to apply special care regarding both the consumer perspective and the producer perspective of their language.

Taking a producer perspective in our case of Active Oberon, we first recall that the compilation units are modules and definitions. From a compilation view, they share the following characteristics:

  • Each compilation unit defines its own namespace.

  • The result of each compilation is an assembly.

Table H.2. Compilation Results

Result from the Compilation of a

Outer Shell

Inner Shell

Contents

Module

Assembly

PE module

One sealed class for each object type declared in the module

One static sealed class representing the static module object

Definition

Assembly

PE module

One sealed static class representing the set of preimplemented methods

One interface representing the signatures of the state variables and methods

From the earlier discussions, we can easily understand the summary provided in Table H.2.

In the interest of interoperability, it could obviously be reasonable to offer options for the following:

  • Unwrapping the static module object from its namespace

  • Compiling definitions into abstract classes

Let us now switch to a consumer perspective. In an interoperable environment, the Active Oberon compiler must obviously be prepared to accept components produced by foreign languages. The abstract vehicle provided by .NET for this purpose is the namespace. Active Oberon supports the use of foreign namespaces within the current module scope via full qualification or, alternatively, via aliasing. For example, the statement

 IMPORTS P.M AS M; 

opens a symbolic portal M to the namespace P.M , and allows the use of its ingredients with simplified qualification by M .

A complication arises due to the fact that there is not in general a one-to-one correspondence between namespaces and deployment units. Command-line hints specifying the assemblies required for the next compilation provide a temporary solution.

However, the crucial questions from a consumer's view are these:

  • What are the reusable components?

  • How can they be used?

The answers are simple in essence: The reusable components are .NET interfaces and .NET classes, and each can be used by Active Oberon either as an abstraction or as a factory for the creation of service objects. Table H.3 clarifies this point.

Note an important technical fine point: The definitions derived from foreign (non-Oberon) classes are precompiled and cannot be mapped to an (interface, static class) pair as explained earlier. Instead, they must be mapped to a base class. As a consequence, at most one definition derived from a foreign class can be implemented per object type.

The code excerpt in Listing H.8 sketches the implementation of a generalized 15-puzzle in Active Oberon for .NET. Module Puzzle defines two object types: PuzzleForm and Launcher . PuzzleForm implements (and inherits from) definition System.Winforms.Form . Because this definition is derived from a foreign class, it must be mapped to Puzzle.PuzzleForm 's base class. Launcher is an active object with an intrinsic behavior. The next section explains how such behavior is mapped to .NET.

Table H.3. Reusability of Components

.NET Component

Active Oberon Entity If Used as Abstraction

Active Oberon Entity If Used for Instantiation

Interface

Definition

Abstract class

Definition

Concrete class

Definition

Object type

Listing H.8
 MODULE Puzzle;   USES System, System.Drawing, System.WinForms; (* namespaces *) TYPE   PuzzleForm = OBJECT IMPLEMENTS System.WinForms.Form;     VAR nofElems: INTEGER; ...     PROCEDURE MoveElem(pbox: System.WinForms.PictureBox;       dx, dy, nofSteps: INTEGER);     VAR i, j: INTEGER; loc: System.Drawing.Point;     BEGIN ...     END MoveElem;     PROCEDURE TimerEventHandler(state: OBJECT);     BEGIN ...     END TimerEventHandler;     PROCEDURE Dispose() IMPLEMENTS                         System.WinForms.Form.Dispose;     BEGIN ...     END Dispose;     PROCEDURE NEW(image: System.Drawing.Image;                  (*  constructor *)     title: System.String; subDivisions: INTEGER);     BEGIN ...     END NEW;   END PuzzleForm;   Launcher = OBJECT     VAR target: System.WinForms.Form;     PROCEDURE NEW (t: System.WinForms.Form);                   (* initializer *)     BEGIN target := t     END NEW;     BEGIN { ACTIVE }             System.WinForms.Application.Run(target)   END Launcher; VAR puzzle: PuzzleForm; launcher: Launcher; ... BEGIN   WRITELN("Puzzle implemented in Active Oberon for .net");   ... END Puzzle. 

Mapping Active Behavior

Probably the main profit we can gain from the active object construct in Active Oberon is its conceptual influence on the mindset of system builders. In terms of functionality, it is basically equivalent to object constructs that support the implementation of a run() method.

In particular, when compiling an active object type, we can easily mimic the active object construct in the .NET Framework by proceeding as follows:

  • Map the object type's body to a method plus a field:

     Method void body() { ... }; Field Thread thread; 
  • Map the generator NEW(x) to the following pair of statements:

      x  .thread = new Thread(new ThreadStart(body));  x.thread.Start() 

Two active object topics still remain to be discussed: mutual exclusion and assertion-oriented synchronization [1]. Regarding mutual exclusion, the mapping is simple:

  • Translate each mutually exclusive block statement BEGIN { EXCLUSIVE } END within an active object scope into a critical section on .NET bracketed by Monitor.Enter(this); ...; Monitor.Exit(this); .

  • The mapping of assertion-oriented synchronization is more complex. Consider a statement AWAIT(c) within any object scope, where c is a local Boolean condition. Then, this would be one possible solution:

    • Translate AWAIT(c) into a loop of the form while !c { Monitor.Wait(this); } .

    • Automatically generate a Monitor.PulseAll(this); call immediately before the exit of every critical section.

Obviously, this topic will need further investigation and optimization, in particular when efficiency matters.

Language Fitting

We should not conclude this section on the mapping of Active Oberon to .NET without mentioning a few minor adjustments of the original Oberon language that should be regarded as a tribute to making the revised language fit optimally in the .NET interoperability scheme.

  • Delegates We extended Oberon's concept of procedure variables to method variables that are able to represent arbitrary method values of arbitrary object instances and are no longer restricted to global procedure values. No language changes were necessary. The compiler now simply allocates a pair of pointers (code pointer, object base) for every method variable.

  • Value Types Besides the ordinary reference types, the .NET object system supports value types. In Active Oberon, value types are called record types. Record types are declared in form of the well-known RECORD END construct. Their members are called fields. Methods in record types are not supported, nor is boxing.

  • Overloading We extended Oberon to support method overloading. In the case of ambiguity, we require calls of an overloaded method to explicitly specify the desired signature as, for example, in

     u := MyModule.MyOverloadedMethod{(S, T): U)}(s, t) 

    with s, t, and u of types S, T, and U, respectively.

  • Base Method Calls Because Oberon does not support subclassing, no special construct is provided for base method calls. However, calls of method implementations in definitions are possible from within the implementing object type simply by using their exact and fully qualified names.

  • Exception Handling Active Oberon provides a rough form of exception handling. The corresponding throw/catch pair of constructs is

    • A built-in THROWEXCEPTION procedure

    • An extended form of the block statement:

       BEGIN (* regular code )    EXCEPTIONALLY ( summary exception handling code *) END 
  • Namespaces Oberon does not provide an explicit namespace construct. However, module packaging can nevertheless be achieved simply by using a qualified module name . For example, a module M that is a member of a module package P could be called P.M .

  • Assemblies Except in the case of Active Oberon modules, there is no guaranteed one-to-one correspondence between namespaces and deployment units in .NET. In Active Oberon, command-line arguments serve as locating hints to the compiler. If the System namespace is imported, then mscorlib is automatically located.

  • Procedure Nesting Because access to intermediate variables is unsupported by .NET, the current Active Oberon compiler does not allow nested procedures.



Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net