One of the many upcoming changes in the BeOS is in the world of input devices and events. The Input Server, slated to debut in R4, is a server that deals with all things "input." Specifically, it serves three functions: manages input devices such as keyboards and mice; hosts a stream of events that those devices generate; and dispatches those events that make it through the stream.
The Input Server is a pretty dumb piece of software. (Cue to Alex: roll
your eyes and say, "What do you expect Hiroshi, you wrote it.") On its
own, the server doesn't know how a keyboard or a mouse works; it relies
on BInputServerDevice
add-ons to tell it.
BInputServerDevice
is a base class from which all input device add-ons
must derive. It provides the basic framework of virtual hook functions
and non-virtual member functions that the Input Server uses to
communicate with an add-on, and that the add-on can use to talk back to
the server. To give a sneak peak of the API, some of the virtuals include
InitCheck()
, Start()
,
Stop()
, and Control()
.
The common sequence of the life of an input device is this:
The Input Server loads an add-on and constructs its
BInputServerDevice
-derived object.
The Input Server calls InitCheck()
on the object. The object
determines whether it is capable of doing its job—that is,
generating input events.
This task may involve the object sniffing around for hardware it
can drive, or looking for a kernel device driver in
/dev
. If the
object is happy, it registers with the Input Server any input
device(s) it finds, and returns B_NO_ERROR
. An error return causes the
Input Server to promptly destruct the object and unload the add-on.
At some point in time, someone will tell the input devices
registered with the Input Server to Start()
. The system automatically
starts keyboards and mice at boot time. Any other type of device (an
"undefined" input device that the system doesn't have any special
knowledge about) can be started by an application using new API in the
Interface Kit.
A registered device, whether it has been started or not, may be
Control()
-ed at any time. Think of Control()
as the ioctl()
equivalent
in input device parlance. Examples of system-defined control messages
include keymap changes and mouse speed changes.
Once a BInputServerDevice
-derived object's input device is up and
running, its primary task is to generate input events. These events are
expressed as BMessages
. For example, a keyboard input device will most
likely generate B_KEY_DOWN
and B_KEY_UP
messages. Similarly, a mouse
input device will probably generate B_MOUSE_UP
, B_MOUSE_DOWN
, and
B_MOUSE_MOVED
events.
There is nothing that prevents an input device from putting arbitrary
data in any of the BMessage
s it generates. So, for example, a tablet may
generate the aforementioned mouse events with extra data such as pressure
and proximity. Any information packed into the BMessage
s is delivered
unmolested by the input server.
When an event is ready to be shipped off, an input device enqueues it
into the Input Server's event stream. Some BHandler
(most likely a BView
)
down the line eventually receives the event by way of the usual hook
functions such as KeyDown()
, MouseDown()
,
and MouseMoved()
.
The Input Server's event stream is open for inspection and alteration by
anyone in the system. This is achieved through another set of add-ons
called BInputServerFilter
. Like
BInputServerDevice
, BInputServerFilter
is
a base class for input filter add-ons to the Input Server.
An input filter add-on is privy to all the events that pass through the
Input Server's event stream. A filter may inspect, alter, generate, or
completely drop input events. It's similar in some ways to the Interface
Kit's BMessageFilter
, but much more low-level.
A BInputServerFilter
sees
all events that exist in the system; BMessageFilter
s are associated with
a specific BLooper
and thus see only the events
targeted to its BLooper
.
Also, filters in the Input Server can generate additional events in place
of, or in addition to, the original input event that it was invoked with.
With the introduction of loadable input device objects, the Input Server enables the BeOS to be used with a wide variety of input devices (and more than one of them at once too). And with the advent of input filters, the Input Server opens the door to a new class of tricks, hacks, and (gulp) pranks for the creative developer. It's going to be fun.
This is my first Newsletter article, so let me introduce myself. I'm Trey Boudreau, and I write graphics drivers at Be. Or not exactly "at" Be, since I'm one of a few Be employees not located in either Menlo Park or Paris.
That's it for the introduction—now some content...
As anybody who's been around the BeOS awhile knows, graphics drivers are app_server add-ons and not kernel drivers. So why is the graphics guy writing about kernel drivers? Because in R4 all graphics drivers have a kernel driver component, as well as a user space add-on (called an accelerant).
I could talk about writing R4 graphics drivers, but since you don't have R4 yet that wouldn't be too useful. Instead, how about some handy tips for writing kernel drivers, since I've been doing a lot of that lately. I'll assume you've read the Be Book Device Drivers chapter - DeviceDrivers.html - even if you haven't written a device driver yet.
Even though we don't come right out and say it, the exported entry points
to your driver are guaranteed to be executed sequentially. Said another
way, the functions init_hardware()
,
init_driver()
, uninit_driver()
,
publish_devices()
, and find_device()
are only executed one at time, so
there's no need to protect them from one another. On the other hand, all
the hook functions must be thread safe. Specifically, open_hook()
must
properly handle simultaneous open attempts.
The PowerPC-based machines we support cannot chain (or share) interrupts,
but Intel-based machines can. The API for installing and removing
handlers in R3.x makes installing interrupt handlers on Intel hardware in
the presence of multiple supported devices more complicated than it first
appears. Here's the API (from
KernelExport.h
):
typedef bool (*interrupt_handler)(void *data
); longinstall_io_interrupt_handler
( longinterrupt_number
, interrupt_handlerhandler
, void *data
, ulongflags
); longremove_io_interrupt_handler
( longinterrupt_number
, interrupt_handlerhandler
);
And here's the scenario:
PCI device A gets IRQ X.
PCI device B gets IRQ X.
Program opens device A, driver installs interrupt handler with device specific data A.
Program opens device B, driver installs interrupt handler with device specific data B.
Program closes device B, driver removes interrupt handler.
Notice that remove_io_interrupt_handler()
doesn't take a
void* data, so
there's no way to know which handler to remove. The implementation of
remove is such that the first entry in the chain matching the handler is
removed—in this case the handler for device A. As a result the handler
for device A is never called, even though device A is still open, and the
handler for B to be called even though the device is closed.
The basic solution is to write the interrupt handler to handle all your supported devices that have the same interrupt number with a single installation of the handler. The easiest way to do this is to install the interrupt handler at driver initialization time, and remove it at driver uninitialization. The more difficult way is to install it at device open and remove at device close, making sure you don't install it twice or remove it before it's finished.
Now the good news. In R4, the prototype for the remove function will change to
longremove_io_interrupt_handler
( longinterrupt_number
, interrupt_handlerhandler
, void *data
);
This change is not source compatible but is binary compatible. The remove function first walks the list trying to match handler *and* data (which should always work for drivers using the new API). If no match is found, it walks the list again attempting to match only the handler.
While we're on the subject of interrupt handlers, here are some tips to help debug them. In his Developer's Workshop article
Developers' Workshop: Welcome to the Cow...Debugging Device Drivers
Victor Tsou talked about using kernel debugger commands to help in the
postmortem afterglow. He mentioned using k
to output info while
in the debugger.
printf
()
It's useful to note that k
also works in the interrupt handler.
If your device is generating interrupts at a decent rate, you can flood
the serial port with *lots* of output using printf
()k
, so use it
sparingly. Because it's possible to share interrupts on Intel platforms,
you may want to have one variable that counts trips through the handler
(whether for your device or not) and one variable for each device which
might generate an interrupt.
printf
()
In your kernel debugger command output, include the current values of the total trips and individual hits for each of your devices. Whenever you want to check the status of your driver, press Alt+SysReq on Intel machines (Command+PowerKey on Power Macs) to drop into the debugger. On a BeBox, just tap the debugger button on the front panel.
Unfortunately, we introduced a bug in R3.1 for Intel regarding
write_pci_config()
. When calling
write_pci_config()
with a size of 1 or 2
bytes, the other bytes in the 32-bit aligned word (i.e., the ones you
wanted to leave unchanged) are zeroed. Here's an example: You want to
change the value of the PCI configuration space byte at offset 0x41. The
byte at offset 0x41 is part of a 32-bit word starting at offset 0x40.
When you call
write_pci_config
(bus
,dev
,fun
,0x41,1,val
)
the bytes at offsets 0x40, 0x42, and 0x43 are zeroed. The work around for this problem is to do a read-modify-write on the 32-bit aligned word:
uint32val
=something
; uint32tmp
=read_pci_config
(bus
,dev
,fun
,0x40,4);tmp
&= 0xffff00ff;tmp
|=val
<< 8;write_pci_config
(bus
,dev
,fun
,0x40,4,tmp
);
This will be fixed in R4, but I don't know if we'll provide a fix for R3.2+ We'll post a notice about the update if we do.
No Newsletter article this close to the R4 release would be complete
without a teaser about new features. In addition to the new
module/bus-based drivers (described by Arve Hjønnevåg in his Newsletter
article Be Engineering Insights: Splitting Device Drivers and Bus Managers, the driver API
sports a few new hooks: readv()
, writev()
,
select()
, and deselect()
.
readv()
and writev()
support
scatter/gather or vector-based I/O. See your
favorite Linux manpages for reasonable documentation. select()
and
deselect()
provide support for (you guessed it)
the select()
system call.
Details of these features are subject to further changes, so all I can
really report is that they exist.
This week, we're going to relax a little and put together a nice, simple class for saving program settings to disk. You can download the source code for this week's project from the Be FTP site:
ftp://ftp/pub/samples/intro/prefs_article.zip
The BMessage
provides a handy container for data. Although its primary
use is for sending data between two pieces of software, its tagged data
item format is ideal for use as a cross-platform data storage mechanism.
Thus we introduce the TPreferences
class, which is derived from BMessage
:
classTPreferences
: publicBMessage
{ public:TPreferences
(char *filename
);~TPreferences
(); status_tInitCheck
(void); status_tSetBool
(const char *name
, boolb
); status_tSetInt8
(const char *name
, int8i
); status_tSetInt16
(const char *name
, int16i
); status_tSetInt32
(const char *name
, int32i
); status_tSetInt64
(const char *name
, int64i
); status_tSetFloat
(const char *name
, floatf
); status_tSetDouble
(const char *name
, doubled
); status_tSetString
(const char *name
, const char *string
); status_tSetPoint
(const char *name
,BPoint
p
); status_tSetRect
(const char *name
,BRect
r
); status_tSetMessage
(const char *name
, constBMessage
*message
); status_tSetFlat
(const char *name
, constBFlattenable
*obj
); private:BPath
path
; status_tstatus
; };
The most obvious additions here, beyond the normal BMessage
functionality, are all the
SetX()
functions. These let an application explicitly set the value of a tagged
item in the TPreferences
object, without having to determine whether to
call AddX()
or ReplaceX()
.
This is very useful when treating a BMessage
as a data container.
We'll be taking advantage of the fact that a BMessage
is derived from
BFlattenable
. Objects derived from
BFlattenable
can be "flattened" into a
dehydrated format and saved to disk, then later reconstituted
("unflattened") into a duplicate of the original. Flattened objects are
endianess-independent, so we get cross-system compatibility between
PowerPC and Intel absolutely 100% free of charge. As programmers, we like
things that are actually free (and there aren't many).
Let's have a look at the constructor:
TPreferences
::TPreferences
(char *filename
) :BMessage
('pref') {BFile
file
;status
= find_directory(B_COMMON_SETTINGS_DIRECTORY
, &path
); if (status
!=B_OK
) { return; }path
.Append
(filename
);status
=file
.SetTo
(path
.Path
(),B_READ_ONLY
); if (status
==B_OK
) {status
=Unflatten
(&file
); } }
The constructor's primary responsibility here is to open the preference
file and read in the original settings. First, find_directory()
is called
to obtain a BPath
referencing the common settings directory (i.e.,
/boot/home/config/settings
).
If this fails, the TParameter
field status
is set to the error code and the constructor returns. The application can
use the InitCheck()
function to determine whether or not the preferences
were read successfully.
If all is well, the preference file name is appended to the path, and the
BFile
is set to read that file. If this succeeds, the file's contents are
unflattened into the TPreferences
object.
The destructor's job is to save the preferences to disk:
TPreferences
::~TPreferences
() {BFile
file
; if (file
.SetTo
(path
.Path
(),B_WRITE_ONLY
|B_CREATE_FILE
) ==B_OK
) {Flatten
(&file
); } }
This creates a BFile
object, sets the path to the preference file's
pathname (which has been saved in the path field in the TPreferences
object), and then flattens the TPreferences
data into the file.
The various SetX()
functions all look about the same, so we'll
arbitrarily look at SetBool()
as a representative of its kin:
status_tTPreferences
::SetBool
(const char *name
, boolb
) { if (HasBool
(name
)) { returnReplaceBool
(name
, 0,b
); } returnAddBool
(name
,b
); }
SetBool()
accepts an item name and a boolean value to save with that
name. The function begins by calling HasBool()
to see if a boolean by the
indicated name already exists. If it does, ReplaceBool()
is used to
replace the existing value. Otherwise, AddBool()
is called to add a new
boolean with the given name. B_OK
is returned if all is well; otherwise,
an error is returned.
Note that the TPreferences
class doesn't support item arrays; you can
only save one item with a given name. In general, this shouldn't be a
problem.
That's the basics of implementing the TPreferences
class. Now, a simple
example that demonstrates its use. This little program keeps two values
in its preference file: the real-time clock value at which the program
was last run, and the number of times it's been run. When you run the
program, it shows you the current values, then updates them.
The whole program (see sample.cpp
) is contained
in main()
. Let's look at
it one bit at a time:
TPreferences
prefs
("PrefsSample_prefs"); // Preferences if (prefs
.InitCheck
() !=B_OK
) {prefs
.SetInt64
("last_used",real_time_clock
());prefs
.SetInt32
("use_count", 0); }
This code instantiates our TPreferences
object. The preference file's
name is PrefsSample_prefs
(so its full path is
/boot/home/config/settings/PrefsSample_prefs
). Once it's instantiated, we
call InitCheck()
to see if all is well. If it's not, we initialize the
two values: last_used
is initialized to the current real- time clock
value, and use_count
is set to zero.
Then we call PrintToStream()
to print out the contents of the
TPreferences
object:
prefs
.PrintToStream
();
Finally, we update the preferences:
int32count
; if (prefs
.FindInt32
("use_count", &count
) !=B_OK
) {count
= 0; }prefs
.SetInt64
("last_used",real_time_clock
());prefs
.SetInt32
("use_count", ++count
);
We call FindInt32()
to get the current
value of the use_count
preference. If an error occurs, we set it to zero as a safe alternative.
Then we call SetInt64()
to set last_used
to the real_time_clock()
value, and SetInt32()
to set
use_count
to one greater than the previous
value.
That's all there is to it. Since the TPreferences
destructor
automatically saves the settings, we don't have to do anything else. Go
ahead and compile the project, then run it a few times, and you'll see
that, indeed, the values go up every time you run it.
That's a wrap for this time. We'll move on to our next project in about six weeks.
Looking back to 1981, when the first IBM PC came out, it's hard not to wonder how far its DNA will allow it to grow. Today's dual-processor 450 MHz Pentium II system is a direct descendant of its 8086-based Apple ][ competitor, down to the cassette player interface.
The good news is the PC market grew out of a succession of compatible developments, both hardware and software. The bad news is today's PC carries with it a certain amount of baggage. At each step along what now looks like a glorious road, compromises were made. New technology had to be grafted onto the existing frame; it had to keep running the old software and keep supporting the previous generation of hardware devices.
Look at the back of a PC today, or open the box, and you'll see a few examples. USB ports must co-exist with serial and parallel connectors, PCI and older ISA buses fight for space on the motherboard, and these are but the most visible examples. Other instances, perhaps more painful ones, are buried in the chipset and the BIOS.
Today's high-end, yet highly affordable dual processor system offers yesterday's supercomputer power. How much is wasted because of the old compatibility layers? How fast would this dual-processor system be if it were built from the ground up with today's technology—and only today's technology? And how much less would it cost than the current standard?
So far, the market has answered: the benefits of the incremental approach outweigh its disadvantages. The PS/2 and ACE attempts to create a *better* PC architecture have failed against the evolutionary approach. But, just as the stock market does not go up forever, just as no tree reaches the sky, the progressive approach is bound to reach its limit someday.
And why should we care? Shouldn't we stick to our unfinished knitting? Certainly, but we can't help lifting our gaze from it and dreaming of even faster and cheaper hardware, even if we're not really in a position to influence hardware standards.
Following last week's OPOS argument OPH and OPOS though, imagine a situation where Microsoft managed to effectively dictate a new PC standard running Windows NT 5.0 at the next WinHEC (Windows Hardware Engineering Conference) and have it embraced by enough vendors to give it critical mass. Wouldn't everyone, including our little company, benefit from such a liberating leap?
A truly perplexing perspective.