Issue 3-35, September 2, 1998

Be Engineering Insights: An Introduction to the Input Server

By Hiroshi Lockheimer

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.

Managing Input Devices

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:

  1. The Input Server loads an add-on and constructs its BInputServerDevice-derived object.

  2. The Input Server calls InitCheck() on the object. The object determines whether it is capable of doing its job—that is, generating input events.

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

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

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

Generating Input Events

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 BMessages 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 BMessages 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 Event Stream

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; BMessageFilters 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.

Conclusion

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.


Be Engineering Insights: Kernel Driver Tips and Traps

By Trey Boudreau

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.

Thread Safety and Such

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.

Interrupt Handlers and Multiple Devices

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);

long install_io_interrupt_handler(
  long   interrupt_number,
  interrupt_handler handler,
  void   *data,
  ulong  flags);

long remove_io_interrupt_handler(
  long   interrupt_number,
  interrupt_handler handler);

And here's the scenario:

  1. PCI device A gets IRQ X.

  2. PCI device B gets IRQ X.

  3. Program opens device A, driver installs interrupt handler with device specific data A.

  4. Program opens device B, driver installs interrupt handler with device specific data B.

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

long remove_io_interrupt_handler(
  long   interrupt_number,
  interrupt_handler handler,
  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.

Debugging Interrupt Handlers

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 kprintf() to output info while in the debugger.

It's useful to note that kprintf() 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 kprintf(), 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.

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.

write_pci_config() Bug on Intel

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:

uint32 val = something;
uint32 tmp = 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.

The Obligatory R4 Teaser

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.

Thanks for Stopping By

That about wraps up my premier Newsletter article. Let me remind you as one of the graphics driver guys, if you've got questions about writing graphics drivers for the BeOS, I'm your man. Drop me a note at trey@be.com.


Developers Workshop: BeOS Programming Basics, Part 6

By Eric Shepherd

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:

class TPreferences : public BMessage {
  public:
    TPreferences(char *filename);
    ~TPreferences();

    status_t    InitCheck(void);
    status_t    SetBool(const char *name, bool b);
    status_t    SetInt8(const char *name, int8 i);
    status_t    SetInt16(const char *name, int16 i);
    status_t    SetInt32(const char *name, int32 i);
    status_t    SetInt64(const char *name, int64 i);
    status_t    SetFloat(const char *name, float f);
    status_t    SetDouble(const char *name, double d);
    status_t    SetString(const char *name,
                          const char *string);
    status_t    SetPoint(const char *name, BPoint p);
    status_t    SetRect(const char *name, BRect r);
    status_t    SetMessage(const char *name,
                           constBMessage *message);
    status_t    SetFlat(const char *name,
                        constBFlattenable *obj);

  private:
    BPath      path;
    status_t    status;
};

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_t TPreferences::SetBool(const char *name, bool b) {
  if (HasBool(name)) {
    return ReplaceBool(name, 0, b);
  }

  return AddBool(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:

int32 count;
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.


The Future of the PC Architecture

By Jean-Louis Gassée

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.

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