dpm_module_info_of becomes dpm_get_module_info, joining the other two readers on the context in stating that a call fetches a value. Recording an error had two entry points doing the same write: the exported dpm_set_last_error and an internal dpm_core::set_last_error taking a std::string. The internal one is gone and the loader calls the exported function, so a reason reaches the context by one path whether a module or the library records it.
6.7 KiB
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 only entry through which the module 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. NULL or empty command must behave as the module's help command. Returns 0 on success, nonzero on failure. Where a nonzero return needs explaining, record the reason with dpm_set_last_error immediately before returning, and the caller reads it back with dpm_get_last_error. 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.
The dpm_ctx type and the service declarations all come from the installed public header:
#include <dpm/core.h>
You Determine Your Own Compatibility With the Library
Your module is built against the system-installed libdpm-core.so and is responsible for being correct against it. Where you need to act on what you are running under, dpm_core_version() reports the running version and you decide what to do:
const char* running = dpm_core_version();
Check it, proceed or fail on your own terms, and report through dpm_log and your return code.
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_get_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_get_module_info, 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 that every reserved contract symbol resolves and that the version and description probes return well-formed values. A module failing either 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 three 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.