Haiku BArchivable: Object State, Versioning, and Safe Reconstruction
Archive Haiku objects with BArchivable by preserving base state, versioning custom fields, validating input, and rebuilding runtime dependencies safely.
BArchivable is Haiku’s base protocol for objects that can be represented in a BMessage and later reconstructed. It is used by many Interface Kit objects and provides a consistent way to preserve view configuration, replicants, and other reconstructable state. Archiving is not the same as saving a complete application database: raw pointers, open files, active threads, locks, and in-flight operations do not become valid simply because their numeric values were copied into a message.
Design an archive as a versioned description of an object, not as a memory dump. Store stable identifiers and user-visible configuration, preserve inherited class state by invoking the base implementation, and rebuild transient dependencies after instantiation. Validate archived values as untrusted input even when the archive was originally produced by the same application; files can be edited, truncated, copied from another release, or supplied by another process.
Preserve the base class contract
A custom class that derives from an archivable Haiku class should normally extend the base archive rather than replacing it. Call the base implementation first, check its status_t, and then add namespaced fields for the custom state. A custom static Instantiate() method should verify that the archive represents the expected class before constructing the object.
status_t ProjectView::Archive(BMessage* into, bool deep) const
{
status_t status = BView::Archive(into, deep);
if (status != B_OK)
return status;
status = into->AddInt32("com.example.project:archive_version", 1);
if (status == B_OK)
status = into->AddString("com.example.project:project_id",
fProjectID.String());
return status;
}
BArchivable* ProjectView::Instantiate(BMessage* archive)
{
if (!validate_instantiation(archive, "ProjectView"))
return NULL;
return new ProjectView(archive);
}
This excerpt illustrates the shape of an override, not a complete class definition. Include the relevant Haiku headers, handle allocation failure where the project requires it, and use a class name consistent with the archived class field and registration path. BView::Archive() is the appropriate base call only when the class derives from BView; substitute the actual direct base class in other hierarchies.
The deep argument matters for composite objects. A deep archive can include child objects according to the base class’s archiving behavior; a shallow archive may preserve only the object itself. Check each class’s API contract and avoid archiving a child tree twice. Test the actual object graph produced by the selected depth rather than assuming every associated resource is included.
Reconstruct through the class’s archive constructor
An archive constructor receives a BMessage* and should initialize the base class from that message before restoring custom fields. Read required fields with explicit type checks, provide safe defaults for optional older fields, and reject values outside the object’s supported range. Do not continue with a partially initialized object when a required identifier or invariant is missing.
Version custom fields so future code can distinguish an old archive from a new one. Versioning does not mean every field needs a version prefix; a single class-level integer is often enough. When a field is added, choose a default that preserves the behavior of older archives. When a value changes meaning, migrate it explicitly rather than interpreting the old representation as if it were already new.
ProjectView::ProjectView(BMessage* archive)
: BView(archive),
fProjectID()
{
int32 version = 0;
if (archive == NULL
|| archive->FindInt32("com.example.project:archive_version",
&version) != B_OK
|| version < 1 || version > 1) {
fArchiveStatus = B_BAD_DATA;
return;
}
if (archive->FindString("com.example.project:project_id",
fProjectID) != B_OK || fProjectID.IsEmpty())
fArchiveStatus = B_BAD_DATA;
}
The sample uses a deliberately narrow version range and assumes the class exposes an fArchiveStatus field checked by its factory. If an archive constructor cannot return a status directly, define a clear initialization-check pattern and do not expose a failed instance as fully usable. On upgrades, add explicit migration branches for older supported versions rather than permanently rejecting every version below the current one.
Use namespaced field names to avoid collisions when several classes contribute to one BMessage. Avoid storing localized labels as identifiers or relying on what values alone to determine schema. Typed Find...() calls detect some mismatches, but applications still need semantic validation of strings, sizes, enum values, and object relationships.
Store state, not runtime handles
Good archive candidates include a stable model ID, window geometry, a selected mode, or a user preference. Poor candidates include a BHandler*, BLooper*, thread ID, semaphore ID, file descriptor, memory address, mutex state, or a pointer into another process. Those handles are meaningful only in a particular runtime and must be recreated or resolved after restoration.
If the object depends on a document, service, device, or file, archive a stable reference appropriate to that domain and resolve it after construction. The referenced object may have been deleted or become unavailable. Handle the missing dependency as a normal restore state, show a useful recovery path, and avoid dereferencing a stale in-memory address from the archive.
Do not store secrets in an archive merely because the message is convenient. A BMessage can be flattened or transferred, and an archive may be copied into a user-visible settings file. Store credentials in the appropriate secure facility and archive a reference or non-sensitive identifier. Treat file paths and serialized fields as data, not as proof that the current user is authorized to access the referenced resource.
Instantiate only recognized classes
instantiate_object() and class-specific Instantiate() methods allow archive consumers to reconstruct known types. Validate the class name and expected object role before constructing or attaching the result. An application that accepts arbitrary archives from disk should not blindly instantiate every class name it encounters and attach it to a privileged window. Limit accepted classes to the extension points the application intends to support.
For custom classes, ensure the registration or instantiation path is available in every process that may read the archive. A class that was registered in the writer process does not automatically exist in another process or an older build. If the application cannot instantiate an optional custom object, preserve the rest of the user document where possible and report which component could not be restored.
An archive is not a cryptographic integrity check. If integrity or authenticity matters, use a separate signed or authenticated container and validate it before instantiation. Even a valid signature does not eliminate schema validation because a trusted older application may have emitted data the current version no longer supports.
Treat deep object graphs and ownership carefully
When a view archives child views, reconstruction can create an object tree with ownership relationships. Do not separately add a reconstructed child that the parent already restored, and do not attach one view to two parents. Keep a clear owner for every object and verify parent/child invariants after loading.
Archiving a control may preserve its message and other state, but the runtime target or application service may need to be reconnected after restore. Rebind targets only after the receiving handlers exist and confirm that restored commands map to supported operations. Do not assume a serialized message target is a durable cross-session routing address.
For large documents, avoid building one unbounded BMessage containing duplicate copies of all content. Store bulk data in a format designed for it and archive a versioned reference or summary. Check flattened size, nested message depth, array counts, and total memory before accepting user-provided archives.
Test migration, corruption, and partial restoration
Keep fixtures from each supported archive version. Test current-version round trips, old-version migration, unknown optional fields, missing required fields, wrong field types, oversized strings, invalid enum values, missing dependencies, and malformed class identifiers. Verify both successful restoration and a useful failure path that does not leak partially created objects.
Round-trip tests should compare semantic state, not byte-for-byte serialized output unless the API promises a canonical representation. Field order or harmless defaults may change without changing object meaning. Separately test child ownership and target rebinding after instantiation, because a serialized message can look correct while the live object graph is invalid.
Treat BArchivable as an object reconstruction protocol with an explicit schema and lifecycle. Preserve base state, validate custom fields, reconstruct transient dependencies, and keep archive readers compatible with the versions users actually have on disk.
Related:
- How to Build a Haiku Replicant That Can Live on the Desktop or Deskbar
- Haiku BFlattenable: Type Codes, Buffer Bounds, and Stable Wire Formats
Sources: