Server-side UDFs via .hrb — design (notes from LetoDB)
Goal
Let app functions (index-key UDFs like Reverse(), plus server-side
routines such as backups) execute inside openads_serverd,
loaded from a developer-supplied Harbour .hrb placed next to the
server. Precedent: Kresin’s original LetoDB letoudf.hrb
(SourceForge p/letodb, source/server/server.prg) — verified in
source. (The elch LetoDBf fork carries the same loading shape, but
per project guidance it is NOT the reference: it forked to fill
the MT gap and never succeeded. Canonical upstream is Kresin’s
LetoDB; our threading design stands on its own.)
How LetoDB does it (verified in Kresin’s source)
- File convention:
letoudf.hrbin the server base dir (cDirBase + "letoudf.hrb"). The original additionally lets its Planner load further.hrbtask modules (pPlan["hrb"] := hb_hrbLoad(...)); the fork also accepts an alternate name through the reload command. - Gate flag: server option
UDFEnabled; with it off, remote functions are refused ("UDF Error: using remote functions is disabled."). Missing file without debug is silent — functions simply unavailable. - Lifecycle: loaded once at startup before serving
(
leto_HrbLoad());UDF_Initcalled if exported;UDF_Exitat shutdown; hot reload without restart via management message (LETOCMD_udf_rel, CLIreloadcommand) which unloads (hb_hrbUnload) and re-loads, with load errors caught and logged (BEGIN SEQUENCE … RECOVER). - Dispatch: resolve dynamic symbol,
hb_vmPushDynSym+ push args +hb_vmDo(n)insidehb_vmRequestReenter(), errors captured (hb_xvmSeqBegin/End), per-thread VM state set before the call (letofunc.c, task dispatch). - Key architectural insight: LetoDB’s server is a Harbour program, so expressions compiled server-side resolve UDF names against the loaded HRB for free. OpenADS’s server is C++ with a hand-rolled evaluator — we must bridge explicitly (below).
OpenADS mapping
- Embed
hbvm/hbrtlbehind a build flag (OPENADS_WITH_HARBOUR_UDF, default off) so stock builds stay dependency-free. Harbour’s license exception permits linking into the server binary — confirm with project licensing before enabling by default anywhere. - Module location:
--udf-module <path>flag + ini key, default<serverdir>/openads_udf.hrb. (Accepting LetoDB’sletoudf.hrbname as an alias costs nothing and eases migration — decide at implementation.) - Lifecycle: load at startup, call
UDF_Initif exported,UDF_Exitat shutdown, log module hash + exported function list. Reload channel TBD (SIGHUP vs. admin wire op — LetoDB uses a management message; mirror that). - Call shape A — scalar expression functions (index keys,
FORconditions): inapply_scalar_fn, unknown builtin → registry lookup → bridge call (engineValue↔HB_ITEM, byte-exact, no codepage translation; stable return width per tag). Name missing everywhere →AE_INVALID_EXPRESSIONat create time (fail fast, never a degenerate tag). - Call shape B — server-side procedures (backups, maintenance
jobs): invoked by admin/client command with an argument array,
mirroring LetoDB’s task dispatch (dynsym +
hb_vmDo+ error capture). Define one wire op, not one per routine.
Hard constraints
- Purity for shape A: deterministic, side-effect-free, no DB I/O inside. Maintenance calls it on every write; side effects corrupt or deadlock. Document; consider a debug-mode re-entrancy tripwire later.
- Threading: builds/maintenance run on worker threads —
per-thread VM attach /
hb_vmRequestReenter, exactly as LetoDB does. Prototype this first; it is the main risk. - Performance: one VM call per record per tag on build and per write on maintenance. Correctness first; bulk-build cost is acceptable, note it in docs.
- Version coupling: the
.hrbmust match the app version — a stale module builds wrong keys silently. Log names + hash at startup; docs state the coupling. - Trust: arbitrary pcode in the server process (an infinite
loop hangs a worker). Vendor-trusted file + supervised
process, same boundary as stored procedures.
UDFEnabled-style kill switch ships with it (default: off unless module loads).
Rollout
- Fail-fast validation on unknown index functions (no silent empty keys).
- Loader + bridge behind the flag; builtins (REVERSE, …) continue in parallel — most tag UDFs may never need the HRB path.
- Pilot: Vouch tag expressions, then one shape-B routine (backup).
- Reload channel + operator docs.
Open questions
- Default module name/location (
openads_udf.hrbvsletoudf.hrbalias). → v1.09.52:openads_udf.hrbnext to the binary, overridable via--udf_module/ini. - Reload via signal vs. admin wire op. → deferred: restart to refresh.
- Allowed value types across the bridge (memo? datetime?). → v1.09.52: strings byte-exact, numbers, logicals as T/F, dates as YYYYMMDD.
- Whether shape-A functions may open other tables (LetoDB UDF areas allow it; index purity says no — decide per shape). → v1.09.52: full RDD linked (parity), purity stays contractual.
v1.09.52 implementation notes (embedded-host lessons)
- Harbour never registers
HB_HRB*for static links (no init table anywhere upstream) — our backend publishes them with its ownHB_INIT_SYMBOLS_BEGINblock, which also pullsrunner.odeterministically. No whole-archive needed. - Dispatch via public
hb_vmTryEval(protected call maintained upstream) + explicithb_vmThreadInitper thread; hand-rolledxvmSeqdiscipline corrupts the stack — do not reinvent it. hb_vmInitexactly once per process (second init corrupts the VM; observed as AV in module_INITSTATICS).- Modules link STRICT (no LAZY): externs must resolve in the bare
host (RT table + our init table). Exotic statements (e.g.
RELEASE→ unregistered__MVXRELEASE) fail the load naming the symbol — surface it, do not degrade. - Module
STATICsdo not resolve when functions run outsidehb_hrbDomodule context (writes land in an implicit memvar, later reads fail “Variable does not exist”). Index UDFs must be stateless; lifecycle proof via return values/PUBLIC, not statics. - Link set (MinGW, whole set required): hbvmmt hbrtl hbmacro hbpcre
hblang hbcpage hbzlib hbrdd rddfpt rddntx hbsix hbcommon + gt
driver + winmm/iphlpapi; GNU needs
--start-groupfor thehbrtl↔hbvmcycles. Local SDKs are often 32-bit-only — CI builds Harbour from source (pinned commit, cached).