Skip to content

Module Development Guide

Germán Luis Aracil Boned edited this page Apr 6, 2026 · 1 revision

Module Development Guide

Quick Start

Create a new module in 5 minutes:

1. Create the directory

mkdir modules/mod_mymod

2. Write the source

// modules/mod_mymod/mod_mymod.c
#include "portal/portal.h"

static portal_core_t *g_core = NULL;

static portal_module_info_t info = {
    .name        = "mod_mymod",
    .version     = "1.0.0",
    .description = "My custom module",
    .author      = "Your Name",
    .depends     = ""              // comma-separated soft deps
};

// 1. INFO — return module descriptor
portal_module_info_t *portal_module_info(void) {
    return &info;
}

// 2. LOAD — initialize, register paths
int portal_module_load(portal_core_t *core) {
    g_core = core;

    // Register paths (with optional ACL labels)
    portal_labels_t labels = { .count = 0 };  // public
    core->path_register(core, "/mymod/hello", "mod_mymod", &labels);
    core->path_register(core, "/mymod/data",  "mod_mymod", &labels);

    // Declare events
    core->event_declare(core, "/events/mymod/action", "mod_mymod", &labels);

    core->log(core, LOG_INFO, "mod_mymod loaded");
    return 0;  // 0 = success, non-zero = failure
}

// 3. UNLOAD — cleanup, unregister
int portal_module_unload(portal_core_t *core) {
    core->path_unregister(core, "/mymod/hello");
    core->path_unregister(core, "/mymod/data");
    core->log(core, LOG_INFO, "mod_mymod unloaded");
    return 0;
}

// 4. HANDLE — process messages
int portal_module_handle(portal_core_t *core,
                         const portal_msg_t *msg,
                         portal_resp_t *resp) {
    // Route by path
    if (strcmp(msg->path, "/mymod/hello") == 0) {
        resp->status = PORTAL_OK;
        snprintf(resp->body, sizeof(resp->body),
                 "Hello, %s!", msg->context.user);
        return 0;
    }

    if (strcmp(msg->path, "/mymod/data") == 0) {
        if (msg->method == PORTAL_METHOD_GET) {
            // Read from storage
            char buf[4096];
            if (core->store_get(core, "mymod.data", buf, sizeof(buf)) == 0) {
                resp->status = PORTAL_OK;
                snprintf(resp->body, sizeof(resp->body), "%s", buf);
            } else {
                resp->status = PORTAL_NOT_FOUND;
                snprintf(resp->body, sizeof(resp->body), "No data stored");
            }
        } else if (msg->method == PORTAL_METHOD_SET) {
            // Write to storage
            core->store_set(core, "mymod.data", msg->body);
            core->event_emit(core, "/events/mymod/action", "data updated");
            resp->status = PORTAL_OK;
            snprintf(resp->body, sizeof(resp->body), "Stored");
        }
        return 0;
    }

    resp->status = PORTAL_NOT_FOUND;
    return 0;
}

3. Compile

The Makefile auto-discovers modules in modules/*/:

make
# or compile manually:
gcc -shared -fPIC -o modules/mod_mymod/mod_mymod.so \
    modules/mod_mymod/mod_mymod.c \
    -Iinclude

4. Configure

# portal.conf
[modules]
load = mod_mymod

5. Test

./portal -c portal.conf &

# CLI
./portal -r
portal:/> mymod hello
Hello, admin!

# HTTP
curl http://localhost:8080/api/mymod/hello

The 4 Required Exports

Every module must export exactly these 4 symbols:

Export Signature Called When
portal_module_info portal_module_info_t *(void) Module discovery
portal_module_load int (portal_core_t *core) Module loaded
portal_module_unload int (portal_core_t *core) Module unloaded
portal_module_handle int (portal_core_t *core, const portal_msg_t *msg, portal_resp_t *resp) Message received

Message Handling

Route by path and method:

int portal_module_handle(portal_core_t *core,
                         const portal_msg_t *msg,
                         portal_resp_t *resp) {
    // Check path
    if (strncmp(msg->path, "/mymod/", 7) != 0) {
        resp->status = PORTAL_NOT_FOUND;
        return 0;
    }

    const char *subpath = msg->path + 7;

    // Check method
    switch (msg->method) {
        case PORTAL_METHOD_GET:
            // read operation
            break;
        case PORTAL_METHOD_SET:
            // write operation
            break;
        case PORTAL_METHOD_ACTION:
            // execute operation
            break;
        case PORTAL_METHOD_DELETE:
            // delete operation
            break;
        default:
            resp->status = PORTAL_BAD_REQUEST;
            break;
    }
    return 0;
}

Soft Dependencies

Declare dependencies in the info struct. They are soft — your module loads even if deps are missing, but you should check at runtime:

static portal_module_info_t info = {
    .name = "mod_mymod",
    .depends = "mod_cache,mod_email"   // soft deps
};

int portal_module_load(portal_core_t *core) {
    if (!core->module_loaded(core, "mod_cache")) {
        core->log(core, LOG_WARN, "mod_cache not loaded, caching disabled");
    }
    return 0;
}

ACL Labels

Protect paths with labels:

portal_labels_t admin_only = {
    .labels = { "admin" },
    .count = 1
};
core->path_register(core, "/mymod/admin", "mod_mymod", &admin_only);

portal_labels_t ops_or_admin = {
    .labels = { "admin", "ops" },
    .count = 2
};
core->path_register(core, "/mymod/manage", "mod_mymod", &ops_or_admin);

// Public path (no labels = anyone can access)
portal_labels_t public = { .count = 0 };
core->path_register(core, "/mymod/status", "mod_mymod", &public);

Inter-Module Communication

Call other modules through the core dispatch:

portal_msg_t req = {0};
portal_resp_t res = {0};

strncpy(req.path, "/cache/mykey", sizeof(req.path));
req.method = PORTAL_METHOD_GET;
req.context = msg->context;  // propagate auth + trace

core->dispatch(core, &req, &res);

if (res.status == 200) {
    // use res.body
}

Events

Declare, emit, and subscribe:

// In load:
core->event_declare(core, "/events/mymod/changed", "mod_mymod", &labels);

// In handle:
core->event_emit(core, "/events/mymod/changed", "something changed");

// Subscribe to other module events:
void my_callback(const char *path, const char *body, void *userdata) {
    // handle event
}
core->event_subscribe(core, "/events/iot/*", "mod_mymod", my_callback);

Release Checklist

  • All paths registered in load, unregistered in unload
  • All events declared in load
  • Handle returns 0 (non-zero = critical error)
  • No global state leaked between unload/load cycles
  • Thread-safe if using shared data
  • Labels set on sensitive paths
  • Soft deps checked at runtime
  • Version string updated

Clone this wiki locally