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.
111 lines
6.4 KiB
Markdown
111 lines
6.4 KiB
Markdown
# Developing DPM Modules
|
|
|
|
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
|
|
|
|
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 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.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 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.
|
|
|
|
The `dpm_ctx` type and the service declarations all come from the installed public header:
|
|
|
|
```
|
|
#include <dpm/core.h>
|
|
```
|
|
|
|
## Your interface is your command vocabulary
|
|
|
|
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.
|
|
|
|
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 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.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
|
|
|
|
A module repository builds with CMake:
|
|
|
|
```
|
|
cmake_minimum_required(VERSION 3.22)
|
|
project(mymodule)
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
|
add_library(mymodule MODULE mymodule.cpp)
|
|
|
|
set_target_properties(mymodule PROPERTIES
|
|
PREFIX ""
|
|
SUFFIX ".so"
|
|
)
|
|
|
|
target_link_libraries(mymodule PRIVATE dpm-core)
|
|
|
|
install(TARGETS mymodule LIBRARY DESTINATION lib/dpm/modules)
|
|
```
|
|
|
|
```
|
|
cmake -B <build-dir>
|
|
cmake --build <build-dir>
|
|
```
|
|
|
|
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` binary without installing anything:
|
|
|
|
```
|
|
dpm --module-path <build-dir> mymodule <command>
|
|
```
|
|
|
|
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.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.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.
|