Guía de módulos
Guía paso a paso para crear un module TLS. Para la referencia completa de la API, consulte API del núcleo.
Inicio rápido
1. Crear el archivo fuente:
modules/mod_hello/mod_hello.c
2. Escribir el 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. Compilar:
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. Configurar — crear /etc/portal/<instance>/modules/mod_hello.conf:
# mod_hello — Hello world module
enabled = true
[mod_hello]
greeting = Hello from TLS!
5. Cargar en tiempo de ejecución:
portal:/> module load hello
portal:/> get /hello/resources/greeting
Hello from TLS!
Anatomía de un module
Cada module es una biblioteca compartida (.so) que exporta exactamente 4 símbolos. Ni más, ni menos. El core los llama en orden: info → load → handle (N veces) → unload.
| Símbolo | Propósito |
|---|---|
portal_module_info() | Devolver nombre, versión, descripción y soft deps |
portal_module_load(core) | Inicializar: registrar path, abrir recursos |
portal_module_unload(core) | Limpieza: desregistrar path, liberar memoria |
portal_module_handle(core, msg, resp) | Gestionar mensajes entrantes |
Nombrado de archivos: mod_<name>.so — la parte <name> debe coincidir con info.name. Si info.name es "hello", el archivo debe ser mod_hello.so.
Gestión de mensajes
El handler recibe cada mensaje enrutado a sus path. Un patrón típico despacha primero por path y luego por método:
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;
}
Leer la entrada: todo lo que necesita está en la estructura portal_msg_t:
| Origen | Cómo leerlo |
|---|---|
| Path | msg->path |
| Método | msg->method |
| Headers | msg->headers[0..header_count-1] |
| Body | msg->body, msg->body_len |
| Autenticación | msg->ctx->auth.user, msg->ctx->auth.labels |
Comunicación con otros módulos
Los módulos se comunican enviando mensajes a través del core. Nunca se llama directamente a las funciones de otro module — se envía un mensaje a su path y el core lo enruta:
/* 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);
Este es el mismo mecanismo tanto si el module de destino es local como si está en un nodo remoto. El core gestiona el enrutamiento de forma transparente (Ley 7).
Soft dependencies
Si su module se beneficia de otro module pero puede funcionar sin él, declárelo como soft dependency en info y compruébelo en tiempo de ejecución:
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 */
}
/* ... */
}
Control de acceso
Cada path que registre debe declarar su modo de acceso (Ley 8). También puede restringir path a etiquetas específicas. El core aplica las ACL automáticamente — su handler solo se ejecuta si el llamante tiene permiso:
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;
}
Un path sin etiquetas está abierto a todos los usuarios autenticados. Un path con etiquetas requiere que el llamante tenga al menos una etiqueta coincidente. El usuario root ignora todas las comprobaciones de ACL.
E/S asíncrona
Si su module lee de un puerto serie, socket o cualquier descriptor de archivo, debe usar core->fd_add() para registrarlo en el event loop. Nunca bloquee el event loop (Ley 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;
}
El callback se ejecuta cada vez que hay datos disponibles en el descriptor de archivo. El event loop permanece libre para procesar otros mensajes. Para trabajo intensivo de CPU, use el pool de hilos en su lugar.
Lista de verificación para publicación
Antes de publicar un module, verifique cada elemento:
- Exporta exactamente 4 símbolos
info.namecoincide con el nombre del archivo.so- Todos los path registrados en
load(), desregistrados enunload() - Todos los fd registrados en
load(), eliminados enunload() - Toda la memoria liberada en
unload() - Usa
core->log()para toda salida - Usa
core->fd_add()para toda E/S - Comprueba
module_loaded()antes de usar soft deps - Establece
resp->statusen cada path del handler - Compila con
-Wall -Wextra -Werror - Cada
path_registerseguido depath_set_access(Ley 8) event_emit()en cada cambio de estado (Ley 10)
module load / module unload en bucle bajo valgrind hasta que esté limpio.