Haiku Kernel Image APIs: Add-On Loading and Symbol Lifetimes
Load Haiku add-on images with checked IDs, resolve a narrow symbol ABI, coordinate active calls, and unload only after all pointers are retired.
Haiku exposes kernel image APIs for loading an add-on binary into the caller’s address space, looking up exported symbols, and unloading the image. The central lifecycle is load_add_on() -> get_image_symbol() -> use the resolved entry point -> stop all activity that can enter the add-on -> unload_add_on(). The returned image_id identifies a loaded image; any function or data pointer obtained from that image is only meaningful while the image remains loaded.
These APIs solve dynamic module loading, not process isolation. load_add_on() loads into the calling team’s address space. load_image() is a different API: it starts a separate program image and returns a thread_id for its entry point. Choose based on architecture: an in-process plugin shares ABI and process lifetime; a separate team has a process boundary and different IPC/lifecycle requirements.
Load and resolve with status checks
The public declarations live in <image.h>. load_add_on() returns an image_id or an error code; get_image_symbol() reports status and writes the address through its final output argument. Use B_SYMBOL_TYPE_TEXT for executable text symbols and B_SYMBOL_TYPE_DATA for data. B_SYMBOL_TYPE_ANY is available where a caller deliberately accepts either section, but a plugin entry point should normally be a function symbol.
#include <image.h>
#include <SupportDefs.h>
typedef status_t (*plugin_init_v1)(uint32 hostApiVersion);
image_id image = load_add_on("/boot/home/config/non-packaged/add-ons/sample.so");
if (image < B_OK)
return (status_t)image;
void* symbol = NULL;
status_t status = get_image_symbol(image, "sample_plugin_init_v1",
B_SYMBOL_TYPE_TEXT, &symbol);
if (status != B_OK) {
unload_add_on(image);
return status;
}
plugin_init_v1 initialize = reinterpret_cast<plugin_init_v1>(symbol);
status = initialize(1);
This is a lifecycle sketch, not a complete plugin manager. The sample path is illustrative and should be replaced with the application’s actual configured add-on location. The reinterpret_cast is an ABI boundary: the caller and module must agree on calling convention, parameter layout, return types, symbol spelling, and lifetime. Prefer a small C-compatible entry point with a versioned function table over exporting compiler-specific C++ object layouts, exceptions, or template types across module boundaries.
Handle both resolution and initialization failures. If a symbol lookup fails, unload the image only after no other pointer or callback from it was published. If initialization partially registered callbacks before returning failure, run the documented rollback path before unloading. Never continue to call a null function pointer or treat a non-B_OK status as success.
The kernel image API reports symbol-resolution mechanics. It does not validate that the plugin’s interface version is compatible with your application. Have the entry point return or fill a versioned interface structure and reject incompatible versions before registering the module for normal work. Keep required structure sizes and optional function pointers explicit so future versions can add capabilities without changing the meaning of older fields.
Symbol types and address lifetimes
get_image_symbol() takes the image ID, exact symbol name, symbol type, and output pointer. A symbol address is not an owned object and does not increment the image’s lifetime. Store the image_id alongside every resolved function pointer and make the plugin manager the sole authority that decides when to unload. Do not distribute untracked raw pointers to arbitrary components.
The lifetime rule applies beyond direct calls. An add-on can hand the host a vtable whose methods point into module text, a callback pointer, a static-data pointer, or an object whose destructor implementation is in the add-on. Every one of those becomes invalid when the image is unloaded. Destroy plugin-created objects and unregister callbacks before unloading; invoke destructors while their code remains mapped. Also stop timers and worker threads that might still jump into the module.
If a plugin API returns an opaque handle, define whether the host or add-on destroys it, and whether that destructor must run before unload_add_on(). Avoid exceptions crossing the module boundary unless both sides intentionally share a compatible C++ runtime and ABI. A plain C-compatible struct of function pointers and opaque handles is easier to version and test.
Coordinate calls and unload as a state machine
An image is not safe to unload merely because the UI no longer displays a plugin. Model module states such as unloaded, loading, active, stopping, and unloading. During shutdown, first prevent new calls from being admitted, then request cancellation, wait for in-flight calls to finish, release plugin-owned objects, unregister event sources, and finally unload the image. If a callback can re-enter the host while shutdown is in progress, make the state transition and callback admission rule explicit.
Do not hold a global plugin-manager lock while calling unknown plugin code. A plugin may synchronously call back into the host or trigger another lookup and deadlock. Instead, acquire a counted lease or increment an in-flight-call counter while the module is active, release the manager lock, invoke the entry point, then decrement the count and notify shutdown waiters. The exact synchronization design belongs to the host; the image API does not provide it.
For an asynchronous plugin API, retain a module lease for every outstanding callback/task. Cancellation is cooperative unless the ABI says otherwise. A successful cancel request is not proof that the plugin can no longer execute; wait for completion or a documented quiescent point before unload. If shutdown times out, keep the image loaded and report that it could not be safely unloaded rather than freeing code that may still be executing.
Unload once, after retirement
unload_add_on(image) returns status_t; check its result. Do not call it repeatedly for the same retired state unless the API contract specifically permits a retry after a reported failure. Keep an explicit loaded flag or state so a second cleanup path cannot double-unload an ID. If unload fails, preserve the diagnostic and avoid using the old symbol pointers regardless; their validity after an unsuccessful unload attempt is not a substitute for normal call management.
The reverse order of ownership is a useful rule: stop host-to-plugin calls, unregister plugin callbacks, destroy plugin objects, release any data owned by the plugin, then unload the image. Do not let an image_id integer outlive the manager state that tracks it. Avoid storing a function pointer in a preference file or message for later use; symbol addresses are process-local and image-specific.
If the add-on is not needed after a single operation, unload it only after all operation resources have been released. If it is a persistent service, keep one long-lived manager entry and expose a small wrapper that checks module state before dispatch. Do not load multiple copies accidentally because two independent managers have separate caches and each may expect to own its own unload.
Diagnostics and deployment layout
Log the requested image path, the resulting image ID, each lookup name and symbol type, API version negotiation, init/shutdown status, and unload result. Do not log pointers as though they were stable identifiers across runs. When diagnosing a failure, distinguish file-not-found/load failure, missing symbol, incompatible ABI, plugin initialization failure, runtime callback failure, and unload failure. Each has a different remediation path.
Choose add-on discovery and installation locations through the application’s documented directory policy. A path lookup and image loading are separate operations: finding a directory does not establish that a module file exists, and resolving a symbol does not prove that its behavior is correct. Record which file was loaded and make the plugin’s ABI version observable in diagnostics.
For tests, build a minimal test add-on that exports one versioned initializer, one ordinary function, and a teardown callback. Verify successful load, successful symbol resolution, a missing symbol, incompatible version rejection, initialization failure cleanup, and unload after all calls finish. Add a test where a background call is in flight while shutdown starts; assert that the manager waits or refuses to unload until the lease is released. Test a callback that attempts host re-entry to expose lock-order problems.
Run the lifecycle test under the supported Haiku architecture and compiler toolchain. Symbol decoration and function pointer conversion are ABI-sensitive. Cross-compile and native-run the add-on with the same compiler/runtime configuration used by the host. A source-level header check cannot prove the binary ABI works, so keep a small integration fixture in CI where Haiku execution is available.
Keep the API boundary narrow
Dynamic loading makes versioning and lifetime part of the application contract. Keep exported names stable, provide an explicit version handshake, and isolate module-specific data types behind an opaque pointer and function table. Make ownership of every callback, returned buffer, thread, and object explicit. Never let plugin code become reachable after its image is unloaded.
load_add_on() provides a handle to an image in the caller’s team and get_image_symbol() resolves addresses within that image. A maintainable host builds the lifecycle around those primitives: checked errors, ABI compatibility, in-flight work coordination, destruction before unload, and observable teardown.
Related:
- Haiku BAppFileInfo: Executable Signatures, Supported Types, and Version Data
- Haiku find_directory and BPathFinder: Resolve Paths at Runtime
Sources: