Skip to content
Haiku OSDeep Dive Published Updated 8 min readViews unavailable

Haiku Application Scripting: BMessage Suites and Discoverable Automation

How Haiku applications publish discoverable scripting suites, resolve property specifiers, validate BMessages, and return stable replies.

Haiku application scripting is a structured protocol built on the same messaging architecture used by native applications and windows. A controller sends a BMessage describing an operation, a property, and the desired object instance. The target resolves that description through its handler hierarchy, performs the operation, and sends a typed reply. The protocol can automate a running application’s real state without scraping pixels or coupling to private C++ objects.

The framework’s most important feature is discoverability. A scriptable object can publish suites describing its properties, supported commands, legal specifiers, and value types. A controller can inspect that description rather than assuming every application implements the same undocumented messages. For developers, this means scripting is a public API: names and reply contracts deserve compatibility discipline equal to a file format or command-line option.

Commands act on properties

The Be scripting model separates a command from the property it affects. Standard command constants include B_GET_PROPERTY, B_SET_PROPERTY, B_CREATE_PROPERTY, B_DELETE_PROPERTY, B_COUNT_PROPERTIES, and B_EXECUTE_PROPERTY. The command occupies the scripting message’s what field. A set request carries the new value in the documented data field.

A property is a scriptable feature, not necessarily one C++ member variable. An application’s Window property can represent a collection. A view’s Frame property may expose a calculated rectangle. An executable property can map to an action rather than stored data. This abstraction keeps the external vocabulary stable even if implementation classes change.

Property names must be programmatic identifiers. Reusing the visible label of a menu item is fragile because localization changes it and UI redesign may remove it. Document, Window, or Selection can be a stable scripting term while the interface displays a translated label. Document the case, allowed commands, type of data, return type, and side effects for each property.

Do not overload one property with several unrelated meanings based on hidden message fields. A controller can only construct a reliable request when the schema is explicit. New optional fields can extend a contract, but silently changing the type of result or interpreting an old command differently breaks existing automation.

Specifiers identify an instance

A property may have one value or many. Specifiers identify the desired instance. B_DIRECT_SPECIFIER is sufficient when the property itself uniquely identifies the target. Collections can support a name, numeric ID, forward index, reverse index, or range. The Be Book lists standard constants including B_NAME_SPECIFIER, B_ID_SPECIFIER, B_INDEX_SPECIFIER, B_REVERSE_INDEX_SPECIFIER, and B_RANGE_SPECIFIER.

Controllers add specifiers with BMessage::AddSpecifier(). That method performs scripting-specific bookkeeping; the scripting documentation explicitly warns that adding an ordinary nested message is not equivalent. Convenience overloads create common direct, name, index, and range forms, while a controller can construct a full specifier message for other forms.

Indexing contracts must be precise. State whether order is stable, what happens when the collection changes between requests, and which error an out-of-range index returns. A persistent object should expose an ID or another durable key when an index could point to a different item after insertion or deletion. A human-readable name is convenient but may not be unique.

Input is untrusted even when it originates on the same machine. Validate the specifier form, required field types, numeric bounds, and whether the selected object still exists. Never use a scripting-supplied index directly for memory access before checking the current collection.

The specifier stack routes through object hierarchies

A controller can usually obtain a BMessenger for an application more easily than for a deeply nested view or document object. The scripting framework solves this with a stack of specifiers. A request might describe the Frame of a particular View inside a named Window. The application resolves the outer target, forwards the request to it, and that handler resolves the next level.

The order is significant. The Be scripting guide explains that repeated AddSpecifier() calls build a stack evaluated in reverse order. Code commonly adds the innermost desired property first and the application-level object last. Each handler peels the specifier it understands, then directs the remaining message toward the selected child.

BHandler::ResolveSpecifier() is the routing hook. An override examines the current property and specifier and returns the handler that should process the next stage. If the object does not own that property, it should delegate to its parent implementation rather than swallowing inherited scripting support. Once resolution reaches the responsible object, MessageReceived() performs the command.

Routing must not bypass lifecycle and locking rules. The target may disappear while a request is in flight, or a scripting call may reach a window from another team. Resolve to objects whose lifetime is protected by the owning looper, perform UI mutations on that looper’s thread, and return a failure if the requested child no longer exists.

Suites make the API discoverable

A scripting suite is identified by a MIME-like string, conventionally using the suite/ supertype. Two objects can implement the same suite and offer compatible properties even if their internal classes differ. This makes suites useful for tools that operate on capabilities rather than hardcoded application versions.

BHandler::GetSupportedSuites() publishes an object’s suites. The standard response can include suite-name strings and flattened BPropertyInfo objects. BPropertyInfo wraps arrays of property_info and value_info, can flatten and unflatten its description, and provides FindMatch() to select the entry compatible with an incoming command and specifier.

Each property description can list accepted commands, specifier forms, usage text, and expected value types. Keep this declaration synchronized with ResolveSpecifier() and MessageReceived(). Advertising a set operation that always fails, accepting a type not listed by the suite, or returning an undocumented type defeats discovery.

A test can query supported suites and automatically compare the flattened declaration with a table of valid and invalid requests. That catches drift when a new property is implemented in one switch statement but omitted from the public suite. It also lets an interactive inspector render available operations without application-specific source code.

Replies are part of the contract

Every scripting request expects a reply. The Be documentation states that the reply includes an error field containing a status code. A successful get request additionally places the requested value or values in the result field. A message the object does not understand is answered with B_MESSAGE_NOT_UNDERSTOOD rather than left to time out without explanation.

Typed errors make automation diagnosable. Distinguish an unknown property, unsupported command, malformed specifier, wrong value type, missing object, permission failure, and internal operation error. A generic failure for every condition forces controllers to parse human text or retry blindly.

Reply exactly once. Synchronous BMessenger::SendMessage() variants can wait for a response, so forgetting the reply can block the controller until timeout. Conversely, sending a preliminary success and later discovering that the mutation failed creates false state. Validate first, perform the operation under the correct looper, then construct the final reply.

Large data sets do not belong in an unbounded reply. A suite can expose count and indexed or ranged access, letting controllers page through a collection. For streaming bulk content, a file, pipe, socket, or dedicated command-line interface may be more appropriate, with scripting used to initiate the operation and report status.

Security and transactional behavior

A local message crosses a process and trust boundary. A scripting interface that can delete documents, overwrite paths, launch programs, or export secrets needs the same authorization review as another IPC surface. Do not infer safety merely because the sender has a local team ID. Validate file references, reject traversal or unexpected schemes, and apply the application’s existing confirmation and permission policy consistently.

Avoid exposing raw object addresses or implementation tokens as durable IDs. They leak layout details, become invalid after restart, and invite unsafe casts. Use explicit identifiers with scoped lookup and clear lifetime. Do not unflatten arbitrary custom types unless their size, version, and invariants are validated.

Multi-property changes need a defined atomicity model. If a controller sets width and height in separate requests, observers may see an intermediate state. A suite can expose one structured property or an execute command that validates the whole update before applying it. If partial application is possible, document which fields changed and return enough information for recovery.

Scripting operations should also respect application state. A command that starts a long export can return an operation identifier and expose progress or cancellation through properties, rather than holding a synchronous reply open indefinitely. The running application stays responsive and the automation receives a stable object to inspect.

Test the protocol independently of the UI

Build tests from the suite contract. Query discovery, then exercise every command/specifier combination with valid values, boundary indices, wrong types, missing fields, stale IDs, and unsupported operations. Verify the error and result fields rather than accepting any reply as success. Test localized UI settings to prove that scripting names remain unchanged.

Test nested resolution across the actual application hierarchy and close a target object while requests are pending. Send concurrent read requests and serialized mutations to confirm the looper ownership model. Fuzz message fields within bounded resources because malformed IPC must not crash the application.

A strong Haiku scripting API is small, discoverable, typed, and boringly stable. It complements a GUI when automation needs the state of a running application and complements a CLI when operations are naturally object-oriented. By grounding commands in documented properties, resolving targets through specifiers, and treating replies as a durable public protocol, an application becomes automatable without making its internal classes public.

Related:

Sources:

Comments