Recapitulation of the .NET Interoperability FrameworkFrom an interoperability perspective, the main constituents of the .NET language framework are as follows :
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:
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:
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
Mapping ModulesLet 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
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.5FOR 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 DefinitionsFor 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:
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:
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.6DEFINITION 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.7public 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:
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
InteroperabilityLanguage 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:
Table H.2. Compilation Results
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:
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:
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
Listing H.8MODULE 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 BehaviorProbably 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:
Two active object topics still remain to be discussed: mutual exclusion and assertion-oriented synchronization [1]. Regarding mutual exclusion, the mapping is simple:
Obviously, this topic will need further investigation and optimization, in particular when efficiency matters. Language FittingWe 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.
|