Top Up Prev Next Bottom Contents Index Search

4.3 Defining New Data Types


The Ptolemy heterogeneous message interface provides a mechanism for stars to transmit arbitrary objects to other stars. Our design satisfies the following requirements:

4.3.1 Defining a new Message class

Every user-defined message is derived from class Message. Certain virtual functions defined in that class must be overridden; others may optionally be overridden. Here is an example of a user-defined message type:


// This is a simple vector message object. It stores
// an array of integer values of arbitrary length.
// The length is specified by the constructor.
#include "Message.h"
class IntVecData: public Message {
private:
int len;
init(int length,int *srcData) {
len = length;
data = new int[len];
for (int i = 0; i < len; i++)
data[i] = *srcData++;
}
public:
// the pointer is public for simplicity
int *data;

int length() const { return len;}

// functions for type-checking
const char* dataType() const { return "IntVecData";}
// isA responds TRUE if given the name of the class or
// of any baseclass.
int isA(const char* typ) const {
if (strcmp(typ,"IntVecData") == 0) return TRUE;
else return Message::isA(typ);
}
// constructor: makes an uninitialized array
IntVecData(int length): len(length) {
data = new int[length];
}
// constructor: makes an initialized array from a int array
IntVecData(int length,int *srcData) { init(length,srcData);}

// copy constructor
IntVecData(const IntVecData& src) { init(src.len,src.data);}

// clone: make a duplicate object
Message* clone() const { return new IntVecData(*this);}

// destructor
~IntVecData() {
delete data;
}
};
This message object can contain a vector of integers of arbitrary length. Some functions in the class are arbitrary and the user may define them in whatever way is most convenient; however, there are some requirements.

The class must redefine the dataType method from class Message. This function returns a string identifying the message type. This string should be identical to the name of the class. In addition, the isA method must be defined. The isA method responds with TRUE (1) if given the name of the class or of any base class; it returns FALSE (0) otherwise. This mechanism permits stars to handle any of a whole group of message types, even for classes that are defined after the star is written.

Because of the regular structure of isA function bodies, macros are provided to generate them. The ISA_INLINE macro expands to an inline definition of the function; for example,

ISA_INLINE(IntVecData,Message) could have been written above instead of the definition of isA to generate exactly the same code. Alternatively, to put the function body in a .cc file, one can write

int isA(const char*) const; in the class definition and put

ISA_FUNC(IntVecData,Message) in the .cc file (or wherever the methods are defined).

The class must define a copy constructor, unless the default copy constructor generated by the compiler, which does memberwise copying, will do the job.

The class must redefine the clone method of class Message. Given that the copy constructor is defined, the form shown in the example, where a new object is created with the new operator and the copy constructor, will suffice.

In addition, the user may optionally define type conversion and printing functions if they make sense. If a star that produces messages is connected to a star that expects integers (or floating values, or complex values), the appropriate type conversion function is called. The base class, Message, defines the virtual conversion functions asInt(), asFloat(), and asComplex() and the printing method print() - see the file $PTOLEMY/src/kernel/Message.h for their exact types. The base class conversion functions assert a run-time error, and the default print function returns a StringList saying

<type>: no print method

where type is whatever is returned by dataType().

By redefining these methods, you can make it legal to connect a star that generates messages to a star that expects integer, floating, or complex particles, or you can connect to a Printer or XMgraph star (for XMgraph to work, you must define the asFloat function; for Printer to work, you must define the print method).

4.3.2 Use of the Envelope class

The Envelope class references objects of class Message or derived classes. Once a message object is placed into an envelope object, the envelope takes over responsibility for managing its memory: maintaining reference counts, and deleting the message when it is no longer needed.

The constructor (which takes as its argument a reference to a Message), copy constructor, assignment operator, and destructor of Envelope manipulate the reference counts of the references Message object. Assignment simply copies a pointer and increments the reference count. When the destructor of a Envelope is called, the reference count of the Message object is decremented; if it becomes zero, the Message object is deleted. Because of this deletion, a Message must never be put inside a Envelope unless it was created with the new operator. Once a Message object is put into an Envelope it must never be explicitly deleted; it will "live" as long as there is at least one Envelope that contains it, and it will then be deleted automatically.

It is possible for an Envelope to be "empty". If it is, the empty method will return TRUE, and the data field will point to a special "dummy message" with type DUMMY that has no data in it.

The dataType method of Envelope returns the datatype of the contained Message object; the methods asInt(), asFloat(), asComplex(), and print() are also "passed through" in a similar way to the contained object.

Two Envelope methods are provided for convenience to make type checking simpler: typeCheck and typeError. A simple example illustrates their use:


if (!envelope.typeCheck("IntVecData")) {
Error::abortRun(*this, envelope.typeError("IntVecData"));
return;
}
The method typeCheck calls isA on the message contents and returns the result, so an error will be reported if the message contents are not IntVecData and are not derived from IntVecData. Since the above code segment is so common in stars; a macro is included in Message.h to generate it; the macro

TYPE_CHECK(envelope,"IntVecData"); expands to essentially the same code as above. The typeError method generates an appropriate error message:

Expected message type 'arg', got 'type' To access the data, two methods are provided: myData() and writableCopy(). The myData function returns a pointer to the contained Message-derived object. The data pointed to by this pointer must not be modified, since other Envelope objects in the program may also contain it. If you convert its type, always make sure that the converted type is a pointer to const (see the programming example for UnPackInt below). This ensures that the compiler will complain if you do anything illegal.

The writableCopy function also returns a pointer to the contained object, but with a difference. If the reference count is one, the envelope is emptied (set to the dummy message) and the contents are returned. If the reference count is greater than one, a clone of the contents is made (by calling its clone() function) and returned; again the envelope is zeroed (to prevent the making of additional clones later on).

In some cases, a star writer will need to keep a received Message object around between executions. The best way to do this is to have the star contain a member of type Envelope, and to use this member object to hold the message data between executions. Messages should always be kept in envelopes so that the user does not have to worry about managing their memory.

4.3.3 Use of the MessageParticle class

If a porthole is of type "message", then its particles are objects of the class MessageParticle. A MessageParticle is simply a particle whose data field is an Envelope, which means that it can hold a Message in the same way that Envelope objects do.

Many methods of the Particle class are redefined in the MessageParticle class to cause a run-time error; for example, it is illegal to send an integer, floating, or complex number to the particle with the << operator. The conversion operators (conversion to type int, double, or Complex) return errors by default, but can be made legal by redefining the asInt, asFloat, or asComplex methods for a specific message type.

The principal operations on MessageParticle objects are << with an argument of type Envelope, to load a message into the particle, and getMessage(Envelope&), to transfer message contents from the particle into a user-supplied message. The getMessage method removes the message contents from the particle1. In cases where the destructive behavior of getMessage cannot be tolerated, an alternative interface, accessMessage(Envelope&), is provided. It does not remove the message contents from the particle. Promiscuous use of accessMessage in systems where large-sized messages may be present can cause the amount of virtual memory occupied to grow (though all message will be deleted eventually).

4.3.4 Use of messages in stars

Here are a couple of simple examples of stars that produce and consume messages. For more advanced samples, look in the Ptolemy distribution for stars that produce or consume messages. The image processing classes and stars, which are briefly described below in "Image particles" on page 4-40, provide a particularly rich set of examples. The matrix classes described on page 4-21 are also good examples. The matrix classes are recognized in the Ptolemy kernel, and supported by pigi and ptlang.


defstar {
name { PackInt }
domain { SDF }
desc { Accept integer inputs and produce IntVecData messages.}
defstate {
name { length }
type { int }
default { 10 }
desc { number of values per message }
}
input {
name { input }
type { int }
}
output {
name { output }
type { message }
}
ccinclude { "Message.h", "IntVecData.h" }
start {
input.setSDFParams(int(length),int(length-1));
}
go {
int l = length;
IntVecData * pd = new IntVecData(l);
// Fill in message. input%0 is newest, must reverse
for (int i = 0; i < l; i++)
pd->data[l-i-1] = int(input%i);
Envelope pkt(*pd);
output%0 << pkt;
}
}
Since this is an SDF star, it must produce and consume a constant number of tokens on each step, so the message length must be fixed (though it is controllable with a state). See "Setting SDF porthole parameters" on page 7-1 for an explanation of the setSDFParams method. Notice that the output porthole is declared to be of type message. Notice also the ccinclude statement; we must include the file Message.h in all message-manipulating stars, and we must also include the definition of the specific message type we wish to use.

The code itself is fairly straightforward-an IntVecData object is created with new, is filled in with data, and is put into an Envelope and sent. Resist the temptation to declare the IntVecData object as a local variable: it will not work. It must reside on the heap. Here is a star to do the inverse operation:

defstar {
name { UnPackInt }
domain { SDF }
desc {
Accept IntVecData messages and produce integers. The first 'length'
values from each message are produced.
}
defstate {
name { length }
type { int }
default { 10 }
desc { number of values output per message }
}
input {
name { input }
type { message }
}
output {
name { output }
type { int }
}
ccinclude { "Message.h", "IntVecData.h" }
start {
output.setSDFParams(int(length),int(length-1));
}
go {
Envelope pkt;
(input%0).getMessage(pkt);
if (!pkt.typeCheck("IntVecData")) {
Error::abortRun(*this,pkt.typeError("IntVecData"));
return;
}
const IntVecData * pd = (const IntVecData *)pkt.myData();
if (pd.length() < int(length)) {
Error::abortRun(*this,
"Received message is too short");
return;
}
for (i = 0; i < int(length); i++) {
output%(int(length)-i-1) << pd->data[i];
}
}
}
Because the domain is SDF, we must always produce the same number of outputs regardless of the size of the messages. The simple approach taken here is to require at least a certain amount of data or else to trigger an error and abort the run.

The operations here are to declare an envelope, get the data from the particle into the envelope with getMessage, check the type, and then access the contents. Notice the cast operation; this is needed because myData returns a const pointer to class Message. It is important that we converted the pointer to const IntVecData * and not IntVecData* because we have no right to modify the message through this pointer. Many C++ compilers will not warn by default about "casting away const"; we recommend turning on compiler warnings when compiling code that uses messages to avoid getting into trouble (for g++, say -Wcast-qual; for cfront-derived compilers, say +w).

If we wished to modify the message and then send the result as an output, we would call writableCopy instead of myData, modify the object, then send it on its way as in the previous star.



Top Up Prev Next Bottom Contents Index Search

1 The reason for this " aggressive reclamation" policy (both here and in other places) is to minimize the number of no-longer-needed messages in the system and to prevent unnecessary clones from being generated by writableCopy() by eliminating references to Message objects as soon as possible.

Copyright © 1990-1997, University of California. All rights reserved.