The third kernel facility that supports messaging is the port mechanism.
Ports provide a system-wide buffered messaging system. These messages
can be of arbitrary size. Anyone with a port_id can read from or write
to the port. When you create a port, you specify the number of messages
that the port can hold. If the port fills, the next call to write_port
will block. At first glance this might seem undesirable, as code cannot
assume that write_port won't block. We chose this design to prevent
an orphaned port (a port that never gets read) from consuming all
of system memory.
Those buffered messages have to be stored somewhere -- the system
wouldn't be too happy buffering 1,493,467 1 K messages or around 1,500
MB of data. The BMessage class, along with BMessenger and BLooper,
make use of ports to pass messages between teams. When using ports,
take care to synchronize the writing and reading of messages. If the
'virtual' message spans multiple calls to write_port, you must take
steps to correctly deal with other threads that might be writing to
the same port.
Similar issues arise if multiple threads read from the same port.
That gives a basic overview of messaging, from the kernel's perspective.
The Be OS also has several higher-level messaging systems, used for
different purposes, that are implemented on top of the lower-level
services. The most visible form of messaging in the Be OS is defined
by the BLooper and BMessage classes. A BLooper is an abstraction of
the thread running a message loop. BMessages are posted or sent to
a looper and then dispatched to a message handler. (Release 1.1d7
sneak preview: The BReceiver class will be renamed BHandler.) A BMessage
is a structured collection of data.
This data includes a single 4-byte 'what' identifier. To this you
can add arbitrary data entries, each entry having both a string name
and a 4-byte type identifier. You can add multiple chunks of data
under the same
name and type. The data in a message is kept in what we call the "shared
heap," a heap shared by all applications in the system. This
heap is created using the kernel area API. When messages move between
threads in the same team -- typically using BLooper::PostMessage --
the team is simply given the pointer to the shared data. Things are
a little more complicated when using BMessenger::SendMessage to send
a message to another team. In this case, the address of the data in
the shared heap is written into a port. Once the destination reads
the address out of the port, it has complete access to the data of
the message. This design provides for fairly efficient messaging,
as the data is only copied once -- into the shared heap. From
then on it can be passed among many different threads and teams.
Volume II, Issue 48; December 2, 1998
DEVELOPERS' WORKSHOP: Back to Basics
By Stephen Beaulieu hippo@be.com
"In this article I'm going to reinvestigate some of the basic
building blocks of the BeOS. We'll look at what I'll call the AppKit
model: BMessages, BLoopers, BHandlers, and BMessengers.
The BMessage is fundamentally a data container, commonly used to hold
both instructions and data to be acted upon. BHandlers are objects
that perform an action when BMessages are delivered to them; they
handle the incoming message. BLoopers are threaded BHandlers that
run a message loop, waiting for incoming BMessages and dispatching
them to the appropriate BHandlers. BMessengers are system wide tokens
that represent a given BLooper-BHandler pair, delivering messages
to (and replies from) the specified BHandler.
The most visible examples of BLoopers and BHandlers are BApplications,
BWindows, and BViews. These lie at the heart of the BeOS APIs, and
are common to most BeOS applications. Many developers, however, seem
to use the AppKit model only in their interface areas, where it is
pretty much required. Since the AppKit model has other valid uses,
I'm going to offer some design ideas that may persuade developers
to take advantage of its versatility. First though, a list of the
AppKit model's advantages and disadvantages to keep in mind while
reviewing my designs.
Advantages:
Uses a common, familiar system where a great deal of organizational
work is handled by the BeOS. This includes a well-defined communication
system, automatic threading of your app, and built-in object management
through the BLooper AddHandler and RemoveHandler functions.
The public class interface is easily extendable by extending the
messaging protocol used. The functions to handle the new messages
are usually private functions, and can be extended as necessary.
The interface can be exposed to interapplication systems by publishing
the messaging protocol. This allows other apps doing complementary
work to interact with your application easily. The system interface
is eligible for scripting, since the AppKit model is the basis of
the BeOS scripting mechanism.
Disadvantages:
A BLooper thread's main responsibility of is to run the message
loop, not some other tasks. To have threads work on other tasks requires
using Kernel Kit threads. The BLooper threading model is therefore
not always appropriate to the task at hand. However, combining the
two models can work well (i.e., a BLooper with extra, special-purpose
threads for other tasks).
BHandlers can belong to only one BLooper, effectively serializing
access to each handler. This can be problematic in a system where
the BHandler would (ideally) be accessible by multiple threads. Some
designs can work around the limitation by creating a new BHandler
subclass for each BLooper, but this works only when the BHandlers
themselves do not encapsulate data that needs to be instantiated only
once.
Adding information to BMessages requires copying that data. This can
introduce significant overhead if large amounts of information need
to be transmitted, or if the data needs to be looked at many times
over the course of an operation. Introducing other methods of data
sharing (like putting a
reference to a shared memory area or a pointer to data into the BMessage)
can reduce the size and complexity of the messages. Note that this
might lead to some undesirable consequences, as the recipient of the
BMessage no longer has to go through the messaging mechanism to access
the data.
Keeping these advantages and disadvantages in mind, here are two design
schemes that use the AppKit model: the Handler as Data Object and
Handler as Operation. Handler as Data Object In this scheme, the BHandler
contains both the data to be acted upon and the knowledge of how modifications
are to be performed. The data is encapsulated in a self-modifying
object.
BMessages serve as instructions for what actions to perform. The
BLooper serves as the initial interface to the various objects, but
would usually pass back BMessengers for the appropriate data objects,
so the outside processes can deal with them directly.
Example: Transaction Server
Here, the data object can be independently acted upon by
multiple threads/applications while in a guaranteed consistent state.
BMessages instruct the server to create, delete, or modify objects.
Change notifications can be sent back to all interested processes.
Furthermore, the transactions can be recorded so that a previous state
could be reinstated by rewinding the transaction stack.
Handler as Operation In this scheme, each BHandler represents an operation
that can be performed on some data. The BMessage carries the data
to modify and instructions about which operations to carry out. The
BLooper serves as a common interface to the operations, investigating
the instructions and passing the data to the operations in the correct
order, then sending the modified data back to its origin.
Example: Data Filters
Use BHandlers to represent add-on filters to manipulate data. Have
each add-on create an entry function that returns a BHandler that
performs the appropriate filter. Then simply pass the data to each
filter as appropriate. This could be parallelized by calling the entry
function for a new BHandler for each thread that needs a copy of the
filter, at the expense of more memory.
Example: State Machine
A BLooper represents a state machine, with BHandlers representing
each state. The looper passes the appropriate instructions off to
each state, which responds to them and asks the BLooper to change
state when appropriate. In this example, the BLoopers might contain
the data to act upon, rather than a BMessage, but the view from the
BHandler is the same.
You can find some simple example code for these two schemes at:
<ftp://ftp.be.com/pub/samples/application_kit/AppKitModel.zip>
Both schemes perform the same work, transforming strings to uppercase,
lowercase, or mixed case (with every word capitalized.) They implement
the code to modify the strings the same way, using the BString class
functions: ToUpper, ToLower, and CapitalizeEachWord. They also act
on the same two strings. The only difference between the two examples
is the schemes used to organize the code (and correspondingly, the
printed output).
This structure is obviously overkill for simple string modification,
but the schemes become more useful as the complexity of the data and
operations increases. Strings are just an easy way to demonstrate
the various designs. The Notes file in each folder explains how the
project is put together. Also, note that both applications are useful
only when run from the command line, as all feedback is through printf()
calls.
I hope these designs can be helpful in your programs, or at least
will start you thinking more about the overall design of your application.
"