[comp.sys.amiga.tech] IPC - Specification

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)