Issue 4-29, July 21, 1999

Be Engineering Insights: BeOS Kernel Programming Part V: Interfacing With ISA and PCI

By Dmitriy Budko

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:

  1. 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.

  2. 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.

  3. 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) (int mapped_io_addr);
    void            (*write io 8) (int mapped_io_addr,
                uint8 value);
    uint16          (*read_io_16) (int mapped_io_addr);
    void            (*write io 16) (int mapped_io_addr,
uint16 value);
    uint32          (*read io 32) (int mapped_io_addr);
    void            (*write io 32) (int mapped_io_addr,
uint32 value);


    void *          (*ram address) (const void *
physical_address_in_system_memory);
 ........................
};

struct pci_module_info {
    bus_manager_info    binfo;

    uint8 (*read_io_8) (int mapped_io_addr);
    void (*write_io_8) (int mapped_io_addr, uint8 value);
    uint16 (*read_io_16) (int mapped_io_addr);
    void (*write_io_16) (int mapped_io_addr, uint16 value);
    uint32 (*read_io_32) (int mapped_io_addr);
    void (*write_io_32) (int mapped_io_addr, uint32 value);

    long (*get_nth_pci_info) (
        long index, /* index into pci device table */
        pci_info *info  /*caller-supplied buffer for info*/
        );

    uint32 (*read_pci_config) (
        uchar   bus,    /* bus number */
        uchar   device, /* device # on bus*/
        uchar   function,   /* function # in device */
        uchar   offset, /* offset in configuration space */
        uchar   size    /* # bytes to read (1, 2 or 4) */
        );

    void (*write_pci_config) (
        uchar   bus,        /* bus number */
        uchar   device,     /* device # on bus */
        uchar   function,   /* function # in device */
        uchar   offset, /* offset in configuration space */
        uchar   size, /* # bytes to write (1, 2 or 4) */
        uint32  value       /* 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.

  1. 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:

    bool uhci_present(void)
    {
       pci_info info;
       int   i;
    
       for (i = 0; ; i++)
       {
           if (pcim->get_nth_pci_info (i, &info) != B_OK)
              return FALSE; /* 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;
        }
        return TRUE;            /* Device was found */
    }
    
  2. 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)
            return B_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");
        return B_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;
    
  3. 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);
    
  4. 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));
    
  5. 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:

    uint16 frame_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;
    
  6. 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_t foo_write(void *cookie, off_t position,
        const void *data, size_t *numBytes)
    {
        int i;
        physical_entry   sg_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();
        return check_status(numBytes);
    }
    

Be Engineering Insights: Farewell BSound and BSoundFile (All Hail BGameSound and BMediaFile)

By Jon Watte

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 BSounds 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>.


Developers' Workshop: The Magic of Messages Part 1: The Sending

By Owen Smith

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 BMessages here. We'll just look at pure, simple messaging—BeOS-style.

The Whole Enchilada

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.

Step 1: Create

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 BMessages 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 = new BMessage(B_QUIT_REQUESTED);
BMenuItem* item = new BMenuItem("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 way
BMessage* msg = ...
const char* msgstr = msg->FindString("my string");
delete msg;
// msg has been deleted, so msgstr is now invalid!

// the right way
BMessage* 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);
delete msg;

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.

Step 2: Target

Handlers and Loopers

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 BLoopers. Applications and windows are both examples of BLoopers.

A BLooper is an object whose job is to receive messages and dispatch them as they arrive. This behavior makes BLoopers the "Grand Central Stations" of the messaging world.

BLoopers maintain a list of targets (i.e., BHandlers). 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 BHandlers 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.)

Messengers

The fundamental delivery mechanism of the BeOS messaging system is BMessenger. BMessengers are lightweight objects that identify a message target in your system. Let's examine how to use BMessengers 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:

    1. 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 BWindows have a special interpretation for the preferred handler. In a BWindow, the preferred handler is the view that currently has the focus.

    2. 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.

    3. 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:

    1. Create a messenger to the application, either by team ID or signature.

    2. Using this messenger, send a message that requests a new messenger for the target that you desire.

    3. 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.

Step 3: Send

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:

    1. 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.

    2. 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():

    1. To specify a particular handler: window->PostMessage(msg, view);

    2. To specify the looper's preferred handler: window->PostMessage(msg, NULL);

    3. 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.

Pay No Attention to the Man Behind the Curtain

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:

  1. 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.

  2. 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...


Be Quiet...

By Jean-Louis Gassée

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.

Creative Commons License
Legal Notice
This work is licensed under a Creative Commons Attribution-Non commercial-No Derivative Works 3.0 License.