shf@well.UUCP (Stuart H. Ferguson) (04/26/88)
IPC - Object-Oriented Approach Specification ------------- This will explain more fully the specifics of the object-oriented interprocess communication standard I'm proposing. The abstract model is that of an object-oriented system, which can be imagined as just a bunch of "objects" sending "messages" to each other. The only things in the system are objects, and the only action that can be performed, at least in the context of the system, is sending a message. While it would be unwieldy and impractical to use a fully object-oriented specification, some aspects of the design are useful and are used to make this IPC system more general and flexible. In order to avoid confusion, however, messages as passed *to Objects* will be called "Commands," and messages passed between processes to service a Command will be called "Messages." The IPC.library: All interprocess communication is mediated by the IPC.library through a set of interface routines called by clients and servers. Servers "register" themselves and list the Objects and Commands that they can "serve," that is, the set of Object/Command messages to which they can reply. The IPC.library maintains information about each of its registered servers and uses that information to service clients. Clients make requests by "Commanding their Objects," and the IPC.library simply matches the Object/Command pair with the appropriate server and dispatchs a Message. Client programs do not deal directly with the server processes, but simply make calls to the IPC.library. This level of insulation maintains some sanity and safety, as well as modularity for future expansion by preserving the object-oriented metaphore. Programmer Interface (Client Programs): The programmer wishing to make use of servers that already exist and not interested in writing his own servers does not need to see much of the internal implementational details of Objects, Commands and other aspects of the IPC.library. He only needs to know something of the basic design and little about their specifics. Objects: Clients make requests in the form of Commands "sent to" Objects. An Object is just a memory structure that describes some abstract "thing." They consist of a class code describing the generic class of this object, and a set of attribute blocks associated with the object. The class code is a four character string encoded as a long value, just like an IFF identifier. The rules for valid class ids are the same as for IFF identifiers. The attribute Chunks, which contain the data specific to this instantiation of the class, are also tagged by similar identifiers. The possible values for these identifiers depend on the Object class. The attribute identifier `DATA', for example, could mean something different if it were part of an Object description of class `OBJ1' than if it were part of one of class `OBJ2'. Object classes are arranged hierarchically so that one class can be a "sub-class" of another. For example, there might be a `FILE' class which can receive commands `OPEN', `DEL', `COPY', etc. There might also be an `IFF' class which is a sub-class of `FILE' and which can receive some additional commands special to IFF files. But because `IFF' is a sub-class of `FILE', all `IFF' class Objects can also receive all the commands that any Object of class `FILE' would. Commands: A command is defined by a longword command code, like `EDIT' or `DISP', plus some optional parameter blocks. This is analogous to CLI-type commands which consist of the name of the command plus parameters and flags. The meaning of the command code can depend on the class of the Object (`EDIT' is different for bitmaps then for text, for example), and the meaning of tagged parameter Chunks depends on the command. Writing a Client Program: A programmer writing a client program does not need to know the declarations for the Command and Object structures, and can imagine them as private to the IPC.library. Manipulating Object and Command structures can all be done with utility routines provided in the IPC.library, but object attributes and command parameters will need to be manipulated directly. All such blocks have a Chunk descriptor at their start, which is defined as: struct Chunk { struct Node chunk_node; long chunk_id; long chunk_size; } where struct Node { struct Node *next; struct Node *prev; } The chunk_size field must be intialized to be the number of bytes that follow the Chunk header in this data block. An example attribute block would be: struct Demo_Attribute { struct Chunk da_chunk; /* data for this chunk */ char *dog; long food; char duhh[27]; } example; example.da_chunk.chunk_size would be set to 35 -- 4 bytes for dog, 4 bytes for food, and 27 bytes for duhh. It could alternatively be set to the value of sizeof(struct Demo_Attribute) - sizeof(struct Chunk). IPC.library Client Functions: The IPC.library contains a series of functions for creating and manipulating Objects and Commands so the programmer does not have to do it directly, although he could if he wanted to. The NewObject() function will return a new instance of an object and will initialize it in a default way. struct Object *NewObject(class) long class; NOTE that some objects will require initialization by the object server. This is accomplished by sending the `NEW' command to this "empty" object. To create new Chunks and attach them to an object as object attributes, use the functions: struct Chunk *NewChunk(type,size,data) long type; long size; char *data; struct Object *AddAttribute(object,attr) struct Object *object; struct Chunk *attr; The new Chunk will have chunk_id "type" and will have chunk_size "size." If data is a non-null pointer, size bytes will be copied from data into the Chunk's data area. The data pointer must be word aligned. AddAttribute() inserts the Chunk "attr" at the head of object's attribute list. AddAttribute will not prevent duplication of nodes with the same attribute code. The function returns its "object" parameter. The functions struct Chunk *FindAttribute(object,type) and struct Chunk *GetAttribute(object,type) struct Object *object; long type; both return pointers to the first attribute Chunk in object's attribute list of type "type." GetAttribute() removes the node from the list before returning it. To free the memory associated with an object, use: void FreeObject(object) struct Object *object; This function traverses the attribute list for the object and frees each Chunk according to its size, and then disposes of the object itself. The programmer must make sure that there are no pointers to allocated resources in any of the Chunks since they will be lost. This might include pointers to bitmaps, locks, windows, etc. Some similar functions for use with Commands are: struct Command *NewCommand(code) long code; struct Command *AddParameter(cmd,parm) struct Command *cmd; struct Chunk *parm; void FreeCommand(cmd) struct Command *cmd; struct Chunk *FindParameter(cmd,type) and struct Chunk *GetParameter(cmd,type) struct Command *cmd; long type; These functions can be used to create new Command structures and attach parameter blocks (created with NewChunk()) to the Command. The constructed command can then be applied to an object. The central function in the use of this IPC facility is the function which applies commands to objects. This function looks up the object class and the message code, finds the appropriate server for the requested operation and dispatches the request as a Message. The function is: long Dispatch(object,cmd,cache) struct Object *object; struct Command *cmd; struct Cache **cache; Cmd is the command to send, and Object is the object to receive the command. Cache is a pointer to a pointer to a structure where the message port used for this Dispatch() will be stored. More on this later. The return value of the function is a code indicating the result of the requested operation. If non-zero, the command completed normally. Commands can return objects as return values which can be extracted from the returned message with the function: struct Object *ResultObject(cmd) struct Command *cmd; If the Dispatch() function returns a zero value, this result object will be an object of type `ERR' and will contain info about the error. These are the basic facilities needed to access the IPC capability from a client program. Caching: The Dispatch() function takes a "cache" argument, which is a pointer *to a pointer* to an instance of a Cache structure. This argument is provided to speed up message passing if several messages of the same type are being passed. Since dispatching a command for an object involves searching a table for the correct message port to use for the given combination of object and command, a call to Dispatch() can take some time to execute. This could be unnecessarily slow if a programmer were sending the same command to the same class of object repeatedly. To help speed things up, the Dispatch() function can remember the last object/class combination and the message port it found for that combination. The next time the function is called, it first checks to see if the object/class combination stored in the cache is the same as the current one, and if it is, it uses the message port stored there. This can be much faster than searching the entire list for the correct message port again. If the combination is not the same, Dispatch() will look up the correct message port and again store that in the cache. The IPC.library manages Cache structures, not the programmer. This is why the cache argument is a pointer *to a pointer* -- so that the programmer does not allocate or free Cache structures. Caches are arguments rather than global variables so that different Dispatch() calls can use different caches. Here's an example of the use of a cache: AFunction() { static struct Cache *cptr = NULL; obj = ...; /* get an object */ cmd = ...; /* get a command to send */ /* send the command */ Dispatch (obj, cmd, &cptr); ... } The cache pointer is declared as static so that it will not change between calls to AFunction(), and is intialized to null. A pointer to this pointer is passed to Dispatch(). When Dispatch() sees that cptr is null, it allocates a new Cache structure and points cptr at it, as well as storing the message port from the current call in this new Cache structure. After the call, cptr points to a real Cache structure and is no longer null. The next time this particular Dispatch() call is executed, this Cache structure will be checked and used if appropriate. On exit, all the caches associated with a task will be freed. Also, the IPC.library may free all caches and zero a task's cache pointers *at any time*. This means that if a program uses any of the values stored in a Cache structure it must do so within Forbid()/Permit()'s! Cache pointers will be zeroed if anything in the service list changes, thus invalidating all cached information. Generally this doesn't happen very often, but since this depends on programs which are running concurrently, it could happen at any time. Client Example: In order to make up an example of a client using a service through the IPC.library, I first need to make up a fictional object class and its description, including what commands it understands. The imaginary object class to use for this example is the `CMAP' class which is to be an instance of a color map. A `CMAP' object has an attribute block tagged as `BODY' which contains the color map, or a `SCRN' attribute block which contains a pointer to a Screen structure. The `CMAP' class can service at least one type of command -- the `EDIT' command. This command will let the user interactively edit the color map specified by the `CMAP' object. If the `EDIT' command has the optional parameter `SCRN' then the editing will occur directly on the screen specified by the `CMAP' object. If the `CMAP' object does not have a `SCRN' attribute, the `SCRN' parameter on the `EDIT' command will be ignored. Although this object does not exist, it could. A necessary and sufficient condition for its existence is a server which handles the `EDIT' command for `CMAP' objects. In this example, the function EditColors() will use another program via IPC to edit its color map. /* Edit the color map on Screen "scr" via IPC * IPC.library already open * NOTE: This function contains no error-checking for readability * Kids - don't try this at home */ #define CMAP MAKE_ID('C','M','A','P') #define EDIT MAKE_ID('E','D','I','T') #define SCRN MAKE_ID('S','C','R','N') void EditColors (scr) struct Screen *scr; { struct Object *cmap_obj; struct Command *edit_cmd; static struct Cache *cache = NULL; /* Create the CMAP object to edit */ cmap_obj = NewObject(CMAP); /* Create SCRN chunk containing pointer to screen and attach * to CMAP object */ AddAttribute (cmap_obj, NewChunk (SCRN, (long)sizeof(struct Screen *), &scr); /* Create the EDIT command with SCRN parameter * note that the SCRN chunk is just a flag and contains no data */ edit_cmd = NewCommand (EDIT); AddParameter (edit_cmd, NewChunk (SCRN, 0L, NULL)); /* Do the editing by dispatching the command on the object */ Dispatch (cmap_obj, edit_cmd, &cache); FreeCommand (edit_cmd); FreeObject (cmap_obj); } A few things to note: The caching will work well in this case, since the object class and command type are the same every time Dispatch() gets called with this cache. The object and command structures could also be defined statically except some possible problems: There could be difficulty with memory protection on future Amigas which would prevent a static structure from being used by another task. The client programmer would also have to be sure that the server didn't try to modify the object or command by adding or deleting Chunk nodes, as this could wreck havoc with static structures. Asynchronous IPC: Dispatch() is synchronous, sending a message to a server and waiting for a reply. It is also possible to send a message to a server and continue on, looking for a reply at some later time. This is accomplished with: DispatchAsync() CheckCmd() WaitCmd() Programmer Interface (Servers): Writing a server can create a new class of object, implement a new command on an old object class, or re-implement existing commands on existing objects. Writing a server requires knowing great deal more about the specifics of the IPC.library and how to use it, as well as an understanding of what sorts of Messages the server task will receive. Objects: Clients make requests in the form of Commands sent to Objects. An Object consists of a base object structure with a linked list of object attributes stored as Chunks with variable size. Objects are defined as: struct Object { long obj_class; struct Object *obj_parent; struct List obj_attr; } The obj_class field is the four character, IFF-style class code. The rules for valid ids are the same as for IFF identifiers. The pointer obj_parent points to another object, one which is an instance of the object's super-class. More about this later. Obj_attr is the head of a linked list of attribute node Chunks which contain the data specific to this instance of the object class. Struct List is an Exec-style linked list header defined as: struct List { struct Node *head; struct Node *tail; /* always NULL */ struct Node *tailprev; } Objects of a "root" class, that is, a class which is not a sub-class of any other, have a null obj_parent pointer. For all other objects, those that are not members of a root class, the obj_parent pointer points to an instance of this object's super-class. So, lurking behind each member of a sub-class is a member of its super-class. The rational for doing it this way is to implement a transparent inheritence. If the IPC.library cannot find a server for a given command on a given class, it will try to Dispatch() the command to the object "object->obj_parent" if it exists. If no parent exists, Dispatch() returns an error. Because of inheritence, and because there is an instance of an object's super-class behind each object, programmers writing servers do not need to implement the parts of a new class of object that are handled by the object's sub-class. Commands: A command is defined as: struct Command { struct Message com_msg; long com_code; long com_special; struct Object *com_obj; long com_retval; struct Object *com_retobj; struct List com_parm; } This command block is exactly what gets sent to method servers. Com_code is the code identifier for this command and com_obj is the Object to be affected by the command. Com_parm is the head of a list of Chunks which are the command parameters associated with the command. As before, the chunk_size field of a parameter Chunk can be zero so that the Chunk acts as a flag simply by its presence or absence. Com_retval is the code returned by Dispatch() after performing the command, and com_retobj is the object returned, if any. Com_special is described later. Possible com_retval values: RV_ERROR(=0), RV_RETOBJ, RV_OK. Servers: A server is just a task that has registered itself with the IPC.library to handle certain command requests on specific objects. The IPC.library will route all requests of that type to the Message Port for this server and will return the replies back to the originator of the request. Just what the objects are and what the commands do is decided by the server author and is communicated, in his documentation, to the authors of client programs. Registering a Server: Registering a program as a server takes two steps: allocating an IPC.library managed message port and specifing what commands this port will handle and for what object classes. The first step is accomplished with the function: struct ServicePort *OpenServicePort() This will create an Exec Message Port in the context of the calling program. A ServicePort structure is just an extended Exec MsgPort structure and so can be passed as an argument to any function on a MsgPort. Services can be registered for a port using: void RegisterService(sport,class,type,special) struct ServicePort *sport; long class, type, special; Sport is a ServicePort returned by the OpenServicePort() call, and class and code are the object class and command code to register on that port. This means that from then on, whenever any program does a Dispatch() on this type of command on this class of object, the Command message will be posted to this ServicePort. The special value will be inserted into the com_special field of the message before it gets posted. A registered service can be unregistered from the port by doing: void UnregisterService(sport,class,type) struct ServicePort *sport; long class, type; The IPC.library keeps a list of all ports registered for a given service, that is for each object/command pair. At any time the IPC.library will post requests to the port at the top of the list. Before exiting, a server should close open service ports with: void CloseServicePort (sport) struct ServicePort *sport; Any outstanding requests waiting in the port's message queue will be re-dispatched to the port which is now at the top of the list. If there are no ports left to service this type of request, the message will be replied to the originating process with an error. In implementing a new object class, the server process must specify the hierarchy for the new class with the function: long SubClass(class,superclass) long class,superclass; Class is the new object class, and superclass is to be this object's superclass. If this class has already been declared in another way, or the server for superclass is not implemented, this call will return an error. So a server that was to implement the `FOO2' class which was a subclass of the `FOO1' class would make the call: error = SubClass (FOO2, FOO1); The class `FOO2' would inherit the characteristics of a `FOO1' class object. In fact, if the server did nothing more than make this call to SubClass(), then the class `FOO2' would act identically to class `FOO1' and objects of this class would be served by the `FOO1' server. The `FOO1' server has to exist for this function to return success, and if the `FOO1' server goes away before the `FOO2' server, the IPC.library will mothball the `FOO2' class and any other subclasses until a `FOO1' server comes on-line again. Servicing Requests: Once a server has opened a service port and registered services on it, the IPC.library considers that port open for business and may begin posting requests to it. These requests will be just Command structures dispatched from client programs which the server is expected to handle and Reply() back to the originating program. The server could look at the command code and object class to determine what kind of request this is, but the IPC.library had to do this once already in order to dispatch the request at all, so it might not be very efficient for the server to have to do this again. This is the use of the com_special field in the Command structure. By using this field, the server can receive messages with the command code and object class pair pre-decoded by the IPC.library. Example Server: In this example code, I will write a skeletal server for the mythical object class `FILE' (although this might make a good real object class) and its subclass `FTXT'. This server will implement the `FILE' class commands `RNAM', `COPY' and `DEL', and the `FTXT' class commands `TYPE' and `EDIT'. The code which acts on the commands is omitted so that this code fragment shows only the IPC.library interface. /* FILE server * services some commands for the the FILE and FTXT class objects * FTXT is a sub-class of FILE * NB: error-checking has been removed to ease readability * your code may be different */ #define FILE MAKE_ID('F','I','L','E') /* define FTXT, RNAM, COPY, DEL, TYPE, EDIT similarly */ /* The "special" codes for each service type */ #define FILE_RNAM 0 #define FILE_COPY 1 #define FILE_DEL 2 #define FTXT_TYPE 3 #define FTXT_EDIT 4 /* The functions to call for servicing requests */ extern void RenameFile(),CopyFile(),DeleteFile(); extern void TypeTextFile(),EditTextFile(); struct IPCBase *IPCBase; main() { struct ServicePort *sp; struct Command *com; /* Open IPC.library */ IPCBase = (struct IPCBase *) OpenLibrary ("IPC.library", 0L); /* Open the IPC port for receiving commands */ sp = OpenServicePort(); /* First declare the FILE class */ RegisterService (sp, FILE, RNAM, FILE_RNAM); RegisterService (sp, FILE, COPY, FILE_COPY); RegisterService (sp, FILE, DEL , FILE_DEL ); /* then declare and define the FTXT class */ SubClass (FTXT, FILE); RegisterService (sp, FTXT, TYPE, FTXT_TYPE); RegisterService (sp, FTXT, EDIT, FTXT_EDIT); while () { com = GetMsg (sp); if (!com) WaitPort (sp); else { switch (com->com_special) { case FILE_RNAM: RenameFile (com); break; case FILE_COPY: CopyFile (com); break; case FILE_DEL : DeleteFile (com); break; case FTXT_TYPE: TypeTextFile (com); break; case FTXT_EDIT: EditTextFile (com); break; } ReplyMsg (com); } } } Note that this program has no facility for exit. If it had a window associated with it, it might exit on a CLOSEWINDOW IntuiMessage, but as it is this program will run till the next re-boot. The `FILE' class gets defined first, since the `FTXT' class depends on it. If the program tried to declare the `FTXT' class first, SubClass() would return an error. Also, even though the command types are processed by a switch, the "special" values are sequential so the compiler should be able to implement this switch efficiently as a jump table. If the compiler cannot, it would be relatively trivial to do this directly in C. This can make a difference when there are lots of cases. For the fearless, the "special" values could even be pointers to the functions themselves. The functions defined as "extern" which take a Command structure as argument pull the data they need out of the structure, process it, and set the com_retval and com_retobj fields in the structure before returning. Recursion: There are possible problems if a program acts as both client and server. If, for example, process A Dispatch()es a request to process B, and process B uses a Dispatch() to service that request. If B's request gets dispatched to process A, a deadlock occurs since A is still waiting for its initial Dispatch() to return. Since A and B are both servers and clients, they must use asynchronous IPC and be re-entrant to permit recursive service requests such as in the example. It's not completely clear just exactly how to program this. Degenerate Cases (Program controls Program): Some programmers will not want or will not understand how to implement the full capability of the object-oriented design. For those who just want to stick a remote control port on their program, the object-oriented IPC paradigm provides a degenerate case -- the case where the object to receive commands is the process itself. What this means in practical terms is that commanding the program takes place through a "dummy" object. The class of the dummy object serves to connect clients with the right server, but no important data is stored in the Object structure. The server can then just process commands as if the program itself were the target of the commands and ignore the dummy object. In an abstract sense, the internal state of the process determines the object to be commanded, so the dummy objects held by client process actually represent the process itself. By way of example, suppose you had a program called "jedit" which was your personal favorite text editor (of course, since you wrote it). Suppose also that you wanted to add a minimal remote control facility to allow client programs to control the operations of the editor. You could have the "jedit" program accept editing commands by creating a dummy object class, say a `JEDI' class. The "jedit" program would register this class with the IPC.library and specify that it can receive certain commands, like `READ', `WRIT', `SUBS', `MOVE', `DELB', etc. Client programs could then create empty objects of class `JEDI' and Dispatch() commands to that object. The `JEDI' object would do nothing more than allow the client program to find the "jedit" program's message port. Once the command is received, the "jedit" program can ignore the com_object field of the Command structure and just execute the command on whatever is currently in the text buffer, just like interactive editing commands. This simple approach will work, but there may be problems. Although there could be many "jedit" programs running in the system, any client programs could only access the one most recently run. This could cause major havoc if two programs tried to use "jedit" to edit different things at the same time. The straightforward way to avoid such problems is to flesh out the `JEDI' object so that it contains the data specific to a particular editing session. This way, several clients could use the "jedit" server at one time but each would have its own private editing context stored in its own instance of the `JEDI' object. Even though the approach described above is a quick and dirty way of using the object-oriented IPC standard for simple remote control, it leads naturally to this more general implementation. Reserved Object and Command IDs: A few object classes and Command codes are reserved and have special meaning. The `ERR' class object is a universal error object used to pass error information around. Someone might want to implement a command server for this class of object. The Command codes `NEW' and `FREE' are reserved for initializing and freeing object structures. Objects which require initialization by the server should accept the `NEW' command, and objects which require allocated resources to be freed prior to the object being FreeObject()ed should accept the `FREE' command. -- Stuart Ferguson (shf@well.UUCP) Action by HAVOC (shf@Solar.Stanford.EDU)