Nested TCC & the plugin system: opening a compiler from your script
xsCreateTCC hands the script a fully preloaded compiler — dynamic rules, expression evaluation, third-party plugins all travel the road of "compiling native code at runtime".
Overview
40.1 Intro
A class of needs can't be met with "hard-coded logic": risk rules changing weekly, report formulas user-defined, third parties wanting to extend your platform. The traditional answer embeds an interpreter (Lua/JS) — at the price of a second runtime and boundary overhead. XServer's answer is natural: the host compiles scripts with TCC; scripts can compile code with TCC too — same language, same symbols, zero boundary. xsCreateTCC preloads include paths and every host symbol, so nested code can call your APIs from birth.
40.2 The five plugin-compile steps
/* requirement: the user supplies a C expression; the server evaluates it */
typedef int (*ExprFn)(int);
static bool CompileExpr(const char* sExpr, ExprFn* ppFn)
{
char sSource[1024];
TCCState* pTcc;
snprintf(sSource, sizeof(sSource),
"int eval(int x) { return %s; }", sExpr);
/* 1. create (include paths and symbols preloaded) */
pTcc = xsCreateTCC();
if ( pTcc == NULL ) return false;
/* 2. compile the string */
if ( tcc_compile_string(pTcc, sSource) < 0 ) {
xsDestroyTCC(pTcc);
return false; /* syntax error — reply 400 */
}
/* 3. relocate into memory */
if ( tcc_relocate(pTcc, TCC_RELOCATE_AUTO) < 0 ) {
xsDestroyTCC(pTcc);
return false;
}
/* 4. take the symbol (declare the function-pointer type first — the signature contract is yours) */
*ppFn = (ExprFn)tcc_get_symbol(pTcc, "eval");
/* 5. pTcc is held by the caller: never destroy while the symbol is in use */
return *ppFn != NULL;
}
/* when done: xsDestroyTCC(pTcc) — ppFn dies immediately after */
What just happened: the key discipline of the five steps (create → compile → relocate → take symbol → destroy) is that a symbol's lifetime binds to the TCCState — the artifact lives in memory the TCCState owns; Destroy kills it. So a plugin cache's correct structure is "cache the TCCState", never "cache the function pointer".
40.3 Error callbacks & symbol injection
- Take over error output:
tcc_set_error_funcroutes compile diagnostics into your collector (default: stderr) — returning diagnostics to a user who submitted a broken expression is part of product quality - Symbol injection:
tcc_add_symbollets plugins call host functions you define — expose a smallplugin_api_*-prefixed function set and you have a plugin SDK - Permissions wide open by default: xsCreateTCC injects the full host symbol set — narrow it yourself (add_symbol only a whitelist), the raw material for designing your plugin sandbox boundary (full discussion in ch. 44)
- VFS available: plugin code includes VFS headers the same way;
tcc_vfs_mount_memorycan inject "virtual files" at runtime — exactly how the host loads scripts
40.4 Concurrency with hot reload
The safety question when nested compilers and reloads race: could a plugin's resolved host function be reclaimed at a swap? func_test's case gives the answer and the bar — 4 threads concurrently running nested compiles × 12 requests, with repeated reloads throughout, zero failures. Mechanically:
- A plugin's TCCState is your generation's state — hang it in a registry, destroy it with the generation, and safety is automatic (DestroyTCC inside ServiceUnit)
- Symbols an old-generation plugin resolved point at old-generation host functions — the old lease keeps them alive until the plugin retires together
- The new generation recompiles plugins (or lazily on demand) — isomorphic with route-table rebuilding
40.5 Contract
- Lifecycle: function pointers live inside the TCCState; the unit of caching is the TCCState; pointers die at Destroy.
- Generations: plugin state is managed per generation (create in Init / destroy in Unit) — concurrency with reload is then safe.
- Signatures: the post-lookup pointer type is your declaration — document the contract; getting it wrong is undefined behavior.
Common mistakes
- Symptom
- Plugin cache hits jump into freed memory.
- Cause
- The artifact memory belongs to the TCCState. Both cache key and value should be "expression → TCCState", lifetime identical to the cache's.
- Symptom
- Arguments misaligned, stack corruption, random crashes.
- Cause
- tcc_get_symbol returns an address without checking signatures — the type contract is entirely yours. Fix one signature template in the plugin SDK (e.g. int fn(int)) and document it hard.
Exercises
Implement POST /expr {code:"x*2+1", x:5} → 11.
Add an "expression → TCCState" cache (LRU capped at 100) with hit stats.
Replicate the func_test case: 4 threads evaluating concurrently × 12 requests, with 5 swaps in between.
Summary
| Point | Conclusion |
|---|---|
| Entry point | xsCreateTCC: include paths + the full host symbol set preloaded |
| Five steps | create → compile → relocate → take symbol → (when done) destroy |
| Cache unit | the TCCState (not the function pointer) |
| Concurrency safety | plugin state managed per generation; func_test has the concurrent acceptance case |
| Boundary | permissions wide open by default; narrowing via your own symbol whitelist (see ch. 44) |
- xrt reference · memory — memory reconciliation for plugin caches and destruction paths