The Application Kit provides a high-level messaging service built on top of the kernel's system of ports, threads, and semaphores. The service lets you:
The following sections examine the messaging system components.
BMessage objects are parcels of information that can be transferred between threads. The message source constructs a BMessage object, adds whatever information it wants to it, and then passes the parcel to a function that delivers it to a destination.
A BMessage can hold structured data of any type or amount. When you add data to a message, you assign it a name and a type code. If more than one piece of information is added under the same name, the BMessage sets up an array of data for that name. The name (along with an optional index into the array) is then used to retrieve the data.
For example, this code adds a floating-point number to a BMessage under the name "pi",
BMessage *msg = new BMessage; float pi = 3.1416; msg->AddData("pi", B_FLOAT_TYPE, &pi, sizeof(float));
and this code locates it:
float *pi; ssize_t numBytes; msg->FindData("pi", B_FLOAT_TYPE, &pi, &numBytes);
A number of specialized functions simplify the code for specific data types. Adding and finding pi would most typically look like this:
BMessage *msg = new BMessage; msg->AddFloat("pi", 3.1416); float pi; msg->FindFloat("pi", &pi);
In addition to named data, a BMessage object also has a public data member, called what, that says what the message is about. The constant may:
Not all messages have data fields, but all should have a command constant. A constant like RECEIPT_ACKNOWLEDGED or CANCEL may be enough to convey a complete message.
The BeOS defines command constants for a number of standard messages--such as B_REPLY, B_KEY_DOWN, and B_RESET_STATUS_BAR. They're discussed where the topics they relate to come up in this manual and are summarized in the Message Protocols appendix.
By convention, the constant alone is sufficient to identify a message. It's assumed that all messages with the same constant are used for the same purpose and contain the same kinds of data.
Data added to a BMessage is associated with a name and stored with two relevant pieces of information:
The Support Kit defines a number of codes for common data types, including these:
Type | Meaning |
---|---|
B_CHAR_TYPE | A single character |
B_INT32_TYPE | A 32-bit integer (int32, uint32, vint32, or vuint32) |
B_FLOAT_TYPE | A float |
B_SIZE_T_TYPE | A size_t value |
B_MESSENGER_TYPE | A BMessenger object |
B_RECT_TYPE | A BRect object |
B_RGB_COLOR_TYPE | An rgb_color structure |
B_STRING_TYPE | A null-terminated character string |
B_COLOR_8_BIT_TYPE | Raw bitmap data in the B_COLOR_8_BIT color space |
In addition, B_MIME_TYPE indicates that the name of the data item is a MIME-like string that conveys the true type of the data. The full list of types can be found under "Type Codes" in The Support Kit.
You can add data to a message even if its type isn't on the list. A BMessage will accept any kind of data; you must simply invent your own codes for unlisted types.
Both the source and the destination of a message must agree upon its format--the what constant and the names and types of data fields. They must also agree on details of the exchange--when the message can be sent, whether it requires a response, what the format of the reply should be, what it means if an expected data item is omitted, and so on.
None of this is a problem for messages that are used only within an application; the application developer can keep track of the details. However, protocols must be published for messages that communicate between applications. You're urged to publish the specifications for all messages your application is willing to accept from outside sources and for all those that it can package for delivery to other applications. The more that message protocols are shared, the easier it is for applications to cooperate with each other and take advantage of each other's special features.
The software kits define protocols for a number of messages. They're discussed in the Message Protocols appendix.
It's important that the message constants and type codes you define not be confused with those already defined by the BeOS, or those that the BeOS might define in the future. For this reason, we've adopted a strict convention for assigning values to all Be-defined constants. The value assigned will always be formed by combining four characters into a multicharacter constant, with the characters limited to uppercase letters and the underbar. For example, B_KEY_DOWN and B_VALUE_CHANGED are defined as follows:
enum { . . . B_KEY_DOWN = '_KYD', B_VALUE_CHANGED = '_VCH', . . . };
And B_DOUBLE_TYPE and B_POINTER_TYPE are defined as follows:
enum { . . . B_DOUBLE_TYPE = 'DBLE', B_POINTER_TYPE = 'PNTR', . . . };
Use a different convention to define your own constants (or you'll risk having your message misinterpreted). Include some lowercase letters, numerals, or symbols (other than the underbar) in your multicharacter constants, or assign numeric values that can't be confused with the value of four concatenated characters. For example, you might safely define constants like these:
#define PRIVATE_TYPE 0x1f3d #define OWN_TYPE 'Rcrd'
Typically, when an application creates an object, it retains responsibility for it; it's up to the application to free the objects it allocates when they're no longer needed. BMessage objects are no exception to this rule. Passing a message to the messaging mechanism doesn't relieve you of responsibility for it. The system makes a copy so you can delete the object immediately if you have no further use for it.
Similarly, when the system delivers a BMessage to you, it retains ownership of the object and will eventually delete it--after you're finished responding to it. A message receiver can assert responsibility for a message--essentially replacing the system as its owner--by detaching it from the message loop (with BLooper's DetachCurrentMessage() function).
In the Be model, messages are delivered to threads running message loops. Arriving messages are first placed in a queue. They're then taken from the queue one at a time and dispatched to an object that can respond. When the response is finished, the thread deletes the message and takes the next one from the queue--or, if the queue is empty, waits until another message arrives.
The message loop therefore dominates the thread. The thread does nothing but get messages and respond to them; it's driven by message input.
BLooper objects set up these message loops. A BLooper spawns a thread and sets the loop in motion. Posting a message to the BLooper delivers it to the thread (places it in the queue). The BLooper removes messages from the queue and dispatches them to BHandler objects. BHandlers are the objects ultimately responsible for received messages. Everything that the thread does begins with a BHandler's response to a message.
Two hook functions come into play in this process--one defined in the BLooper class and one declared by BHandler:
void MyHandler::MessageReceived(BMessage *message) { switch ( message->what ) { case COME_HERE: . . . break; case GO_THERE: . . . break; default: baseClass::MessageReceived(message); break; } }
There's a close relationship between the BLooper role of running a message loop and the BHandler role of responding to messages. The BLooper class inherits from BHandler, so the same object can fill both roles. The BLooper is the default handler for the messages it receives.
While a thread is responding to a message, it keeps the BLooper that dispatched the message locked. The thread locks the BLooper before calling DispatchMessage() and unlocks it after DispatchMessage() returns.
To be notified of an arriving message, a BHandler must "belong" to the BLooper; it must have been added to the BLooper's list of eligible handlers. The list can contain any number of objects, but at any given time a BHandler can belong to only one BLooper.
Handlers that belong to the same BLooper can be chained in a linked list. If an object can't respond to a message, the system passes the message to its next handler.
BLooper's AddHandler() function sets up the looper-handler association; BHandler's SetNextHandler() sets the handler-handler link.
You can arrange to have a filtering function examine an incoming message before the BLooper dispatches it--before DispatchMessage() and the target BHandler's hook function are called. The filter can set up conditions for handling the message, change the target handler, or even prevent the message from being dispatched and respond to it directly.
The filtering function is associated with a BMessageFilter object, which holds the criteria for when the filter should apply. If a BMessageFilter is attached to a BHandler (AddFilter()), it filters only messages destined for that BHandler. If it's attached as a common filter to a BLooper object (AddCommonFilter()), it can filter any message that the BLooper dispatches, no matter what the handler. (In addition to the list of common filters, a BLooper can, like other BHandlers, maintain a list of filters specific to its role as a target handler.)
Special dispatching is provided for a subset of messages defined by the system. These system messages are dispatched not by calling MessageReceived(), but by calling a BHandler hook function specific to the message.
System messages generally originate from within the Be operating system (from servers, the kits, or the Tracker). They notify applications of external events, usually something the user has done--moved the mouse, pressed a key, resized a window, selected a document to open, or some other action of a similar sort. The command constant of the message names the event--for example, B_KEY_DOWN, B_SCREEN_CHANGED, or B_PATHS_RECEIVED--and the message may carry data describing the event. The receiver is free to respond to the message (or not) in any way that's appropriate.
A few system messages name an action the receiver is expected to take, such as B_ZOOM or B_MINIMIZE. The message tells the receiver what must be done. Even these messages are prompted by an event of some kind--such as the user clicking the zoom button in a window tab.
System messages have a fixed format. The names and types of data fields are always the same for each kind of message. For example, the system message that reports a user keystroke on the keyboard--a "key-down" event--has B_KEY_DOWN as the command constant, a "when" field for the time of the event, a "key" field for the key that was hit, a "modifiers" field for the modifier keys that were down at the time, and so on.
Although the set of system-defined messages is small, they're the most frequent messages for most applications. For example, when the user types a sentence, the application receives a series of B_KEY_DOWN messages, one for each keystroke.
System messages aren't delivered to just any BLooper object. The software kits derive specialized classes from BLooper to give significant entities in the application their own threads and message loops. These are the objects that receive system messages and define how they're dispatched. Each message is matched to the specific kind of BLooper that's concerned with the particular event it reports or the particular instruction it delivers.
More specifically, both the BApplication class in this kit and the BWindow class in the Interface Kit derive from BLooper:
Each of these classes is concerned with only a subset of system messages--BApplication with application messages (discussed Application Messages ) and BWindow objects with interface messages (discussed in The Interface Kit). In addition, the generic BLooper class defines a B_QUIT_REQUESTED message that's common to both groups. Each class arranges for special handling of the system messages it's concerned with.
Every system message is dispatched by calling a specific virtual "hook" function, one that's matched to the message. For example, when the Application Server sends a B_KEY_DOWN message to the window where the user is typing, the BWindow determines which object is responsible for displaying typed characters and calls that object's KeyDown() virtual function. Similarly, a message that reports a user request for basic information about the application is dispatched by calling the BApplication object's AboutRequested() function. Messages that report the movement of the cursor are dispatched by calling MouseMoved(), those that report a change in the screen configuration by calling ScreenChanged(), and so on.
These hook functions are declared in classes derived from BHandler and are often recognizable by their names. In the introductory chapter, it was explained that hook functions fall into three groups:
The hook functions that are matched to system messages can fall into any of these three categories. Since most system messages report events, they mostly fall into the first two categories. The function is named for the message, and the message for the event it reports.
However, if a system message delivers an instruction for the application to do something in particular, its hook function falls into the third group. The function is fully implemented in system software, but can be overridden by the application. The function is named for what it does, and the message is named for the function.
A BLooper picks a BHandler for a system message based on what the message is. For example, a BWindow calls upon the object that displays the current selection to handle a B_KEY_DOWN message. It asks the object in charge of the area where the user clicked to handle a B_MOUSE_DOWN message. And it handles messages that affect the window as a whole--such as B_WINDOW_RESIZED--itself.
The BLooper identifies system messages by their command constants alone (their what data members). If a message is received and its command constant matches the constant for a system message, the BLooper will dispatch it by calling the message-specific hook function--regardless what data field the message may have.
If the constant doesn't match a system message that the BLooper knows about, the message is dispatched by calling MessageReceived(). MessageReceived() is, therefore, reserved for application-defined messages. It's typically implemented to distribute the responsibility for received messages to other functions. That's something that's already taken care of for system messages, since each of them is mapped to its own function.
Although the system creates and delivers most messages, an application can create messages of its own and have them delivered to a chosen destination. There are three ways to initiate a message:
Messages are posted by calling a BLooper's PostMessage() function. For example:
BMessage message(FORGET_ABOUT_IT); myLooper->PostMessage(&message, targetHandler);
This hands the message to the BLooper so that it can be dispatched in sequence along with other messages the thread receives. Posting depends on the message source knowing the address of the destination BLooper; it therefore works only for application-internal messages.
As the example above shows, it's possible to name a target BHandler when posting a message. The only requirement is that the BHandler must belong to the BLooper; it must have been added to the BLooper's list of eligible handlers.
The target is respected when the message is dispatched; the dispatcher always calls a hook function belonging to the designated BHandler. If the message matches one that the system defines and the target BHandler is the kind of object that's expected to handle that type of message, the dispatcher will call the target's message-specific hook function. However, if the designated target isn't the handler of design for the message, the BLooper will call its MessageReceived() function.
For example, if a B_KEY_DOWN message is posted to a BWindow object and a BView is named as the target, the BWindow will dispatch the message by calling the BView's KeyDown() function. However, if the BWindow itself is named as the target, it will dispatch the message by calling its own MessageReceived() function. BView objects are expected to handle keyboard messages; BWindows are not.
A BLooper can also keep track of a preferred handler for the messages it receives. When the target handler is a NULL pointer,
BMessage message(FORGET_ABOUT_IT); myLooper->PostMessage(&message, NULL);
the BLooper resorts to its preferred handler.
This feature allows messages to be targeted dynamically. The BLooper can change the object it considers its preferred handler to fit the exigencies of the moment. For example, a BWindow makes sure that whatever object is the current focus of the user's actions is its preferred handler. Thus, PostMessage() calls that don't name another handler will always affect the current focus.
Messages can be posted only within an application--where the thread that calls PostMessage() and the thread that responds to the message are in the same address space (are part of the same "team") and may even be the same thread.
To send a message to another address space, it's necessary to first set up a BMessenger object as a local representative of the remote destination. BMessengers can be constructed in two ways:
The first method constructs a BMessenger that can send messages to the main thread of the remote application, where they'll be received and handled by its BApplication object.
The second method constructs a BMessenger that's targeted to a BLooper and BHandler (including, possibly, the BLooper's preferred handler) in your own application. However, you can place the BMessenger in a message and send it to a remote application. That application can then employ the BMessenger to target messages to your objects.
Thus, a BMessenger can be seen as a local identifier for a remote BLooper/BHandler pair. Calling the object's SendMessage() function delivers the message to the remote destination. BMessengers can also send messages to local destinations. However, it's generally simpler to post a local message than to send it--although the results are the same and posting a message isn't any more efficient than sending it.
Through a service of the Interface Kit, users can drag messages from a source location and drop them on a chosen destination, perhaps in another application. The source application puts the message together and hands it over to the Application Server, which tracks where the user drags it. The drag-and-drop session is initiated by BView's DragMessage() function. For example:
BMessage message(COLOR); message.AddData("color", B_RGB_COLOR_TYPE, &theColor, sizeof(rgb_color)); myView->DragMessage(&message, aBitmap, offsetsIntoTheBitmap);
When the user drops the message inside a window somewhere, the server delivers it to the BWindow object and targets it to the BView (a kind of BHandler) that's in charge of the portion of the window where the message was dropped. The message is placed in the BWindow's queue and is dispatched like all other messages. In contrast to messages that are posted or sent in application code, only the user determines the destination of a dragged message.
A message receiver can discover whether and where a message was dropped by calling the BMessage object's WasDropped() and DropPoint() functions.
See "Responding to the User" in The Interface Kit chapter for details on how to initiate a drag-and-drop session.
A delivered BMessage carries a return address with it. The message receiver can reply to the message by calling the BMessage's SendReply() function. Replies can be synchronous or asynchronous.
A message sender can ask for a synchronous reply when calling the sending function. For example:
BMessage reply; myMessenger.SendMessage(&message, &reply); if ( reply.what != B_NO_REPLY ) { . . . }
In this case, SendMessage() waits for the reply; it doesn't return until one is received. (In case the message receiver refuses to cooperate, a default reply is sent when the original message is deleted.)
A message receiver can discover whether the sender is waiting for a synchronous reply by calling the BMessage's IsSourceWaiting() function.
A message sender can provide for an asynchronous reply by designating a BHandler object for the return message. For example:
myMessenger.SendMessage(&message, replyHandler);
In this case, the sending function doesn't wait for the reply; the reply message will be directed to the named BHandler. An asynchronous reply is always possible. If a BHandler isn't designated for it, the BApplication object will act as the default handler.
You can also name a target BHandler for an asynchronous reply to posted and dragged messages:
myLooper->PostMessage(&message, NULL, replyHandler); myView->DragMessage(&message, aBitmap, offsets, replyHander);
BMessage's SendReply() function has the same syntax as SendMessage(), so it's possible to ask for a synchronous reply to a message that is itself a reply,
BMessage message(READY); BMessage reply; theMessage->SendReply(&message, &reply); if ( reply->what != B_NO_REPLY ) { . . . }
or to designate a BHandler for an asynchronous reply to the reply:
theMessage->SendReply(&message, someHandler);
In this way, two applications can maintain an ongoing exchange of messages.
You're encouraged to publish the data types that your application defines and can accept in a BMessage, along with their assigned type codes.
Contact Be (devsupport@be.com) to register any types you intend to publish, so that you can be sure to choose a code that hasn't already been adopted by another developer.
If your application can respond to certain kinds of remote messages, you should publish the message protocol--the constant that should initialize the what data member of the BMessage, the names of expected data fields, the types of data they contain, the number of data items allowed in each field, and so on. If your application sends replies to these messages, you should publish the reply protocols as well.
The Be Book, in lovely HTML, for BeOS Release 4.
Copyright © 1998 Be, Inc. All rights reserved.
Last modified December 23, 1998.