Dispatch returns a result envelope

dpm_execute fills a dpm_result on every call: status and error_code
from the module's return value, and a payload the module hands back
through dpm_set_result. The payload's layout belongs to the module and
is documented per command; its release function is carried in the
envelope and invoked by dpm_result_release. The context keeps a stack
of the envelopes in progress, so a module calling a peer receives the
peer's payload in its own envelope and its caller sees only what the
module sets itself.

The good fixture returns a payload and counts its releases, and the
modules test covers the envelope fields, the payload round trip, single
release, discard on a NULL envelope, and a payload set outside any
dispatch. The prose documents describe the envelope and the payload
header a module ships.
This commit is contained in:
2026-09-07 14:53:24 -04:00
parent 0291e61fd8
commit 55c852586b
13 changed files with 404 additions and 39 deletions

View File

@@ -66,10 +66,32 @@ Resolves the module `name` in the module path and runs the full load-time valida
`int dpm_get_module_info(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`
Fills `out` with the loaded module's name, version, and description exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. The library attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on.
`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`
Dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). The library adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define.
`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv, dpm_result* out)`
Dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). Argument semantics beyond "argv[0] is the command" are the module's to define. Fills the envelope at `out` on every call; NULL discards the payload before the call returns.
This is the entire path into module code. A caller addresses a module by name and a capability by command string, so it holds no compile-time knowledge of the module it is calling — no headers, no struct layouts, no symbols. That is what allows a module to be developed, built, and tested with no peer present.
This is the entire path into module code. A caller addresses a module by name and a capability by command string. The only compile-time knowledge it holds of the module is the layout of the payload that module documents for the commands the caller reads.
### Results
Every dispatch fills a `dpm_result`:
```
typedef struct dpm_result {
int status;
int error_code;
void* data;
void (*release)(void* data);
} dpm_result;
```
`status` is `DPM_RESULT_OK` when the module returned 0 and `DPM_RESULT_ERROR` otherwise; `error_code` is the module's return value. Both are written by the library and mean the same thing for every module and every command.
`data` and `release` are the payload. The module sets them with `dpm_set_result` during the call. `data` is whatever the module returns for that command, in a layout the module documents for that command at that version; the caller that dispatched the command knows the layout it asked for. `release` frees `data`, and `dpm_result_release` calls it. A module that returns nothing leaves both NULL.
The context holds a stack of the envelopes belonging to the dispatches in progress. Dispatch pushes the caller's envelope before entering the module and pops it after, so a module that calls a peer receives the peer's payload in its own envelope, and its caller sees only what the module itself sets.
`void dpm_result_release(dpm_result* result)`
Calls `release` on `data` when both are set, then clears both. `status` and `error_code` are left as they were. NULL is a no-op.
### Installation
@@ -120,12 +142,15 @@ Records `msg` as the most recent failure on this context; the string is copied.
`const char* dpm_get_last_error(dpm_ctx* ctx)`
Returns a human-readable description of the most recent failure recorded on this context, or NULL if none. Overwritten by the next failing call on the same context.
`void dpm_set_result(dpm_ctx* ctx, void* data, void (*release)(void* data))`
Hands a payload to the dispatch in progress. Called by a module from inside `dpm_module_execute`, with the context handed to its entry point; the payload lands in the envelope its caller passed to `dpm_execute`. A second call during the same dispatch releases the earlier payload and replaces it. Outside any dispatch there is no envelope, and the payload is released at once.
## Module Contract
A module is one .so in the module directory. It includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following four reserved symbols as extern "C". Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; the library and consumers never free them.
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
The module's command entry point, and the only entry through which it performs work. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_get_resolved_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
The module's command entry point, and the only entry through which it performs work. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_get_resolved_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure; the return value becomes the caller's `status` and `error_code`. A payload for the caller is handed to `dpm_set_result` before returning. It must be callable immediately after load with no other setup.
`const char* dpm_module_version(void)`
Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value the library reports to consumers, and the value they judge compatibility against.
@@ -140,7 +165,7 @@ Returns a comma-separated list of alternate names the module answers to, or NULL
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and for libdpm-core.so.
**A module publishes no headers to other modules.** Its capabilities are addressed by command string through `dpm_execute`, so nothing about its internals — types, struct layouts, symbol names — is ever compiled into a caller. A module's documented command vocabulary is its interface.
**A module publishes its payload layouts and nothing else.** Its capabilities are addressed by command string through `dpm_execute`. For each command that returns data, the module documents the layout of `data` and ships it as a header from its own repository, versioned with the module. A caller includes that header for the commands it reads. Nothing else about the module — its symbols, its internal types — is compiled into a caller. A module's documented command vocabulary, with the payload layout of each command, is its interface.
## Load-Time Enforcement
@@ -182,7 +207,7 @@ Build systems and Dark Horse components link `libdpm-core.so` as an ordinary sha
- dispatch commands to it.
The header installs to the standard include path and the library to the standard lib path. A consumer that opens a default context (no overrides) is working against system configuration, the system module path, the system tree, and system locking — the same environment the installed `dpm` binary sees, because that binary is just another caller of the same library. Overrides redirect individual paths only when a caller explicitly sets them. Whether a consumer addresses raw only (image builders that just deploy trees) or pkg (dependency-aware tooling) is their choice of require(); behavior is identical to the `dpm` binary's because it is the same implementation.
## Bootstrap Chain
```