The goal of this article is to describe how BeOS device drivers communicate with devices on the most common busses—ISA and PCI.
There are many types of hardware busses in the current PCs and Macs: ISA, ADB, SCSI, PCI USB, AGP, VLB, IEEE 1394, I2C, etc., but the majority of devices are located on ISA and PCI busses, so this article will focus on them. The question I'll answer is how a BeOS device driver can access a hardware device on ISA and PCI busses. But before going into the BeOS details I'll briefly describe the software-visible characteristics of ISA and PCI.
ISA appeared on the first IBM PC (8088), and was slightly refined in the later IMB PC AT (80286). Since then there have been no changes and ISA has become old and difficult to use. Fortunately, it's begun to be replaced. The latest Intel chipset i810 doesn't directly support ISA.
There are two address spaces on ISA: one is a 64kB I/O space and another is a 16 MB memory space. An ISA device may occupy portions of both address spaces and respond to I/O and memory read/write cycles initiated by another device on the bus, usually a CPU. In this case the device is passive and the CPU has to read/write data from/to the device to/from system RAM.
An ISA device can be more active and can transfer data without constant attention from a CPU by using an 8237-style DMA controller (as the majority of ISA sound cards do) or by being an ISA bus master (a few SCSI and LAN adapters do this). ISA DMA is obsolete and convoluted; this article does not discuss it.
The PCI bus is a modern, sane replacement for ISA and has multiple advantages over it. A system can have multiple PCI busses, PCI has *good* PnP, it's faster (132 MB/s theoretical bandwidth, 100 MB/s throughput with the real hardware: Matrox Millennium II and Intel 440LX chipset), etc.
In general PCI is a 32-bit bus. There are 64-bit data transfer and 64-bit addressing versions of PCI but they are currently used only in high-end servers.
PCI (32-bit version) has three address spaces:
A 4 GB I/O space (all real devices use only the first 64 kB because x86 CPUs can *directly* address only 64 kB of I/O address space, PPC doesn't have *special* I/O instructions.) Normally, devices use this space for I/O control and status registers. The system has to be very conservative in reordering/combining/caching all accesses to this space.
4 GB of memory space. Usually devices use this space for high-performance I/O registers, data FIFOs, access to internal RAM (like frame buffers), etc. Depending on the purpose these registers may be cacheable or uncacheable.
PCI specifications recommend using the memory space. Some devices include both versions (I/O and memory) of the same registers for compatibility with old 16-bit software.
PCI configuration address space. This supports 256 busses; each bus can have 32 devices; each device can have eight functions; and each function can have 256 bytes of registers. The configuration address of a device is fixed by the PCI slot it's plugged into or by the motherboard if it is a fixed motherboard device.
This address space is used to provide information about devices, to configure PCI devices before they can appear in I/O or memory space, and to set up device specific options.
Many PCI devices, especially high-performance ones, can be PCI bus
masters, which lets them transfer data from/to RAM or another PCI device
without direct intervention from the CPU. How then, does a BeOS kernel
device driver interact with ISA and PCI? By way of BeOS ISA and PCI
kernel modules, defined in
ISA.h
and
PCI.h
.
The relevant portion of these modules is as follows:
typedef struct isa_module_info isa_module_info; struct_isa_module_info { .............................. uint8 (*read_io_8) (intmapped_io_addr
); void (*write io 8) (intmapped_io_addr
, uint8value
); uint16 (*read_io_16) (intmapped_io_addr
); void (*write io 16) (intmapped_io_addr
, uint16value
); uint32 (*read io 32) (intmapped_io_addr
); void (*write io 32) (intmapped_io_addr
, uint32value
); void * (*ram address) (const void *physical_address_in_system_memory
); ........................ }; struct pci_module_info { bus_manager_infobinfo
; uint8 (*read_io_8
) (intmapped_io_addr
); void (*write_io_8
) (intmapped_io_addr
, uint8value
); uint16 (*read_io_16
) (intmapped_io_addr
); void (*write_io_16
) (intmapped_io_addr
, uint16value
); uint32 (*read_io_32
) (intmapped_io_addr
); void (*write_io_32
) (intmapped_io_addr
, uint32value
); long (*get_nth_pci_info
) ( longindex
, /* index into pci device table */ pci_info *info
/*caller-supplied buffer for info*/ ); uint32 (*read_pci_config
) ( ucharbus
, /* bus number */ uchardevice
, /* device # on bus*/ ucharfunction
, /* function # in device */ ucharoffset
, /* offset in configuration space */ ucharsize
/* # bytes to read (1, 2 or 4) */ ); void (*write_pci_config
) ( ucharbus
, /* bus number */ uchardevice
, /* device # on bus */ ucharfunction
, /* function # in device */ ucharoffset
, /* offset in configuration space */ ucharsize
, /* # bytes to write (1, 2 or 4) */ uint32value
/* value to write */ ); void* (*ram_address
) ( const void *physical_address_in_system_memory
); };
The general API of BeOS modules has already been described in a previous newsletter article, "Be Engineering Insights: BeOS Kernel Programming Part IV: Bus Managers," by Brian Swetland
So I'll focus on ISA and PCI specifics.
First, how do find your device? Usually this is done in
init_hardware()
and/or init_driver()
hooks. For ISA, however, there is no easy
way to find or detect a device, so an ISA driver has to:
Just assume that its hardware is here, or
Try to detect the device by poking into the appropriate places, or
Use the Configuration Manager, which is a theme for another article.
Finding a PCI device is easy. Use get_nth_pci_info()
to iterate through
the list of all PCI devices in the system to find your device. For
example, the following code shows how to find a PCI USB UHCI controller
and check what specific version it is:
booluhci_present
(void) { pci_infoinfo
; inti
; for (i
= 0; ;i
++) { if (pcim
->get_nth_pci_info
(i
, &info
) !=B_OK
) returnFALSE
; /* Error or end of device list */ /* do not support PIIX3 - too many HW bugs */ if (info
.vendor_id
== 0x8086 &&info
.device_id
== 0x7020) continue; if (info
.class_base
==PCI_serial_bus
&&info
.class_sub
==PCI_usb
&&info
.class_api
==PCI_usb_uhci
) break; } returnTRUE
; /* Device was found */ }
Next, how do you find the resources the device uses (I/O and/or memory
addresses, IRQs, etc). This is done in init_driver()
hook. For ISA you
have to use the same methods as you would for finding the device. For PCI
use the pci info structure that you use to find the device. Remember that
init_hardware()
is called only once and the driver can be unloaded
afterwards, so the driver can't easily remember the information from
init_hardware()
. For example:
/* find PCI bus, device, function, IO, IRQ */ for (i
= 0; ;i
++) { if (pcim
->get_nth_pci_info
(i
, &info
) !=B_OK
) returnB_ERROR
; /* Error or end of device list*/ if (info
.class_base
==PCI_serial_bus
&&info
.class_sub
==PCI_usb
&&info
.class_api
==PCI_usb_uhci
) break; } /* Handle broken devices that violate PCI_spec and don't use base register 0. */ for(base_reg_num
=0; (base_reg_num
< 6) && (info
.u
.h0
.base_registers
[base_reg_num
] == 0);base_reg_num
++) ; /* refuse to find the controller and don't load the driver if the controller is disabled in BIOS. */ if( (base_reg_num
== 6) || (info
.u
.h0
.interrupt_line
== 0) || (info
.u
.h0
.interrupt_line
== 0xFF) ) {dprintf
("USB HC is disabled by BIOS\n"); returnB_ERROR
; } /* remember the resources */access_range
.range_start
=info
.u
.h0
.base_registers
[base_reg_num
];access_range
.range_length
=info
.u
.h0
.base_register_sizes
[base_reg_num
];access_range
.range_in_memory_space
= !(info
.u
.h0
.base_register_flags
[base_reg_num
] &PCI_address_space
);irq
=info
.u
.h0
.interrupt_line
;
Now you enable and map registers. To do this, set the appropriate bits in the control registers of the PCI device, including I/O access enable, memory access enable, and bus master enable. For example:
command_reg
=pcim
->read_pci_config
(bus
,device
,function
,PCI_command
, 2);command_reg
|=PCI_command_io
|PCI_command_memory
|PCI_command_master
;pcim
->write_pci_config
(bus
,device
,function
,PCI_command
, 2,command_reg
);
If the device registers are located in memory space, the device driver
has to map this memory by map_physical_memory()
with the appropriate
flags, then use the returned virtual address of the area as a pointer to
the registers. For example, (without error handling) from the generic
graphics driver, frame buffer in [0], control registers in [1] (complete
source code is on BeOS CD):
sprintf
(buffer
, "%04X %04X %02X%02X%02X regs",di
->pcii
.vendor_id
,di
->pcii
.device_id
,di
->pcii
.bus
,di
->pcii
.device
,di
->pcii
.function
);si
->regs_area
=map_physical_memory
(buffer
, (void *)di
->pcii
.u
.h0
.base_registers
[1],di
->pcii
.u
.h0
.base_register_sizes
[1],B_ANY_KERNEL_ADDRESS
, 0, /* B_READ_AREA + B_WRITE_AREA, */ /* neither read nor write, to hide it from user space apps */ (void **)&(di
->regs
));sprintf
(buffer
, "%04X %04X %02X%02X%02X framebuffer",di
->pcii
.vendor_id
,di
->pcii
.device_id
,di
->pcii
.bus
,di
->pcii
.device
,di
->pcii
.function
);si
->fb_area
=map_physical_memory
(buffer
, (void *)di
->pcii
.u
.h0
.base_registers
[0],di
->pcii
.u
.h0
.base_register_sizes
[0],B_ANY_KERNEL_BLOCK_ADDRESS
| /* BLOCK - try to use special features of the CPU like BAT or large pages */B_MTR_WC
, /* use write combining */B_READ_AREA
+B_WRITE_AREA
, &(si
->framebuffer
));
Now use read/write_io_xx()
functions to read/write 1/2/4 bytes from/to
a device register if the register is in the I/O space of ISA or PCI.
Example from the USB HC driver:
uint16frame_number
=pcim
->read_io_16
(access_range
.range_start
+ 6);
Use pointers to read/write data if the registers are located in memory space.
Writing four bytes to the beginning of the frame buffer:
*(uint32*)(si
->framebuffer
) = 0x44332211;
The purpose, arguments, and use of all the functions above should be
clear to anyone who is familiar with ISA and PCI. But what does void* ram
address( const void *physical address in system memory); do? If the
device is using bus mastering, the driver has to lock_memory()
and
get_memory_map()
for all data buffers and tell the device to use returned
physical RAM addresses. However, this is not enough. On some systems,
like the BeBox, the RAM address ! = PCI address, so the driver has to
convert the RAM address to a PCI address for each physical entry by
calling ram_address()
. Here's an example, with no error handling:
status_tfoo_write
(void *cookie
, off_tposition
, const void *data
, size_t *numBytes
) { inti
; physical_entrysg_list
[MAX_FOO_SG_ENTRIES
];lock_memory
(data
, *numBytes
,B_DMA_IO
|B_READ_DEVICE
); /* flags for cache coherency on some systems */get_memory_map
(data
, *numBytes
, &sg_list
,MAX_FOO_SG_ENTRIES
); for(i
=0;sg_list
.size
!= 0;i
++)sg_list
[i
].address
=pcim
->ram_address
(sg_list
[i
].address
);send_sg_list_to_foo
(&sg_list
);start_foo_bus_master_read
();block_until_foo_interrupt
(); returncheck_status
(numBytes
); }
Among other important improvements in BeOS Release 4.5 is the BMediaFile
,
which gives access to various kinds of media file formats, and BGameSound
(with subclasses), which allows simple but efficient playback of sound
effects and background sounds. The BSoundFile
class has been with us for
a long time, and was starting to show its age. Many older programs that
still run on PowerPC depend on idiosyncrasies of this class, so rather
than make it use the same mechanism as BMediaFile
to access data, which
would break the previous semantic of the file (we tried this), we decided
to stay compatible, and suggest that all newer applications use
BMediaFile
for all their media reading/writing needs.
However, if you have an application which uses BSoundFile
, you may need
some features that BMediaFile
and BMediaTrack
don't provide. Most
notably, BMediaTrack
reads audio frames in blocks of a predetermined
size, whereas BSoundFile
lets you read any number of frames at any time.
BMediaTrack
also may not be precise in seeking to a specified frame
location (because of compression algorithm constraints). I present here a
simple wrapper for BMediaTrack
, known as ATrackReader
. It lets you treat
a generic media file, accessed internally through a BMediaFile
object,
much like a BSoundFile
. It's also a good introduction to using
BMediaFile
/BMediaTrack
to read data in general.
If you use a BSoundPlayer
with a number of
BSound
s to play sound effects,
you'll probably want to change over to the new BGameSound
system the next
time you overhaul your code. BGameSound
is designed to allow for hardware
acceleration in a future version of BeOS (when this will happen is TBD),
and it's also designed to be really simple to use! If you used BSound
with a chunk of data in memory as your data, you now create a
BSimpleGameSound
object. If you use BSound
with a large-ish sound file on
disk for background music or other something similar, you now create a
BFileGameSound
.
BSimpleGameSound
can be created either with a pointer to data and a
description of the data pointed to (it should be uncompressed PCM sample
data), or with an entry ref, in which case it will load the sound file
from disk (uncompressing, if necessary) into memory so it's always
readily available to be played. The BGameSound
system makes a copy of the
data you provide it, so you can free that memory as soon as the object is
created. If you need more than one copy of the same sound running, you
can call Clone()
to get a second BSimpleGameSound
which references the
same data as the first. When you make a Clone()
, that clone references
the same internal copy with a reference count, so no extra memory is
wasted. The Be Book accidentally documents an earlier behaviour where
data was copied inside Clone()
.
To play the sound, just call StartPlaying()
on it.
BFileGameSound
is created with an entry_ref as argument, and can
optionally be set to looping or non-looping mode. When you call
StartPlaying()
, it will start playing, and keep going until you stop it
with StopPlaying()
, or, if it's not looping, until it reaches the end of
the file.
It's important to note that the first BGameSound
instance you create
determines the format of the connection between BGameSound
and the Audio
Mixer. In our sample program, we create a dummy 44 kHz stereo sound and
immediately delete it to establish the connection in a known format,
since otherwise the first file the user drags into the program will
determine the format that all files will be played back as.
All BGameSound
instances that are playing are mixed into one connection
to the Audio Mixer; this connection is currently named after your
application with no way of changing it. In some future version of the
API, we may let you create more than one connection, and name these
connections. That's what the BGameSoundDevice
argument is for in the
constructors for these classes; however, we currently only support the
default (NULL
) device, so you can leave it to the default value without
worrying about it.
If you want to set the pan position or gain (volume) of a BGameSound
, you
do that by calling SetPan()
and
SetGain()
. The "duration" parameter
(which is optional) allows you to specify that the change should take
place over some amount of time, if the sound is currently playing. Thus,
if a file was playing, and you wanted to fade it out over the course of
two seconds for a soft ending, you could call
SetGain(0.0, 2000000LL)
.
You can also change the effective sampling rate of a BGameSound
. This
changes both the pitch and duration of the sound. The BGameSound
system
contains a built-in software resampler which uses a fast, reasonable
quality 0-th order resampler. There is no additional overhead of playing
a sound at some other sampling frequency than the one you initially
specify. However, there is no SetSamplingRate()
function; instead, you
have to use the low-level SetAttributes()
function to change the
B_GS_SAMPLING_RATE
attribute. Again, you can specify a duration during which
the change ramps in. Thus, if you're playing a sound at a 22000 Hz
sampling rate, and ramp it to 12000 Hz with a duration of 500000, it will
take approximately half a second for the full change to take effect. The
resulting sound effect is similar to a tape deck or record slowing down.
Specific details are found in the gameplay.cpp
file in the source code
that goes with this article:
<ftp://ftp.be.com/pub/samples/game_kit/gameplay.zip>.
In the next couple of installments, we'll delve into the gory details of
what happens when you send a BMessage
in the BeOS. I won't be discussing
archival or other ancillary uses of BMessage
s here. We'll just look at
pure, simple messaging—BeOS-style.
Let's start with an overview of the messaging process. To begin, let's say I have a message that I want to deliver to a particular messaging target (called a "handler" in BeOS). That messaging target lives in a process, called a "looper," somewhere in my system. The looper's job is to receive incoming messages and reroute them to the appropriate messaging target.
To send a message to a handler, I create an object which acts as the delivery mechanism for the message, called a messenger. I set this messenger up to point at my handler, and tell it to send my message, also specifying a place where replies to this message can go. The messenger, in turn, turns my message into a flattened stream of data and writes it to a low-level data queue called a port. Once the writing is done, the message has been delivered.
On the destination end, the port serves as the mailbox of the looper with whom my target resides. The looper reads the data from the port and reconstructs a message from the data. It then passes the message through a series of steps that determine who the final handler of the message should be. Finally, once the handler has been determined, the looper tells the handler to handle the message. The handler does whatever is necessary to respond to the message, including the option to send back a reply to the message, or to claim ownership of the message for later processing. Once the handler is done with the message, the looper gets rid of the message (unless it's been detached), and goes back to look for any other incoming messages.
Now that you've seen what the whole enchilada looks like, let's get down to business.
The first step to sending a message, of course, is creating it. As you
probably know, a BMessage
contains a what
field that briefly identifies
the contents of the BMessage
, and a number of labeled fields that contain
the data.
Generally, when you're creating BMessage
s to send to somebody, it works
extremely well to allocate them on the stack. You retain ownership of the
message when you send it, and the message is automatically cleaned up for
you when you're done.
BMessage
myMsg
;myMsg
.what
= 'RUSH';myMsg
.AddInt32
("shrug", 2112);be_app
->PostMessage
(&myMsg
);
One exception to this is if you're creating a "model message" for some
other object to use for sending messages, such as BInvoker
-derived
classes. In these cases, you'll be handing the messages off to somebody
else, so you'll need to allocate them on the free store:
BMessage
*myMsg
= newBMessage
(B_QUIT_REQUESTED
);BMenuItem
*item
= newBMenuItem
("Quit",myMsg
, 'Q'); // item now owns myMsg, and will delete myMsg when it's // done with it
There are a host of functions that let you throw all kinds of data into a
BMessage
, including raw data if you need to. There is a similar set of
functions for retrieving stuff from a BMessage
. One stumbling point that
I regularly see has to do with ownership of the data that gets stashed in
the BMessage
. When you use the
BMessage
::Add...
functions, the data is
always copied into the message for you, so you're responsible for
cleaning up anything that you add to the message. For example, let's say
you had an array of data you wanted to stuff into a BMessage
. You can't
just add the data into a BMessage
and forget about it; you have to clean
it up afterwards:
BMessage
myMsg
('BARF'); float*buf
= new float[256]; ...myMsg
.AddData
("stuff",MY_STUFF_TYPE
, &buf
, 256*sizeof
(float)); // buf still belongs to us, so we have to clean it up! delete []buf
;
The ownership rules for retrieving data are trickier, and are a common
source of errors. Most of the time when you retrieve data, the data is
copied into whatever you pass into the BMessage
::Find....
functions, so
there are no complications. However, in the special cases of FindData()
and
FindString()
, the pointer you get back actually points to data inside the
BMessage
, and you have to copy it out yourself!
For example, let's say you're going to retrieve a string from a message. Be careful that you're doing the right thing...
// the wrong wayBMessage
*msg
= ... const char*msgstr
=msg
->FindString
("my string"); deletemsg
; // msg has been deleted, so msgstr is now invalid! // the right wayBMessage
*msg
= ... const char*msgstr
=msg
->FindString
("my string"); // copy the data out of the message, so that // it'll be valid when msg goes away! // BString does this for us . . .BString
str
(msgstr
); deletemsg
;
A BMessage
can technically store as much data as you have memory for,
though you'll see that it's probably not a good idea to stash megabytes
of data into a BMessage
for purposes of messaging.
Once we've created the BMessage
, we need to know where to send it. All
potential targets of a message derive from a class called BHandler
. Many
Application and Interface Kit classes derive from BHandler
: applications,
windows, views, controls, and so forth.
One interesting fact about the BeOS is that you cannot send messages
directly to a BHandler
. The only objects capable of actually receiving a
BMessage
are objects called BLooper
s.
Applications and windows are both examples of
BLooper
s.
A BLooper
is an object whose job is to receive messages and dispatch them
as they arrive. This behavior makes BLooper
s the "Grand Central Stations"
of the messaging world.
BLoopers maintain a list of targets (i.e.,
BHandler
s). When you send a message to a target, it
actually makes its way to the BLooper
that owns the
target. The BLooper
finds the target among its list
of BHandler
s and dispatches the message to the
target. Because of this, your target must belong to some
BLooper
; you cannot just send a message to a
BHandler
floating in free space.
(BLooper
also derives from
BHandler
, so you can also send messages to the
BLooper
itself.)
The fundamental delivery mechanism of the BeOS messaging system is
BMessenger
. BMessenger
s
are lightweight objects that identify a message
target in your system. Let's examine how to use BMessenger
s to specify
various kinds of targets:
Local Targets
The easiest case is sending a message to a target in our own application
(what I call "app-local targets"). In this case, we can create the
BMessenger
and target the recipient directly.
If you look at the BMessenger
constructor, you'll see that there are
three useful ways you can construct the messenger for delivery to
app-local targets:
To specify the "preferred handler" of a looper (i.e., the handler
that you get with BLooper
::PreferredHandler()
):
BMessenger
msgr
(NULL
,window
);
If there is no preferred handler, this will target the looper itself.
Note that BWindow
s have a special interpretation for the preferred
handler. In a BWindow
, the preferred handler is the view that
currently has the focus.
To specify a particular handler:
BMessenger
msgr
(view
,NULL
);
The looper in this case is assumed to be the handler's owner, but you
can redundantly specify the looper if you want; the BMessenger
will
perform a sanity check for you.
To specify the looper itself as the target:
BMessenger
msgr
(window
,NULL
);
Although this looks similar to (1), there's a big difference in behavior! Be sure that you recognize this distinction.
For app-local targets, there are also ways to send a message that don't
require us to create a BMessenger
; I'll talk about those a little later.
Remote Targets
Now, what if we want to send a message to a target in some other
application? In this case, the BHandler
lives in a different address
space, so we unfortunately can't create a BMessenger
to target that
BHandler directly. What we *can* do, however, is target the remote
application itself, and ask the application to create the messenger for
us. Here's what you do:
Create a messenger to the application, either by team ID or signature.
Using this messenger, send a message that requests a new messenger for the target that you desire.
Retrieve the new messenger from the reply, and use that to send a message to your target.
This technique requires you to work out a messaging protocol between yourself and the remote application for identifying the target. Scripting is a great way to do this if the application supports it (as many Be apps do). For example, using Attila Mezei's "hey" command line tool (available on BeWare), I can do the following:
hey Tracker get Window 0
This sends a message off to the Tracker application, and receives in return a messenger that targets the first window in the Tracker's window list. If you want to learn more about scripting, take a gander at:
http://www-classic.be.com/developers/developer_library/scripting.html
Extremely Remote Targets
Finally, let's entertain the possibility that we want to send a message to a target on some other machine. Interestingly, there are a few third-party developers that have created solutions for this, providing BMessenger-derived classes that allow you to specify targets on machines across a network. See BeWare (http://www.be.com/beware/) for more details.
Now that you have a BMessage
and a target
BHandler
, how do you send the
message? There are two approved ways of doing this, and one sneaky
shortcut. I'll discuss the approved ways first; the sneaky shortcut will
have to wait until next week.
BMessenger
::SendMessage()
can send a message to either app-local or
remote targets. It can either take a BMessage
or just a what
code
(which it quickly wraps a BMessage
around). It can also deliver messages
in one of two ways:
In an "asynchronous send," you specify an optional target to send the reply to. Any reply to this message will be sent to that target. If no target is specified, the reply is sent to your application object.
In a "synchronous send," you can receive the reply directly. In
this case, the BMessenger
sets up a temporary reply mechanism and
waits until the recipient sends a reply back to it before returning.
If you choose to use a synchronous send, make sure you're not sending
the message to your own looper, or a deadlock is almost sure to
result! There's also an optional "reply timeout" value if you're only
willing to wait a certain amount of time for a reply to get back to
you.
BLooper
::PostMessage()
is a method you can use to send messages to
app-local targets. It effectively does the work of creating a BMessenger
and calling SendMessage()
for you. You call it on the BLooper
that owns
your target. Here are three ways you can identify targets with
PostMessage()
:
To specify a particular handler:
window
->PostMessage
(msg
, view
);
To specify the looper's preferred handler:
window
->PostMessage
(msg
, NULL
);
There are two ways to specify the looper itself:
window
->PostMessage
(msg
);
window
->PostMessage
(msg
, window
);
As you can see from the above, there is an important, and often
confusing, distinction to make between passing a NULL
handler and passing
no handler at all!
Like SendMessage()
, PostMessage()
allows you to pass a what
code instead of
a full-fledged BMessage
. Unlike
SendMessage()
, PostMessage()
does NOT allow
you to do a synchronous send: replies go to your application object, or
to a reply handler if you've specified one.
Some of you may be wondering how messages actually travel from one application to another. I'll break the magician's creed of secrecy and tell you that no voodoo is involved. In fact, the underlying mechanism for passing messages in the BeOS is the port. If you've ever run 'listport' from a Terminal, this will probably come as little surprise to you.
For those of you who think the Kernel Kit is a package of frozen corn, a little orientation here may be in order. A port is a kernel primitive that implements a "message queue." At this low level, a "message" is little more than a buffer of raw data. There are two basic operations you can do with a port:
Write to the port. You provide a buffer of data. This data is copied into the port's queue as a brand-new message—in other words, each time you write to the port, the data is treated as a new entity. When you create the port, you tell it the maximum number of items that it can contain. If you try to write a new item when the port is full, you generally wait until items are removed before placing your item in the queue (with the option to just give up if a specified amount of time has elapsed, and the queue is still full).
Read from the port. Again, you provide a buffer of data. If there are any items in the queue, the oldest item's data is copied into your buffer, and the item is removed from the queue. If you try to read from an empty port, you generally wait until an item arrives before reading it (again, with the option to bail out if you feel that you've spent too long waiting).
One nice thing about ports is that they work extremely well in multithreaded situations. Generally, you use ports by having one thread read from the port, and many threads write data to the port. By using ports to send data between threads, you can avoid the Evil Deadlocks that direct data access can cause. Even better, ports can be accessed from any address space, so inter-application communication is a snap with them as well.
How are ports used in the messaging system? Well, each BLooper
maintains
its own port, which serves as the delivery repository for incoming
messages. The looper's thread then repeatedly reads items from this port
and handles them as it sees fit.
So, here's what happens behind the scenes when you send the message:
The message is flattened into a raw data buffer. Flattening turns
the BMessage
into a stream of raw data, which can be reconstituted
elsewhere. Information about the message's intended target is also
saved into this buffer as well.
This raw data is written to the target looper's port.
Because of this delivery mechanism, the message you hand off to
SendMessage()
is NOT the same message that the target receives! Instead, a
copy of the data is sent to the destination. Because the flattening and
copying of data takes a certain amount of time, it's definitely a good
idea to keep the size of your BMessage
contents down. If you must throw
around large amounts of data between applications, consider using shared
memory areas to store the bulk of your data instead.
Another important detail is that the looper's port has a limited size. Most of the time, the port is more than big enough for your messaging needs, but under heavy load, that port can fill up—and if you're not ready to handle this case, there's a subtle bug just waiting to bite you when you need it least!
If a looper's port is chock full of messages when you try to send a
message, you'll have to wait until the port has emptied a bit before you
can write the message data to the port. This will affect you in different
ways, depending on whether you're using PostMessage()
or SendMessage()
. If
you're using PostMessage()
, the function will immediately return with an
error (B_WOULD_BLOCK
) if it can't write the message data, and the message
won't be sent. If you're using SendMessage()
, you specify a "send timeout"
value to indicate how long you're willing to wait for the message to be
delivered. If you don't specify a timeout, the BMessenger
will patiently
wait forever for the write to succeed.
The very important corollary to this behavior is that, if you use
PostMessage()
or specify a send timeout
in SendMessage()
, there's a
possibility that your message won't be delivered because the function has
timed out. Many people don't take this detail into account in their code,
and during crunch time, they'll sometimes get bit by this subtle problem.
So, if your message absolutely has to get delivered, make sure you check
the return value from PostMessage()
and SendMessage()
, and do something
appropriate if the function times out!
That's it for this week. Next week, we'll examine what happens on the other side of the connection...
Concerned readers have written to ask why I stopped writing my weekly column. The cheeky answer is that I've been suspended by the SEC, but the truth is quite the contrary, as you'll read in a moment.
For the past four weeks, we've been involved in what is ritually called an IPO Road Show, touring the U.S. and Europe to meet with institutional investors. During that time, and for 25 days following today's IPO, we're in what is called a "quiet period." This means that we cannot make any comments that could be construed as "promoting" our public offering. The company's only allowable public statement on this matter is contained in the prospectus, a document filed under SEC supervision.
Under such conditions, given my occasional recourse to poetic license, I decided that temporary silence was the safest choice of words. Also, the Road Show process undeniably consumed a great deal of time and psychic energy.
But to return to the SEC—I come from a different culture. Not so long ago in France and other European countries, insider trading wasn't a crime. In any case, Europe's clubby, opaque business culture makes enforcement of prohibitions on such things difficult. Publicly traded companies publish their numbers months, not days after the close, and shareholders are subjects, not bosses.
Consequently, I like the climate of greater trust the SEC fosters in its watchdog and, I might add, guide dog role here in the U.S. I've read many complaints about the "Plain English" rule. From my perspective, however, purging prospectuses and other filings of "whereases" and "foregoings" can only benefit normal humans who want to trust the investment process. In our case, the SEC was extremely punctual, helpful, civil, and service oriented, right through the very last nervous moments of the process.
I'll be back soon with road stories and other anecdotes—stand by.