Designed with software reuse in mind
Hannes is a member of the Institute for Computer Systems, ETH, Zurich and can be contacted at [email protected].
If nothing else, history has taught us that technological progress is achieved by reusing, refining, and building upon the ideas and work of others. In the world of software, "componentware" or "interoperable objects," as the current push for reuseable, off-the-shelf software has been dubbed, is viewed as just such a technological advance.
True componentware is, however, an elusive goal that requires us to rethink how software systems are constructed and programmed in-the-large. Issues we must consider involve how software systems can be extended with new parts and how parts can be exchanged at run time. Even more problematic is how to extend or subclass existing parts of a running system, exemplified by the many technical discussions about inheritance, delegation, message forwarding, and aggregation. Not only do we have to specify how components are created with programming languages, but also how the components are combined with each other to form a system. One approach is to regard both the programming language and the operating system as a symbiotic whole. There are many reasons why this is meaningful, but none illustrates it as well as memory deallocation.
In a truly extensible system, components use other components. The network of these components is continuously expanded at run time by the addition of new or existing subclassed components. Since you don't know what components will be added to the component soup tomorrow, you cannot identify a single instance responsible for deallocating components when they are no longer used (referenced). Clearly, what's needed is a system-wide garbage collector, which implies a type-safe programming language. If these concepts sound foreign, then you are probably used to linking all your components together in one monolithic block, blocking the ability of other programmers to extend your application.
The Oberon Project
Such considerations were the driving force behind the Oberon project at the Institute for Computer Systems, ETH Zurich. Oberon is both a programming language and a programming environment or system. (Incidentally, the name "Oberon" was taken from one of the moons of Uranus, which the Voyager probe had photographed around the time we started our project.) The Oberon language, created by Niklaus Wirth in 1986, is the successor to the Algol, Pascal, and Modula-2 generation of languages. The Oberon system, developed in cooperation with Jürg Gutknecht of ETH, illustrates how the Oberon language supports programming in-the-large. Completed in 1989, the Oberon system and language were ported from special-purpose hardware to platforms such as DOS, Windows, and UNIX, and documented in books such as The Oberon System: User Guide and Programmer's Manual, by M. Reiser (Addison-Wesley, 1992), Project Oberon: The Design of an Operating System and Compiler, by N. Wirth and J. Gutknecht (Addison-Wesley, 1992), and others. The Oberon system is available from the ETH Zurich anonymous ftp server neptune.inf.ethz.ch in the /pub/Oberon directory. Versions may also be ordered on diskette for a small fee from the Institute for Computer Systems, ETH-Zentrum, CH-8092, Zurich. A version for SPARC workstations is also available.
In 1991, Gutknecht and his group began revising the Oberon system. Their resulting work shows some similarities with commercial componentware systems, although it is much simpler and cleaner to understand, more flexible in many respects, and more compact. Today's system, called "Oberon System 3," is the basis for further research and experimentation at the Institute (see Jürg Gutknecht's "Oberon System 3: Vision of a Future Software Technology" in Software Concepts and Tools, 1994). The system has been ported to a number of hardware platforms and is also available at no charge.
The Object Model
Oberon System 3 adds several new concepts to the classic Oberon implementation. Most importantly, System 3 supports persistence by binding or inserting objects into an object library, although they can also be unbound or free when of a more temporary nature. A library, normally stored in a file, functions as a container for objects and allows applications to access objects through a simple indexing mechanism.
Public and private libraries allow applications to control the visibility and sharing of objects. Interestingly, fonts in Oberon System 3 are nothing more than public libraries of character-pattern objects indexed by ASCII code. Text is then reduced to a stream of (Library, Index) pairs. Note that this implies text may contain objects that do not have a pattern nature. Documents, or collections of objects with arbitrary relationships (pointers between each other), are often stored in the same library, although pointers between libraries may also exist. In this case, one library is importing an object from another library. Such references can be made by normal pointer variables, by (Library, Index) pairs, or by names in the form "L.O", where L indicates the library and O the object. The names of public or exported objects in a public library are stored in an associated-library dictionary mechanism. The public library has the useful property of allowing applications to share objects. In Oberon System 3, this allows you to link objects into other documents; for example, an icon library collection is shared by many applications in the system.
Objects have local states and respond to messages. The local state of an object is called the "object attributes." To allow the user to configure the state of an object, the system defines a special message to retrieve, set, and enumerate attributes, and a special universal editor called the "Inspector" to edit them. This is in addition to setting local variables directly by the program. The system uses three larger message classes: one that all objects should respond to, another for objects of a visual nature, and a third class of application-defined messages.
Gadgets
The object model forms the basis of a GUI toolkit called "gadgets." Each dialog element, or gadget, is an object that can be embedded in any UI or application. Gadgets can float inside the text stream of a text document and be embedded in a panel interface, graphic editor, or even our System 3 page-layout system. In fact, all gadgets can be integrated and reused in any other System 3 environment. Often, container gadgets manage other gadgets as their children. They act as the glue to build bigger components out of smaller ones. The principal containers are the panel gadgets (two-dimensional edit surfaces) and the text gadgets (complete text editors with support for embedding), although more-refined containers like books and special elements are available for building menu-like structures. Most containers specify relatively few rules for parentship, meaning that almost all gadgets can be inserted into all containers. Listing One presents the source for a simple gadget: a small, rectangular block that can be embedded in all System 3 environments, colored using the Inspector, and moved and resized with the mouse. This module (discussed in detail later) is often the starting point for creating your own gadgets.
Another property of gadgets is that they can be modified and used wherever they are located. In effect, UI construction is reduced to document editing. Oberon System 3 users create new UIs or modify existing ones in typical drag-and-drop fashion. A UI is frozen only after it is explicitly locked with the Inspector, and users can unlock a UI at any time to make adjustments. Such dynamic behavior requires a relatively thin interface between the UI and the application. This interface is governed by several rules and system guidelines.
First, you need to specify what the gadgets should do when they are used (clicking on a button, for example). Using the Inspector and a very simple script facility to pass parameters, a user can associate an action with each gadget. This action is coded as a procedure in the Oberon language. The procedure can search for objects in the user interface and change their state accordingly. Often, special-purpose gadgets acting as abstract data types of commonly used data structures can be linked interactively into a UI, which is then visualized by other components in the UI. This is, of course, the well-known Smalltalk Model-Viewer-Controller (MVC) framework which allows us to decouple applications even further from their UIs. With these and other techniques, it is possible to decouple the UI to such a degree from the application that it can be modified or even exchanged while the application is running.
It is also possible to construct so-called "links" or connections between objects at run time and follow these links using the Inspector. The resulting network-like structure of objects can easily be made persistent by inserting all objects involved into a library. The links relay messages between objects and form the basis on which the whole hierarchical display space, or screen, is organized. The screen itself is an object, which is then decomposed into document gadgets, which in turn have menu panels and contents right down to the simplest gadgets, like buttons and check boxes. We have experimented with very fine-grained gadgets, too; the System 3 graphic editor provides line, spline, circle, ellipse, and other gadgets, which can be used in any other UI. Using special tuning measures, we are able to edit very complicated diagrams that contain thousands of single gadgets with respectable speed.
The success of such a system hinges on a strict message protocol. The complete integration property of gadgets requires an exact message protocol, and this is where the application programmer comes in.
Programming Oberon System 3
Oberon System 3 supports several layers: The first and simplest is creating UI/documents interactively and adding simple behaviors to them; the next is programming Oberon code fragments that manipulate existing gadgets; and the third is programming your own gadget. Here, you must distinguish between two difficulty levels: constructing your own leaf gadget (that is, a gadget that does not contain any further gadgets) and programming a container gadget that manages other gadgets. In this context, "programming" means extending an existing gadget or creating a new gadget from scratch. System 3 provides code skeletons that can be modified quickly to reach your goal.
The Oberon system is structured as a tree of Oberon modules in a shared address space. Modules are loaded and linked dynamically when required. All imported modules are also loaded on command. Modules normally remain in memory until they are explicitly freed. Each module contains one or more implementations of software components and allocates global storage from a system-wide, shared-memory heap. Memory is recycled automatically when not referenced by a mark-and-sweep garbage collector. In addition to type definitions, variables, and procedures, each module may contain several exported, parameterless procedures called "commands." Command procedures can be called by the user directly from the UI. Behind the scenes, the system converts strings of the form "M.P", where M is the module name and P is the name of the parameterless procedure, to a jump address. Commands are typically invoked by simply clicking on the string "M.P" written somewhere on the display. Commands are also invoked as a side effect of using a gadget. Commands operate on global variables of their own or imported modules, and are executed in sequence by the user. Parameters are passed from the UI to commands by global variables. All Oberon systems contain a set of standard modules for keyboard and mouse input, display output, file manipulation, system management, and the like. Most important, support for a few abstract data types like text and bitmaps are provided, in addition to standard editors for them. You can write new modules that use all the existing modules; for example, you can write modules that operate directly on the text-editor components without having its source code (the compiler generates an interface definition or symbol file for each module). The editor extension will link itself dynamically to the editor module as soon as one of its commands is executed. The Oberon system provides a comfortable environment for writing, compiling, loading, testing, and then unloading modules again.
Objects and Messages
Oberon System 3 components, called "Objects," are defined as a base type in a module with the same name. The Objects module defines the messages that each component should respond to. Typical messages store or load an object, make a copy of an object, or request attributes of an object. These messages form a contract that all components should obey. Typical objects have local variables and respond to messages with a so-called "message handler"--a procedure variable that determines what message an object has received, then acts on it. By convention, only one message handler is used, although you may define more. The messages are recognized as RECORDs and passed to the message handler on the stack; see Example 1. In addition, you have to pass the self parameter explicitly to the message handler. Message-type extension allows you to build an additional type hierarchy next to the normal hierarchy of objects. Example 2(a), for instance, creates its own object, a new message type, and a message handler. The message handler determines what to do by explicitly testing for each message type. Example 2(b) creates a new instance of the object and sends a message to it. In languages such as C++, you don't need to initialize method handlers or define message record types. In Oberon, however, it is necessary (a variant of Oberon called "Oberon-2" provides some relief here at the cost of some flexibility).
Messages form another type hierarchy orthogonal to the object hierarchy (although new message types normally belong to a new object type), allowing you to layer or screen messages. For example, you can create an extension of the object's base message type that defines a new base type, which is sent to objects that can display themselves. In Oberon System 3, these objects are called "Frames" and have a corresponding FrameMsg base type with additional information useful to frames; see Example 3(a). A Frame can, for example, determine if the message it receives is based on a Frame message before responding to it. In this way, an object can selectively respond or screen messages based on a certain type without knowing the message's real type. This is useful with message forwarding. Example 3(b) defines a new Frame message to display a frame on the display. Note that you sometimes collapse slightly different semantics of an action into one message and distinguish between them through an id field in the message itself. The Gadget's base type is again a type extension of the Frame type and defines further local variables for attribute management, its visible region on the display, current state, and so on.
One reason for handling messages this way is that you can send messages to objects you don't know yet, and objects can forward messages they don't know to other objects. In most other object-oriented languages, you at least have to know the type of the object in which you activate a method, and each object can only respond to messages that it knows about. Oberon System 3's message handling is exactly what's needed for building extensible systems. First we investigate why objects need to respond to messages they don't understand. This is necessary because you need to structure the system, and you need special objects that manage other objects. These container objects manage a set of child objects. Here you can imagine a display-manager component that arranges a set of documents on the display and forwards unknown messages to its children, which might in turn forward it to their subcomponents. For example, the display manager might not know about a message to the documents that some shared global data value has changed. We cannot determine beforehand what the system will be used for, how exactly components will be structured, or what messages are required to manage objects. Therefore, we must program all containers or component managers to forward messages to their children, in the hope that the children will make some sense of them. Often the containers may determine to what class a message belongs and take some special action (like refusing to forward the message when it does not manage any displayable objects).
Beyond this, the system needs to be able to communicate with objects that it does not know about yet (otherwise we can't extend our system!). We need assume only that the component is an object and thus understands at least the base object-message type. The message handler, not the type definition of an object, determines what messages an object understands. This means that an object can respond to messages declared in different modules, perhaps logically belonging to different object classes. This can, when compared to other object-oriented languages, be regarded as a sort of message multiple inheritance. Oberon System 3 has a set of messages that handle the interaction with text editors and function with all the different text editors available for the system. In this way, you can create a spelling checker that functions for all the different text editors. In other systems, this is often solved with special scripting languages (AppleScript or Visual Basic, for instance) that glue applications together.
Another advantage of this model is that you can change the behavior of objects without affecting their type definition, and thus without invalidating all clients or extensions of this object. For example, you can change objects to respond to more messages, alleviating the so-called "fragile-base" problem at the cost of opaque object implementations. The latter is a major problem because it is not always explicit what messages an object will respond to. In Oberon System 3, each message has a result field in which an object must indicate if it responded to a message or not. Another problem is that explicitly testing message types takes time. Thus, it is imperative that the handlers and message hierarchy be structured to facilitate quick determination of the message type.
The Gadget Example
As mentioned earlier, Listing One is the code for a small, rectangular gadget. Lines 3--6 declare the new gadget as a type extension of the base gadget type with a local variable to store the color of the block instance. The asterisks following the variable and type names indicate that they are exported from the module (unlike Modula-2, Oberon does not have separate definition and implementation modules). A new instance of the gadget is allocated by the Oberon command procedure NewFrame (lines 103--106). After initialization, the object is stored in the global variable NewObj in the Objects module. This location allows other Oberon commands to process the gadget, the simplest process being the insertion of the gadget at the caret position into an existing user interface. The W and H fields, declared in the base type, indicate the width and height of the gadget in screen pixels.
The message handler (44--102) is the largest part of a gadget and is responsible for processing all messages sent to a gadget instance. Messages addressing the visual aspects of the gadget (Frame messages) appear in lines 48--78 and are distinct from those messages that all objects should understand (lines 80--97). The FrameHandler handles messages that are not understood (line 99) and acts as the gadget's base-class-handler implementation.
Lines 91--97 handle the request to an object to make a copy of itself. The resulting copy is returned in the obj field of the message M. No global copy service exists, all copying operations are distributed throughout the network of objects, and the network of objects can be of any topology. Therefore, copy messages might arrive twice at the same object, although the object should only be copied once. This is detected by testing the time-stamp of the message against the time-stamp of the last copy operation, which is conveniently remembered when the message first arrives. In the meantime, the copy is cached in a private field called dlink. The actual copying is done in lines 39--43, including a call to a procedure to copy the fields of the base gadget type (line 42).
In lines 81--89, the object is stored to and loaded from disk, where the id field of the FileMsg indicates which of these operations should be performed. The field R of the message M contains a Rider, the access mechanism for reading and writing files in Oberon. The FrameHandler stores and loads the fields of the base type. Files.ReadInt and Files.WriteInt store the object's color in a portable fashion to ensure that the gadget can be used on all other hardware architectures. The attribute handling is completed in lines 80 and 7--22. The FrameAttr procedure checks whether attributes must be retrieved, updated, or enumerated. The field Name of the message indicates which attribute, the field class determines its type, and diverse other fields in the message indicate the value (M.i stores an INTEGER value). It is important to note that the attribute Gen specifies the procedure that allocates another instance of this gadget (line 10). Thus, a call to this procedure followed by a FileMsg sent to the resulting object will restore the previously stored state of a gadget. In lines 11 and 15--16, the color attribute is retrieved and updated, and in line 20 it is "advertised" by invoking a procedure variable in the message's Enum field. The advantage of such a strategy is that we can use Inspector to edit the attributes of all gadgets, rather than building a new attribute editor for each gadget class.
The second part of the message-handler processes Frame messages (lines 48--78). Most messages are handled in a default way (lines 70--73, 75). Messages are normally addressed to a distinct frame F (as indicated by the field F in a FrameMsg) or to all objects on the display (a so-called "message broadcast"). For the latter, the destination frame F is set to NIL. Line 50 checks if one of these two cases applies before calculating the coordinates of the gadget on the display by adding the origin (M.x, M.y) to the actual frame coordinates (F.X, F.Y, F.W, F.H) in line 51. The coordinates of a frame are always stored relative to its parent or container frame, allowing us to move whole containers without updating all the coordinates of its contents.
In this particular case, you are interested only in messages related to display events (handling DisplayMsg in lines 52--62) and keyboard and mouse events (handling InputMsg in lines 63--69). A gadget can either be displayed as a whole (lines 54--56) or as a selected rectangular part of it (lines 57--60). The calls to Gadgets.MakeMask (lines 55 and 58) create a so-called "display mask" that acts as a clipping region for all further drawing operations in the Restore procedure (lines 23--29). The call to ReplConst in line 25 has the actual task of displaying our block. Lines 26--28 display the gadget in a highlighted state if it is selected. The handling of InputMsg is also quite straightforward. If the mouse is located in the active area of the gadget (line 65) (that is, not inside the sensitive control areas around each frame used for moving and resizing), the mouse is simply drawn in the form of a pointing hand at position (M.X, M.Y) (line 66). Otherwise the default message handler overtakes all moving and resizing operations (line 67).
Printing is done in the same fashion as displaying (lines 74, 30--38). There are no device-independent devices in Oberon, so display coordinates are converted to printer coordinates using the formula in lines 32--34. In essence, such measurements are made in special units organized so that no rounding errors occur when converting between different devices.
Example 1: The Object base type.
MODULE Objects; TYPE Object = POINTER TO ObjDesc; ObjMsg = RECORD END; (* base message type *) Handler = PROCEDURE (obj: Object; VAR M: ObjMsg); ObjDesc = RECORD (* base object type *) lib: Library; ref: INTEGER; handle: Handler END;
Example 2: MyObject definition.
(a) MODULE MyObjects; IMPORT Objects; TYPE MyObject = POINTER TO MyObjDesc; MyObjDesc = RECORD (Objects.Object) myColor: INTEGER; END; SetColorMsg = RECORD (Objects.ObjMsg) (* message to set color of an object*) newColor: INTEGER; END; PROCEDURE MyHandler(obj: Object; VAR M: ObjMsg); BEGIN WITH obj: MyObject DO IF M IS SetColorMsg THEN WITH M: SetColorMsg DO obj.myColor := M.newColor END ELSIF M IS Objects.CopyMsg THEN WITH M: Objects.CopyMsg DO (* ... make a copy of the object ... *) END ELSIF (* ... etc ... *) END END END MyHandler; (b) PROCEDURE Do: VAR me: MyObject; M: SetColorMsg; BEGIN NEW(me); (* allocate new object on the heap *) me.handle := MyHandler; (* don't forget to install the message handler *) M.newColor := 1; (* init message *) me.handle(me, M) (* send message *) END Do;
Example 3: The Frame base class.
(a) TYPE Frame = POINTER TO FrameDesc; (* base type of all displayable objects *) FrameDesc = RECORD (Objects.ObjDesc) next, dsc: Frame; X, Y, W, H: INTEGER (* display coordinates of the frame *) END; FrameMsg = RECORD (Objects.ObjMsg) F: Frame; (* target frame*) x, y: INTEGER; (* origin of the frame receiving this message *) res: INTEGER (* operation result code *) END; (b) DisplayMsg = RECORD (FrameMsg) id: INTEGER; (* flag to display the whole frame, or just an area of it *) u, v, w, h: INTEGER (* area of destination frame to be displayed *) END;
Listing One
MODULE Skeleton; 1 IMPORT Files, Display, Display3, Printer3, Effects, Objects, Gadgets, Oberon; 2 TYPE 3 Frame* = POINTER TO FrameDesc; 4 FrameDesc* = RECORD (Gadgets.FrameDesc) 5 mycol*: INTEGER; 6 END; 7 PROCEDURE FrameAttr(F: Frame; VAR M: Objects.AttrMsg); 8 BEGIN 9 IF M.id = Objects.get THEN 10 IF M.name = "Gen" THEN M.class := Objects.String; COPY("Skeleton.NewFrame", M.s); M.res := 0 11 ELSIF M.name = "Color" THEN M.class := Objects.Int; M.i := F.mycol; M.res := 0 12 ELSE Gadgets.framehandle(F, M) 13 END 14 ELSIF M.id = Objects.set THEN 15 IF M.name = "Color" THEN 16 IF M.class = Objects.Int THEN F.mycol := SHORT(M.i); M.res := 0 END; 17 ELSE Gadgets.framehandle(F, M); 18 END 19 ELSIF M.id = Objects.enum THEN 20 M.Enum("Color"); Gadgets.framehandle(F, M) 21 END 22 END FrameAttr; 23 PROCEDURE RestoreFrame(F: Frame; M: Display3.Mask; x, y, w, h: INTEGER); 24 BEGIN 25 Display3.ReplConst(M, F.mycol, x, y, w, h, Display.replace); 26 IF Gadgets.selected IN F.state THEN 27 Display3.FillPattern(M, Display3.white, Effects.selectpat, x, y, x, y, w, h, Display.paint) 28 END 29 END RestoreFrame; 30 PROCEDURE Print(F: Frame; VAR M: Display.PrintMsg); 31 VAR R: Display3.Mask; 32 PROCEDURE P(x: INTEGER): INTEGER; 33 BEGIN RETURN SHORT(x * LONG(10000) DIV Printer3.Unit) 34 END P; 35 BEGIN 36 Gadgets.MakePrinterMask(F, M.x, M.y, M.dlink, R); 37 Printer3.ReplConst(R, F.mycol, M.x, M.y, P(F.W), P(F.H), Display.replace); 38 END Print; 39 PROCEDURE CopyFrame*(VAR M: Objects.CopyMsg; from, to: Frame); 40 BEGIN 41 to.mycol := from.mycol; 42 Gadgets.CopyFrame(M, from, to); 43 END CopyFrame; 44 PROCEDURE FrameHandler*(F: Objects.Object; VAR M: Objects.ObjMsg); 45 VAR x, y, w, h: INTEGER; F0: Frame; R: Display3.Mask; 46 BEGIN 47 WITH F: Frame DO 48 IF M IS Display.FrameMsg THEN 49 WITH M: Display.FrameMsg DO 50 IF (M.F = NIL) OR (M.F = F) THEN (* message addressed to this frame *) 51 x := M.x + F.X; y := M.y + F.Y; w := F.W; h := F.H; (* calculate display coordinates *) 52 IF M IS Display.DisplayMsg THEN 53 WITH M: Display.DisplayMsg DO 54 IF (M.id = Display.frame) OR (M.F = NIL) THEN 55 Gadgets.MakeMask(F, x, y, M.dlink, R); 56 RestoreFrame(F, R, x, y, w, h) 57 ELSIF M.id = Display.area THEN 58 Gadgets.MakeMask(F, x, y, M.dlink, R); 59 Display3.AdjustMask(R, x + M.u, y + h - 1 + M.v, M.w, M.h); 60 RestoreFrame(F, R, x, y, w, h) 61 END 62 END 63 ELSIF M IS Oberon.InputMsg THEN 64 WITH M: Oberon.InputMsg DO 65 IF (M.id = Oberon.track) & Gadgets.InActiveArea(F, M) THEN 66 Oberon.DrawCursor(Oberon.Mouse, Effects.PointHand, M.X, M.Y); M.res := 0 67 ELSE Gadgets.framehandle(F, M) 68 END 69 END 70 ELSIF M IS Display.ModifyMsg THEN Gadgets.framehandle(F, M) 71 ELSIF M IS Oberon.ControlMsg THEN Gadgets.framehandle(F, M) 72 ELSIF M IS Display.SelectMsg THEN Gadgets.framehandle(F, M) 73 ELSIF M IS Display.ConsumeMsg THEN Gadgets.framehandle(F, M) 74 ELSIF M IS Display.PrintMsg THEN Print(F, M(Display.PrintMsg)) 75 ELSE Gadgets.framehandle(F, M) 76 END 77 END 78 END 79 (* Object messages *) 80 ELSIF M IS Objects.AttrMsg THEN FrameAttr(F,M(Objects.AttrMsg)) 81 ELSIF M IS Objects.FileMsg THEN 82 WITH M: Objects.FileMsg DO 83 IF M.id = Objects.store THEN (* store private *) 84 Files.WriteInt(M.R, F.mycol); 85 Gadgets.framehandle(F, M) 86 ELSIF M.id = Objects.load THEN (* load private *) 87 Files.ReadInt(M.R, F.mycol); 88 Gadgets.framehandle(F, M) 89 END 90 END 91 ELSIF M IS Objects.CopyMsg THEN 92 WITH M: Objects.CopyMsg DO 93 IF M.stamp = F.stamp THEN M.obj := F.dlink (* message arrives again *) 94 ELSE (* First time copy message arrives *) 95 NEW(F0); F.stamp := M.stamp; F.dlink := F0; CopyFrame(M, F, F0); M.obj := F0 96 END 97 END 98 ELSE (* an unknown message arrived; framehandler might know it *) 99 Gadgets.framehandle(F, M) 100 END 101 END 102 END FrameHandler; 103 PROCEDURE NewFrame*; 104 VAR F: Frame; 105 BEGIN NEW(F); F.W := 20; F.H := 20; F.mycol := Display3.red; F.handle := FrameHandler; Objects.NewObj := F; 106 END NewFrame; 107 END Skeleton.
Copyright © 1994, Dr. Dobb's Journal