Issue 3-33, August 19, 1998

Be Engineering Insights: Fun with Threads, Part 2

By Pavel Císler

In the first part of this article we discussed fire-and- forget threads. These are pretty straightforward, with a very simple setup. You'll definitely want to have read that article before continuing. You can find it at:

Be Engineering Insights: Fun with Threads, Part 1

Usually, though, you'll use threads that interact more with the world around them and need some level of synchronization. In our next example we'll implement a simple mouse-tracking thread that can be used for, say, implementing a nice button class. I'm sure you've done this before—you override MouseDown and keep looping until the user lets go of the mouse button. You animate your button, pressing it whenever the mouse tracks into its rectangle.

All is well, except that the rest of your window is not getting any updates, pulse tasks of views aren't being called, etc.—not surprising, since in the MouseDown() you blocked the entire window thread. You could use the Pulse calls, keep a tracking state in your button and track it by getting pulse events. That's a little messy though. You have to require pulse events for the view, but you don't really need them unless you're actually tracking the button, and you end up wasting cycles.

A better way to fix this is by spawning a thread that takes care of the mouse tracking asynchronously, allowing the rest of your window to be live. This thread is still pretty autonomous—it can delete itself when you're done pressing and clean up its state, etc. It will, however, have to access the button it's tracking—state owned by a different thread. Let's see how to do that properly. This time I'll write the Button class itself first and while I'm writing it I'll be able to decide on a convenient interface with the mouse-tracking thread:

class SomeButton : public BControl {
public:
  SomeButton(BRect frame, const char *name, uint32 resizeMask,
    uint32 flags)
    :  BControl(frame, name, "", 0, resizeMask, flags),
       pressing(false)
    {}

  void MouseDown(BPoint)
    {
      MouseDownThread::TrackMouse(
        this, &DoneTracking, &Track);
    }

  void Draw(BRect)
    {
      // draw pressed/normal button based on value of
      // <pressing>, left as an exercise for the reader
    }

private:
  bool pressing;

  friend void DoneTracking(BView *view, BPoint point);
  friend void Track(BView *view, BPoint point, uint32);
};

Note the simple MouseDown() call. The TrackMouse() call then targets two callback functions:

void
DoneTracking(BView *view, BPoint point)
{
  SomeButton *button = dynamic_cast<SomeButton *>(view);
  button->pressing = false;
  button->Invalidate();
  if (button->Bounds().Contains(point)) {
    button->SetValue(!button->Value());
    button->Invoke();
  }
}

void
Track(BView *view, BPoint point, uint32 /*unusedButtons*/)
{
  SomeButton *button = dynamic_cast<SomeButton *>(view);
  bool newPressing = button->Bounds().Contains(point);
  if (newPressing != button->pressing) {
    button->pressing = newPressing;
    button->Invalidate();
  }
}

That looks like an easy enough way to set up control tracking. MouseDown() just calls MouseDownThread::TrackMouse(), which passes in the functions used to track the button as the mouse moves in an out of the control and as the user lets go of the buttons.

Here's what the tracking thread will be like:

class MouseDownThread : private ThreadPrimitive {
public:
  static void TrackMouse(BView *view,
    void (*donePressing)(BView *, BPoint),
    void (*pressing)(BView *, BPoint, uint32) = 0,
    bigtime_t pressingPeriod = 100000)
    {
      MouseDownThread *thread = new MouseDownThread(
        view, donePressing, pressing, pressingPeriod);

      if (thread->Go() != B_OK)
        // failed to launch, clean up
        delete thread;
    }

protected:
  MouseDownThread(BView *view,
    void (*donePressing)(BView *, BPoint),
    void (*pressing)(BView *, BPoint, uint32),
    bigtime_t pressingPeriod)
     :  ThreadPrimitive(B_LOW_PRIORITY, "MouseTracker"),
        view(view),
        donePressing(donePressing),
        pressing(pressing),
        pressingPeriod(pressingPeriod)
    {
      parent = view->Window();
    }

  virtual void Run()
    {
      for (;;) {
        if (!parent->Lock())
          break;

        uint32 buttons;
        BPoint location;
        view->GetMouse(&location, &buttons, false);
        if (!buttons) {
          (*donePressing)(view, location);
          parent->Unlock();
          break;
        }

        if (pressing)
          (*pressing)(view, location, buttons);

        parent->Unlock();
        snooze(pressingPeriod);
      }

      delete this;
      ASSERT(!"should not be here");
    }

private:
  BWindow *parent;
  BView *view;
  void (*donePressing)(BView *, BPoint);
  void (*pressing)(BView *, BPoint, uint32);
  bigtime_t pressingPeriod;
  thread_id threadID;

  typedef ThreadPrimitive _inherited;
};

Where is the synchronization? In the Run call we lock the window of the button we're operating on. If the window is gone when we wake up from our snooze, the call to Lock will fail and the thread will exit, deleting itself.

Note one very important point in this class. I could have omitted the BWindow* parent member—saving the BView* view would have been enough -- and I can get the window from the view by calling view->Window(), right? WRONG!

Think again about what happens when the tracking thread wakes up from the snooze and the window our button is in has closed for some reason in the mean time. The view pointer will probably point to some random memory, possibly a corpse of the original button view. Trying to call Window on it will most likely crash.

Lock() is designed to handle being called on Loopers that have been deleted, so if the window is gone, the lock will fail and we'll just bail. In our mouse tracking example this would usually not happen. A window is not likely to go away while you're tracking the mouse, so you'll have a subtle, hard to reproduce bug waiting to fire when you're doing that important demo.

Using Lock() on the spawning thread like this is the most common way of synchronizing. It serves two purposes: it serves as a lock for the shared state, allowing the thread to access state in the spawning thread in a mutually exclusive way; and it allows the thread to exit in a reasonably clean way after the spawning thread dies itself.

In some cases synchronization like this is not good enough. You may, for some reason, need to quit the thread as a part of deleting the spawning thread. For one thing, the lock synchronization shown above doesn't cover one obscure case: if, while you're snoozing, your spawning window is deleted and a similar one is created in its place, aliased to the same pointer value. This is practically impossible in our mouse tracking sample, but may be very well possible if the thread heartbeat is longer in some other application. You may use a more sophisticated locking technique, involving a BMessenger—a messenger-based lock has a more elaborate locking check and handles an aliasing issue like this completely.

You may however have different reasons to have a synchronization model where the spawning thread also quits the new thread in, say, it's destructor. This time we'll imagine a thread that calls a function object periodically (the function object can be configured to traverse the disk, a few entries each time it is called, deleting old query files that are no longer needed). Again, if function objects are too much for you, you can imagine a concrete subclass of this example thread that gets all the parameters and the function pointer passed as arguments and stores them until the time comes to do the work inside Run().

class OwnedPeriodicThread : private ThreadPrimitive {
public:
  static OwnedPeriodicThread *Launch(FunctionObject *functor,
    bigtime_t period, int32 priority = B_LOW_PRIORITY,
    const char *name = 0)
    {
      OwnedPeriodicThread *thread = new OwnedPeriodicThread(
        functor, period, priority, name);

      if (thread->Go() != B_OK) {
        // failed to launch, clean up
        thread->Die();
        return 0;
      }
      return thread;
    }

  virtual ~OwnedPeriodicThread()
    {
      for (;;) {
        requestedToQuit = true;
        if (readyToQuit)
          break;
        snooze(period);
      }
      delete functor;
    }

private:
  void Die()
    {
      readyToQuit = true;
      delete this;
    }

  OwnedPeriodicThread(FunctionObject *functor,
    bigtime_t period, int32 priority, const char *name)
    :  ThreadPrimitive(priority, name),
       functor(functor),
       requestedToQuit(false),
       readyToQuit(false)
    {}

  virtual void Run()
    {
      for (;;) {
        if (requestedToQuit) {
          readyToQuit = true;
          break;
        }

        (*functor)();
          // do the work

        snooze(period);
      }
    }

  FunctionObject *functor;
    // functor owned by the thread
  bigtime_t period;
    // the time period at which we call functor

  // a pair of synchronization bits
  bool requestedToQuit;
  bool readyToQuit;
};

In this case the static Launch() call actually returns a pointer to the thread object. Also the destructor is public. It is up to the thread owner—the spawner—to delete the thread once all the work is done. The destructor waits for the next Run task to go through its next loop and realize its work is over.

If the task of the thread represented by the function object needs to access the spawning window, it may do so by locking down its own copy of the window pointer. It will always know that as long as the window got locked it is the same window, because the window wouldn't quit until it stopped the thread. And in that case we wouldn't be halfway through invoking the work task—the function object, right?

You could further enhance this thread by having the worker function or function object return when it finishes or when an error condition occurs. In that case you would just set the readyToQuit to true, break out of the Run() loop, and wait patiently for the spawner to delete it.

More Thread Ideas

The Tracker uses a lot of "run later" tasks for things like selecting the parent folder when you choose Open Parent in a window, after the child window closes and the parent opens. This just begs using a separate thread; you're closing the current window—the same window that belongs to the thread you're executing in. Sometime in the near future the parent window will open because you sent the Tracker app a message to open it. The best way to do this is to spawn a thread that waits around until the parent window opens and then waits until it finds the right folder in it and selects it for you.

To make using threads like this easier, the Tracker uses a task queue class—a thread and a list of tasks that wait to be executed periodically until they are done. This saves the overhead of creating a separate thread for each of them and of synchronizing the deletion of each of them individually—you just delete the entire task queue when the Tracker is quitting. The task queue has different flavors of tasks -- one-shot, periodic, periodic with a timeout, etc. Too bad the class doesn't fit in here—maybe I'll be able to sneak it into a future Newsletter article.

The Tracker also uses the task queue to consolidate different tasks that require a common, expensive action. To give you an example, the Tracker has to update an icon of a file once it gets a node monitor notification about some attribute change. When mimeset runs, for instance, the Tracker gets several node monitors for the single mimeset call—one about each icon being added, one about a type being set, one about a preferred app being set, etc. Each of these has an effect on what the resulting icon will be.

If the Tracker responded to each of them, the icon would update four times. Instead, a task is enqueued for each of them, waiting around for a fraction of a second. If another task for the same attribute change gets generated, it merges with the previous one and only one update is performed. The worker function objects that make this happen have two virtual calls:

virtual bool
  CanAccumulate(constAccumulatingFunctionObject *) const;
virtual void
  Accumulate(AccumulatingFunctionObject *);

These two virtuals are overridden in a specific flavor of a function object, implementing a method of determining whether two scheduled tasks can be coalesced into one and actually preforming the coalescing. Using a scheme like this dramatically reduces the ammount of updates performed when mimeset or other attribute modifying operation is in action.

This concludes the second part of this article. At this point I wish to thank the Metrowerks compiler for kindly interpreting specialized templates and converting them into an executable binary for me. Further, I would like to note that this article was written mostly in Gobe Productive and using it has been a truly enjoyable experience.


Be Engineering Insights: Pixel Packing Mama

By William Adams

As I write this I'm sitting by a pool typing on a Sony Vaio portable, which is of course running the BeOS. We've come a long way, baby! Why by a pool? Because my daughter Yasmin is taking swimming lessons, and seeing her and the other kids flailing around helplessly (with adult supervision) reminds me of my state of mind sometimes when I'm programming.

So I'm here thinking, you know, I bet every developer in the world is wondering how to do something as esoteric as easily read and write pixels in a BBitmap. So I queried at least one developer as to how they would write 16-bit pixels in a BBitmap, and thus an article was born.

In this article I would like to explore two issues. One is pixel formats, and the other is how to do a whole bunch of work without bothering the app_server.

For the first topic, let's look at some pixel formats typically found in the BeOS. In the file GraphicsDefs.h you'll find this big enum that contains color_space types that we support or at least define. You'll find these among them:

B_CMAP8
B_RGB32
B_RGB15

There are many others, but I'll focus on these for now. Are they big endian, little endian, what component goes where, and why should you care?

You'll also find the BBitmap object in the Kits. This is the well-defined object used to display bitmap images in the BeOS interface. When you construct a BBitmap, you give it a size, one of these defined color spaces, and a couple of flags to do stuff we won't worry about at the moment.

BBitmap(BRect bounds, color_space depth,
  bool accepts_views = false, bool need_contiguous = false);

The Kits also supply some ways to draw a BBitmap in a BView. There are eight different calls, but we'll concentrate on one for now:

void DrawBitmap(constBBitmap *aBitmap,
  BRect srcRect, BRect dstRect);

With this method you take a BBitmap and you tell it what part of it you want to display where in the BView. Nothing could be simpler. End of article...

But, in the interest of filling space, let's say you want to copy one BBitmap to another. Or even more interesting, you have some random bits lying around that aren't necessarily in a BBitmap right now, but you want to copy them into a BBitmap quickly and easily. First let's do some name wrangling. You can find all this code online at:

ftp://ftp.be.com/pub/samples/r3/interface_kit/pixelbuff.zip

class PixelBuffer
{
public:
      PixelBuffer(void *data, color_space, int32 width,
        int32 height, int32 bytesperRow);
  virtual ~PixelBuffer();


  virtual void SetPixel(
     constint32 x, constint32 y, constrgb_color &);
  virtual void GetPixel(
     constint32 x, constint32 y, rgb_color &);


  int32  Width()  {return fWidth;};
  int32  Height() {return fHeight;};
  int32  BytesPerRow()  {return fBytesPerRow;};
  color_space  CSpace() {return fColorSpace;};


protected:
  void   *fData;
  int32   fWidth;
  int32   fHeight;
  int32   fBytesPerRow;
  color_space  fColorSpace;
private:
};

This is a convenience class that can be used to represent any pixel buffer, including BBitmaps, BDirectWindow frame buffers, or just random chunks of memory. The most interesting thing it does is allow you to set and get pixels using the coordinates and an rgb_color structure. So, if you want to copy from anywhere to anywhere else, just do this:

void
copyPixels(PixelBuffer *dest, PixelBuffer *source)
{
  int numRows = source->Height();
  int numCols = source->Width();


  for (int row = 0; row < numRows; row++)
  {
    for (int col = 0; col < numCols; col++)
    {
      rgb_color aColor;
      source->GetPixel(col, row, aColor);
      dest->SetPixel(col, row, aColor);
    }
  }
}

You go row by row, pixel by pixel copying stuff as you need to and you're done, right? This is probably the slowest method on earth, but as long as the GetPixel() and SetPixel() methods do what they're supposed to, it will always be correct, no matter what the source and destination color spaces are. And of course you'd want to throw in some error checking, boundary constraints, and the like.

So what do these two methods have to look like? First, we'll introduce one more method. For any given location in a pixel buffer, we need to be able to find the pointer in memory that represents the start of that pixel. In all the cases we'll deal with here, pixels can align to byte boundaries. If we had 1-, 2-, or 4-bit colors, things would be slightly different. So here's a method to find the pointer to any particular pixel:

void * GetPointer(constint32, constint32);

void *
PixelBuffer::GetPointer(constint32 x, constint32 y)
{
  void *retValue=0;
  switch (CSpace())
  {
    case B_CMAP8:
    {
      uint8 indexValue;
      retValue = &((uint8 *)Data())[y * BytesPerRow() + x];
    }
    break;


    case B_RGB15:
    case B_RGBA15:
    case B_RGB16:
    {
      uint32 offset = (y*BytesPerRow() / 2)+x;
      retValue = &((uint16 *)Data())[offset];
    }
    break;


    case B_RGB32:
    case B_RGBA32:
    {
      uint32 offset = (y*BytesPerRow() / 4)+x;
      retValue = &((uint32 *)Data())[offset];
    }
    break;
  }
  return retValue;
}

With this method in hand, we can now do the GetPixel() call:

union colorUnion { rgb_color color; uint32 value; };

void
PixelBuffer::GetPixel(
  constint32 x, constint32 y, rgb_color &aColor)
const
{
  switch (ColorModel())
  {
    case B_CMAP8:
    {
      constcolor_map  *colors = system_colors();


      uint8 indexValue;
      indexValue = *((uint8 *)GetPointer(x,y));


      aColor = colors->color_list[indexValue];
    }
    break;

    case B_RGB15:
    case B_RGBA15:
    case B_RGB16:
    {
      uint16 indexValue = *((uint16 *)GetPointer(x,y));
      aColor.blue = (indexValue & 0x1f) << 3;  // low 5 bits
      aColor.green = ((indexValue >> 5) &0x1f) << 3;
      aColor.red = ((indexValue >> 10) &0x1f) << 3;
      aColor.alpha = 255;
    }
    break;


    case B_RGB32:
    case B_RGBA32:
    {
      colorUnion aUnion.value = *((uint32 *)GetPointer(x,y));
      aColor = aUnion.color;
    }
    break;
  }
}

What's going on here? We use the GetPointer() method to find the location of the pixel in the buffer, then we handle color conversion. That's really the crux of this and the SetPixel() calls. They do color conversion so you don't have to worry about it. These conversions can be as optimal as you like. The SetPixel() method is similar but reversed, so check the code online for that one.

Of course, SetPixel() and GetPixel() are the absolute minimum set of operations required to build any graphics system. Once you have these you can do primitive drawing of all kinds, but that's not what we're after here.

One more thing I'm after, though, is being able to draw icons into a BBitmap in a nice, efficient manner. I'll start by saying BBitmap objects are the greatest thing on earth, but they're not free. They are allocated in shared memory so that both the app_server and your application can have access to them. This saves on copying.

Since they're allocated in shared memory, that means they are of a minimum page size, which is 4K. If you have hundreds of icons in an application, the easiest thing to do is to create a BBitmap for each one of them. When it's time to display them, you call BView::DrawBitmap() and you're done with it. However, having hundreds of little 4K-minimum bitmaps, even if the actual number of pixels is a paltry 1024, may not be the most efficient thing to do.

You could load all your little icons into a single BBitmap and keep track of their relative locations within it, and use the DrawBitmap() method that takes a source rect and a destination rect. But let's say you want to go one step further. Your interface uses an offscreen bitmap for flicker-free updates. Many times you want to draw various icons into this offscreen BBitmap, and then display it to the screen. Well, the PixelBuffer object gives us the means to do the following:

void
DisplayIcon(constint32 x, constint32 y,
  PixelBuffer *dest, PixelBuffer *icon)
{
  int numRows = icon->Height();
  int numCols = icon->Width();


  for (int row = 0; row < numRows; row++)
  {
    for (int col = 0; col < numCols; col++)
    {
      rgb_color aColor;
      icon->GetPixel(col, row, aColor);
      dest->SetPixel(x+col, y+row, aColor);
    }
  }
}

#define img_width 25
#define img_height 11
unsigned char img_bits[] = {/* actual data goes here */};

PixelBuffer *icon =
  new PixelBuffer(img_bits, B_CMAP8, img_width,
    img_height, img_width);

To display the icon on the offscreen display at any location, do this:

DisplayIcon(10,10, fOffscreenBitmap, icon);

In this way you can create an icon that takes exactly as much memory as you need it to, which saves on memory resources. The second benefit is that you don't actually have to talk to the app_server in order for your icon to be drawn into your pixel buffer. And why not talk to the app_server? Because it's a busy team and you would probably rather not make requests if you really don't have to.

There's no real savings when all you're doing is drawing a single icon. As soon as you copy the bits for the icon, you'll be displaying the BBitmap, but when you want to batch the drawing of multiple icons, this will really help. That's typically the case when you're using an offscreen buffer to reduce flicker in your display.

The third benefit of this method is that the destination pixel buffer could easily represent the frame buffer of your display. In that case, when you draw your icon, it's on screen. No further processing or copying is required. This is a tremendous boost if your icons happen to be frames of decoded video that you're trying to display in real time. And by the by, who needs a copy constructor or operator = on BBitmap when you have these mechanisms.

So, there you have it. A little bit of abstraction on what a bitmap is, a little bit of color conversion, and you have a basic framework for flexibly dealing with bitmaps. There are many ways these methods can be optimized. For instance, you could add some methods to the PixelBuffer like this:

void SetPixel8(constint32 x, constint32 y, uint8 color);
void SetPixel15(constint32 x, constint32 y, uint16 color16);

The same goes for the GetPixel() methods. That way, the copyPixels() function and the DisplayIcon() functions can use an optimized interface based on the color space of the source and destination. You could also optimize the copyPixels() method by introducing a row at a time copy into the PixelBuffer object that is optimized in a similar fashion as the SetPixel8() and SetPixel15() methods. Once you have that, your life will be better, the sun will shine brighter, and all will be right with the world.

You can probably imagine other convenient things that can be done, like alpha blending and other point operations. But that's another story. I think I've been on the stage too long, so I'll get off now.


The European Dream

By Hans Speijer

It takes more than a web site to make a company truly international. Sometimes you have to leave cyberspace and touch down in the real world. This is often the time when a computer company realizes it needs a presence where the market is. Jean-Louis Gassée understood this when he asked Jean Calmon to start a Be Europe office in 1994. The wisdom of this decision is borne out by the size of the European software market—$37 billion in 1997.

In 1997 EITO/IDC estimated that in Y2K there would be 41 million households with a PC, and 19 million would be connected to the Internet. This means that a company relying exclusively on the Internet for its sales and advertising would not be reaching more then 50 percent of its potential customers in Europe. Even if the average BeOS user is more net savvy than the average Windows 98 user, the a virtual presence is still essential if you want to sell products in Europe.

Europe is also significant in software development, with many high-quality software products and engineers to offer. In fact, 26% of all registered BeOS developers, 37% of current BeWare entries, and 52% of the Master Award winners are from Europe. To name just a few prominent European developers: Attila Mezei (2 Master Awards and 2 Honorable Mentions), Maarten Hekkelman (1 Master Award and 1 Honorable Mention) and Marco Nelissen (1 Master Award and 7 BeWare entries).

In addition to its developer community, Europe has active user and hobbyist communities. One example of this is the demo scene that originated on the Commodore 64 and later boomed on the Commodore Amiga and the PC. These demo coders, musicians, and graphic artists became important contributors to the gaming and multimedia industries. And they took their experience and beloved systems with them into their professional lives.

Our first mission at Be Europe is to get as many shipping BeOS applications as possible. The BeOS platform needs applications to attract users. The more applications that show the power of the BeOS, the more copies of the BeOS we will sell, and the bigger the market for BeOS software developers. To help bring more applications to market we will assist current BeOS developers in any way we can. In addition, we're contacting companies in the multimedia sector about developing on the BeOS.

There's also lot we can do for non-European developers who want to sell their products in Europe. If you are one of them, keep us up to date in your project status and tell us where and when you need help. This goes for companies as well as for individual developers.

Our second mission is to build European distribution channels. Although the web is a good way to get products into customers' hands, many people (especially in Europe) are used to going to a shop, talking to a salesperson, looking at all the boxes, and playing with some demo systems. Not only do we need to get the BeOS into these shops, but we also need to make sure that people will be able to buy third-party products in the same shops. We've begun to explore the different channels and will gradually build indirect distribution, especially with the availability of a larger public BeOS release and more user-ready commercial applications in the near future.

Our third and final mission is to spread BeOS awareness in Europe. We do this by keeping close contact with the press, going to trade shows and conferences, and adding a good healthy dose of evangelism. In addition to our own activities on behalf of the BeOS, we owe thanks to our many supporters who speak positively about the BeOS; word of mouth is as good -- or maybe even better than—classical marketing. People often have more confidence in a friend's pitch about the virtues of a product than in an ad they see in the paper. To get the word out, we'll be on the road a lot, showing the BeOS at universities and informal gatherings of technology enthusiasts.

Not everything smells of roses and the streets are not paved with gold. We deal with more then 30 different countries, more than 15 different languages, and at least 9 significantly different cultures. In some countries the law even prevents us from putting the BeOS on the shelves if we don't have packaging and manuals in the native language, and many people will not buy a product that is not localized to their own language in any case.

We are only a small team (currently four persons), and we're all multitasking—but the Be spirit burns brightly in all of us. Let us know how you're doing and help us spread the BeOS gospel in Europe.


Developers Workshop: The Slave of Duty

By Doug Fulton
Q:

I've spent a decade stuffing the find, grep, awk, and sed syntax into the tips of my fingers. Steadfast and faithful, I like the shell; I feel a communion with the ancients in the necropsy of a diff. But how do I open a file from the command line? Of course I understand that I can invoke an application by name, but I concede that the Tracker's preferred app intelligence is greater than my $PATH. I want the Tracker to open the file for me. How?

-- Mabel Stanley, Penzance, England

A:

Wander not, Mabel. The short answer: Type

/system/Tracker filename

at the command line prompt and your file will open. But, you object, that's a lot to type and hard-coded pathnames make you queasy? And when your tiny alabaster fingers slip while typing ".alias" such that you open the non-existent ".alais" instead, the Tracker doesn't object to your wrong tree up-barking, but bounds slobberingly to the front gate with no slippers in its mouth? And, what's more, you are an orphan? Then try spoon feeding the app roster.

The little app shown below, which we'll call "open", calls on the app roster to open the files that are passed as command line arguments. The heart of the code is the Launch() call; Launch() finds and launches the preferred app for each file. This works fine for files, but it doesn't work for directories, so we catch the directories that are passed in and open them with Tracker (i.e. a directory is opened as a Tracker folder window). The code is small; you can fit the entire thing in your mouth:

#include <Roster.h>
#include <Application.h>
#include <signal.h>

int main(int argc, char **argv)
{
  int n;
  entry_ref ref;
  BEntry entry;
  status_t err;
  char *arv[1];
  BApplication("application/x-open");

  if (argc == 1) {
    printf("Usage: open <file1> <file2> <file3> ...\n");
    return -1;
  }

  signal(SIGINT, SIG_IGN);

  for (n = 1; n < argc; n++) {
    get_ref_for_path(argv[n], &ref);
    entry.SetTo(&ref, false);

    if (entry.IsDirectory()) {
      arv[0] = strdup(argv[n]);
      err = be_roster->Launch("application/x-vnd.Be-TRAK",
        1, arv);
      free(arv[0]);
    }
    else
      err=be_roster->Launch(&ref);

    if (err != B_OK && err != B_ALREADY_RUNNING)
      printf("open: %s\n", strerror(err));
  }
  return 0;
}

The only line that needs an explanation is the signal() call:

#include <signal.h>
...
  signal(SIGINT, SIG_IGN);

This tells open to ignore Control+C. We don't actually care that the signal affects open itself, but we do care about how a Control+C from the command line affects an app that's launched with open. For the launched app inherits open's stdio descriptors and signal handlers, and thus it is that Control+C will kill the launched app if we don't tell the handler to ignore it.

When you launch open, it does its thing and then dies—you don't have to heave it into the shell background (i.e. you don't have to "$ open file &"). However, you should use FileTypes to set open to be a "Background App". This will prevent open from (briefly) appearing as an icon in the DeskBar. You should also set it to be "Multiple Launch", just in case (of something that isn't worth describing).

How is open different from invoking /system/Tracker? Error handling, mainly. If you try to open a non-existent file (or a file that doesn't want to be opened for whatever reason), open will tell you about it. And for the scopophiliac, "open" lets you see the normally-suppressed printf() messages that are generated by the launched app. Fun you betcha sam.


Amiga Rumors...

By Jean-Louis Gassée

Lately, I've received several e-mail messages asking Amiga-related questions. Is it true we're going to rewrite the AmigaOS? Gateway "acquired" the Amiga—what is our relationship with Gateway?

As to the first one, no, we're not going to rewrite the AmigaOS. Personally, I don't think it needs rewriting. From the very beginning, it's been a modern OS. I remember how we feared its impact at Apple when the Amiga first came out in 1986. Multitasking, great graphics, sound, animation, and video. This was a gifted baby, and the Commodore family promptly adopted it.

To make a long and sad story short, in spite of the Amiga achieving sales in the vicinity of 5 million units, Commodore failed to invest in its future, then went belly up. The Amiga was sold to Escom, a German company, and later to Gateway when Escom closed shop.

To us at Be, the Amiga was an inspiration because of its audio and video capabilities. Also, we drew a distinction between Commodore and the Amiga, which investors didn't always do. I remember times during our fund raising when the mention of Amiga as a model drew alarmed looks. Telling them they were wrong wasn't really an option. Nevertheless, because of our old Amiga connection, I still have a license plate that reads AMIGA 96—given to me when we introduced the BeBox.

So, as rumors often do, the BeOS/Amiga rumors have a foundation. We always liked the Amiga and were happy to see Gateway adopt it. This is a strong company and their interest in new approaches to new media—as with the Destination PC—creates a nice potential cultural fit for the Amiga heritage.

As to the second question—about a BeOS-Gateway relationship—the recent availability of the BeOS on Intel-based PCs logically prompts sensible questions about Be, the AmigaOS, and Gateway.

Does this mean that Gateway is going to announce tomorrow a BeOS-based Amiga computer? That would be nice, but I doubt it. Speaking only for Be, combining two operating systems—or if you prefer, two sets of APIs -- looks like a major technical challenge. If the Amiga heritage isn't maintained in some way, the result of such a marriage is not an Amiga -- you might as well buy a straight PC. If, on the other hand, BeOS applications don't run on the new hybrid, it's a problematic proposition for Be developers.

In any case, I can't remember an instance when combining two sets of very different APIs resulted in success. At first glance, combining the Amiga and Be cultures and technologies looks like a nice, almost natural idea. Unfortunately, though, while some crossbreeding produces stronger hybrids, I fear that this one might disappoint.

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