Scripting Engines
Write application logic in Lua, Python, C, or Pascal. All four languages participate equally in the same universe — same paths, same events, same resources.
The Logic Layer
Application scripting in TLS follows a three-part architecture that separates the framework from the languages from the user code:
1. mod_logic — the language-agnostic framework. This module owns the /logic/ path namespace. It manages the script directory, maintains the route table that maps HTTP paths to script handlers, and delegates execution to the appropriate language engine. When a request arrives for a scripted route, mod_logic determines which language owns it and forwards the message to that engine via its path (e.g., /logic_lua/functions/execute, /logic_python/functions/execute). mod_logic never interprets user code — it orchestrates.
2. Language engines — one module per language. Each engine (mod_logic_lua, mod_logic_python, mod_logic_c, mod_logic_pascal) is a standard TLS module that registers its own paths and handles script loading, compilation where needed, and execution. Each engine exposes the full Portal API to its language in the most natural idiom for that language. The engines are independent — you can load only Lua, or all four, or none.
3. Scripts — user application code. Scripts live in /var/lib/portal/<instance>/logic/<appname>/ and are written in the language of choice. Each script registers routes, subscribes to events, and calls portal paths to interact with the rest of the system. A script is an application — it has a lifecycle (load, run, unload) managed by its engine.
Lua 5.4 (mod_logic_lua)
Lua runs embedded in-process with zero-copy data transfer between the C core and the Lua VM. This makes it the fastest scripting option and the natural choice for high-frequency handlers, event processors, and anything that needs to stay close to the metal.
The full Portal API is exposed as a native Lua table:
local portal = require("portal")
-- Read any path
local status = portal.get("/core/status")
local devices = portal.get("/iot/resources/devices")
-- Call functions
local result, code = portal.call("/cache/functions/set",
{key="temp", value="23.5"})
-- Register HTTP routes
portal.route("GET", "/app/dashboard", "handle_dashboard")
-- Event handlers
portal.on("/events/iot/state_change", "on_change")
-- Logging
portal.log("info", "App loaded")
function handle_dashboard(req)
local metrics = portal.get("/metrics/resources/memory")
return "Dashboard: " .. metrics
end
Scripts location: /var/lib/portal/<instance>/logic/<appname>/main.lua
Python 3 (mod_logic_python)
Python runs in a forked subprocess to avoid CPython signal handler conflicts with the core's libev event loop. Communication between the parent C process and the Python subprocess flows through a JSON pipe bridge:
Parent (C) ──stdin──> Python subprocess ──stdout──> Parent
JSON request JSON response
stderr (ROUTE:, LOG:, READY:)
The subprocess architecture means Python scripts have access to the entire Python ecosystem — numpy, pandas, machine learning libraries — without risking the core's stability. The JSON bridge is invisible to the script author:
import portal
def handle_hello(req):
return "Hello from Python!"
def handle_compute(req):
import math
return f"Pi = {math.pi:.20f}"
portal.route("GET", "/app/pyapp/hello", handle_hello)
portal.route("GET", "/app/pyapp/compute", handle_compute)
Scripts location: /var/lib/portal/<instance>/logic/<appname>/main.py
C (mod_logic_c)
C scripts are compiled from .c source files using gcc at load time. The engine produces a shared object (.so), then dlopens it into the running process. This gives scripts native execution speed with direct access to the portal.h API — no marshalling, no serialization, no overhead.
Every C script exports three lifecycle functions:
#include "portal/portal.h"
int app_load(portal_core_t *core) {
core->path_register(core, "/app/capp/fast", "logic_c");
return 0;
}
int app_handle(portal_core_t *core,
const portal_msg_t *msg,
portal_resp_t *resp) {
portal_resp_set_status(resp, PORTAL_OK);
portal_resp_set_body(resp, "Native speed!\n", 14);
return 0;
}
int app_unload(portal_core_t *core) {
core->path_unregister(core, "/app/capp/fast");
return 0;
}
Scripts location: /var/lib/portal/<instance>/logic/<appname>/main.c
Pascal (mod_logic_pascal)
Pascal scripts are compiled from .pas source files using Free Pascal Compiler (fpc 3.2.2) at load time. The engine follows the same dlopen pattern as the C engine — compile to shared object, load into the process, call exported functions with the cdecl calling convention.
Pascal brings the same native speed as C with stronger type safety and a syntax familiar to a different audience. The Portal API is available through a Pascal unit that mirrors the C header.
Scripts location: /var/lib/portal/<instance>/logic/<appname>/main.pas
The Composition Pattern
The real power emerges when all layers work together. A single HTTP request can flow through mod_web, into mod_logic, down to a Lua script, which calls mod_iot, which talks to physical hardware — all through the same message-passing mechanism:
Browser → GET /api/app/dashboard
→ mod_web (HTTP → Portal message)
→ core routes to mod_logic (owns the path)
→ mod_logic sees it's a Lua route
→ sends message to /logic_lua/functions/execute
→ Lua function runs:
portal.get("/iot/resources/devices")
→ core routes to mod_iot
→ mod_iot queries Tapo via KLAP
→ returns device list
→ Lua formats response
→ flows back as HTTP response
Every arrow in that chain is a standard Portal message. The Lua script does not know how mod_iot talks to hardware. mod_web does not know that a Lua script handled the request. Each component sees only paths and messages.
portal.get('/nodeB/serial/com1/read') and transparently read a physical serial port on a remote machine. The Python script doesn't know about TCP, TLS, or that nodeB is a separate machine.
CLI Management
The logic layer and each engine expose standard resources for monitoring and debugging from the Portal CLI:
portal:/> get /logic/resources/status # framework status
portal:/> get /logic/resources/routes # all registered routes
portal:/> get /logic_lua/resources/status # Lua engine status
portal:/> get /logic_python/resources/status # Python engine status