plugin ABI
Native plugin ABI
A native plugin is a shared library that whoever runs the workstation installs in its plugin folder. It adds namespaces and handle classes to the datumhue.* API, and runs with the same authority as the workstation itself. An app never gains that authority by calling one: it reaches a plugin only when it is granted the plugin, as it is granted any other permission. A plugin exports a fixed set of C symbols and speaks MessagePack across the boundary.
The contract is a single frozen generation, 1. The host resolves dh_plugin_abi first and refuses the library unless it returns exactly that number: there is no negotiation and no cross-generation compatibility. Any change to the symbol list, a signature, the vtable layout, the value encoding, or the catalog schema is a new generation.
Every exported symbol and every vtable entry is an unsafe extern "C" function. No panic or foreign exception may unwind across the boundary; a plugin is built panic = "abort" or wraps each export in a shim that returns an encoded error instead.
Exported symbols
The host calls, in order: dh_plugin_abi (refusing on mismatch), dh_plugin_catalog (validated before any further plugin code runs), dh_plugin_init, then any number of dh_plugin_call / dh_plugin_begin invocations, and finally dh_plugin_shutdown exactly once. dh_plugin_call and dh_plugin_begin are invoked only on the host's exclusive Lua thread, one at a time.
| Symbol | Signature | Role |
|---|---|---|
dh_plugin_abi | fn() -> u32 | ABI generation handshake; returns the generation the plugin was built against. |
dh_plugin_catalog | fn(out_len: *mut usize) -> *const u8 | Return the MessagePack catalog fragment (plugin-owned, valid until shutdown). |
dh_plugin_init | fn(host: *const DhHostV1) -> i32 | Receive the host services vtable and initialize. |
dh_plugin_call | fn(fn_id: u32, args: *const u8, args_len: usize, out: *mut DhBuf) -> i32 | Invoke a catalog function synchronously; must return promptly. |
dh_plugin_begin | fn(fn_id: u32, args: *const u8, args_len: usize, ticket: u64) -> i32 | Start an asynchronous catalog function; one completion is posted later. |
dh_plugin_shutdown | fn() | Orderly teardown, called exactly once; the library is then unloaded. |
Host services vtable
dh_plugin_init receives a pointer to a DhHostV1, the whole world a plugin sees: completion posting, logging, buffer allocation, and bulk-data handle creation. It stays valid until dh_plugin_shutdown returns, and every entry takes the struct's opaque ctx as its first argument. dh_plugin_call and dh_plugin_begin run on the host's exclusive Lua thread and must return promptly (the call blocks every running app for its duration); the vtable entries themselves are callable from any thread, so slow work runs on the plugin's own threads and reports back through post_completion.
| Field | Type | Role |
|---|---|---|
ctx | *mut c_void | Opaque host context; passed back verbatim as the first argument of every entry. |
post_completion | fn(ctx, ticket: u64, status: i32, payload: *const u8, payload_len: usize) | Post the one completion for a begin ticket; callable from any thread. |
log | fn(ctx, level: u32, msg: *const u8, msg_len: usize) | Emit a log line at a DH_LOG_* level (unknown levels treated as info). |
buf_alloc | fn(ctx, len: usize) -> DhBuf | Allocate a host-owned DhBuf of len zeroed bytes. |
buf_free | fn(ctx, buf: DhBuf) | Release a DhBuf obtained from buf_alloc. |
create_data_handle | fn(ctx, arrow_ipc: *const u8, arrow_ipc_len: usize, out_handle: *mut u64) -> i32 | Register an Arrow IPC stream and receive a bulk-data id. A function whose catalog type is data_handle returns the id, and the host delivers it to Lua as a DataHandle. Each id is consumed exactly once. |
A byte buffer that crosses the boundary is a DhBuf { ptr: *mut u8, len: usize } (ptr is null exactly when len == 0). It is allocated by buf_alloc and released by buf_free, or handed back to the host as a call out-buffer, which then owns it: each buffer is released exactly once and memory never crosses allocators. Argument and payload pointers are borrowed only until the callee returns; the callee copies what it needs.
Status and log codes
A call or completion returns one status code:
| Constant | Value | Meaning |
|---|---|---|
DH_STATUS_OK | 0 | Success; result values (or an empty buffer) in the out-buffer or completion payload. |
DH_STATUS_ERROR | 1 | Failure; a MessagePack PluginError in the out-buffer or completion payload. |
DH_STATUS_UNSUPPORTED | 2 | The callee does not support the operation; carries no payload. |
The vtable log entry takes one of these levels:
| Constant | Value |
|---|---|
DH_LOG_ERROR | 1 |
DH_LOG_WARN | 2 |
DH_LOG_INFO | 3 |
DH_LOG_DEBUG | 4 |
DH_LOG_TRACE | 5 |
Value model
Arguments, results, and typed errors cross as MessagePack. A single value encodes as a two-element array [tag, payload], where the tag is a small unsigned integer naming the variant. The integer tag is rename-immune, keeps integer and float distinct even for an integral float, keeps bin distinct from str, and makes a map an ordered list of pairs (a Lua table's keys may repeat and its order is meaningful). A tag the decoder does not know is an error, never a panic.
| Tag | Variant | Payload |
|---|---|---|
0 | Nil | MessagePack nil |
1 | Bool | MessagePack bool |
2 | Int | MessagePack signed integer (i64) |
3 | Float | MessagePack f64 |
4 | Str | MessagePack str (UTF-8) |
5 | Bytes | MessagePack bin |
6 | Array | MessagePack array of encoded values |
7 | Map | MessagePack array of [key, value] pairs |
8 | Handle | [class, id] (an opaque plugin handle) |
Worked examples. Each value encodes to these bytes, and the bytes decode back to it:
| Value | Encoding |
|---|---|
Nil | 92 00 c0 |
Bool(true) | 92 01 c3 |
Bool(false) | 92 01 c2 |
Int(0) | 92 02 00 |
Int(-1) | 92 02 ff |
Int(9223372036854775807) | 92 02 cf 7f ff ff ff ff ff ff ff |
Int(-9223372036854775808) | 92 02 d3 80 00 00 00 00 00 00 00 |
Float(3.5) | 92 03 cb 40 0c 00 00 00 00 00 00 |
Float(-0.5) | 92 03 cb bf e0 00 00 00 00 00 00 |
Str("hello") | 92 04 a5 68 65 6c 6c 6f |
Str("h\u{00e9}\u{2603}") | 92 04 a6 68 c3 a9 e2 98 83 |
Bytes([222, 173, 190, 239]) | 92 05 c4 04 de ad be ef |
Array([Int(1), Str("x"), Bool(true)]) | 92 06 93 92 02 01 92 04 a1 78 92 01 c3 |
Map([(Str("k"), Int(1)), (Int(2), Nil)]) | 92 07 92 92 92 04 a1 6b 92 02 01 92 92 02 02 92 00 c0 |
Handle(PluginHandle { class: "Scene", id: 42 }) | 92 08 92 a5 53 63 65 6e 65 2a |
The dh_plugin_call argument and result form is a MessagePack array of encoded values. [Int(1), Str("a")] encodes as 92 92 02 01 92 04 a1 61.
On DH_STATUS_ERROR the payload is a PluginError, encoded as the array [code, message, detail] (detail is nil or an ordered list of [key, value] pairs):
| Error | Encoding |
|---|---|
PluginError { code: "bad", message: "nope", detail: None } | 93 a3 62 61 64 a4 6e 6f 70 65 c0 |
PluginError { code: "e", message: "m", detail: Some([("k", Int(7))]) } | 93 a1 65 a1 6d 91 92 a1 6b 92 02 07 |
Catalog fragment
A plugin declares its API surface as data, not from its symbols: dh_plugin_catalog returns a MessagePack CatalogFragment that mirrors the host registry's own model. The host validates it (schema, legal names, collisions) before dh_plugin_init runs, then merges it so plugin functions install through the same interpreter that installs core ones.
Type expressions (FragmentType) mirror the host registry type model. Optionality is a sibling bool on a parameter or field, not a variant; a nullable property is a Union with Nil.
| Variant | Meaning |
|---|---|
Number | A Lua number (float subtype). |
Integer | A Lua integer. |
String | A Lua string. |
Boolean | A Lua boolean. |
Function | An untyped Lua function value. |
Table | An untyped Lua table. |
Nil | Lua nil. |
Any | An arbitrary Lua value. |
Handle(name) | A userdata handle class, referenced by name. |
Array(T) | A homogeneous array (T[]). |
Map { key, value } | A keyed table (table<K, V>). |
Callback { params, returns } | A typed Lua function the host invokes. |
Enum { name, variants, variant_docs } | A string restricted to a fixed set of variants. |
Union(members) | A union of alternatives (A | B). |
InlineTable { name, fields, extends } | A typed option table, optionally extending a base table. |
StringLiteral(s) | A single string literal (an overload discriminant). |
Records. Each encodes as a MessagePack array of its fields in declaration order:
CatalogFragment
| Field | Type | Description |
|---|---|---|
plugin | String | The plugin identifier (dotted path allowed). |
version | semver | The plugin's own semantic version. |
min_abi | u32 | Reserved: the handshake is exact-match, so the loader refuses any generation other than its own regardless of this field. Decoders still reject a fragment whose min_abi exceeds the host generation. |
namespaces | [FragmentNamespace] | The namespaces this plugin contributes. |
handle_classes | [FragmentHandleClass] | The handle classes this plugin contributes. |
extensions | [FragmentExtension] | Methods added to host-owned extensible classes (Bytes). |
FragmentNamespace
| Field | Type | Description |
|---|---|---|
name | String | Mounted as datumhue.<name> (dotted path allowed). |
doc | String | Namespace documentation (may be empty). |
functions | [FragmentFn] | The namespace functions. |
properties | [FragmentProperty] | The namespace computed properties. |
capability | Option<String> | A capability the whole namespace requires, if any. |
FragmentHandleClass
| Field | Type | Description |
|---|---|---|
name | String | The class short-name, referenced by Handle. |
doc | String | Class documentation (may be empty). |
methods | [FragmentFn] | The class methods. |
properties | [FragmentProperty] | The class computed properties. |
FragmentExtension
| Field | Type | Description |
|---|---|---|
class | String | The host-owned class the methods attach to. Hosts extend Bytes only; any other class refuses at load, as does a method name the core class or an earlier plugin already claims. Each method's receiver arrives as the class's marshalled value -- for Bytes, the raw payload bytes. |
methods | [FragmentFn] | The added methods; dispatch ids share the fragment-wide id space. |
FragmentFn
| Field | Type | Description |
|---|---|---|
id | u32 | The dispatch id (see below). |
name | String | The Lua-visible name. |
params | [FragmentParam] | Parameters in declared order (no receiver for a method). |
returns | [FragmentType] | The return types. |
doc | String | Documentation (may be empty). |
suspends | bool | Whether the call is async, driven through the begin path. |
FragmentParam
| Field | Type | Description |
|---|---|---|
name | String | The parameter name (documentation only; calls are positional). |
ty | FragmentType | The parameter type. |
optional | bool | Whether the parameter may be omitted. |
FragmentProperty
| Field | Type | Description |
|---|---|---|
name | String | The property name (a table key). |
ty | FragmentType | The property type (Union([T, Nil]) when nullable). |
doc | String | Documentation (may be empty). |
get_id | u32 | The dispatch id of the getter. |
set_id | Option<u32> | The dispatch id of the setter, present iff writable. |
FragmentField
| Field | Type | Description |
|---|---|---|
name | String | The option-table key. |
ty | FragmentType | The field type. |
optional | bool | Whether the key may be omitted. |
doc | String | The field documentation (may be empty). |
Wire stability differs by shape. A struct encodes as an array of its fields in declaration order, so reordering fields breaks the wire while renaming one does not. A FragmentType variant encodes by name, so renaming a variant breaks the wire while reordering variants does not. Value tags, by contrast, are rename-immune integers. Any such change bumps the ABI generation.
Every callable and computed property carries a plugin-assigned dispatch id, unique across the whole fragment: FragmentFn.id for a namespace function or handle method, and FragmentProperty.get_id / set_id for a property's getter and setter. That id, and only that id, is what the call and begin symbols receive to select the callee; a property accessor is reached exactly like a function, and a fragment that reuses an id anywhere is rejected.
An example fragment declares plugin acme version 1.0.0 at min_abi 1: namespace acme with 1 function and 1 property, and handle class H. It encodes to 113 MessagePack bytes and decodes back to the same fragment.