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

Haiku USBKit: Device Enumeration, Descriptor Trees, and Transfer Lifetimes

Use Haiku USBKit to monitor hot-plug, inspect configurations and endpoints, select alternate interfaces, and handle transfer and object lifetimes.

Haiku’s USBKit exposes USB devices to userland through a roster and an object tree: BUSBRoster reports attached and removed devices, BUSBDevice describes a device, configurations contain interfaces, and interfaces expose endpoints. This is a different API boundary from writing a kernel USB controller or device driver. It is also more specific than checking whether a USB identifier appears in a system inventory: a client must understand descriptors, choose a valid configuration and interface, select an endpoint matching its protocol, and treat hot-plug as a lifetime event.

USBKit is documented as part of Haiku’s Device Kit and its public header is USBKit.h. The following workflow is grounded in Haiku’s current upstream API documentation and headers. Confirm the exact behavior against the headers installed with the Haiku revision you target, especially when building against a nightly.

Start from the roster, not a remembered device path

Subclass BUSBRoster and implement DeviceAdded() and DeviceRemoved(). Start() begins monitoring for USB device additions and removals; Stop() stops the roster looper. In Haiku’s current implementation the roster discovers entries below /dev/bus/usb and watches that hierarchy, so start the roster before relying on removal notifications and stop it as part of orderly teardown.

The DeviceAdded() return value controls the object lifetime. Returning B_OK keeps the initialized BUSBDevice object alive for the roster and causes DeviceRemoved() to be called when it disappears. Returning another status causes the object to be deleted, and the removal callback is not called for it. DeviceRemoved() receives an object that becomes invalid and is deleted after the callback returns. Remove references and stop work that could dereference it before returning.

class DeviceWatcher : public BUSBRoster {
public:
    status_t DeviceAdded(BUSBDevice* device) override
    {
        if (device == nullptr || device->InitCheck() != B_OK)
            return B_ERROR;

        fCurrentDevice = device;
        InspectActiveConfiguration(device);
        return B_OK; // Keep this roster-owned object until removal.
    }

    void DeviceRemoved(BUSBDevice* device) override
    {
        if (device == fCurrentDevice)
            fCurrentDevice = nullptr;
        StopTransfersFor(device);
        // Do not delete or retain device after this callback returns.
    }

private:
    BUSBDevice* fCurrentDevice = nullptr;
};

This excerpt assumes the helper functions and header are supplied by the application. A real watcher must handle more than one device, avoid doing long work on the roster’s callback path, and synchronize any worker that uses the device. BUSBDevice and its child descriptor objects are not independent reference-counted handles; their lifetime is tied to the roster callback and parent objects.

Read descriptors as a hierarchy

BUSBDevice exposes vendor, product, version, class, subclass, protocol, and string descriptor accessors, as well as CountConfigurations() and ConfigurationAt(). Check InitCheck() after constructing a device directly from a path. String descriptors can be unavailable; the convenience methods return empty strings in that case, so do not use manufacturer or product text as a required unique identifier.

Use ActiveConfiguration() to inspect the current configuration. A configuration provides CountInterfaces() and InterfaceAt(); an interface exposes class information, alternate settings, and endpoints. The returned pointers are owned by the parent chain. Destroying or resetting a BUSBDevice invalidates its configurations and descendants. Likewise, changing an interface’s alternate setting invalidates endpoint objects retrieved from that interface; reacquire the endpoints afterward.

The configuration’s array index is not necessarily the USB descriptor’s bConfigurationValue. Haiku’s public header calls out this distinction. Pass the BUSBConfiguration* returned by ConfigurationAt() to SetConfiguration() rather than treating an array index as a wire value or constructing a configuration object yourself.

void InspectActiveConfiguration(const BUSBDevice* device)
{
    const BUSBConfiguration* configuration =
        device->ActiveConfiguration();
    if (configuration == nullptr)
        return;

    for (uint32 i = 0; i < configuration->CountInterfaces(); ++i) {
        const BUSBInterface* interface = configuration->InterfaceAt(i);
        if (interface == nullptr)
            continue;

        for (uint32 j = 0; j < interface->CountEndpoints(); ++j) {
            const BUSBEndpoint* endpoint = interface->EndpointAt(j);
            if (endpoint == nullptr)
                continue;

            printf("interface %u endpoint %u: bulk=%d input=%d\\n",
                i, j, endpoint->IsBulk(), endpoint->IsInput());
        }
    }
}

This read-only excerpt checks nullable results at each level. Descriptor class values and endpoint numbers should be interpreted according to the USB class specification and the device’s actual descriptors, not guessed from a product label. A class-specific or vendor-specific descriptor may need a parser for its own format; OtherDescriptorAt() is the API for retrieving generic interface descriptors where available.

Select configuration and alternate setting carefully

USB devices may expose multiple configurations. Before changing one, inspect the available configuration descriptors and understand what the application needs. SetConfiguration() activates the selected object and returns a status; the call can fail. After switching, discard assumptions derived from the prior active configuration and retrieve the active interface and endpoint objects again.

An interface can have alternate settings, often used to vary endpoint arrangements or bandwidth-related behavior. AlternateAt() lets code inspect an alternate’s descriptor, but endpoints reached through that inspection-only alternate are not usable for I/O. Switch the live interface with SetAlternate(), then call EndpointAt() on the interface object you obtained from the configuration. Because setting an alternate invalidates prior endpoint objects even if the requested alternate matches the current one, always refresh pointers after the call.

Use endpoint attributes rather than hard-coded transfer assumptions. USBKit exposes IsBulk(), IsInterrupt(), IsIsochronous(), IsControl(), IsInput(), IsOutput(), MaxPacketSize(), and Interval(). Match the endpoint transfer type, direction, size expectations, and device protocol. A bulk endpoint is not interchangeable with an interrupt endpoint merely because both can move bytes.

Issue transfers with explicit buffer and result handling

BUSBDevice::ControlTransfer() issues a request on the default pipe. BUSBEndpoint provides control, bulk, interrupt, and isochronous transfer methods. The transfer methods return the actual byte count or an error code; code should treat a negative result as failure and should not assume that every successful transfer filled the requested buffer. Validate the received length against the operation’s protocol before parsing.

ssize_t SendCommand(const BUSBEndpoint* endpoint,
    void* buffer, size_t length)
{
    if (endpoint == nullptr || !endpoint->IsBulk()
        || !endpoint->IsOutput())
        return B_BAD_VALUE;

    ssize_t transferred = endpoint->BulkTransfer(buffer, length);
    if (transferred < 0)
        return transferred;
    if (static_cast<size_t>(transferred) != length)
        return B_ERROR; // Apply the device protocol's short-write policy.

    return transferred;
}

The exact error-recovery policy belongs to the class protocol. Some exchanges permit short transfers or require additional requests; others require an exact message length. Keep those conditions explicit and test them with both real hardware and controlled failure paths. Avoid parsing past the returned byte count or reusing a buffer whose ownership has moved to another worker.

USBKit’s direct transfer calls are not a substitute for a responsive application architecture. If a request can take noticeable time, perform it away from the window’s message loop and report completion through the normal application messaging path. Cancellation and device removal must wake or fail that work so a worker cannot wait forever on a device that has gone away.

Treat hot-plug as invalidation, not just notification

On removal, the roster’s BUSBDevice becomes invalid and is deleted after the callback. Any configuration, interface, endpoint, descriptor pointer, or worker state that can reach it must be quiesced. Set a removal flag, stop scheduling new transfers, coordinate in-flight work, clear stored pointers, and only then let the callback return. Do not cache a BUSBEndpoint* in a long-lived object without a parent lifetime rule.

If a reconnect occurs, treat it as a fresh enumeration. Reinspect descriptors, choose the active configuration, rebuild interface and endpoint state, and restore application policy. A similar VID/PID does not prove that the device instance, firmware revision, topology, or selected alternate is identical. Serial strings can be empty, and user-facing product strings are labels rather than protocol guarantees.

BUSBRoster is the right place to observe device arrival/removal, but it does not guarantee that every USB class works with generic transfers. Many functions belong to a system class driver and a higher-level Haiku Kit such as Media or Input. Prefer that class API when it provides the operation your application needs; raw descriptor work should have a clearly stated class or vendor protocol and a tested device matrix.

Build a reproducible validation matrix

Test initial enumeration, a device already present when the roster starts, attach, detach during idle, detach during transfer, rapid reconnect, multiple identical devices, missing string descriptors, an unsupported configuration, and SetAlternate() while stale endpoint pointers exist. Check that Stop() and destruction stop watching before callback targets disappear. Test short and failed transfers and ensure the application reports the actual outcome rather than logging every return as success.

Record the Haiku revision, architecture, controller, device VID/PID and revision, selected configuration, interface/alternate, endpoint address and type, byte counts, and removal sequence. Keep raw paths and roster objects transient; reconnect by observing the new object and evaluating its descriptors again. This evidence separates a device that never enumerated from one that was enumerated but lacked the expected interface or failed during a transfer.

USBKit is powerful because it exposes a structured device view without forcing every client to become a kernel driver. Its safe use depends on honoring that structure: roster callbacks define device lifetime, configurations and alternates change the object graph, endpoints encode transfer contracts, and returned lengths define what the application may parse.

Related:

Sources:

Comments