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:
@@ -7,7 +7,7 @@ A DPM module is one shared object in the module directory. libdpm-core.so loads
|
||||
A module includes `<dpm/core.h>`, 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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user