From 55c852586bf8cd497838770334a43a34bd122bbf Mon Sep 17 00:00:00 2001 From: "Christopher M. Punches" Date: Mon, 7 Sep 2026 14:53:24 -0400 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + docs/PROSE/CONSUMERS.md | 29 +++++++++-- docs/PROSE/DESIGN.md | 37 +++++++++++--- docs/PROSE/MODULES.md | 42 ++++++++++++++-- docs/PROSE/OVERVIEW.md | 13 +++-- include/dpm/core.h | 98 +++++++++++++++++++++++++++++++++--- include/internal/context.hpp | 4 ++ src/cli/dpm.cpp | 7 ++- src/core/modules.cpp | 37 ++++++++++++-- src/core/results.cpp | 79 +++++++++++++++++++++++++++++ tests/CMakeLists.txt | 4 ++ tests/fixtures/src/good.cpp | 30 +++++++++-- tests/test_modules.cpp | 62 +++++++++++++++++++++-- 13 files changed, 404 insertions(+), 39 deletions(-) create mode 100644 src/core/results.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e5dd23..646ce3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ add_library(dpm-core SHARED src/core/logging.cpp src/core/metadata.cpp src/core/modules.cpp + src/core/results.cpp src/core/sanitizers.cpp src/core/version.cpp ) diff --git a/docs/PROSE/CONSUMERS.md b/docs/PROSE/CONSUMERS.md index 030e9fb..1e8363f 100644 --- a/docs/PROSE/CONSUMERS.md +++ b/docs/PROSE/CONSUMERS.md @@ -70,13 +70,28 @@ dpm_get_module_info(ctx, mod, &info); Deciding whether that version is suitable is yours. libdpm-core.so applies no version criterion of its own — a handle means the module is valid, not that it suits you. -`dpm_execute` invokes the module — a command name and arguments: +`dpm_execute` invokes the module — a command name, arguments, and an envelope for the result: ``` -int rc = dpm_execute(ctx, mod, "command", argc, argv); +dpm_result result; +int rc = dpm_execute(ctx, mod, "command", argc, argv, &result); ``` -This is the only path into module code, and it is the same path the `dpm` binary uses and the same path a module uses to reach a peer. You address a module by name and a capability by command string, so your program compiles against no module header, no struct layout, and no module symbol. What a module accepts as commands and arguments, and what its return codes mean, is documented by that module. +This is the only path into module code, and it is the same path the `dpm` binary uses and the same path a module uses to reach a peer. You address a module by name and a capability by command string. What a module accepts as commands and arguments, what its return codes mean, and what it returns as a payload, is documented by that module. + +## Results + +Every dispatch fills the envelope. `result.status` is `DPM_RESULT_OK` when the module returned 0 and `DPM_RESULT_ERROR` otherwise; `result.error_code` is the module's return value. Those two fields mean the same thing for every module and every command. + +`result.data` is the payload: what the module returns for that command, in a layout the module documents for that command at that version. The module ships that layout as a header from its own repository, and your program includes it for the commands it reads. A module that returns nothing leaves `data` NULL. + +The payload belongs to the module that produced it. When you are done with it: + +``` +dpm_result_release(&result); +``` + +That calls the release function the module supplied and clears `data`. Passing NULL as the envelope to `dpm_execute` discards the payload before the call returns, for a command whose result you do not need. ## Enumerating Modules @@ -106,10 +121,12 @@ A name is recorded once. Adding one that is already an alias, or that belongs to - `dpm_get_resolved_module_path`(ctx) — the resolved module directory. - `dpm_set_last_error`(ctx, msg) — records a failure reason on the context; what a module calls to explain a nonzero return. - `dpm_get_last_error`(ctx) — a human-readable description of the most recent failure on the context, or NULL. +- `dpm_set_result`(ctx, data, release) — hands a payload to the dispatch in progress; what a module calls to return data. +- `dpm_result_release`(result) — releases a payload through the function the module supplied. ## Ownership and Errors -Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until `dpm_close`; callers never free them. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from `dpm_get_last_error`. +Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until `dpm_close`; callers never free them. A payload in a `dpm_result` is owned by the module that produced it and is released with `dpm_result_release`. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from `dpm_get_last_error`. ## Complete Example @@ -131,7 +148,9 @@ int main(void) { return 1; } - int rc = dpm_execute(ctx, mod, "version", 0, NULL); + dpm_result result; + int rc = dpm_execute(ctx, mod, "version", 0, NULL, &result); + dpm_result_release(&result); dpm_close(ctx); return rc; diff --git a/docs/PROSE/DESIGN.md b/docs/PROSE/DESIGN.md index babe04c..00df912 100644 --- a/docs/PROSE/DESIGN.md +++ b/docs/PROSE/DESIGN.md @@ -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 ``, 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 ``` diff --git a/docs/PROSE/MODULES.md b/docs/PROSE/MODULES.md index d744b4d..8e51d28 100644 --- a/docs/PROSE/MODULES.md +++ b/docs/PROSE/MODULES.md @@ -7,7 +7,7 @@ A DPM module is one shared object in the module directory. libdpm-core.so loads A module includes ``, links `-ldpm-core`, and exports the following four 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. +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; the return value becomes the caller's `status` and `error_code`. 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`. Where the command returns data, hand it to `dpm_set_result` before returning. 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. @@ -45,9 +45,37 @@ Check it, proceed or fail on your own terms, and report through `dpm_log` and yo ## 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. +Everything a module offers is reached through `dpm_module_execute`, addressed by command string, with arguments passed as an argument vector, a status returned as an int, and data returned as a payload. -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. +A caller compiles against a module name, a command name, and the layout of the payload for the commands it reads. Document your commands, their arguments, their return codes, and the payload each returns — that documentation is your interface, and it is the only thing a consumer can depend on. No symbol of yours is reachable from a caller. + +## Returning Data + +A command returns data by handing a payload to the library before the entry point returns: + +``` +dpm_set_result(ctx, payload, mymodule_release_payload); +``` + +`payload` is whatever the command returns, in a layout you define for that command. `mymodule_release_payload` frees it, and the caller invokes it through `dpm_result_release` when it is done. Modules are loaded RTLD_LOCAL, so that function pointer is the only way a caller can reach your release code. A command that returns nothing calls nothing, and the caller sees a NULL payload. + +The layout of each command's payload is part of your interface. Ship it as a header from your module's repository, versioned with the module, and a caller includes it for the commands it reads: + +``` +struct mymodule_list_entry { + const char* name; + const char* version; +}; + +struct mymodule_list { + size_t count; + struct mymodule_list_entry* entries; +}; +``` + +A caller that dispatched "list" to your module reads `result.data` as a `struct mymodule_list*`, because it asked for "list" and your documentation says that is what "list" returns. + +The payload lands in the envelope of the dispatch in progress. When your command calls a peer, the peer's payload lands in the envelope you passed for that call, and your own caller sees only what you set yourself. **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. @@ -64,13 +92,17 @@ int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv) return 1; } - return dpm_execute(ctx, peer, "somecommand", argc, argv); + dpm_result result; + int rc = dpm_execute(ctx, peer, "somecommand", argc, argv, &result); + /* read result.data as the layout othermodule documents for "somecommand" */ + dpm_result_release(&result); + return rc; } ``` 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. +**Never link or hardcode anything belonging to a peer.** The one thing of a peer's you include is the header carrying the payload layouts of the commands you read. 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 knowledge you hold about a peer is its name, the commands it documents, and the payloads those commands return. 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. diff --git a/docs/PROSE/OVERVIEW.md b/docs/PROSE/OVERVIEW.md index de6febf..dde00f7 100644 --- a/docs/PROSE/OVERVIEW.md +++ b/docs/PROSE/OVERVIEW.md @@ -60,9 +60,11 @@ The context owns everything it hands out. Every string a caller receives stays v **Calling into a module.** `dpm_execute` passes a command name and an argument vector to the module's entry point and returns its result. This is the only path into module code, and it is the same one the `dpm` binary takes. +**Reading what a module returns.** Every dispatch fills a `dpm_result`: a status and error code the library writes from the module's return value, and a payload the module hands back through `dpm_set_result`. The payload's layout is defined and documented by the module, per command, and shipped as a header from the module's repository; the caller that asked for the command reads it as that layout and releases it with `dpm_result_release`. + **Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the `dpm` binary prints. -**Services.** A module reaches the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_get_resolved_module_path` to learn where modules live, `dpm_core_version` to learn the library's version, `dpm_set_last_error` to record why it failed. A module needs no file handling and no logging machinery of its own. +**Services.** A module reaches the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_get_resolved_module_path` to learn where modules live, `dpm_core_version` to learn the library's version, `dpm_set_last_error` to record why it failed, `dpm_set_result` to return data to its caller. A module needs no file handling and no logging machinery of its own. ## How a Module Reaches Another Module @@ -76,13 +78,16 @@ int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv) dpm_log(ctx, DPM_LOG_ERROR, dpm_get_last_error(ctx)); return 1; } - return dpm_execute(ctx, peer, "somecommand", argc, argv); + dpm_result result; + int rc = dpm_execute(ctx, peer, "somecommand", argc, argv, &result); + dpm_result_release(&result); + return rc; } ``` The `ctx` a module needs is the one handed to it in its own entry point, so it requires nothing else to reach the library. -**A module never links, includes, or hardcodes anything belonging to another module.** No peer headers, no shared struct layouts, no peer symbols. The only build dependency a module has is libdpm-core.so, and the only knowledge it holds about a peer is the peer's name and the command it wants to run. That is what allows every module to live in its own repository and be built with no peer present anywhere on the machine. +**A module never links or hardcodes anything belonging to another module.** No peer symbols, no peer internals. The only build dependency a module has is libdpm-core.so. The knowledge it holds about a peer is the peer's name, the command it wants to run, and the layout of the payload that command returns, which the peer publishes as a header from its own repository. That is what allows every module to live in its own repository and be built with no peer present anywhere on the machine. ## What DPM Reads and Writes on Disk @@ -113,7 +118,7 @@ The `ctx` a module needs is the one handed to it in its own entry point, so it r A module is one `.so` in the module directory exporting four reserved symbols: a command entry point, its own version, a one-line description, and the alternate names it answers to. It includes `` for those declarations and links `-ldpm-core`, and that is its entire build dependency. -The entry point receives the context that dispatched the call, the command name, and an argument vector, and returns an int. Everything a module offers the rest of the system is reachable through that one function, addressed by command name — which is what keeps a caller free of any compile-time knowledge of the module it is calling. +The entry point receives the context that dispatched the call, the command name, and an argument vector, and returns an int. Data for the caller is handed to `dpm_set_result` before returning, in a layout the module documents for that command. Everything a module offers the rest of the system is reachable through that one function, addressed by command name. A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to know what it is running on, `dpm_core_version()` reports the running version and the module acts on that itself. diff --git a/include/dpm/core.h b/include/dpm/core.h index 4ba9a40..38b58bc 100644 --- a/include/dpm/core.h +++ b/include/dpm/core.h @@ -134,6 +134,49 @@ enum { DPM_LOG_DEBUG = 4 }; +/* ------------------------------------------------------------------ */ +/* Results */ +/* ------------------------------------------------------------------ */ + +/** + * @brief Outcome of a dispatch, carried in dpm_result.status + */ +enum { + /** The module returned 0. */ + DPM_RESULT_OK = 0, + + /** The module returned nonzero; error_code carries the value. */ + DPM_RESULT_ERROR = 1 +}; + +/** + * @brief What a dispatch returns + * + * Filled by dpm_execute() on every call. status and error_code are the + * envelope, the same for every module and every command. data is the + * payload: what the module chose to return for the command, in a + * layout that module documents for that command at that version. The + * caller that dispatched the command knows the layout it asked for. + * + * The payload belongs to the module that produced it, and release is + * how that module frees it. dpm_result_release() calls release and + * clears both fields. A module that returns no payload leaves data and + * release NULL. + */ +typedef struct dpm_result { + /** DPM_RESULT_OK or DPM_RESULT_ERROR. */ + int status; + + /** The module's return value; 0 when status is DPM_RESULT_OK. */ + int error_code; + + /** The payload, or NULL. */ + void* data; + + /** Frees data; NULL when data needs no release. */ + void (*release)(void* data); +} dpm_result; + /* ------------------------------------------------------------------ */ /* Context configuration overrides */ /* ------------------------------------------------------------------ */ @@ -285,21 +328,40 @@ int dpm_get_module_info(dpm_ctx* ctx, dpm_module* mod, * argc > 0; semantics beyond that are the module's to define. * * This is the only 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 — the same call + * by name and a capability by command string, and the same call is what * a module makes to reach a peer. * + * The envelope at `out` is filled on every call: status and error_code + * from the module's return value, and data and release from what the + * module handed to dpm_set_result() during the call. The caller owns + * the envelope's storage and releases the payload with + * dpm_result_release() when it is done with it. Passing NULL for `out` + * discards the payload before this call returns. + * * @param ctx The libdpm-core.so context * @param mod A module handle from dpm_require() * @param command The command name; NULL or empty behaves as the * module's help command * @param argc Number of arguments * @param argv Argument vector + * @param out Receives the result envelope; NULL discards the payload * @return The module's return value verbatim; 0 on success */ DPM_PUBLIC_ABI_EXPORT int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, - int argc, char** argv); + int argc, char** argv, dpm_result* out); + +/** + * @brief Releases a result's payload + * + * Calls the release function the module supplied, when there is one, + * and clears data and release. status and error_code are left as they + * were. + * + * @param result The envelope whose payload is released; NULL is a no-op + */ +DPM_PUBLIC_ABI_EXPORT +void dpm_result_release(dpm_result* result); /* ------------------------------------------------------------------ */ /* Installation */ @@ -520,6 +582,28 @@ void dpm_set_last_error(dpm_ctx* ctx, const char* msg); DPM_PUBLIC_ABI_EXPORT const char* dpm_get_last_error(dpm_ctx* ctx); +/** + * @brief Hands a payload to the dispatch in progress + * + * A module returns data to its caller by calling this from inside + * dpm_module_execute. The payload lands in the envelope the caller + * passed to dpm_execute(), and release is what frees it when the caller + * is done. Nested dispatches each have their own envelope, so a payload + * set by a peer the module called does not reach the module's own + * caller unless the module sets it again itself. + * + * A second call during the same dispatch releases the earlier payload + * and replaces it. Outside any dispatch there is no envelope to receive + * the payload, so it is released at once. + * + * @param ctx The libdpm-core.so context handed to the module's entry + * point; NULL is a no-op + * @param data The payload; NULL clears it + * @param release Frees data; NULL when data needs no release + */ +DPM_PUBLIC_ABI_EXPORT +void dpm_set_result(dpm_ctx* ctx, void* data, void (*release)(void* data)); + /* ------------------------------------------------------------------ */ /* Module contract (implemented by modules, called by libdpm-core.so) */ /* ------------------------------------------------------------------ */ @@ -534,9 +618,11 @@ const char* dpm_get_last_error(dpm_ctx* ctx); * const char* dpm_module_aliases(void); * * dpm_module_execute is the only entry through which a module performs - * work. The other three are what it reports about itself; the library - * reads them at load, serves the version and description through - * dpm_get_module_info, and records the aliases at installation. + * work. Its return value becomes the envelope's status and error_code, + * and a payload for the caller is handed to dpm_set_result before it + * returns. The other three are what it reports about itself; the + * library reads them at load, serves the version and description + * through dpm_get_module_info, and records the aliases at installation. * * dpm_module_aliases returns a comma-separated list of alternate names * the module answers to, or NULL to declare none. NULL is an answer; the diff --git a/include/internal/context.hpp b/include/internal/context.hpp index bac3026..e5a5194 100644 --- a/include/internal/context.hpp +++ b/include/internal/context.hpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "internal/metadata.hpp" #include "internal/modules.hpp" @@ -73,6 +74,9 @@ struct dpm_ctx { /** Reason for the most recent failure. */ std::string last_error; + + /** Envelopes of the dispatches in progress, innermost last. */ + std::vector result_frames; }; /** diff --git a/src/cli/dpm.cpp b/src/cli/dpm.cpp index 266245e..3397b01 100644 --- a/src/cli/dpm.cpp +++ b/src/cli/dpm.cpp @@ -403,7 +403,12 @@ int main(int argc, char** argv) { } const char* command = module_argc > 0 ? module_argv[0] : nullptr; - int rc = dpm_execute(ctx, mod, command, module_argc, module_argv); + + // The CLI knows no module's payload layout, so whatever the module + // hands back is released unread. + dpm_result result; + int rc = dpm_execute(ctx, mod, command, module_argc, module_argv, &result); + dpm_result_release(&result); dpm_close(ctx); return rc; diff --git a/src/core/modules.cpp b/src/core/modules.cpp index 1152246..c2e48fd 100644 --- a/src/core/modules.cpp +++ b/src/core/modules.cpp @@ -275,15 +275,44 @@ extern "C" { * @brief Dispatches a command to a module * * Hands the call to the module's entry point and returns what it - * returned, adding nothing to the result. This is the only path into - * module code. + * returned. This is the only path into module code. + * + * The envelope is the innermost result frame for the duration of the + * call, which is where dpm_set_result writes. A module that calls a + * peer pushes a frame of its own for that inner call, so the peer's + * payload lands in the module's envelope and never in its caller's. + * The frame is popped before the return value is written into the + * envelope, so a module cannot see a status it has not yet produced. */ int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, - int argc, char** argv) { + int argc, char** argv, dpm_result* out) { + // A caller passing no envelope still needs a frame to exist, so + // that a payload the module sets has somewhere to land and is + // released rather than leaked. + dpm_result local; + dpm_result* frame = out ? out : &local; + + frame->status = DPM_RESULT_ERROR; + frame->error_code = 1; + frame->data = nullptr; + frame->release = nullptr; + if (!ctx || !mod || !mod->execute) { return 1; } - return mod->execute(ctx, command, argc, argv); + + ctx->result_frames.push_back(frame); + int rc = mod->execute(ctx, command, argc, argv); + ctx->result_frames.pop_back(); + + frame->status = rc == 0 ? DPM_RESULT_OK : DPM_RESULT_ERROR; + frame->error_code = rc; + + if (!out) { + dpm_result_release(&local); + } + + return rc; } /** diff --git a/src/core/results.cpp b/src/core/results.cpp new file mode 100644 index 0000000..040ddf6 --- /dev/null +++ b/src/core/results.cpp @@ -0,0 +1,79 @@ +/** + * @file results.cpp + * @brief The result envelope: how a payload reaches the caller + * + * A dispatch fills one dpm_result. The library writes the envelope + * fields, status and error_code, from the module's return value. The + * module writes the payload fields, data and release, by calling + * dpm_set_result while its entry point is running. + * + * The context keeps a stack of the envelopes belonging to the dispatches + * in progress. dpm_execute pushes the caller's envelope before entering + * the module and pops it after, so the innermost frame is always the one + * the running module's payload belongs in. A module calling a peer + * pushes another frame for that call, and the peer's payload lands there. + * + * @copyright Copyright (c) 2026 SILO GROUP LLC + * @author Chris Punches + * + * Part of the Dark Horse Linux Package Manager (DPM) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "internal/context.hpp" + +extern "C" { + /** + * @brief Releases a result's payload + * + * The release function is the module's own, supplied with the + * payload, so the library frees nothing itself. The payload fields + * are cleared afterwards and the envelope fields are left alone. + */ + void dpm_result_release(dpm_result* result) { + if (!result) { + return; + } + + if (result->release && result->data) { + result->release(result->data); + } + + result->data = nullptr; + result->release = nullptr; + } + + /** + * @brief Hands a payload to the dispatch in progress + * + * Writes into the innermost result frame. A payload already there + * is released first, so replacing a payload leaks nothing. With no + * dispatch in progress there is no frame, and the payload is + * released at once for the same reason. + */ + void dpm_set_result(dpm_ctx* ctx, void* data, void (*release)(void* data)) { + if (!ctx || ctx->result_frames.empty()) { + if (release && data) { + release(data); + } + return; + } + + dpm_result* frame = ctx->result_frames.back(); + dpm_result_release(frame); + + frame->data = data; + frame->release = release; + } +} /* extern "C" */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7f0023f..670586c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,10 @@ foreach(fixture good missing_symbols bad_version) ) endforeach() +# The known-good fixture returns a payload through the library, so it +# links libdpm-core.so the way a module does. +target_link_libraries(fixture_good PRIVATE dpm-core) + # --------------------------------------------------------------------- # API test binaries — one per area of the library, each its own ctest # case, so a failure names the area it happened in diff --git a/tests/fixtures/src/good.cpp b/tests/fixtures/src/good.cpp index 1729ba0..afdc836 100644 --- a/tests/fixtures/src/good.cpp +++ b/tests/fixtures/src/good.cpp @@ -24,6 +24,23 @@ */ #include +/* The one library call a module makes to return data, declared here + the way the rest of the contract is. */ +extern "C" void dpm_set_result(void* ctx, void* data, void (*release)(void*)); + +namespace { + /* Counts how many times the payload below has been released, so a + test can observe the release path. */ + int releases = 0; + + void release_payload(void* data) { + (void)data; + releases++; + } + + const char* payload = "pong"; +} // namespace + extern "C" const char* dpm_module_version(void) { return "1.2.3"; } @@ -38,16 +55,23 @@ extern "C" const char* dpm_module_aliases(void) { return "goodie, gd"; } -/* Answers "ping" with 42 so a dispatch round trip is observable, and - 0 for anything else. */ +/* Answers "ping" with 42 so a dispatch round trip is observable, + "payload" with the static payload above, "releases" with the number + of times that payload has been released, and 0 for anything else. */ extern "C" int dpm_module_execute(void* ctx, const char* command, int argc, char** argv) { - (void)ctx; (void)argc; (void)argv; if (command && std::strcmp(command, "ping") == 0) { return 42; } + if (command && std::strcmp(command, "payload") == 0) { + dpm_set_result(ctx, const_cast(payload), release_payload); + return 0; + } + if (command && std::strcmp(command, "releases") == 0) { + return releases; + } return 0; } diff --git a/tests/test_modules.cpp b/tests/test_modules.cpp index a5a09bc..86be43d 100644 --- a/tests/test_modules.cpp +++ b/tests/test_modules.cpp @@ -77,12 +77,64 @@ int main(void) { /* dispatch returns the module's own value, so a round trip is observable with no compile-time knowledge of the module */ - CHECK(dpm_execute(ctx, good, "ping", 0, nullptr) == 42); - CHECK(dpm_execute(ctx, good, "anything_else", 0, nullptr) == 0); - CHECK(dpm_execute(ctx, good, nullptr, 0, nullptr) == 0); + dpm_result result; + CHECK(dpm_execute(ctx, good, "ping", 0, nullptr, &result) == 42); + CHECK(result.status == DPM_RESULT_ERROR); + CHECK(result.error_code == 42); + CHECK(result.data == nullptr); + CHECK(result.release == nullptr); - CHECK(dpm_execute(nullptr, good, "ping", 0, nullptr) == 1); - CHECK(dpm_execute(ctx, nullptr, "ping", 0, nullptr) == 1); + CHECK(dpm_execute(ctx, good, "anything_else", 0, nullptr, &result) == 0); + CHECK(result.status == DPM_RESULT_OK); + CHECK(result.error_code == 0); + CHECK(result.data == nullptr); + + CHECK(dpm_execute(ctx, good, nullptr, 0, nullptr, &result) == 0); + CHECK(result.status == DPM_RESULT_OK); + + CHECK(dpm_execute(nullptr, good, "ping", 0, nullptr, &result) == 1); + CHECK(result.status == DPM_RESULT_ERROR); + CHECK(dpm_execute(ctx, nullptr, "ping", 0, nullptr, &result) == 1); + CHECK(result.status == DPM_RESULT_ERROR); + } + + /* ---- a payload reaches the caller and is released once ---- */ + { + dpm_module* good = dpm_require(ctx, "good"); + CHECK(good != nullptr); + if (!good) { + dpm_close(ctx); + return harness_report("modules"); + } + + dpm_result result; + CHECK(dpm_execute(ctx, good, "payload", 0, nullptr, &result) == 0); + CHECK(result.status == DPM_RESULT_OK); + CHECK(result.data != nullptr); + CHECK(result.release != nullptr); + CHECK(result.data != nullptr && + std::strcmp(static_cast(result.data), "pong") == 0); + + /* nothing has been released yet */ + CHECK(dpm_execute(ctx, good, "releases", 0, nullptr, nullptr) == 0); + + dpm_result_release(&result); + CHECK(result.data == nullptr); + CHECK(result.release == nullptr); + CHECK(result.status == DPM_RESULT_OK); + CHECK(dpm_execute(ctx, good, "releases", 0, nullptr, nullptr) == 1); + + /* releasing again calls nothing */ + dpm_result_release(&result); + CHECK(dpm_execute(ctx, good, "releases", 0, nullptr, nullptr) == 1); + + /* a NULL envelope discards the payload before dispatch returns */ + CHECK(dpm_execute(ctx, good, "payload", 0, nullptr, nullptr) == 0); + CHECK(dpm_execute(ctx, good, "releases", 0, nullptr, nullptr) == 2); + + /* setting a payload outside any dispatch releases it at once */ + dpm_set_result(ctx, &result, nullptr); + dpm_result_release(nullptr); } dpm_close(ctx);