Route all module interaction through dispatch
A module is addressed by name and command string, and nothing else. Typed API access handed a caller a pointer into the callee's function table, which meant compiling against that module's struct layout — a build-time dependency between modules that the design does not permit. Removing it also removes the manifest, the table magic constant, and the table size field, which existed only to describe and validate those tables. Load validation is now three steps: reserved contract symbols resolve, the minimum-version handshake passes, and the version and description probes return well-formed values. The contract is four reserved symbols, and a module's interface is the command vocabulary it documents. Documentation is brought in line, and artifacts are named exactly: libdpm-core.so for the library, <dpm/core.h> for the header, the dpm binary for the command-line tool.
This commit is contained in:
@@ -1,43 +1,63 @@
|
||||
# Developing DPM Modules
|
||||
|
||||
A DPM module is one shared object in the module directory. libdpm-core loads it, validates it completely, routes commands to it, and serves its functions to other modules and to programs linked against libdpm-core. This document covers writing, building, testing, and installing a module.
|
||||
A DPM module is one shared object in the module directory. libdpm-core.so loads it, validates it completely, and dispatches commands to it on behalf of whatever asked — the `dpm` binary, a build system, or another module. This document covers writing, building, testing, and installing a module.
|
||||
|
||||
## The module contract
|
||||
|
||||
Every module exports the following symbols as extern "C". All returned strings must be non-NULL, static or module-owned, and valid for the lifetime of the loaded module; callers never free them.
|
||||
A module includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following symbols as extern "C". All returned strings must be non-NULL, static or module-owned, and valid for the lifetime of the loaded module; callers never free them.
|
||||
|
||||
**`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`**
|
||||
The generic command entry point. `ctx` is the host context that dispatched the call; the module reaches every libdpm-core service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it. `command` is the subcommand name, equal to argv[0] when argc > 0. 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 command entry point, and the module's entire functional surface. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name, equal to argv[0] when argc > 0. 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.
|
||||
|
||||
**`const char* dpm_module_version(void)`**
|
||||
The module's own version as an X.Y.Z string. libdpm-core reports this value to consumers, and each consumer decides for itself whether the version suits it.
|
||||
The module's own version as an X.Y.Z string. libdpm-core.so reports this value to consumers, and each consumer decides for itself whether the version suits it.
|
||||
|
||||
**`const char* dpm_module_description(void)`**
|
||||
A one-line human-readable description, shown in module listings.
|
||||
|
||||
**`const char* dpm_module_core_min(void)`**
|
||||
The minimum libdpm-core version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core older than that refuses to load the module, and the refusal message says so.
|
||||
The minimum library version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core.so older than that refuses to load the module, and the refusal message says so.
|
||||
|
||||
**`const dpm_manifest* dpm_module_manifest(void)`**
|
||||
A static table declaring the module's entire functional surface: an entry count and, per entry, the API name, its table version, and the exact exported symbol carrying the table — for example { "mymodule", 1, "mymodule_api_v1" }. Only manifest-declared tables are ever handed to consumers; an API absent from the manifest does not exist, even if its symbol does. A module providing no API tables returns a manifest with a count of zero.
|
||||
|
||||
The `dpm_manifest` and `dpm_manifest_entry` types, the table header, and the service declarations all come from the installed public header:
|
||||
The `dpm_ctx` type and the service declarations all come from the installed public header:
|
||||
|
||||
```
|
||||
#include <dpm/core.h>
|
||||
```
|
||||
|
||||
## API tables
|
||||
## Your interface is your command vocabulary
|
||||
|
||||
A module's functions are published to consumers as API tables: one exported symbol per API version (`mymodule_api_v1`), pointing to a plain C struct of function pointers. Every table opens with a `dpm_api_table_header` — the `DPM_API_TABLE_MAGIC` constant, then the table struct's size in bytes as the module compiled it. A consumer compares that size against the layout it was built against to see what it has been handed.
|
||||
A module publishes no headers, no struct layouts, and no symbols to anything that calls it. Everything it offers is reached through `dpm_module_execute`, addressed by command string, with arguments passed as an argument vector and a status returned as an int.
|
||||
|
||||
All parameters and return values crossing a table are C types only. State passes through opaque handles; errors are int codes.
|
||||
That is what a caller compiles against: a module name and a command name, both strings. Document your commands, their arguments, and their return codes — that documentation is your interface, and it is the only thing a consumer can depend on.
|
||||
|
||||
**Symbol naming**: every functional export is prefixed with the module's name (mymodule_\*). The dpm_ prefix is reserved for the contract symbols and libdpm-core.
|
||||
**Symbol naming**: every functional export is prefixed with the module's name (mymodule_\*). The dpm_ prefix is reserved for the contract symbols and for libdpm-core.so.
|
||||
|
||||
## Calling another module
|
||||
|
||||
A module reaches a peer by performing the same two steps its own caller performed — ask libdpm-core.so for the module by name, then ask libdpm-core.so to invoke it:
|
||||
|
||||
```
|
||||
int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
|
||||
{
|
||||
dpm_module* peer = dpm_require(ctx, "othermodule");
|
||||
if (!peer) {
|
||||
dpm_log(ctx, DPM_LOG_ERROR, dpm_last_error(ctx));
|
||||
return 1;
|
||||
}
|
||||
|
||||
return dpm_execute(ctx, peer, "somecommand", argc, argv);
|
||||
}
|
||||
```
|
||||
|
||||
The `ctx` is the one handed to your entry point. Nothing else is needed to reach the library.
|
||||
|
||||
**Never link, include, or hardcode anything belonging to a peer.** No peer headers, no shared types, no peer symbols. Modules are loaded with `RTLD_LOCAL`, so a peer's symbols are not reachable from your module even if you tried — libdpm-core.so is the only path, and the only knowledge you hold about a peer is its name and the commands it documents.
|
||||
|
||||
A module that depends on a peer is the party that judges the peer's version. Require it, read its reported version with `dpm_module_info_of`, and decide whether it is suitable for the commands you intend to issue. libdpm-core.so reports; it does not rule.
|
||||
|
||||
## Validation at load
|
||||
|
||||
libdpm-core is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies, in order: every reserved contract symbol resolves; the minimum-version handshake passes; the version and description probes return well-formed values; every manifest-declared symbol resolves; and every declared table carries the correct magic and a sane size. A module failing any step is refused with an itemized reason, visible in the load-failure output. A module that loads is fully valid — consumers never defend against partial states.
|
||||
libdpm-core.so is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies, in order: every reserved contract symbol resolves; the minimum-version handshake passes; and the version and description probes return well-formed values. A module failing any step is refused with an itemized reason, visible in the load-failure output. A module that loads is fully valid — consumers never defend against partial states.
|
||||
|
||||
## Building
|
||||
|
||||
@@ -67,24 +87,24 @@ cmake -B <build-dir>
|
||||
cmake --build <build-dir>
|
||||
```
|
||||
|
||||
libdpm-core is the only DPM link dependency a module ever has. Dependencies on other modules are runtime concerns, resolved through the require/get_api pair — a peer module is never linked and never needs to be present to build.
|
||||
|
||||
A module that depends on a peer is the party that judges the peer's version. Require it, read its reported version with `dpm_module_info_of`, and decide whether it is too new or too old for the calls you intend to make. libdpm-core reports; it does not rule.
|
||||
libdpm-core.so is the only DPM link dependency a module ever has. Dependencies on other modules are runtime concerns, resolved by name through require and dispatch — a peer module is never linked, never included, and never needs to be present to build or to test.
|
||||
|
||||
## Running and testing locally
|
||||
|
||||
Load the freshly built module through a locally run `dpm` without installing anything:
|
||||
Load the freshly built module through a locally run `dpm` binary without installing anything:
|
||||
|
||||
```
|
||||
dpm --module-path <build-dir> mymodule <command>
|
||||
```
|
||||
|
||||
libdpm-core runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The `--config-dir` flag points the module's configuration namespace at local files during development, and `--root` directs package operations at a scratch tree.
|
||||
libdpm-core.so runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The `--config-dir` flag points the module's configuration namespace at local files during development, and `--root` directs package operations at a scratch tree.
|
||||
|
||||
Where your module calls a peer, put a **stub module** in the fixture module path: a small .so exporting the four reserved symbols and answering the commands your module issues. libdpm-core.so validates and dispatches to it exactly as it would the real peer. Because a peer is addressed only by name and command string, the stub is a complete substitute — there is nothing else about the real peer your module could have depended on.
|
||||
|
||||
## Installing
|
||||
|
||||
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core discovers the module on its next scan; no registration step exists beyond the file being present and valid.
|
||||
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core.so discovers the module on its next scan; no registration step exists beyond the file being present and valid.
|
||||
|
||||
## A working example
|
||||
|
||||
The info module bundled with the `dpm` binary and libdpm-core, at `src/bundled-modules/info/`, tests and demonstrates full DPM system functionality, and in doing so shows the contract, an API table, and this build structure in working form.
|
||||
The info module bundled with the `dpm` binary and libdpm-core.so, at `src/bundled-modules/info/`, tests and demonstrates full DPM system functionality, and in doing so shows the contract, command routing, and this build structure in working form.
|
||||
|
||||
Reference in New Issue
Block a user