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

Haiku Clipboard Transactions: BClipboard, MIME Formats, and Change Watching

Implement reliable Haiku clipboard reads and writes with BClipboard locking, MIME-typed BMessage data, commit and revert semantics, and change notifications.

Haiku’s BClipboard is a shared, short-term data exchange service, not a general document store and not a promise that the last copied value survives a reboot. Applications read or publish a BMessage containing one or more MIME-typed representations. The API’s lock, local message, commit, and revert operations form a disciplined edit sequence, but they do not turn the clipboard into a multi-application database transaction with a long-held system lock.

That distinction matters for race handling. A client edits a local snapshot while holding its BClipboard object locked. Another process can change the system clipboard while that edit is in progress. A production client must choose whether it is allowed to replace newer data, detect a competing change, or abandon the update and ask the user to retry.

Select the system clipboard deliberately

An application with a BApplication normally uses the global be_clipboard object. The Application Kit initializes it for the default system clipboard. Code that runs before an application object exists, such as a command-line utility, can construct BClipboard with the name "system"; the Haiku documentation explicitly describes that path. A custom clipboard name creates a separate named clipboard for cooperating applications. It does not become a private copy of the system clipboard, and applications must agree on the same name to share it.

Treat clipboard access as a direct consequence of user intent. A Copy, Cut, or Paste command is an understandable boundary for reading or replacing system clipboard contents. Polling it in the background or copying sensitive data into it as an invisible side effect surprises users and can overwrite something they meant to paste elsewhere.

The constructor’s transient argument is documented as currently unused. Do not build a persistence policy around it or promise that clipboard contents will be retained after restart. If an application needs durable history, it must implement an explicit history feature with its own storage and user controls.

Treat the payload as a MIME-typed message

BClipboard::Data() returns the clipboard’s data BMessage only while the object is locked. The message’s what field is unused. Each field is named for a MIME type and has type code B_MIME_TYPE; different fields can carry the same logical content in different formats. A text producer might publish text/plain and a richer representation such as HTML. A consumer should request the format it understands and fall back deliberately rather than assuming there is exactly one field or one encoding.

The bytes returned by a message lookup are owned by the message. Check the lookup status and the reported length, parse only within that length, and copy data into application-owned storage before unlocking if later work must outlive the locked access. Do not treat a clipboard field name as proof that the bytes are well-formed or harmless. MIME type identifies the representation; the application still validates its syntax and size.

Write by editing a local copy, then committing

The common write path is: lock, clear the local message, add representations, commit, and unlock. A failed Lock() means no Data() access is valid. Clear() affects the locked message being prepared; the new data becomes visible to other clients only when Commit() succeeds.

status_t CopyPlainText(BClipboard& clipboard, const char* text)
{
    constexpr size_t kMaximumBytes = 4 * 1024 * 1024;
    if (text == nullptr)
        return B_BAD_VALUE;
    size_t length = std::strlen(text);
    if (length > kMaximumBytes)
        return B_BAD_VALUE;
    if (!clipboard.Lock())
        return B_ERROR;
    struct UnlockOnExit {
        BClipboard& clipboard;
        ~UnlockOnExit() { clipboard.Unlock(); }
    } unlock{clipboard};

    BMessage* data = clipboard.Data();
    status_t status = data != nullptr ? B_OK : B_ERROR;
    if (status == B_OK)
        status = clipboard.Clear();
    if (status == B_OK) {
        status = data->AddData("text/plain", B_MIME_TYPE, text,
            static_cast<ssize_t>(length));
    }
    if (status == B_OK)
        status = clipboard.Commit();

    if (status != B_OK)
        clipboard.Revert();
    return status;
}

This is a focused method excerpt; the translation unit needs the appropriate Haiku headers, including Clipboard.h and Message.h, plus <cstring> for std::strlen. The four-megabyte cap is an application policy, not a Haiku API limit. Production code should preserve the first failure status and handle a Revert() failure separately for diagnostics. The local guard ensures the clipboard unlocks on every exit path.

When publishing multiple formats, add each independently and check each AddData() result before committing. A failed rich representation should not silently leave a half-built message. Decide whether the application should discard all new formats and revert, or deliberately publish a documented fallback such as plain text only.

Read defensively and keep borrowed bytes in scope

The reader follows the same lock discipline. It should check the requested field, its MIME type, the returned byte count, and any terminator or encoding requirements before using it. FindData() returns a pointer into the BMessage; copy the bytes if processing continues after the clipboard unlocks. For a text value, do not assume a terminating NUL when the stored representation is length-delimited.

status_t ReadPlainText(BClipboard& clipboard, std::string& result)
{
    if (!clipboard.Lock())
        return B_ERROR;
    struct UnlockOnExit {
        BClipboard& clipboard;
        ~UnlockOnExit() { clipboard.Unlock(); }
    } unlock{clipboard};

    const void* bytes = nullptr;
    ssize_t length = 0;
    BMessage* data = clipboard.Data();
    status_t status = data == nullptr
        ? B_ERROR
        : data->FindData("text/plain", B_MIME_TYPE, &bytes, &length);

    constexpr ssize_t kMaximumBytes = 4 * 1024 * 1024;
    if (status == B_OK) {
        if (length < 0 || length > kMaximumBytes
            || (length > 0 && bytes == nullptr)) {
            status = B_BAD_DATA;
        } else {
            const char* text = length == 0
                ? "" : static_cast<const char*>(bytes);
            result.assign(text, static_cast<size_t>(length));
        }
    }

    return status;
}

The example uses std::string’s length-aware assignment rather than reading past the stored buffer; its translation unit also needs <string>. The four-megabyte limit is an illustrative application policy, not a Haiku API limit. A real consumer should choose a maximum appropriate to its data format before allocating or parsing. If the requested field is absent, that is a normal unsupported-format path, not necessarily a corrupt clipboard; try the next representation your application explicitly supports.

Understand commit conflicts and counts

Commit(bool failIfChanged) is the API’s conflict-aware option: its documented purpose is to fail when clipboard data has changed. Use it when the application prepared a transformation based on the previously read contents and replacing a concurrent user’s copy would be wrong. On conflict, unlock, fetch a fresh snapshot, and reevaluate the operation. Blindly resubmitting the same stale message defeats the check.

Ordinary Copy and Cut commands usually represent a new user-directed value, so Commit() may be the intended operation. Do not infer that Lock() locks out every other process until Unlock(): the implementation keeps a local lock and uploads the edited message to the registrar during commit. LocalCount() is a locally cached commit count and can be stale. SystemCount() asks the system service for a fresher count at additional cost. Counts help detect that something changed; they are not content hashes or durable transaction IDs.

If an edit fails before commit, Revert() restores the local message from the system clipboard while the object remains locked. Preserve and report the original failure even if the revert also fails. Always unlock before returning to the event loop so another operation on the same object is not left blocked.

Watch for changes without assuming the notification is the data

StartWatching() registers a BMessenger target. The target receives B_CLIPBOARD_CHANGED when the clipboard changes; StopWatching() removes that registration. A notification is a reason to fetch a fresh snapshot, not a replacement for locking and reading the actual message. It contains no guarantee that the clipboard still holds the same contents by the time the handler runs.

Avoid expensive parsing in the watcher’s looper. Record that the clipboard changed, then schedule bounded work or compare the current system count where appropriate. Stop watching during teardown before destroying the target handler or its looper. Handle an invalid messenger and errors returned by the registration calls instead of assuming observation was established.

DataSource() returns a messenger to the application that last modified the clipboard. It can be useful for diagnostics or application-specific coordination, but it is not a substitute for reading the clipboard message and should not be treated as a permanent identity for the source process.

Test the race and lifetime boundaries

Verify a complete copy/paste cycle between two applications, not just within one process. Test plain text, each richer format, missing formats, empty content, large-but-allowed content, and malformed bytes. Force an AddData() failure or a commit failure where feasible and confirm the previous system value remains usable. Change the clipboard from another application between read and commit to exercise the conflict path.

Also test StartWatching() and StopWatching() through repeated window creation and destruction. Confirm handlers do not retain the Data() byte pointer after unlocking, and confirm every failed lock, lookup, allocation, or upload reaches Unlock(). The clipboard is a compact API, but reliable behavior depends on respecting its local snapshot, MIME contract, borrowed memory, and user-driven lifecycle.

Related:

Sources:

Comments