Issue 4-27, July 7, 1999

Be Engineering Insights: Device Drivers

By Rico Tudor

This article describes the structure of BeOS device drivers, with sample code showing the more popular kernel services. The PC serial port serves quite well as an example: ubiquitous, interrupt-capable, straightforward. And should you adopt the sample "qq" driver code as a testbed, there is no danger that your bugs will reformat the hard drive!

A driver is a kind of module, and can be loaded and unloaded from memory on demand. Unlike other types of modules, it can be accessed directly by user programs. To be reachable, a driver must "publish" a name in /dev -- this is done when the system boots. Once loaded, it has access to all hardware. Memory mapping is in effect in kernel mode, so the driver's access to memory is limited to kernel data and to user data of the current thread.

In keeping with BeOS practice, the driver operates in a multi- processing and multi-threaded environment. For example, while an interrupt is being serviced by CPU 0, background processing by CPU 1 may be underway for the same device. Attention to detail is recommended.

The first step is supplying code that initializes the driver when it is loaded. api_version is used to notify the kernel loader that we are using the latest driver API. The device struct contains private info for operating the serial port.

#include    <Drivers.h>
#include    <ISA.h>
#include    <KernelExport.h>

int32   api_version     = B_CUR_DRIVER_API_VERSION;

struct device {
    char    *name;
    uint    ioport,
            irq,
            divisor,
            nopen;
    bool    exists;
    sem_id  ocsem,
            wbsem,
            wfsem;
    uchar   wbuf[100];
    uint    wcur,
            wmax;
};

#define NDEV    ((sizeof devices) / (sizeof *devices))
static struct device devices[] = {
    { "qq1", 0x03F8, 4, 6 },
    { "qq2", 0x02F8, 3, 6 },
    { "qq3", 0x03E8, 4, 6 },
    { "qq4", 0x02E8, 3, 6 },
};

static isa_module_info  *isa;

static status_t
    qq_open( const char *, uint32, void **),
    qq_close( void *),
    qq_free( void *),
    qq_read( void *, off_t, void *, size_t *),
    qq_write( void *, off_t, const void *, size_t *),
    qq_control( void *, uint32, void *, size_t);

status_t init_driver( )
{
    status_t s = get_module( B_ISA_MODULE_NAME,
                                    (module_info **) &isa);
    if (s == B_OK) {
        struct device *d;
        for (d=devices; d<devices+NDEV; ++d) {
            d->exists = TRUE;
            d->ocsem = create_sem( 1, "qqoc");
            d->wbsem = create_sem( 1, "qqwb");
            d->wfsem = create_sem( 0, "qqwf");
        }
    }
    return (s);
}

void uninit_driver( )
{
    struct device *d;
    for (d=devices; d<devices+NDEV; ++d) {
        delete_sem( d->ocsem);
        delete_sem( d->wbsem);
        delete_sem( d->wfsem);
    }
    put_module( B_ISA_MODULE_NAME);
}

The kernel loader looks for the init_driver and uninit_driver symbols, and will call them just after loading and just before unloading, respectively. This code is guaranteed to execute single-threaded, and is a good place to prepare your synchronizing method. This driver uses three semaphores for each device. Note the matching calls to load and unload the ISA bus module: This was discussed by Ficus in last week's article.

At load time, the system will query for device names by calling the publish_devices() function. If you cannot supply the names yet, this step can be deferred using a method shown later. Finally, find_device() is called to get the general entry points into your driver. Note that the semantic meanings of the entry points are defined by their position in the device_hooks structure, eg. the open function entry point is always the first element of the structure. You can choose whatever names you want for these functions.

const char **publish_devices( )
{
    const static char *list[NDEV+1];
    uint i;
    uint j = 0;
    for (i=0; i<NDEV; ++i)
        if (devices[i].exists)
            list[j++] = devices[i].name;
    list[j] = 0;
    return (list);
}

device_hooks *find_device( const char *dev)
{
    static device_hooks dh = {
        qq_open, qq_close, qq_free,
        qq_control, qq_read, qq_write
    };
    return (&dh);
}

To use a device, the application invokes the open() system call, which ultimately results in a call on the device open function (qq_open() in the code below). The driver, having published the device names, must now map them to an internal form. This internal form is combined with the flags arg, creating a cookie which will be used in subsequent I/O. The cookie is handed back to the kernel through the v arg. A device can be open for multiple clients; if not already open, an interrupt handler is installed and the hardware is activated.

/* PC serial port (16550A UART) */
#define THR 0
#define DLL 0
#define DLH 1
#define IER 1
#define FCR 2
#define IIR 2
#define LCR 3
#define MCR 4
#define LCR_8BIT        0x03
#define LCR_DLAB        (1 << 7)
#define IER_THRE        (1 << 1)
#define MCR_DTR         (1 << 0)
#define MCR_RTS         (1 << 1)
#define MCR_IRQ_ENABLE  (1 << 3)
#define have_int( iir)  (((iir)&0x01) == 0)
#define THRE_int( iir)  (((iir)&0x0F) == 0x02)

struct client {
    uint            flags;
    struct device   *d;
};

static int32    qq_int( void *);
static void     setmode( struct device *);
void            *malloc( );

static status_t
qq_open( const char *name, uint32 flags, void **v)
{
    struct client *c;
    struct device *d = devices;
    while (strcmp( name, d->name) != 0)
        if (++d == devices+NDEV)
            return (B_ERROR);
    c = malloc( sizeof *c);
    if (c == 0)
        return (ENOMEM);
    *v = c;
    c->flags = flags;
    c->d = d;
    acquire_sem( d->ocsem);
    if (d->nopen == 0) {
        setmode( d);
        install_io_interrupt_handler( d->irq, qq_int, d, 0);
        (*isa->write_io_8)( d->ioport+FCR, 0);
        (*isa->write_io_8)( d->ioport+MCR,
                            MCR_DTR|MCR_RTS|MCR_IRQ_ENABLE);
        (*isa->write_io_8)( d->ioport+IER, 0);
    }
    ++d->nopen;
    release_sem( d->ocsem);
    return (B_OK);
}

The application concludes device use with the close system call, or by exiting. This triggers the device close function which, for the last client, entails hardware shutdown and removal of the interrupt handler. The kernel is also aware when the last close occurs, and will commence driver unloading.

Note the use of the open/close semaphore. In a multi-processor machine, the device may be opening and closing simultaneously. ocsem prevents a potential desktop firework display. (Awww!)

Bear in mind that the driver is subject to visitation by multiple threads, even if you adopt a "single open" policy. This is because the file descriptors returned by the open() system call are inherited by child processes (created by the fork() system call).

The device free() function simply frees the cookies, if you are using them.

static status_t
qq_close( void *v)
{
    struct client *c = v;
    struct device *d = c->d;
    acquire_sem( d->ocsem);
    if (--d->nopen == 0) {
        (*isa->write_io_8)( d->ioport+MCR, 0);
        (*isa->write_io_8)( d->ioport+IER, 0);
        remove_io_interrupt_handler( d->irq, qq_int, d);
    }
    release_sem( d->ocsem);
    return (B_OK);
}

static status_t
qq_free( void *v)
{
    free( v);
    return (B_OK);
}

For brevity, this "qq" device read simply returns EOF. It does demonstrate the use of the flags arg, as originally passed in qq_open(). The only flag currently is O_NONBLOCK: when clear, the driver may block the thread to wait for I/O completion. Otherwise, the driver must return immediately with a short byte count, or with the B_WOULD_BLOCK error. Formally, O_NONBLOCK applies to device read, write, open, and close.

static status_t
qq_read( void *v, off_t o, void *buf, size_t *nbyte)
{
    struct client *c = v;
    *nbyte = 0;
    return ((c->flags & O_NONBLOCK)? B_WOULD_BLOCK: B_OK);
}

Here is a table of permissible read returns:

full read:      *byte = *byte;      return (B_OK);
partial read:   *nbyte = *nbyte/2;  return (B_OK);
null read:      *nbyte = 0;         return (B_OK);
error:          *nbyte = 0;         return (B_OUCH);

When writing a device write routine, the first decision to make is: How is data to be fetched from user space? Schematically (the four lines below show possible data flow—they are NOT C++ code), the four possible flows are:

u -> kb -> hw
u -> kb -> ikb -> hw
u -> ikb -> hw          // use lock_memory
u -> hw                 // dma

u is user space, kb is a kernel buffer, ikb is a kernel buffer accessed concurrently with the interrupt handler, and hw is the hardware. DMA gets into many complications, and usually involves writing for a BeOS subsystem (e.g. SCSI CAM), so I'll ignore that class. The code below uses u->kb->hw.

Data is copied by the thread into the wbuf kernel buffer. No data-structure contention exists because the interrupt handler is quiescent (interrupts are disabled), and wbsem locks out other writers. Interrupts are enabled after the copying, and the write synchronizes on wfsem. As a side feature, signals can terminate the write.

static status_t
qq_write( void *v, off_t o, const void *buf, size_t *nbyte)
{
    cpu_status cs;
    struct client *c = v;
    struct device *d = c->d;
    uint n = 0;
    while (n < *nbyte) {
        acquire_sem_etc( d->wbsem, 1, B_CAN_INTERRUPT, 0);
        if (has_signals_pending( 0)) {
            *nbyte = n;
            return (B_INTERRUPTED);
        }
        d->wcur = 0;
        d->wmax = min( *nbyte-n, sizeof( d->wbuf));
        memcpy( d->wbuf, (uchar *)buf+n, d->wmax);
        (*isa->write_io_8)( d->ioport+IER, IER_THRE);
        acquire_sem( d->wfsem);
        n += d->wmax;
        release_sem( d->wbsem);
    }
    return (B_OK);
}

Code to implement the flow u->kb->ikb->hw yields smoother data flow, but is harder to write. You must use a semaphore, spinlock and bool to synchronize with live interrupts while accessing ikb.

For the efficiency fiend, only u->ikb->hw will do: include the above mechanisms, and lock_memory() as well. The latter ensures the user buffer is resident (not swapped to disk). Otherwise, while filling ikb, your may page-fault with a spinlock set. This invites deadlocks and reset buttons. Watch for our exciting article on BeOS Synchronization, coming soon.

The permissible write returns are the same as those for read. However, avoid the null write, since it causes some apps to loop forever.

The interrupt handler, if you install one, is called with the argument of your choice: often it's the cookie, but "qq" passes the device struct. Since "qq" only does output, you can match this code with the device write routine. Briefly, the hardware is fed data until none remains: then interrupts are disabled and wfsem is used to unblock the background.

Warning
Warning

The handler interrupts whatever thread is running, and the memory map varies correspondingly. Therefore, all accesses to user space by the handler are strictly forbidden. That is why the device write routine is committed to the user/kernel data shuffle.

Since multiple devices may share the same IRQ, return B_UNHANDLED_INTERRUPT if your hardware was not the interrupt cause. Otherwise, return B_HANDLED_INTERRUPT, or B_INVOKE_SCHEDULER for immediate scheduling.

static int32
qq_int( void *v)
{
    struct device *d = v;
    uint h = B_UNHANDLED_INTERRUPT;
    while (TRUE) {
        uint iir = (*isa->read_io_8)( d->ioport+IIR);
        if (have_int( iir) == 0)
            return (h);
        h = B_HANDLED_INTERRUPT;
        if (THRE_int( iir)) {
            if (d->wcur < d->wmax)
                (*isa->write_io_8)( d->ioport+THR,
                                        d->wbuf[d->wcur++]);
            else {
                (*isa->write_io_8)( d->ioport+IER, 0);
                release_sem_etc( d->wfsem, 1,
                                    B_DO_NOT_RESCHEDULE);
            }
        }
        else
            debugger( "we never make mistakes");
    }
}

Next, we have the catch-all function, device control. Unlike the other entrypoints, you need to range check the user addresses. This is achieved with is_valid_range(). The com, buf and len args are yours to interpret, although BeOS defines some standard cases. One pair of commands, B_SET_NONBLOCKING_IO and B_SET_BLOCKING_IO, allows changing our friend O_NONBLOCK. Oddly, the relevant system call is fcntl().

For other commands, use the ioctl() system call to reach this routine. 'getd' and 'setd' allow the baud rate divisor (19200 baud by default) to be controlled by an app: set it to 1 to go really fast.

#define PROT_URD    0x00000004  /* user read */
#define PROT_UWR    0x00000008  /* user write */

static status_t
qq_control( void *v, uint32 com, void *buf, size_t len)
{
    struct client *c = v;
    struct device *d = c->d;
    switch (com) {
    case 'getd':
        if (is_valid_range( buf, sizeof( int), PROT_UWR)) {
            *(int *)buf = d->divisor;
            return (B_OK);
        }
        return (B_BAD_ADDRESS);
    case 'setd':
        if (is_valid_range( buf, sizeof( int), PROT_URD)) {
            d->divisor = *(int *)buf;
            setmode( d);
            return (B_OK);
        }
        return (B_BAD_ADDRESS);
    case B_SET_NONBLOCKING_IO:
        c->flags |= O_NONBLOCK;
        return (B_OK);
    case B_SET_BLOCKING_IO:
        c->flags &= ~ O_NONBLOCK;
        return (B_OK);
    }
    return (B_DEV_INVALID_IOCTL);
}

static void setmode( struct device *d)
{
    uint div = d->divisor;
    (*isa->write_io_8)( d->ioport+LCR, LCR_DLAB);
    (*isa->write_io_8)( d->ioport+DLL, div & 0x00ff);
    (*isa->write_io_8)( d->ioport+DLH, div >> 8);
    (*isa->write_io_8)( d->ioport+LCR, LCR_8BIT);
}

To republish your device names, execute the following code fragment:

int fd = open( "/dev", O_WRONLY);
write( fd, "qq", strlen( "qq"));
close( fd);

This will work in your driver, or in an app. This is a handy feature for hot-swapping, or decommissioning faulty equipment.

Finally, to bring your driver creation to life, you need to compile and install. Here are the commands for the "qq" driver:

gcc -O -c qq.c
gcc -o qq qq.o -nostdlib /system/kernel_intel
mv qq /system/add-ons/kernel/drivers/bin/qq
cd /system/add-ons/kernel/drivers/dev
ln -s ../bin/qq

After republishing or rebooting, your devices will appear in /dev.


Be Engineering Insights: Locking Tricks

By Pavel Cisler

Understanding locking in BeOS kits is necessary for writing code that doesn't crash in subtle and hard-to-reproduce ways, deadlock or otherwise oddly misbehave. Here are a few techniques you can use when dealing with locking in the BeOS kits.

Using the BAutolock Class

When locking, it is necessary to balance calls to Lock() and Unlock(). The BAutolock class is a convenient way of taking care of this. Instead of using the usual lock sequence:

{
    if (!window->Lock())
        // failed to lock
        return;


    // access the window
    if (window->IsHidden()) {
        window->Unlock();
        return;
    }


    window->MoveTo(20, 20);
    // ... more stuff here


    window->Unlock();
}

you can use:

{
    BAutolock lock(window);
    if (!lock.IsLocked())
        // failed to lock
        return;


    // access the window
    if (window->IsHidden())
        // Unlock automatically called here
        return;


    window->MoveTo(20, 20);
    // ... more stuff here


    // Unlock automatically called here
}

BAutolock works much like auto_ptr, which you might be familiar with as a part of the standard C++ library. When the object constructed by BAutoLock() goes out of scope, its destructor is called and unlocks the locked target automatically. This makes for simpler and better organized code, particularly in cases where there are several paths out of the code block—it's easy to forget to put an Unlock() immediately before every return statement, but impossible to mess up with BAutolock. When your code exits, the BAutolock object will unlock the target.

Another advantage of using autolocks is that when used properly, they perform a correct cleanup even when an exception is thrown. (Did you say you don't use exceptions in your code? Do you ever allocate any objects with 'new'? If you do, you are using exceptions—new throws 'bad alloc' when you run out of memory). In our example, if any of the code that accesses the window throws an exception, the traditional coding using calls to Lock() and Unlock() will either allow the exception to leave the code with the window locked, or will have to include an explicit try-catch clause, to unlock the window and rethrow the exception. The example with autolock works without any additional code—the exception will cause all destructors for objects in scope to be called and so the window will correctly get unlocked.

You may use BAutolock from Autolock.h, or it's easy to write your own version. Since autolocks are usually fully inlined, you won't incur a performance penalty by writing your own version. Here's a quick example of a custom Autolock:

template<class T>
class AutoLock
{
public:
    AutoLock(T *target, bool lockNow = true)
        :   target(target),
            hasLock(false)
        {
            if (lockNow)
                hasLock = target->Lock();
        }

    ~AutoLock()
        {
            if (hasLock)
                target->Unlock();
        }

    operator!() const
        { return !hasLock; }

    bool IsLocked() const
        { return hasLock; }

    bool Lock()
        {
            if (!hasLock)
                hasLock = target->Lock();

            return hasLock;
        }

    void Unlock()
        {
            if (hasLock) {
                target->Unlock();
                hasLock = false;
            }
        }

private:
    T *target;
    bool hasLock;
};

This Autolock class permits "lazy locking"—if the boolean argument to the Autolock constructor is false, the Autolock object will be created but the target object will not be locked. You can then lock the target only if it become necessary, by using the Lock() member function. As with the BAutolock class, the above class handles unlocking of the target automatically, when its destructor is invoked.

As an added benefit, Autolock above is a template class, so it will work on BLoopers, BLockers, BBitmaps or any other classes that implement the three member functions Lock(), Unlock() and IsLocked().

The Mechanics of a BLooper Lock

When you lock a BWindow or any other BLooper, you need to check the result of the lock operation. For example:

AutoLock<BLooper> lock(window);
if (!lock)
    // failed to lock the window
    return ...

What exactly does it mean when the lock fails? Most likely the window is no longer around, it has been deleted. Wait a minute, if the window is deleted, that means the window pointer points to some random memory, how come the call window->Lock() doesn't crash? The answer is because Lock() is designed to work correctly on any random value of object pointer. Either window above is a valid pointer to a BLooper and Lock() will succeed (leaving the target locked), or window is not a valid pointer to a BLooper and Lock() will return false. You could do the following:

((BLooper *)0xdeadbeef)->Lock()

in your code and it will still not crash (it is very unlikely that any BLooper will get locked, though). Lock() actually accesses a list of all the live BLoopers in a team and when it is asked to lock one, it does a lookup in the list to see if the looper is valid. Note that in order for this to work, Lock() must not be a virtual function—when calling a virtual function, a dispatch is performed during which the object's 'this' pointer is used to access the vtable. If 'this' were garbage, the dispatch would crash.

When to Lock with a BMessenger

Locking a BLooper with Lock() is guaranteed to give you a valid, locked BLooper if the lock succeeds, but there is still an issue you need to be aware of. Consider the following scenario:

  1. You create a BWindow, and store a pointer to it, intending to use the window at some point.

  2. At some point, the window gets deleted.

  3. Then, a new BLooper is created, and it just happens to reuse the heap space that was formerly occupied by the window.

  4. Finally, your code that has stored a pointer to what was once the original window calls Lock() on that pointer. The lock will succeed, since the pointer is to a valid BLooper—but it will be a lock on the wrong object. This may lead to some very subtle, hard to reproduce bugs.

Unless the code that does the locking has a clear knowledge about the lifetime of the object it is trying to lock, you should use an even safer locking technique, using a BMessenger To do this, create a BMessenger associated with the target BLooper (a window, in the code below), and hold on to the instance of the BMessenger:

BMessenger windowMessenger;

...
// window creation code
BWindow *window = new BWindow ...
BMessenger tmp(window);
windowMessenger = tmp;
...

Now, you can use the BMessenger's LockTarget() member function to obtain a lock on the targeted BLooper:

{
    if (!windowMessenger.LockTarget())
        // failed to lock
        return;

    BWindow *window;
    windowMessenger.Target(&window);

    // access the window
    if (window->IsHidden()) {
        window->Unlock();
        return;
    }

    window->MoveTo(20, 20);
    // ... more stuff here

    window->Unlock();
}

Note that the BMessenger windowMessenger is initialized as a BMessenger for 'window' immediately after window is created. BMessenger::LockTarget() is guaranteed to return false when the messenger's target is no longer valid.

So How About a BMessenger Autolock?

class MessengerAutoLocker {
public:
    MessengerAutoLocker(BMessenger *messenger)
        :   messenger(messenger),
            hasLock(messenger->LockTarget())
        { }

    ~MessengerAutoLocker()
        { Unlock(); }

    operator!() const
        { return !hasLock; }

    bool IsLocked() const
        { return hasLock; }

    void Unlock()
        {
            if (hasLock) {
                BLooper *looper;
                messenger->Target(&looper);
                if (looper)
                    looper->Unlock();
                hasLock = false;
            }
        }

private:
    BMessenger *messenger;
    bool hasLock;
};

Using the MessengerAutoLocker class, we can rewrite our example:

BMessenger windowMessenger;
    ...
// window creation code
BWindow *window = new BWindow ...
BMessenger tmp(window);
windowMessenger = tmp;
...

{
    MessengerAutoLocker lock(&windowMessenger);
    if (!lock)
        // failed to lock
        return;

    BWindow *window;
    windowMessenger.Target(&window);

    // access the window
    if (window->IsHidden())
        return;

    window->MoveTo(20, 20);
    // ... more stuff here
}

Finally, the BHandler::LockLooper() Member Function

This lock call handles a race condition where a BHandler's looper may be changed while you are trying to lock it—you may end up having locked the old looper right before the new looper is set. If the call returns with success, you are guaranteed that you locked the BLooper that is identical to the handler's looper.

Note that this call may still crash if the handler you are trying to access (and lock it's owner) is deleted. When you are calling this locking call, you need to be sure that the BHandler is still alive. This limits this locking call to a pretty narrow set of uses.


Developers' Workshop: Stepping Up To the Deskbar

By Eric Shepherd

The new BDeskbar class in BeOS Release 4.5 has made it easier than ever to add replicants to the Deskbar's replicant tray. You know, that little area next to the time where the mailbox replicant shows up.

Replicant tray items are typically used for services that don't have any other user interface, or to add quick access to features that exist elsewhere in a more complex environment. For instance, the Media preferences panel lets you control sound volume, but you can optionally add a replicant to the tray that lets you control the volume quickly and easily, without having to open the Media panel. Talk about convenience!

Any running application can add replicants to the tray, and the best part is that items can remain in the tray even after the application that installed them quits (thanks to the magic of replicants).

Let's consider an application that adds an item to the tray while it's running but will remove the item when the application is shut down; maybe it's a utility that doesn't need any user interface beyond a single menu for selecting options to control it, including a Quit option. Let's take a look at how this application would create, manage, and delete its Deskbar tray item.

As is the case with any replicant, a Deskbar tray item requires an archivable BView, like this one:

class _EXPORT MyUtilDeskbarView : public BView {
public:
    MyUtilDeskbarView();
    MyUtilDeskbarView(BMessage *msg);
    ~MyUtilDeskbarView();
    status_t Archive(BMessage *data, bool deep) const;
    static BArchivable *Instantiate(BMessage *data);
    void Draw(BRect updateRect);
    void MouseDown(BPoint where);
    void AttachedToWindow();
    void DetachedFromWindow();
    void Init(void);

private:
    BBitmap *bitmap;
    entry_ref app_ref;
};

Note that the declaration of the class exports the class; this is necessary so the Deskbar can have access to the class when it tries to instantiate it.

There are two versions of the constructor. The first is used when instantiating the view within our application:

MyUtilDeskbarView::MyUtilDeskbarView() :
    BView(BRect(0,0,B_MINI_ICON-1,B_MINI_ICON-1),
    "MyUtility Control", B_FOLLOW_LEFT | B_FOLLOW_TOP,
    B_WILL_DRAW) {
    app_info info;
    be_app->GetAppInfo(&info);
    app_ref = info.ref;
    Init();
}

This configures the replicant's size, flags, and resizing mode, just like you normally do with BViews. It does do one interesting thing: in this example class, the icon displayed in the Deskbar tray will be the application's icon. Because the icon will be drawn from within the Deskbar, we need to obtain an entry_ref to the application in advance so the replicant can get access to our icon from the Deskbar.

So we use the BApplication::GetAppInfo() function to get information about the running application; this includes the needed entry_ref, which is then stored in a private field in the object.

The Init() function is called next; this function (we'll see it in a moment) performs any initialization common to the two forms of the constructor.

The second constructor looks like this:

MyUtilDeskbarView::MyUtilDeskbarView(BMessage *msg) : BView(msg) {
    msg->FindRef("app_ref", &app_ref);
    Init();
}

This one is used to instantiate the object as a replicant, using a BMessage representing a dehydrated MyUtilDeskbarView object. The application's entry_ref has been added to the BMessage by the Archive() implementation, so we extract it and then call Init() to complete initialization.

void MyUtilDeskbarView::Init(void) {
    bitmap = NULL;
}

The Init() function should handle any common initialization; in this case, all we do is make the bitmap pointer NULL so we know it hasn't been loaded yet.

The Archive() function's job is to wrap up the data that describes the MyUtilDeskbarView object into a BMessage; this information is then used by the Instantiate() function to reconstitute a copy of the object by the Deskbar.

status_t MyUtilDeskbarView::Archive(BMessage *msg,
bool deep) const {
    status_t err;
    app_info info;

    err = BView::Archive(msg, deep);
    be_app->GetAppInfo(&info);
    msg->AddRef("app_ref", &info.ref);
    msg->AddString("add_on", APP_SIGNATURE);
    return err;
}

For the most part, this is done by simply calling through to BView::Archive(), but we do also add two more fields to the archive BMessage. app_ref contains the entry_ref to the application so we can track it down later to load our icon.

The add_on field contains our application's signature. This is very important, and is required; without it, the archive can't be reconstituted because in order for the object's code to run, the BeOS needs to know what application file its code resides in. The signature is used to figure this out. We could probably use this information instead of an entry_ref to get our application's icon, but adding the entry_ref to the object is good practice in creating archives.

The static Instantiate() function is called by the Deskbar to construct a copy of your application's MyUtilDeskbarView object. It's static so it can be called without first instantiating an MyUtilDeskbarView object (can you see the chicken-or-egg scenario there?). This function's primary mission is to simply return a new object, constructed from the passed-in BMessage archive.

BArchivable *MyUtilDeskbarView::Instantiate(BMessage *msg) {
    if (!validate_instantiation(msg, "MyUtilDeskbarView")) {
        return NULL;
    }
    return new MyUtilDeskbarView(msg);
}

The validate_instantiation() function is used to confirm that the message does in fact represent an archived MyUtilDeskbarView object; if it doesn't, we return NULL, thereby indicating an error.

The AttachedToWindow() function handles loading and preparing the icon for display in the view:

void MyUtilDeskbarView::AttachedToWindow() {
    BAppFileInfo appInfo;
    BFile file;

    file.SetTo(&app_ref, B_READ_ONLY);
    appInfo.SetTo(&file);
    bitmap = new
    BBitmap(BRect(0,0,B_MINI_ICON-1,B_MINI_ICON-1), B_CMAP8,
    false, false);
    if (appInfo.GetIcon(bitmap, B_MINI_ICON) != B_OK) {
        delete bitmap;
        bitmap = NULL;
    }

    if (Parent()) {
        SetViewColor(Parent()->ViewColor());
    }
}

A BFile is created, referring to the application file whose entry_ref we've already obtained from the archive message. This is used to prepare a BAppFileInfo object, whose GetIcon() function we call to obtain the application's icon. This is read into a bitmap of the appropriate size to contain the application's small (16x16) icon.

We also set the view's background color to match the Deskbar's replicant tray color by looking at the Parent() view's color.

DetachedFromWIndow() is called by the application server just after the replicant view is removed from the Deskbar tray. We can use this to clean up after ourselves:

void MyUtilDeskbarView::DetachedFromWindow() {
    delete bitmap;
}

In this case we just dispose of the bitmap containing the application's icon.

The Draw() function draws the replicant view; this can draw anything you want, but in our case we'll just draw the application's icon from our BBitmap:

void MyUtilDeskbarView::Draw(BRect updateRect) {
    if (bitmap) {
        SetDrawingMode(B_OP_OVER);
        DrawBitmap(bitmap, BPoint(0,0));
    }
}

To be safe, we make sure we don't draw if the bitmap is NULL; this prevents us from dying if the icon wasn't loaded for some reason. You could add code here to alter the appearance of the icon under certain conditions; the mail replicant for example would draw the mailbox full if there was mail waiting, or empty if no mail has been received.

The only remaining thing is to implement MouseDown() to handle clicks in the view. You could pop up a menu, or you could create a window, or run an application, or do anything you want to:

void MyUtilDeskbarView::MouseDown(BPoint where) {
    BWindow *window = Window();
    if (!window) {
        return;
    }

    BMessage *current = window->CurrentMessage();
    if (!current) {
        return;
    }

    if (current->what == B_MOUSE_DOWN) {
        uint32 buttons = 0;
        uint32 modifiers = 0;

        current->FindInt32("buttons", (int32 *) &buttons);
        current->FindInt32("modifiers", (int32 *)
        &modifiers);

        switch(buttons) {
            case B_PRIMARY_MOUSE_BUTTON:
                beep();
                break;
            case B_SECONDARY_MOUSE_BUTTON:
                beep();
                snooze(1000000);
                beep();
                break;
        }
    }
}

In this case, the sample code demonstrates how to use BWindow::CurrentMessage() to get at the B_MOUSE_DOWN message and peel out information on which buttons on the mouse were pressed and the modifier keys that were down at the time. This information can then be used to alter the response to the click depending on what the user did. For example, the volume control replicant brings up a volume slider control if you click normally, but a popup menu if you click with the secondary mouse button.

Installing the item into the Deskbar is as simple as:

BDeskbar db;
db.AddItem(new MyUtilDeskbarView);

And you can remove the item by doing:

BDeskbar db;
db.Removeitem("MyUtility Control");

If you plan to remove your Deskbar item by name, be sure the name is unique—use your application's name and possibly other text to make it as unlikely as possible that some other replicant might use the same name. You can avoid this risk by using the replicant's ID number, like this:

BDeskbar db;
int32 id;
db.AddItem(new MyUtilDeskbarView, &id);

And you can remove it like this:

BDeskbar db;
db.RemoveItem(id);

Now you're armed with all the ammunition you need to create a great add-on and slide it into the Deskbar for all to see.

One last point: think carefully about whether or not a Deskbar tray item is really necessary for your application to be useful. Dropping replicants into the tray from every application you write is just going to clutter up the tray and render it useless—only critical or often-used items belong in the tray. When in doubt, make it a user preference whether the tray item should be installed or not.

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