Native plugins
Add whole new namespaces and handle classes to the datumhue.* API
with a native plugin: a library of native code the workstation
loads. Your app can then drive a vendor's SDK, an in-house system, a
piece of hardware or a store platform that DatumHue does not ship.
A plugin runs as native code, with the same authority as the workstation itself, outside the Lua sandbox. Your app never gains that authority by calling one: it calls a plugin only when it is granted the plugin, as it is granted a folder or a permission. Whoever runs the workstation decides which plugins exist at all, by installing them.
The Native plugin ABI reference lists the symbols a plugin exports, the value encoding and the catalog schema.
First question: new verbs, or new data?
Before you write a plugin, decide what you are adding.
If you are reading a proprietary file format, such as a vendor's
container or a bespoke dataset, you usually want new data, not new
verbs. A native data mount gives you that. It returns the same
DataHandle values the built-in loaders return, so queries and charts
work on it unchanged, and it adds nothing to the Lua API. It is cheaper
to write and cheaper to trust, so reach for it first.
Write a plugin when you need new verbs: operations that are not "load
these rows", such as unlocking an achievement, querying a device, or
calling a vendor SDK whose surface is a set of actions. Then you want
new functions and new handle classes on datumhue.*, and a plugin is
the tool.
How a plugin extends the API
A plugin declares its API as a catalog. Before any app runs, the
workstation checks the catalog and adds it to datumhue.*, so a
plugin's functions are typed, documented and granted exactly like the
built-in ones.
An installed plugin's namespaces appear in this manual's reference
(datumhue docs) and in the datumhue.d.lua that datumhue pkg refresh
writes for your editor. Both read the folder the
DATUMHUE_PLUGINS_DIR environment variable names, without starting any
plugin. Set the variable, not
--plugins-dir, which sets the folder for
one launch only. Completion and type checks then cover your plugins
with no extra step.
A plugin adds three kinds of thing:
- Namespaces, mounted as
datumhue.<name>besidedatumhue.drawand the rest, holding functions and properties. - Handle classes: typed values with their own methods and
properties, shaped like the built-in handles such as
ImageorDataHandle. You hold them and pass them back to the plugin's functions, as you would any other handle. - Methods on
Bytes: decoders that read a format the wayBytes:textandBytes:csvdo. A decoder that yields a table returns aDataHandle, the same as a built-in loader.Bytesis the one class a plugin may extend. A name the core class or another plugin already uses refuses the whole library at load, so start each name with your vendor's prefix, as inBytes:acme_table.
Two conventions complete the contract. An asynchronous plugin function
that can fail accepts the call and reports the failure through its
completion; the caller's pcall then receives the full message.
Refusing the call at the start reaches pcall only as a generic
failure. And a handle that holds a native
resource, such as a session or an open file, has an explicit close
method: garbage collection never reaches into a plugin, so nothing is
released for you.
Reaching a plugin from a package
A plugin the workstation has loaded is not callable until a package
asks for it. Declare it in package.kdl, by name and by a range of the
plugin's own versions:
requires_plugin "acme_formats" "^1.2"
A plugin's version is its own, separate from the DatumHue API version. When your app starts, DatumHue checks that a plugin of that name is loaded at a version in the range. If it is missing or too old, the app does not start, and the error names the plugin.
Otherwise the app is granted "plugin:acme_formats". That grant
follows the rules every grant does: it flows down to the apps yours
starts, and only narrows
(Permissions and capabilities).
A call into a plugin your app was not granted raises.
A package that works without the plugin adds optional=#true to the
line. The app then starts either way, and while the plugin is missing
its calls raise, so make the first call through pcall to find out
whether it is there. The
manifest reference
has every form of the key.
Inside your app, a plugin's API reads like any other:
local acme = datumhue.acme_formats
local data = datumhue.args.files.report:read():ready()
-- A namespace function, returning the plugin's own handle.
local doc = acme.open(data)
print(doc.page_count, doc:page_text(1))
doc:close()
-- A decoder the plugin added to Bytes.
local sales = data:acme_table()
print(sales:count())
acme_formats.opentakes the report's bytes and returns anAcmeDoc, a handle class the plugin declares, with apage_countproperty and methods of its own.AcmeDoc:closereleases what the plugin holds for the document.Bytes:acme_tabledecodes the same bytes into aDataHandle, which queries and charts take like any other table.
The shape of a plugin
A plugin is a shared library: a .so on Linux, a .dll on Windows, a
.dylib on macOS. It speaks a fixed contract with the workstation, and
four things about that contract shape how you call a plugin:
- A function returns at once or suspends. A synchronous function returns its result on the spot. An asynchronous one is a suspending call: it pauses your app until the plugin posts its result, then returns it. You write no callback.
- Handles are typed. A result that is not a plain value comes back as a handle of one of the plugin's classes, which compares and prints like a built-in handle.
- Errors raise. A plugin's own failure raises a Lua error, like
every other
datumhue.*call. Wrap a call inpcallto handle the failure instead of passing it on. - Values cross as data. Arguments and results are copied across, encoded as MessagePack. A plugin never touches your app's Lua state.
Writing one
The steps, at a glance; the Native plugin ABI reference holds the exact contract:
- Build a shared library that exports the entry points the
reference lists under exported symbols,
for the ABI generation it names. In Rust, that is a
cdylibcrate. The library returns its catalog as data, implements each function it declares, and posts the results of its asynchronous ones. - Add a sidecar if you want one. A
<stem>.kdlfile beside the library can pin the ABI generation the library expects (abi) and its content hash (hash). It takes the library's file stem:acme.soreadsacme.kdl. The workstation then refuses a library that does not match, which catches a swapped or damaged file. - Install it in the plugin folder. The workstation loads plugins
from the folder
--plugins-dirorDATUMHUE_PLUGINS_DIRnames. Every library in it built for the right ABI generation loads, with no signature check, so protect the folder as you protect the workstation's own files.
The hard rules
Two rules are part of the contract, not style. Breaking either crashes the workstation:
- No panic may cross the boundary. No Lua error, Rust panic or
foreign exception may unwind out of a plugin's entry point. Build the
library with
panic = "abort", or wrap each export so it catches a failure and returns it as an encoded error. An error is a value you return, never an unwind. - A crash takes the workstation down. A plugin runs inside the workstation's own process, in the same memory, with no sandbox around it. If it faults or aborts, the whole workstation goes with it, so hold a plugin to the standard you hold the workstation to.
See also
- Native plugin ABI, for the exact contract.
- Permissions and capabilities, for how the plugin grant flows down.
- Waiting: suspending calls and handles, for calls that pause your code.
Reference used on this page: datumhue.args File:read Bytes:ready datumhue.draw Bytes:text Bytes:csv