Using Scripting in Haiku

Body

With this article I hope to explain the basics of scripting using BeOS and how it can enhance the Haiku project. To try out several examples mentioned in this article, you'll have to download a program called hey, which enables you to script BeOS applications from a Terminal.

The program hey was developed by Atilla Mezei, an early BeOS developer, who unfortunately seems to have left the BeOS community. He created this tool to have a good testing tool when implementing scripting in BeOS applications.

As an example, I'm going to use my pet project, the print kit for Haiku. Or to be more specific, the print_server. The print_server presents most of the functionality of the printing architecture of BeOS, and is used by both the BPrintJob class and the Printers preference application.

The requests to the print_server range from selecting a printer to putting up a page setup dialog for a specific printer. Also removing or adding a printer is actually done by the print_server. Here's an example of using hey with a scriptable application:

$ hey print_server DO PageSetup OF Printer "MyPSPrinter"

This will make the print_server put up the page setup dialog for the printer named "MyPSPrinter", and return the settings made in the reply BMessage object. Another option is:

$ hey print_server CREATE Printer WITH name = "DummyPrinter" AND driver="Postscript" AND Transport="Serial Port" AND TransportAddress="ser1"

Which will create a printer with the name "DummyPrinter" using the Postscript printer driver and using the 1st serial port as output port.

To see what other commands the print_server will respond to, tell it to show its vocabulary:

$ hey print_server GETSUITES

Also, the classes from the Application and Interface kit both contain lots of scripting support. This is all described in the BeBook, as all these classes have a Scripting Support table explaining what you can script with them. Just for a cool example, try this:

$ hey Tracker GET Frame OF Window 1

You'll see returned a BRect() description that looks like the resolution of your current workspace. If you do this:

$ hey Tracker SET Frame OF Window 1 TO BRect[0,0,200,200]

The window of the Tracker will be resized to a really small one. If you now drag your Terminal around, you'll see that the largest part of your screen will not get updated anymore, as the window is smaller, and outside that window there is nothing :)

Another useful one is this:

$ hey Terminal SET Title OF Window 0 TO "Ithamar's teaching me Scripting :)"

This is mostly useful if you're opening a lot of Terminals and want to quickly see which is which in the window list from the Deskbar. Hope this shows a few simple neat tricks with BeOS Scripting :)

Implementing scripting in the print_server

How does this work internally? Well, I'll explain to you how I did it within the print_server. Most of this code is in the current Haiku tree, so you should be able to view it from the web or download it using SVN tools on your favorite platform.

Please note that I'm assuming, for the sake of space saving here, that you've read the Application Kit's introduction to messaging, and have had some experience with messaging on the BeOS (that means, you've written a GUI app for BeOS, that had a clickable something, like menus, or buttons, or such).

First thing to do is to make the application object scriptable. As described in the BeBook (see links on the bottom of this article), this involves basicaly a few simple overrides. First of all, we'll add a method called GetSupportedSuites() to the application class, which looks like this:

status_t
PrintServer::GetSupportedSuites(BMessage *message)
{
message->AddString("suites", B_PSRV_PRINTSERVER_SUITE);

BPropertyInfo prop_info(prop_list);
message->AddFlat("messages", &prop_info);
return Inherited::GetSupportedSuites(message);
}

This method is called by the BHandler class as soon as any app sends the B_GET_SUPPORTED_SUITES message to it.

As you can see, it adds a string "suites" to the message passed, that should look like "suites/vnd.VENDOR-NAME" where VENDOR and NAME are chosen by you. Next, it adds a flattened object of type BPropertyInfo under the name "messages", which are the actual scripting commands which are defined under the above mentioned scripting suite. The format of this table we will see later on in this article.

Next thing is that the application object will have to know how to resolve the scripting specifiers, like "OF Printer 1", which is handled by a method called ResolveSpecifier. See an example of such a method below:

BHandler*
PrintServer::ResolveSpecifier(BMessage *message, int32 index,
BMessage *specifier, int32 what,
const char *property)
{
BPropertyInfo prop_info(prop_list);
Printer* printer;
switch(prop_info.FindMatch(message, 0, specifier, what, property)) {
case B_ERROR: // Not found!
break;
case 0: // Return Printer handler
<snip>
break;
case 1: // Create new Printer and return it
<snip>

break;
default:
return this;
}
return Inherited::ResolveSpecifier(message, index, specifier, what, property);
}

I've snipped a few pieces of code to keep this listing short -- look at the Haiku tree for details on the snipped code. Basically, it creates an object of type BPropertyInfo again, using the scripting suite description table, and asks the object to find a match with what it gets in the arguments. It then returns an index in the scripting suite description table of the match found, or B_ERROR if no match was found. The arguments are as follows:

  • message

    Complete scripting message (message->what will be one of B_GET_PROPERTY, B_SET_PROPERTY, etc)

  • specifier

    The current specifier (Printer 0, PageSetup, etc.)

  • what

    short for message->what

  • property

    The name of the property referenced in the current specifier (Printer, PageSetup, etc.)

What this method should do, is simply return a BHandler to the object that the specifier resolves to. For the print_server, I create a BHandler derived object for every printer definition on the system. When I get a "OF Printer x" in here, I lookup the printer definition object, pop the specifier (BMessage::PopSpecifier()), and return the printer definition object (a BHandler derived object). This will let the specific printer def object handle the next specifier.

If you've found a specifier which is an end-point, so to speak, like the PageSetup specifier in one of my examples above, you can just return this and the specifier will be sent to the MessageReceived() function, so you can handle it there. If you cannot recognize the message, call the inherited version of ResolveSpecifier(), and the classes will see if they can "get" it, and if not, return a "not understood" message to the scripting originator.

This is basically all :) You repeat this code for every object that needs to understand scripting (this example was done with the PrintServer application, but also my Printer object can understand printing of course!) and you're set.

Hope this clarifies it a bit -- for more in depth explanations, only trust the original authors :) :

  • The BeBook: Application Kit: Scripting
  • Be Developer Library: Scripting