Module Guide
Step-by-step guide to creating a TLS module. For the complete API reference, see Core API.
Quick Start
1. Create the source file:
modules/mod_hello/mod_hello.c
2. Write the module:
#include "portal/portal.h"
#include <string.h>
static portal_module_info_t info = {
.name = "hello", .version = "1.0.0",
.description = "Hello world module",
.soft_deps = NULL
};
portal_module_info_t *portal_module_info(void) { return &info; }
int portal_module_load(portal_core_t *core) {
core->path_register(core, "/hello/resources/greeting", "hello");
core->path_set_access(core, "/hello/resources/greeting", PORTAL_ACCESS_READ);
core->log(core, PORTAL_LOG_INFO, "hello", "Module loaded");
return PORTAL_MODULE_OK;
}
int portal_module_unload(portal_core_t *core) {
core->path_unregister(core, "/hello/resources/greeting");
return PORTAL_MODULE_OK;
}
int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
portal_resp_t *resp) {
(void)core;
if (strcmp(msg->path, "/hello/resources/greeting") == 0) {
portal_resp_set_status(resp, PORTAL_OK);
portal_resp_set_body(resp, "Hello from TLS!\n", 16);
return 0;
}
portal_resp_set_status(resp, PORTAL_NOT_FOUND);
return -1;
}
3. Build:
gcc -shared -fPIC -Wall -Wextra -Werror -std=c11 -D_GNU_SOURCE \
-Iinclude -Isrc -Ilib/libev \
-o modules/mod_hello.so \
modules/mod_hello/mod_hello.c src/core/core_message.c
4. Configure — create /etc/portal/<instance>/modules/mod_hello.conf:
# mod_hello — Hello world module
enabled = true
[mod_hello]
greeting = Hello from TLS!
5. Load at runtime:
portal:/> module load hello
portal:/> get /hello/resources/greeting
Hello from TLS!
Module Anatomy
Every module is a shared library (.so) that exports exactly 4 symbols. No more, no less. The core calls them in order: info → load → handle (N times) → unload.
| Symbol | Purpose |
|---|---|
portal_module_info() | Return name, version, description, soft deps |
portal_module_load(core) | Initialize: register paths, open resources |
portal_module_unload(core) | Cleanup: unregister paths, free memory |
portal_module_handle(core, msg, resp) | Handle incoming messages |
File naming: mod_<name>.so — the <name> part must match info.name. If info.name is "hello", the file must be mod_hello.so.
Handling Messages
The handler receives every message routed to your paths. A typical pattern dispatches on path first, then on method:
int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
portal_resp_t *resp) {
if (strcmp(msg->path, "/mymod/items") == 0) {
switch (msg->method) {
case PORTAL_METHOD_GET: /* return list */ break;
case PORTAL_METHOD_SET: /* create/update */ break;
default:
portal_resp_set_status(resp, PORTAL_BAD_REQUEST);
return -1;
}
}
portal_resp_set_status(resp, PORTAL_NOT_FOUND);
return -1;
}
Reading input: everything you need is in the portal_msg_t structure:
| Source | How to read |
|---|---|
| Path | msg->path |
| Method | msg->method |
| Headers | msg->headers[0..header_count-1] |
| Body | msg->body, msg->body_len |
| Auth | msg->ctx->auth.user, msg->ctx->auth.labels |
Talking to Other Modules
Modules communicate by sending messages through the core. You never call another module's functions directly — you send a message to its path and the core routes it:
/* Ask the cache module for a value */
portal_msg_t *req = portal_msg_alloc();
portal_msg_set_path(req, "/cache/functions/get");
portal_msg_set_method(req, PORTAL_METHOD_GET);
portal_msg_add_header(req, "key", "session_token");
portal_resp_t resp;
int rc = core->send(core, req, &resp);
if (rc == 0 && resp.status == PORTAL_OK) {
/* resp.body contains the cached value */
core->log(core, PORTAL_LOG_DEBUG, "mymod",
"Got value: %.*s", (int)resp.body_len, resp.body);
}
portal_msg_free(req);
This is the same mechanism whether the target module is local or on a remote node. The core handles routing transparently (Law 7).
Soft Dependencies
If your module benefits from another module but can work without it, declare it as a soft dependency in info and check at runtime:
static const char *deps[] = { "cache", NULL };
static portal_module_info_t info = {
.name = "mymod", .version = "1.0.0",
.description = "My module with optional caching",
.soft_deps = deps
};
int portal_module_handle(portal_core_t *core, const portal_msg_t *msg,
portal_resp_t *resp) {
/* Check if cache is available before using it */
if (core->module_loaded(core, "cache")) {
/* Use cache — send message to /cache/... */
} else {
/* Proceed without cache — degrade gracefully */
}
/* ... */
}
Access Control
Every path you register must declare its access mode (Law 8). You can also restrict paths to specific labels. The core enforces ACL automatically — your handler only runs if the caller has permission:
int portal_module_load(portal_core_t *core) {
/* Register a read-only public path */
core->path_register(core, "/mymod/resources/status", "mymod");
core->path_set_access(core, "/mymod/resources/status", PORTAL_ACCESS_READ);
/* Register a read-write path restricted to admins */
core->path_register(core, "/mymod/resources/config", "mymod");
core->path_set_access(core, "/mymod/resources/config", PORTAL_ACCESS_RW);
core->path_add_label(core, "/mymod/resources/config", "admin");
/* Register a write-only path for operators and admins */
core->path_register(core, "/mymod/functions/reset", "mymod");
core->path_set_access(core, "/mymod/functions/reset", PORTAL_ACCESS_WRITE);
core->path_add_label(core, "/mymod/functions/reset", "admin");
core->path_add_label(core, "/mymod/functions/reset", "operator");
return PORTAL_MODULE_OK;
}
A path with no labels is open to all authenticated users. A path with labels requires the caller to have at least one matching label. The root user bypasses all ACL checks.
Async I/O
If your module reads from a serial port, socket, or any file descriptor, you must use core->fd_add() to register it with the event loop. Never block the event loop (Law 13):
static void serial_read_cb(portal_core_t *core, int fd, void *userdata) {
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
core->log(core, PORTAL_LOG_DEBUG, "serial",
"Read %zd bytes from serial port", n);
/* Process data, emit event */
core->event_emit(core, "/events/serial/data", buf, n);
}
}
int portal_module_load(portal_core_t *core) {
int fd = open("/dev/ttyUSB0", O_RDONLY | O_NONBLOCK);
if (fd < 0) {
core->log(core, PORTAL_LOG_WARN, "serial", "Cannot open serial port");
return PORTAL_MODULE_OK; /* degrade gracefully */
}
core->fd_add(core, fd, serial_read_cb, NULL);
core->log(core, PORTAL_LOG_INFO, "serial", "Listening on /dev/ttyUSB0");
return PORTAL_MODULE_OK;
}
int portal_module_unload(portal_core_t *core) {
core->fd_remove(core, fd);
close(fd);
return PORTAL_MODULE_OK;
}
The callback fires whenever data is available on the file descriptor. The event loop remains free to process other messages. For CPU-intensive work, use the thread pool instead.
Release Checklist
Before shipping a module, verify every item:
- Exports exactly 4 symbols
info.namematches.sofilename- All paths registered in
load(), unregistered inunload() - All fds registered in
load(), removed inunload() - All memory freed in
unload() - Uses
core->log()for all output - Uses
core->fd_add()for all I/O - Checks
module_loaded()before using soft deps - Sets
resp->statusin every handler path - Compiles with
-Wall -Wextra -Werror - Every
path_registerfollowed bypath_set_access(Law 8) event_emit()on every state change (Law 10)
module load / module unload in a loop under valgrind until it's clean.