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.
#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
u -> hw
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
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
#define PROT_UWR
0x00000008
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
())
return;
if (window
->IsHidden
()) {
window
->Unlock
();
return;
}
window
->MoveTo
(20, 20);
window
->Unlock
();
}
you can use:
{
BAutolock
lock
(window
);
if (!lock
.IsLocked
())
return;
if (window
->IsHidden
())
return;
window
->MoveTo
(20, 20);
}
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 BLooper
s, BLocker
s,
BBitmap
s 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
)
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 BLooper
s 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:
You create a BWindow
, and store a pointer to it, intending to use
the window at some point.
At some point, the window gets deleted.
Then, a new BLooper
is created, and it just happens to reuse the
heap space that was formerly occupied by the window.
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
;
...
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
())
return;
BWindow
*window
;
windowMessenger
.Target
(&window
);
if (window
->IsHidden
()) {
window
->Unlock
();
return;
}
window
->MoveTo
(20, 20);
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
;
...
BWindow
*window
= new BWindow
...
BMessenger
tmp
(window
);
windowMessenger
= tmp
;
...
{
MessengerAutoLocker
lock
(&windowMessenger
);
if (!lock
)
return;
BWindow
*window
;
windowMessenger
.Target
(&window
);
if (window
->IsHidden
())
return;
window
->MoveTo
(20, 20);
}
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 BView
s. 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.