aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2026-09-22 21:52:01 +0200
committersaturneric <[email protected]>2026-09-22 21:52:01 +0200
commit6f18552b7eb2a25de2373ba3cfbfc680097c87e0 (patch)
treec8c9cd32d5b47272ccfc18c23c2182f9a3639e88
parentrefactor(version-check): migrate to SDK context APIs (diff)
downloadmodules-6f18552b7eb2a25de2373ba3cfbfc680097c87e0.tar.gz
modules-6f18552b7eb2a25de2373ba3cfbfc680097c87e0.zip
docs(modules): document SDK and packaging workflow
- explain module identity, capabilities, events, and runtime hooks - update module creation and CMake integration instructions - document SDK contexts, memory ownership, logging, and host APIs - describe translations, caching, UI integration, and GPG operations - add packaging, signing, distribution, and external module guidance - document the new PGP inspection module in the integrated module list
-rw-r--r--README.md708
1 files changed, 485 insertions, 223 deletions
diff --git a/README.md b/README.md
index d523326..16d8e2c 100644
--- a/README.md
+++ b/README.md
@@ -3,39 +3,54 @@
This directory is a sub-repository of the main GpgFrontend project and must
live at `GpgFrontend/modules`. It cannot be built standalone: toolchains, SDK
headers, Qt setup, and output paths all come from the superproject's CMake. The
-modules are built alongside the application and loaded at startup. You can
-disable individual modules in the application settings, or remove a module from
-an archive install by deleting its shared-library file.
+modules are built alongside the application, packaged into signed `.gfmodule`
+descriptors, and loaded at startup. You can disable individual modules in the
+application settings, or remove one from an archive install by deleting its
+namespace directory under the modules root (see
+[Packaging, Signing & Distribution](#packaging-signing--distribution)).
## Integrated Modules
-| Directory | Module ID | Description |
-| ------------------- | ------------------------------ | --------------------------------------------- |
-| `m_ver_check` | `…module.version_checking` | Checks for new GpgFrontend releases. |
-| `m_gpg_info` | `…module.gnupg_info_gathering` | Displays local GnuPG installation details. |
-| `m_key_server_sync` | `…module.key_server_sync` | Searches and syncs keys with HKP/VKS servers. |
-| `m_email` | `…module.email` | E-mail helpers built on vmime. |
+| Directory | Module ID | Description |
+| ------------------- | ------------------------------ | ---------------------------------------------- |
+| `m_ver_check` | `…module.version_checking` | Checks for new GpgFrontend releases. |
+| `m_gpg_info` | `…module.gnupg_info_gathering` | Displays local GnuPG installation details. |
+| `m_key_server_sync` | `…module.key_server_sync` | Searches and syncs keys with HKP/VKS servers. |
+| `m_email` | `…module.email` | E-mail helpers built on vmime. |
+| `m_pgp_inspect` | `…module.pgp_inspect` | Inspects the packet structure of OpenPGP data. |
## Repository Layout
```
modules/
-├── include/ # Shared module-side helpers (header-only)
-│ ├── GFModuleDeclare.h # GF_MODULE_API_DECLARE — forward-declares all entry points
-│ ├── GFModuleDefine.h # GF_MODULE_API_DEFINE_V2 — implements metadata & event dispatch
-│ ├── GFModuleCommonUtils.hpp # DUP/UDUP macros, logging, CB_SUCC/CB_ERR, Qt<->C helpers
-│ └── GFModuleExport.h # GF_MODULE_EXPORT visibility attribute
├── src/
│ ├── m_ver_check/ # Version checking module
│ ├── m_gpg_info/ # GnuPG info gathering module
│ ├── m_key_server_sync/ # Key server sync module
-│ └── m_email/ # E-mail module
+│ ├── m_email/ # E-mail module
+│ ├── m_pgp_inspect/ # OpenPGP structure inspector
+│ └── CMakeLists.txt
└── CMakeLists.txt
```
-SDK headers are at `src/sdk/` in the main tree and added to the include path
-automatically. See [`src/sdk/README.md`](../src/sdk/README.md) for the full API
-reference.
+Each module directory holds a `module.json` (its signed identity — see
+[Writing a New Module](#writing-a-new-module)) beside its `CMakeLists.txt`.
+
+The module-facing SDK is not in this sub-repository. Two layers live in the
+main tree:
+
+- `src/sdk/` — the public SDK (`gf_sdk`): the `GFSDK*` functions and structs a
+ module calls directly (`GFGpgEncrypt`, `GFStorageStateGetText`, …), or
+ through the `gf::sdk::` C++ conveniences in `GFSDK.hpp`. Stateless — every
+ call that needs the host takes an explicit context argument. See
+ [`src/sdk/README.md`](../src/sdk/README.md) for the full architecture.
+- `src/module_runtime/include/` — `GFModule.h` and friends, statically linked
+ into every module. This is what a module author actually includes: it
+ supplies the event/result types, memory helpers, logging macros, and the
+ one bootstrap function (`GFModuleGetApi`) that negotiates the ABI with the
+ host and dispatches events to your hooks.
+
+Both are added to the include path automatically by `gf_add_module()`.
## Build Notes
@@ -43,15 +58,16 @@ reference.
superproject CMake configuration.
- The default toolchain targets **Qt 6**. If `GPGFRONTEND_QT5_BUILD` is
enabled, the modules target is skipped entirely.
-- Module shared libraries are placed under the `modules/` subdirectory of the
- runtime output directory.
- `CMAKE_AUTOMOC`, `CMAKE_AUTORCC`, and `CMAKE_AUTOUIC` are enabled
automatically for all modules.
-- Translations use `qt_add_translations`; `.ts` files live under each module's
- `ts/` subdirectory. The set of locales is shared with the application: it is
- declared once as `GPGFRONTEND_SUPPORTED_LOCALES` in
- [`cmake/Translations.cmake`](../cmake/Translations.cmake), and each module
- expands it into its `TS_FILES` with `gpgfrontend_collect_ts_files()`. Run
+- Translations, packaging, and signing are all handled by `gf_add_module()` —
+ see below. There is nothing left to wire up by hand in a module's own
+ `CMakeLists.txt`.
+- Translations use the shared locale set: it is declared once as
+ `GPGFRONTEND_SUPPORTED_LOCALES` in
+ [`cmake/Translations.cmake`](../cmake/Translations.cmake), and `.ts` files
+ live under each module's `ts/` subdirectory, named after its
+ `translation_context`. Run
[`scripts/update_translations.sh`](../scripts/update_translations.sh)
to create/refresh the `.ts` files for the app and every module at once.
@@ -65,174 +81,373 @@ modules/src/my_module/
Add `add_subdirectory(my_module)` to `modules/src/CMakeLists.txt`.
-### 2. Declare the entry points
+### 2. Declare the module's identity in `module.json`
-```cpp
-// MyModule.h
-#pragma once
-#include "GFModuleDeclare.h"
-GF_MODULE_API_DECLARE
+```json
+{
+ "schema_version": 1,
+ "id": "com.example.my_module",
+ "version": "1.0.0",
+ "name": "MyModule",
+ "description": "What this module does.",
+ "author": "Your Name",
+ "capabilities": ["ui"],
+ "events": ["APPLICATION_LOADED"],
+ "translation_context": "ModuleMyModule"
+}
```
+An optional `"min_host_version"` field pins the oldest GpgFrontend release the
+module requires; it defaults to the version of GpgFrontend it was built
+against.
+
+This file is the single source of truth for the module's identity: it feeds
+the generated C++ identity header, the translation wiring, and the signed
+package manifest, so none of those can drift apart.
+
+`events` is an allowlist, enforced by the **host**: it refuses to subscribe a
+packaged module to an event its signed manifest does not name, and refuses
+any id this host never fires at all. The runtime additionally reconciles the
+list against your handler table and will not activate if they disagree, so
+listing an event you don't handle — or handling one you didn't list — is an
+error rather than a silent no-op.
+
+`capabilities` decides what your module can reach, and it is enforced. At
+activation the host mints a capability table from this list; a group you did
+not declare is simply absent, and the matching SDK call returns its failure
+value and logs once instead of doing anything. Two kinds of name go in the
+list:
+
+| declared | kind | effect |
+| --------- | -------- | ---------------------------------------------------------------- |
+| `gpg` | granted | sign, encrypt, decrypt, verify, keys, key lists, result analysis |
+| `pgp` | granted | packet-structure inspection, no keyring or engine |
+| `ui` | granted | widgets, dialogs, theme colours, settings pages, tab views |
+| `editor` | granted | reading the document the user currently has open |
+| `storage` | granted | application settings, the caches, the runtime register table |
+| `process` | granted | running an external program |
+| `network` | recorded | **not** enforced — see below |
+
+Buffers, memory, logging, event subscription and translations are always
+available and are not declared.
+
+`network` is recorded, signed and shown to the user, but the host does not
+mediate it: your module opens a socket through Qt, and there is nothing in
+between for the host to withhold. It is kept separate from the granted list
+rather than mixed in, so that a permission list never claims to enforce
+something it cannot.
+
+Declaring a capability you do not use costs the user trust for nothing.
+Failing to declare one you do use is caught at run time, by a log line naming
+the capability and the entry point that wanted it — for example:
+
+```
+GFGpgDecrypt needs the "gpg" capability, which this module's signed manifest
+does not declare. Add it to module.json's "capabilities" and rebuild; the
+call did nothing.
+```
+
+An unknown capability name fails the build, at configure time, naming the
+vocabulary.
+
### 3. Implement the module
```cpp
// MyModule.cpp
-#include "MyModule.h"
-
-#include <GFSDKBasic.h>
-#include <GFSDKLog.h>
+#include <GFModule.h>
+#include "GFModuleIdentity.h" // generated from module.json by gf_add_module()
-#include "GFModuleDefine.h"
-#include "GFModuleCommonUtils.hpp"
+auto OnActivate() -> GFResult {
+ LOG_INFO("MyModule activating");
+ return GFResult::Ok();
+}
-GF_MODULE_API_DEFINE_V2(
- "com.example.my_module", // unique ID — lower-case, dot-separated
- "MyModule", // display name
- "1.0.0", // version
- "What this module does.", // description
- "Your Name" // author
-);
+auto OnMainWindowMenuMounted(const GFEvent& event) -> GFEventResult {
+ QMenu* help_menu = nullptr;
+ if (auto r = event.RequireGui("help_menu", help_menu); !r.ok) return r;
+ // ... add actions to help_menu ...
+ return GFEventResult::Ok();
+}
-auto GFRegisterModule() -> int { return 0; }
+auto OnDeactivate() -> GFResult { return GFResult::Ok(); }
+auto OnUnload() -> void { LOG_INFO("MyModule unloading"); }
-auto GFActiveModule() -> int {
- LISTEN("APPLICATION_LOADED");
- return 0;
-}
+constexpr GFEventBinding kEvents[] = {
+ {"MAINWINDOW_MENU_MOUNTED", &OnMainWindowMenuMounted},
+};
-REGISTER_EVENT_HANDLER(APPLICATION_LOADED, [](const MEvent& event) -> int {
- LOG_INFO("Hello from MyModule!");
- CB_SUCC(event);
-});
+constexpr GFModuleHooks kHooks = {
+ sizeof(GFModuleHooks),
+ GF_MODULE_ID, GF_MODULE_VERSION, GF_MODULE_TRANSLATION_CONTEXT,
+ &OnActivate, &OnDeactivate, &OnUnload,
+ kEvents, std::size(kEvents),
+};
-auto GFDeactivateModule() -> int { return 0; }
-auto GFUnregisterModule() -> int { return 0; }
+extern "C" GF_MODULE_EXPORT auto GFModuleGetApi(uint32_t abi)
+ -> const GFModuleApi* {
+ return GFModuleRuntimeGetApi(abi, &kHooks);
+}
```
+Every hook in `GFModuleHooks` is optional — pass `nullptr` for one you have
+nothing to say about, as the real `m_ver_check` module does for
+`on_deactivate` when everything it registered was event-driven rather than
+handed to the host directly. `GFModuleGetApi` is the one piece of ABI a
+module writes by hand, deliberately not hidden behind a macro: it is what
+makes the linker keep `gf_module_runtime`'s entry point, since nothing else
+in a module references it — the host resolves the symbol after the module is
+linked.
+
### 4. Add CMakeLists.txt
```cmake
-# Collect all source files in this directory
-set(MODULE_SOURCE "")
-aux_source_directory(. MODULE_SOURCE)
+gf_add_module(
+ NAME my_module
+ QT Core Widgets
+ # UI_DIR ui # if you have a Qt Designer forms directory
+ # LINK some_target # extra libraries, if any
+)
+```
+
+`gf_add_module()` reads `module.json`, builds the target from every source
+file in the directory (or `SOURCES` if given explicitly), generates the
+identity header, wires up translations for every locale in
+`GPGFRONTEND_SUPPORTED_LOCALES`, and builds + signs the resulting
+`.gfmodule` package — all from those handful of values. See
+[`modules/src/m_ver_check/CMakeLists.txt`](src/m_ver_check/CMakeLists.txt)
+for a real, minimal example.
+
+After adding or changing `tr()`/`GC_TR()` strings, run
+[`scripts/update_translations.sh`](../scripts/update_translations.sh) to sync
+every locale's `.ts` file — it creates any that are missing, so you never
+hand-author them.
+
+> Match the Qt major version of the host build — a version mismatch will
+> prevent the module from loading.
+
+## Key Concepts
+
+### What a module may link
+
+Two static archives, both module-side, both linked for you by
+`gf_add_module()`:
+
+- **`gf_sdk`** — the public SDK. Every `GFSDK*` function you call is defined
+ here, forwarding to the capability table the host handed your module. It is
+ **stateless**: no bound table, no current module, no thread-local.
+- **`gf_module_runtime`** — the per-module runtime state: your context,
+ identity, hooks, event dispatch, translations.
-# Register the module; MODULE_TARGET is set to the resulting target name
-register_module(my_module MODULE_TARGET ${MODULE_SOURCE})
+Your module has no link edge to `gf_core`, `gf_ui` or `gf_host_api`, and
+cannot acquire one: `gf_add_module()` fails at configure time if a host
+library appears anywhere in your link closure, the module is linked with
+`-Wl,--no-undefined`, and `scripts/check_module_boundary.sh` inspects the
+built native afterwards and fails if it imports any `GF*` symbol or exports
+anything but `GFModuleGetApi`.
-# Link Qt modules your code needs
-target_link_libraries(${MODULE_TARGET} PRIVATE Qt::Core Qt::Widgets)
+If you need something the SDK does not offer, the change is a primitive in
+`src/sdk/GFSDKHostApi.h`, its implementation in `src/sdk/host/`, and a wrapper
+in `src/sdk/api/` — not a link line.
-# --- Translations (optional) ---
-# Expand the shared locale set (cmake/Translations.cmake) into the .ts list so
-# the module ships exactly the languages the application does. "ModuleMyModule"
-# is the base name prefixing each ts/<base>.<locale>.ts path.
-set(LOCALE_TS_PATH ${CMAKE_CURRENT_SOURCE_DIR}/ts)
-gpgfrontend_collect_ts_files(TS_FILES "ModuleMyModule" "${LOCALE_TS_PATH}")
+### Every host call takes a context
-module_add_translations(${MODULE_TARGET}
- TS_FILES ${TS_FILES}
- SOURCES ${MODULE_SOURCE}
- INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR})
+```cpp
+auto OnSomething(const GFEvent& e) -> GFEventResult {
+ auto* ctx = e.Context(); // from the event
+ auto keys = gf::sdk::ExportKey(ctx, channel, key_id, true);
+ LOG_I() << "exported" << keys.size() << "bytes";
+ return GFEventResult::Ok();
+}
```
-Then run
-[`scripts/update_translations.sh`](../scripts/update_translations.sh) to
-generate the `ts/ModuleMyModule.<locale>.ts` files for every supported locale —
-it creates any that are missing, so you never hand-author them.
+Outside a handler, `GFModuleSdkContext()` returns the same context. It is a
+plain load of a pointer that never changes after activation, so it is safe
+from any thread — but work you start on a worker should **capture** the
+context rather than look it up there. Capturing is what makes a piece of
+asynchronous code say which module it belongs to, which is the whole reason
+the context is an argument instead of a global.
-> Match the Qt major version of the host build — a version mismatch will
-> prevent the module from loading.
+Functions that need nothing from the host take no context at all:
+`GFCompareSoftwareVersion` and `GFUIHumanSize` are the two.
-## Key Concepts
+### Logging
+
+Five levels, `LOG_T` (trace) through `LOG_E` (error). The stream form is the
+one to reach for:
+
+```cpp
+LOG_W() << "open" << path << "failed:" << file.errorString();
+LOG_T() << "option line:" << line << "fields:" << fields.size();
+```
+
+It takes no format string, so there is no placeholder to get wrong and no
+arity to mismatch, and it prints anything QDebug can — enums, `bool`,
+`QByteArray`, containers. It is spelled exactly like the host's own
+`LOG_W() << ...`, and — like every other module-side call — needs no explicit
+context: it reads `GFModuleSdkContext()` internally.
+
+The older forms remain:
+
+```cpp
+LOG_INFO("started"); // one fixed string
+FLOG_INFO("started with %1 accounts", count); // Qt placeholders, NOT printf
+```
+
+`FLOG_*` uses `QString::arg`, so `%1`/`%2` — a printf `%d` prints itself, and
+an argument `arg()` has no overload for must be converted by hand. That is
+what the stream form avoids.
+
+Each module logs under its own category, named for its id, so a log line says
+which module wrote it and which of its source lines it came from:
+
+```
+[module.email] [W] EMailImapController.cpp:412 - imap connect failed
+```
+
+Trace is for per-item chatter and stays off at `--log-level debug`; run with
+`--log-level trace` to see it.
### Memory management
-All strings and buffers crossing the module boundary must use the SDK
-allocator. Never mix `new`/`delete` or `malloc`/`free` with SDK pointers.
+**SDK arguments are borrowed.** Pass a string straight through —
+`id.toUtf8().constData()` for a `QString`, or a string literal — and keep
+owning it. Do not pre-allocate a copy to hand over; nothing on the other side
+frees it.
+
+**SDK return values are owned.** Reclaim a returned `char*` with `UDUP` (or
+`USECDUP` for one from the secure/wiping allocator):
```cpp
-DUP("hello") // char* owned by callee — pass to SDK functions
-UDUP(ptr) // consume char*, return QString and free
-SECDUP("secret") // same as DUP but uses the secure (zeroed-on-free) allocator
-USECDUP(ptr) // same as UDUP but frees with the secure allocator
+QString locale = UDUP(GFAppLocale(GFModuleSdkContext()));
```
-A parameter typed `char*` (not `const char*`) takes ownership — always pass
-`DUP(...)` at the call site. A returned `char*` is caller-owned — use `UDUP`
-to convert and free in one step.
+The one exception to "arguments are borrowed" is a struct a module builds and
+hands over _whole_ — `GFModuleEvent`, `GFModuleEventParam`,
+`GFCommandExecuteContext` and the like — whose `char*` members still have to
+be allocated with `GFModuleStrDup` (`DUP(...)`), because ownership of the
+whole struct is what transfers.
+
+```cpp
+DUP("hello") // char* owned by the callee — only for a transferred struct field
+SECDUP("secret") // same, from the wiping allocator, for a secret
+UDUP(ptr) // consume an owned char* return value, get a QString
+USECDUP(ptr) // same as UDUP, but frees with the wiping allocator
+```
### Events
-Subscribe in `GFActiveModule` and handle with `REGISTER_EVENT_HANDLER`.
-Every handler must end with `CB_SUCC` or `CB_ERR` to continue the callback
-chain.
+Bind event ids to handlers in a static `GFEventBinding[]` table (see step 3
+above) — there is no separate subscribe call, and the set must match
+`module.json`'s `events` array exactly. Every handler returns a
+`GFEventResult`; the runtime does the transport, so there is nothing to
+answer manually on the common path.
+
+Which events exist, and what each one MEANS, is written down once in
+[`src/core/module/ModuleEventRegistry.cpp`](../src/core/module/ModuleEventRegistry.cpp).
+That catalogue is what the host checks a subscription against, so an id that
+is not in it is refused rather than silently never delivered. Each entry
+records:
+
+- **observe or extend.** Most events tell you something happened and do not
+ read your reply. A small, deliberate set are extension points where what
+ you return, or what you do to a borrowed host object, changes the outcome —
+ the `EDIT_TAB_TYPE_*_OP_*` operations (your reply replaces the user's
+ document), the `FILE_EXT_*` handoffs (the host stops and leaves the work to
+ you), the key-server requests, and the menu/tab mounting points.
+- **whether the reply is read at all.** `MAINWINDOW_MENU_MOUNTED` hands you a
+ menu and ignores what you return; you extend by mutating the menu, not by
+ answering.
+- **whether you may defer.** Where the flag allows it, return
+ `GFEventResult::Deferred()` and answer later through `event.Answer()`,
+ from any thread.
+
+Nothing is a veto. No trigger site cancels an operation on a module's word,
+and `TriggerEvent` is asynchronous and returns nothing, so a veto is not
+expressible today.
```cpp
-auto GFActiveModule() -> int {
- LISTEN("MAINWINDOW_MENU_MOUNTED");
- return 0;
+auto OnMainWindowMenuMounted(const GFEvent& event) -> GFEventResult {
+ QMenu* menu = nullptr;
+ if (auto r = event.RequireGui("help_menu", menu); !r.ok) return r;
+ // ... add actions ...
+ return GFEventResult::Ok();
}
-
-REGISTER_EVENT_HANDLER(MAINWINDOW_MENU_MOUNTED, [](const MEvent& event) -> int {
- auto* menu = GFUIGetGUIObjectAs<QMenu>(event["help_menu"]);
- if (!menu) CB_ERR(event, -1, "no help_menu");
- // ... add actions ...
- CB_SUCC(event);
-});
```
+A handler that starts asynchronous work returns `GFEventResult::Deferred()`
+and answers later through `event.Answer()`, from any thread, at most once —
+see `CheckUpdate()` in
+[`m_ver_check/VersionCheckingModule.cpp`](src/m_ver_check/VersionCheckingModule.cpp)
+for a real example.
+
### Runtime values
-A shared in-process key-value store for live configuration. Keys and
-namespaces are normalised to lower-case.
+A shared, typed register table (`storage` capability) for live
+configuration, visible to the host and to other modules. Keys and namespaces
+are lower-case; text and bool are kept apart because a lookup of the wrong
+type is a miss, not a conversion. Use the `gf::sdk::` wrappers
+(`GFSDK.hpp`), which apply a fallback where the raw calls only report
+absence:
```cpp
-GFModuleUpsertRTValue(DUP("my_module"), DUP("ready"), DUP("true"));
-QString v = UDUP(GFModuleRetrieveRTValueOrDefault(
- DUP("my_module"), DUP("ready"), DUP("false")));
+auto* ctx = GFModuleSdkContext();
+gf::sdk::SetStateBool(ctx, "my_module", "ready", true);
+bool ready = gf::sdk::StateBool(ctx, "my_module", "ready", /*fallback=*/false);
+
+gf::sdk::SetStateText(ctx, "my_module", "last_error", message);
+QString v = gf::sdk::StateText(ctx, "my_module", "last_error");
```
### Cache
-```cpp
-// Session cache
-GFCacheSave(DUP("key"), DUP("value"));
-GFCacheSaveWithTTL(DUP("token"), DUP("abc"), 3600);
-QString v = UDUP(GFCacheGet(DUP("key")));
+Three tiers, one set of calls, differing in how long a value lives and
+whether it is wiped — `GF_STORE_SESSION` (in memory), `GF_STORE_DURABLE`
+(disk, plain), `GF_STORE_SECURE_DURABLE` (disk, wiping allocator):
-// Durable cache (survives restart, JSON values)
-GFDurableCacheSave(DUP("state"), DUP(QJsonDocument(obj).toJson()));
-QString raw = UDUP(GFDurableCacheGet(DUP("state")));
+```cpp
+auto* ctx = GFModuleSdkContext();
+gf::sdk::SetCacheText(ctx, GF_STORE_SESSION, "key", "value");
+gf::sdk::SetCacheText(ctx, GF_STORE_DURABLE, "token", "abc", /*ttl_seconds=*/3600);
+QString v = gf::sdk::CacheText(ctx, GF_STORE_DURABLE, "token");
+gf::sdk::RemoveCache(ctx, GF_STORE_DURABLE, "token");
```
### GPG operations
-```cpp
-// Sign
-GFGpgSignResult* sr = nullptr;
-GFGpgSignData(channel, key_ids, count, DUP(plain), 0, 1 /*ascii*/, &sr);
-QString sig = UDUP(sr->signature);
-GFGpgFreeResult(sr->gpgme_sign_result);
-GFFreeMemory(sr);
+Every host call takes the context explicitly, first argument, per
+[Every host call takes a context](#every-host-call-takes-a-context). Crypto
+entry points return through a move-only `GFGpgResult(ctx)`, which owns
+whatever native result it wraps and releases it on every path (including an
+early return) through its own destructor. Payloads cross the boundary as
+`GFBuf`/`GFBufferView`, not raw `char*`, since a signature or an encrypted
+blob is exact octets rather than text.
-// Encrypt
-GFGpgEncryptionResult* er = nullptr;
-GFGpgEncryptData(channel, key_ids, count, DUP(plain), 1, &er);
-QString cipher = UDUP(er->encrypted_data);
-GFGpgFreeResult(er->gpgme_encrypt_result);
-GFFreeMemory(er);
+```cpp
+auto* ctx = GFModuleSdkContext();
+auto in = GFBuf::Copy(ctx, plain_bytes);
+QVector<const char*> key_ids = ...; // one entry per recipient/signer key id
-// Decrypt
-GFGpgDecryptResult* dr = nullptr;
-GFGpgDecryptData(channel, DUP(cipher), &dr);
-QString plain = UDUP(dr->decrypted_data);
-GFGpgFreeResult(dr->gpgme_decrypt_result);
-GFFreeMemory(dr);
+GFGpgResult r(ctx);
+if (GFGpgEncrypt(ctx, channel, key_ids.data(), key_ids.size(), in.View(),
+ /*ascii=*/1, r.Out()) != GF_GPG_OK) {
+ // r.ErrorString() explains why the call itself failed
+}
+if (r.Error() != GF_GPG_ERR_NO_ERROR) {
+ // the operation ran but gpg reported an error; r.ErrorString() again
+}
+auto encrypted = r.DataCopy();
```
-Use `GFGpgCurrentGpgContextChannel()` to obtain the active channel from the
-main window.
+`GFGpgSign`, `GFGpgDecrypt` and `GFGpgVerify` follow the same shape. Use
+`GFGpgCurrentChannel(ctx)` to obtain the active channel from the main window.
+See
+[`m_email/EMailBasicGpgOpera.cpp`](src/m_email/EMailBasicGpgOpera.cpp) for
+real, complete call sites including error handling.
+
+If you need the same structured result the host's own crypto dialogs show
+(recipients, signatures, validity), use `gf::sdk::AnalyseResult(ctx,
+operation, channel, err, r.CapsuleId())` — this is engine neutral, working
+the same way whether the active engine is GnuPG or rPGP.
### UI
@@ -240,141 +455,188 @@ All Qt widget creation and dialog display must happen on the main thread. The
SDK handles the dispatch automatically.
```cpp
-// Create a widget on the main thread
+// Create a widget on the main thread (dispatches there for you if called
+// from a worker)
void* dlg = GUI_OBJECT(MyDialogFactory, QVariant("arg"));
-// Show a dialog (non-blocking)
-GFUIShowDialog(dlg, GFUIGetGUIObject(DUP("main_window")));
+// Show it (non-blocking); "main_window" style handles come from an event's
+// own parameters, resolved with event.RequireGui<T>() — see Events below
+GFUIShowDialog(GFModuleSdkContext(), dlg, parent_handle);
```
+None of the shipped modules currently build a dialog off the main thread —
+`m_ver_check`'s update dialog, for example, is built directly with `new
+QDialog(parent)` from a menu-click handler, which already runs on the main
+thread. Reach for `GUI_OBJECT`/`GFUIShowDialog` only when you need to build
+or show UI from code that is not already there.
+
#### Settings pages
-A module can own a page in the application's Settings dialog. Register it from
-`GFActiveModule()` and drop it again in `GFDeactivateModule()` — the registry
-holds a function pointer into your shared object, and leaving it behind would
-crash the next time the dialog is built.
+A module can own a page in the application's Settings dialog. Register it
+from `OnActivate()` and drop it again in `OnDeactivate()` — the registry
+holds a function pointer into your shared object, and leaving it behind
+would crash the next time the dialog is built.
```cpp
-constexpr auto kPageId = "com.example.mymodule.settings";
+constexpr auto kSettingsPageId = "com.example.mymodule.settings";
auto MySettingsPageFactory(void* /*data*/) -> void* {
return new MySettingsPage(); // fresh, unparented, one per dialog
}
-auto GFActiveModule() -> int {
- const auto keywords =
- QStringList{GC_TR("proxy"), GC_TR("timeout")}.join('\n');
- GFUIRegisterSettingsPage(DUP(kPageId), DUP("features"),
- DUP(GC_TR("My Module")), QDUP(keywords),
- MySettingsPageFactory, nullptr);
- return 0;
+auto OnActivate() -> GFResult {
+ const auto keywords = QStringList{GC_TR("proxy"), GC_TR("timeout")}.join('\n');
+ gf::sdk::RegisterSettingsPage(GFModuleSdkContext(), kSettingsPageId,
+ "features", GC_TR("My Module"),
+ keywords.toUtf8().constData(),
+ MySettingsPageFactory, nullptr);
+ return GFResult::Ok();
}
-auto GFDeactivateModule() -> int {
- GFUIUnregisterSettingsPage(DUP(kPageId));
- return 0;
+auto OnDeactivate() -> GFResult {
+ GFUIUnregisterSettingsPage(GFModuleSdkContext(), kSettingsPageId);
+ return GFResult::Ok();
}
```
-The dialog finds your page's `SetSettings()` and `ApplySettings()` **by name**,
-so declare both as `public slots` (or `Q_INVOKABLE`). `SetSettings()` loads the
-stored values and is called again if the user cancels; `ApplySettings()` writes
-them on OK. Stage edits in the widget and only persist them in `ApplySettings()`
-— that is what makes Cancel discard them. Declare a
-`void SignalRestartNeeded(int)` signal if a change on your page needs one; the
-dialog connects to it if it is there.
+`gf::sdk::RegisterSettingsPage` (from `GFSDK.hpp`) fills in the append-only
+`GFUISettingsPageSpec` struct for you; see
+[`m_key_server_sync/KeyServerSyncModule.cpp`](src/m_key_server_sync/KeyServerSyncModule.cpp)
+for the real, complete pattern.
+
+The dialog finds your page's `SetSettings()` and `ApplySettings()` **by
+name**, so declare both as `public slots` (or `Q_INVOKABLE`). `SetSettings()`
+loads the stored values and is called again if the user cancels;
+`ApplySettings()` writes them on OK. Stage edits in the widget and only
+persist them in `ApplySettings()` — that is what makes Cancel discard them.
+Declare a `void SignalRestartNeeded(int)` signal if a change on your page
+needs one; the dialog connects to it if it is there.
Register `title` and `keywords` untranslated, with `GC_TR(...)`: modules are
-activated before the module translators are installed, so anything translated at
-registration time would be frozen at its source text for the rest of the session.
-`section_id` is one of `application`, `keys_engines`, `features`, `system`.
+activated before their translator is installed, so anything translated at
+registration time would be frozen at its source text for the rest of the
+session. `section_id` is one of `application`, `keys_engines`, `features`,
+`system`; anything else becomes its own section after those.
### Project settings
-Use `GFUIGlobalSettings()` to read and write persistent application settings.
-Settings are shared across all modules and the host application, so prefix
-every key with your module name to avoid collisions.
+Use `GFStorageSettingsRoot(ctx)` (`storage` capability — reading a
+preference is not drawing, so it does not need `ui`) to read and write
+persistent application settings. Settings are shared across all modules and
+the host application, so prefix every key with your module name to avoid
+collisions.
```cpp
auto* settings = qobject_cast<QSettings*>(
- static_cast<QObject*>(GFUIGlobalSettings()));
+ static_cast<QObject*>(GFStorageSettingsRoot(GFModuleSdkContext())));
// Write a value
settings->setValue("my_module/check_on_startup", true);
-settings->setValue("my_module/api_endpoint", "https://example.com");
// Read a value with a default
bool check = settings->value("my_module/check_on_startup", false).toBool();
-QString endpoint = settings->value("my_module/api_endpoint", "").toString();
-
-// Check whether a key has ever been written
-if (!settings->contains("my_module/check_on_startup")) {
- // first run — apply defaults
-}
```
-Settings are typically read in a `LOAD_SETTINGS` event handler and written in
-an `APPLY_SETTINGS` event handler, following the pattern used by the built-in
-modules:
+Settings are typically read in a `..._LOAD_SETTINGS` event handler and
+written in a `..._APPLY_SETTINGS` handler, following the pattern used by the
+built-in modules — see `OnNetworkSettingsTabLoadSettings()` /
+`OnNetworkSettingsTabApplySettings()` in
+[`m_ver_check/VersionCheckingModule.cpp`](src/m_ver_check/VersionCheckingModule.cpp)
+for the full, real pattern, including dispatching the actual widget access
+onto the main thread with `QMetaObject::invokeMethod`.
-```cpp
-REGISTER_EVENT_HANDLER(NETWORK_SETTINGS_TAB_LOAD_SETTINGS,
- [](const MEvent& event) -> int {
- auto* settings = qobject_cast<QSettings*>(
- static_cast<QObject*>(GFUIGlobalSettings()));
- if (!settings) CB_ERR(event, -1, "settings unavailable");
+### Translations
- auto* tab = GFUIGetGUIObjectAs<QWidget>(event["network_settings_tab"]);
- if (!tab) CB_ERR(event, -1, "tab unavailable");
+Wrap strings with `QCoreApplication::translate("GTrC", "...")` (or `GC_TR`
+for a string that must stay untranslated until later — see
+[Settings pages](#settings-pages)). `GTrC` is a single, fixed translation
+context declared by `GFModule.h`; it is unrelated to a module's own
+`translation_context` field in `module.json`, which only names the `.qm`
+file. There is nothing left to register: `gf_module_runtime` installs the
+module's translator automatically during activation, before any event
+subscription and before `OnActivate()` runs.
+
+`.ts` files go in `ts/<translation_context>.<locale>.ts` and are embedded as
+Qt resources under `:/i18n/`. The locale set is not chosen per module — it
+comes from the shared `GPGFRONTEND_SUPPORTED_LOCALES` list (see
+[Build Notes](#build-notes)), so the module always offers the same languages
+as the application. After adding or changing translatable strings, run
+[`scripts/update_translations.sh`](../scripts/update_translations.sh) to sync
+every locale's `.ts`.
- QMetaObject::invokeMethod(QApplication::instance(), [=]() {
- auto* cb = tab->findChild<QCheckBox*>("my_check_box");
- if (cb)
- cb->setChecked(
- settings->value("my_module/check_on_startup", false).toBool());
- }, Qt::BlockingQueuedConnection);
+## Packaging, Signing & Distribution
- CB_SUCC(event);
-});
+Every module ships as a signed `.gfmodule` package (a manifest, its Ed25519
+signature, and — for an externally-distributed module — the build public key
+that signature was made with) plus the native shared library it names. The
+package format, verification rules, and trust model live in
+[`src/core/module/`](../src/core/module) — this section only summarises the
+parts that matter for building and distributing a module.
-REGISTER_EVENT_HANDLER(NETWORK_SETTINGS_TAB_APPLY_SETTINGS,
- [](const MEvent& event) -> int {
- auto* settings = qobject_cast<QSettings*>(
- static_cast<QObject*>(GFUIGlobalSettings()));
- if (!settings) CB_ERR(event, -1, "settings unavailable");
+### Integrated modules (the ones in this directory)
- auto* tab = GFUIGetGUIObjectAs<QWidget>(event["network_settings_tab"]);
- if (!tab) CB_ERR(event, -1, "tab unavailable");
+Nothing to do by hand: `gf_add_module()` builds, signs, and installs the
+package for you as part of the normal build, using an Ed25519 keypair
+generated once per build tree (`gf_module_keygen`, invoked automatically by
+the top-level CMake configure). That key is compiled into the host binary as
+its trust root and is never exported, published, or reused across builds —
+it exists only to prove a package was produced by _this_ build tree, not to
+identify a publisher.
- QMetaObject::invokeMethod(QApplication::instance(), [=]() {
- auto* cb = tab->findChild<QCheckBox*>("my_check_box");
- if (cb)
- settings->setValue("my_module/check_on_startup", cb->isChecked());
- }, Qt::QueuedConnection);
+The resulting layout, under the modules root (`build/artifacts/modules` in a
+dev build):
- CB_SUCC(event);
-});
+```
+<namespace-root>/<key>/module.gfmodule # manifest + signature
+<namespace-root>/<key>/native/libgf_mod_xxx.so
```
-### Translations
+`<key>` is derived from the module's id (`ModuleDirectoryKey()`, e.g.
+`com.bktus.gpgfrontend.module.email` → `email-a1b2c3d4e5f60718293a`); it is a
+locator only, never trusted on its own — the manager re-derives it from the
+signed manifest's `id` and refuses a descriptor sitting in the wrong
+directory.
-```cpp
-DEFINE_TRANSLATIONS_STRUCTURE(ModuleMyModule); // creates GTrC context
+### External modules (a user's own)
-auto GFRegisterModule() -> int {
- REGISTER_TRANS_READER();
- return 0;
-}
-```
+An external module lives outside the application entirely, in the user's
+profile under `mods/<key>/`, with the same `module.gfmodule` + `native/`
+layout as above. For it to load:
-Wrap strings with `QCoreApplication::translate("GTrC", "...")`.
-`.ts` files go in `ts/ModuleMyModule.<locale>.ts` and are embedded as Qt
-resources under `:/i18n/`. The locale set is not chosen per module — it comes
-from the shared `GPGFRONTEND_SUPPORTED_LOCALES` list (see [Build
-Notes](#build-notes)), so the module always offers the same languages as the
-application. After adding or changing `tr()` strings, run
-[`scripts/update_translations.sh`](../scripts/update_translations.sh) to sync
-every locale's `.ts`.
+1. The user sets **Settings → General → Module Discovery** to "Also Look For
+ Modules I've Added". This only controls whether the `mods/` directory is
+ scanned at all — finding a module there never implies running it.
+2. In the **Module Controller**, the user trusts the build key the module was
+ signed with ("signatures by this key are worth considering") and then
+ enables that specific module ("run this one"). These are deliberately two
+ separate decisions: trusting a key applies to every module that key ever
+ signs, so collapsing it with "run this one" into a single click would
+ make the first of them too easy to grant by accident.
+3. Both take effect on the next restart.
+
+**Current limitation:** there is no first-party tool to sign an external
+module with your own keypair. `gf_module_packager` (the CLI `gf_add_module`
+calls internally) only signs against the build tree's own compiled-in key,
+so it can only ever produce _integrated_-shaped packages. Producing a valid
+external `.gfmodule` today means reproducing the format by hand: a
+canonically-serialised `manifest.json` (see
+[`ModuleManifest.h`](../src/core/module/ModuleManifest.h) for the schema),
+an Ed25519 detached signature over its exact bytes, and the signer's public
+key, zipped together as `META-INF/manifest.json`, `META-INF/manifest.sig`,
+and `META-INF/build-key.pub`. The test suite constructs exactly this by hand
+with libsodium — see
+[`src/test/core/ModuleDescriptorArchive.h`](../src/test/core/ModuleDescriptorArchive.h)
+and
+[`src/test/core/GpgCoreTestModuleExternalTrust.cpp`](../src/test/core/GpgCoreTestModuleExternalTrust.cpp)
+— but there is no supported command-line workflow for it yet.
+
+### macOS has no external modules
+
+External modules are refused unconditionally on macOS: Library Validation
+means the process cannot load a `dylib` that does not carry this
+application's own Team ID, and a third-party module by definition does not.
+This is a platform policy, not a missing feature — admitting one past
+GpgFrontend's own checks would still have it refused by `dyld`.
## Licensing