Compile Lua into Fast, Standalone Native Executables
clx is an ahead-of-time (AOT) Lua compiler and runtime for Linux, macOS, and Windows. It turns your Lua 5.5 scripts into fast, self-contained native binaries — no interpreter, no virtual machine, and no runtime dependencies to ship. Build once, and your program runs instantly, anywhere.
Everything you already know about Lua just works: the full language, standard libraries, and coroutines. When you need to load code at runtime, clx's optional dynamic mode will run a Lua virtual machine alongside your compiled code, giving you the best of both worlds: speed and flexibility.
Native modules are supported: write them against the clx C++ API or against the standard Lua 5.5 C API (lua.h, lauxlib.h, lualib.h) and link them with --modules. See the Modules and Lua C API sections for more information.
Features
Installation
clx is available as source code (build from any platform) and as pre-built binaries for Linux (x86_64), macOS (ARM64), and Windows (x86_64) from GitHub Releases, built automatically via CI. Linux binaries require glibc ≥ 2.39.
Build from source
Clone the repository and run the build script on macOS or Linux:
This installs the clx compiler to /usr/local/bin, the runtime libraries (libclx.a, libclx_size.a, libclx_capi.a, libclx_capi_size.a, libclx_lua.a) to /usr/local/lib, and the headers to /usr/local/include. Run ./build.sh uninstall to remove it.
Windows
This installs the compiler to %ProgramFiles%\clx\bin, the libraries (clx.lib, clx_size.lib, clx_capi.lib, clx_capi_size.lib) to %ProgramFiles%\clx\lib, and the headers to %ProgramFiles%\clx\include. Run build.bat uninstall to remove it.
On either platform, override the install location with -DCMAKE_INSTALL_PREFIX=<dir> when configuring.
Pre-built binaries
The archives from GitHub Releases ship the same layout as a source install, inside a clx-<platform>/ folder (bin/, include/, lib/), so you can extract them directly into the install prefix of your choice:
After this, clx is on your PATH with the libraries and headers under /usr/local.
Target architecture (CLX_ARCH)
By default clx targets the widest compatibility baseline: sse2 on x86_64 (-msse2 / /arch:SSE2) and native on ARM64 (-mcpu=native). The same flag is baked into the runtime libraries and injected into every binary clx compiles (including --debug builds), unless you override it with an explicit compiler flag.
Override with an environment variable before the wrapper script (any CLX_* CMake option works the same way):
When invoking cmake directly, use -D instead: cmake -S . -B build -DCLX_ARCH=avx2.
| CLX_ARCH | x86 flag (Clang/GCC) | x86 flag (MSVC) | ARM flag |
|---|---|---|---|
| sse2 (x86 default) | -msse2 | /arch:SSE2 | — |
| avx | -mavx | /arch:AVX | — |
| avx2 | -mavx2 | /arch:AVX2 | — |
| native | -march=native | /arch:AVX2 | -mcpu=native (ARM default) |
| generic | — | — | no flag, portable |
If you pass an explicit arch flag to clx itself, it takes precedence and the CLX_ARCH default is not added: clx file.lua -march=native.
Verify installation
Benchmarks
Performance comparison against Lua 5.5 and LuaJIT. The speedup is relative to Lua 5.5. Results are averages from 10 runs using hyperfine (Linux / macOS) or benchmarks\run.bat (Windows) on a single CPU; they vary with the C++ toolchain and environment.
| Runtime | Time | Speedup |
|---|
Getting Started
Build clx
Clone the repository and build from source:
Add a flag to vary the build: ./build.sh debug for a debug build, ./build.sh install to install, or ./build.sh clean to clear the build directory.
Alternatively, build manually with CMake:
Compile Your First Program
Create hello.lua:
Compile and run:
Your Second Program
Let's try something more interesting:
Compile it with --fast flag for better performances:
Language Features
clx supports most Lua 5.5 features including variables, control flow, functions, tables, metatables, coroutines, standard libraries, and bitwise operations. load(), loadfile(), and dofile() are registered only for programs compiled with --dynamic; they execute source in the embedded Lua VM and are absent from ordinary AOT and --minimal builds. require() of Lua file modules through package.path also requires --dynamic: those files are compiled and executed on the embedded VM rather than AOT-compiled. The AOT runtime does not provide string.dump() or debug directly; the dynamic VM provides its own library behavior.
Variables and Types
Control Flow
Functions
Tables and Metatables
Coroutines
String Module
Bitwise Operations
Performance Tips
Use local variables, prefer numeric for loops, and avoid mixing types for optimal performance.
Common Issues
Debugging Compilation Errors
If you get a C++ compilation error, you can see the generated code with --cpp:
This creates script.cpp in the current directory, which you can examine to see what's being generated.
Understanding Runtime Errors
Runtime errors show the Lua line where the error occurred:
The format is filename:line: message.
Next Steps
Read the Dynamic execution, CLI, Modules, and Compatibility documentation.
Dynamic execution
What is it?
Usually clx compiles your Lua ahead of time into a native program. Sometimes you also want to run Lua source at runtime — for example a user-supplied script, a config file, or a plugin. The optional --dynamic switch enables this by embedding a Lua 5.5 engine.
Don't combine --dynamic with --minimal if you need runtime loading — minimal builds leave out the library setup that enables it.
Runtime loading
A --dynamic build adds three familiar functions. load compiles a string of source and returns a callable function, loadfile does the same for a file, and dofile loads and immediately runs a file:
Always check the first result before calling the chunk, especially if the source comes from an untrusted user.
Requiring modules at runtime
A --dynamic build also completes require(): package.searchers[2] finds Lua files via package.path and runs them on the embedded VM — the same way loadfile does. Required modules are not AOT-compiled, and the value they return crosses the boundary back to your compiled code:
Modules bundled at compile time or registered in package.preload are found by the preload searcher first and never touch the VM. In plain AOT builds the file searcher locates the file but reports that --dynamic is required.
Things to keep in mind
- Your compiled code is usually faster. Dynamic calls cross between the two environments with some overhead, so keep performance-critical loops out of loaded code.
- Libraries are self-contained. Loaded code uses its own copy of the standard libraries; a compiled module isn't automatically visible to it through require.
- Values are converted, not shared. A table passed across the boundary is a copy or a proxy — don't rely on shared identity or metatables.
- Coroutines stay on one side. Keep create/resume/yield inside either the compiled code or the loaded chunk, not across the boundary.
Sharing values with loaded code
A loaded chunk can read your program's globals. Here your program sets a global that a loaded chunk then reads back:
You can also give a chunk its own environment table instead of using your globals.
Limitations
- Requires compiling with --dynamic; skipped in --minimal builds.
- load accepts a source string only (no reader-function form).
- string.dump and loading dumped bytecode aren't provided by the compiled runtime.
- Tables, metatables, userdata, and coroutines don't share identity across the two environments.
For full details, see doc/dynamic-lua.md.
CLI Reference
Usage
Options starting with - that are not recognized by clx are automatically passed through to the C++ compiler.
Build Mode
Output Options
Compilation Options
Choosing speed vs. size
The two common build modes boil down to a simple tradeoff:
For most ordinary programs the difference is small. Pick --fast when your program is dominated by computation, and --size when binary size matters more.
If you pass your own compiler options (for example -O2 or -march=native), clx uses exactly those and skips its default flags.
Platform-Specific
The C++ compiler is fixed when clx is built (the same compiler that built clx compiles your Lua scripts), which keeps toolchains consistent.
- Linux/macOS produce an executable with no extension, an object as .o, and a static library as .a.
- Windows produces .exe, .obj, and .lib.
Examples
Compile to an executable (the default):
Give the output a custom name:
Build for the fastest execution (or use --size, the default, for the smallest binary):
Build a debuggable program so you can step through the Lua source in a debugger:
Write out the generated C++ without compiling (useful when debugging clx itself):
Pass your own compiler flags (this replaces clx's default flags):
Produce an object file or a static library instead of an executable:
Environment Variables
clx respects these environment variables:
Exit Codes
Build with CMake
If building from source:
Modules
clx supports four ways to organize and load modules: Lua source modules compiled alongside your entry point (static preload), statically linked native modules — C++ or C — with --modules, and — with --dynamic — Lua files loaded at runtime through package.path, which execute on the embedded Lua 5.5 VM. All four are consumed via Lua's require().
A native module is either a C++ module built against the clx C++ API, or a C module built against the standard Lua 5.5 C API (lua.h, lauxlib.h, lualib.h). clx classifies each archive by scanning its symbols, so both kinds link with the same --modules argument.
Lua Source Modules
Pass multiple .lua files to clx — the first is the entry point, the rest become modules loadable via require:
Inside main.lua, require them by name (filename without .lua):
How it works
clx compiles each .lua file into a function luaopen_<module>, and your generated main() registers each module so it becomes available to require:
When your code calls require("mymodule"), clx checks whether the module was already loaded, and if not, runs its luaopen_ function once and caches the result. Later calls return the cached value without re-running it.
Linking
All builds link statically against libclx.a. No shared library is needed at runtime.
Module convention
A Lua source module should return a table (or any value) that becomes the result of require:
Runtime-Loaded Lua Modules (package.path)
With --dynamic, require() can also load Lua files at runtime through package.path, using the same searcher chain as stock Lua (package.searchers: preload → Lua file → C). Required files are compiled and executed on the embedded Lua 5.5 VM — they are not AOT-compiled, so keep hot loops in your bundled modules:
In plain AOT builds, package.path, package.searchers, and package.searchpath exist, but require of a file module reports that --dynamic is required. See Dynamic execution.
C++ Native Modules (Statically Linked)
You can link precompiled C++ code using --modules:
The function must use CLX_API (which provides extern linkage and proper symbol visibility):
The generated main() calls register_module, which stores the wrapper in package.preload — the function runs only on first require().
Writing a C++ native module
Compile it to an object file with your C++ compiler:
Then link with your Lua script. clx looks for my_native_mod.a (or .lib on Windows) in the current directory, then in <clx-install-dir>/lib/clx/, and on POSIX also in /usr/local/lib/clx/:
If your module depends on external libraries, pass link flags directly:
Native C Modules (Standard Lua 5.5 C API)
clx ships the standard Lua 5.5 C API headers — lua.h, lauxlib.h and lualib.h — plus a bridge that implements that API on top of the clx runtime, so existing C modules written against the official API compile and run unchanged:
The --modules argument is the archive filename without its extension, and the module must export a C luaopen_<name> symbol:
clx classifies each archive before code generation by scanning its symbols:
- An Itanium/MSVC-mangled luaopen_<name> is treated as a C++ module and declared as clx::LValue luaopen_<name>(clx::LState*).
- A plain, unmangled luaopen_<name> is treated as a C module; clx emits extern "C" int luaopen_<name>(struct lua_State*) and a small adapter that runs it and seeds package.loaded for require.
- clx_luaopen_<name> (the prefixed form some wrappers emit) is also treated as a C module, declared under that symbol.
Names are resolved in the current directory first, then in clx's own lib/clx install locations and any -L flags you pass. An input .lua file whose stem collides with a precompiled C module name is rejected.
Two things to know when writing C modules:
- luaL_checkoption returns the index of the chosen option in lst, not the string (this is a Lua 5.5 change).
- luaL_openlibs and luaL_openselectedlibs live in lualib.h, not lauxlib.h — include it if you call them.
How to write a module against this API, what the bridge implements, and where it differs from stock Lua are covered in the Lua C API section.
Linking: the C API wrapper is its own archive
The C API bridge is not part of libclx.a / libclx_size.a. It is built into a dedicated archive installed alongside them:
| Archive | Contents |
|---|---|
| libclx.a / clx.lib | core runtime — values, tables, GC, stdlibs, coroutines |
| libclx_size.a / clx_size.lib | the same core runtime, -Os (default) |
| libclx_capi.a / clx_capi.lib | Lua 5.5 C API wrapper — lua_*, luaL_*, luaopen_* adapters |
| libclx_capi_size.a / clx_capi_size.lib | the same wrapper, -Os (default) |
clx links libclx_capi*.a only when --modules pulls in an archive that classifies as a C module. C++ modules and plain Lua programs never pull it in, so they pay nothing for the wrapper.
The core archive and the wrapper always come as a matched pair — pick the variant once with --size (default) or --fast:
Both archives are resolved together, in the same places, in order: build/, lib/ and lib64/ next to the clx binary, then the install prefix's libdir (the one used at cmake --install time). If the wrapper archive cannot be found the link stops with a "library not found" error; if it is found but stale, the link fails with undefined lua_* or clx::luaapi_c_module_open symbols.
If you link by hand instead of going through the driver:
Keep the pair consistent: -Os and -O3 objects are interchangeable at link time, but only one core/wrapper variant should be pulled into a given binary.
Compiling Lua to Libraries
Static Library
This produces libmylib.a on Linux/macOS or mylib.lib on Windows.
Object File
This produces mylib.o on Linux/macOS or mylib.obj on Windows.
All export luaopen_mylib. A host C++ program links against the static library:
Combining All Approaches
All AOT modules in this build are registered in clx's package.preload and loaded via the AOT require. This does not populate the embedded VM's package registry.
Options Reference
Writing native modules
A native module is a C or C++ file that exports one luaopen_<name> function returning a table, then gets linked with --modules. Write it against the clx C++ API (see doc/modules.md) or against the standard Lua 5.5 C API — see the Lua C API section for the worked example, what the bridge implements, and how it differs from stock Lua.
Clx C++ API
If you want to extend clx with native C++ code — for example a high-performance library module — this page shows you the API. Everything lives in namespace clx and starts with #include <clx.h>.
One thing sets clx apart from the classic Lua C API: there is no stack. You work directly with LValue objects, which is closer to normal C++. That makes the API smaller and less error-prone. If you would rather keep an existing C module written against the standard API, see Lua C API.
Setting up and tearing down
L owns all the memory (strings, tables, threads). Call close() exactly once when you are done.
Values: LValue
An LValue can hold any Lua value: nil, boolean, number, string, table, function, userdata, or thread. You create values with factory functions rather than pushing them onto a stack:
| Function | Gives you |
|---|---|
| nil() | nil |
| boolean(bool) | a boolean |
| number(double) | a floating-point number |
| integer(int64_t) | an integer |
| string(L, s) | a string (short strings are stored in place, no allocation) |
| table(L) | a table |
| cfunction(L, func) | a function |
| lightuserdata(void*) | an opaque pointer |
Read values back with the as_* helpers:
Quick type checking
Conversions
Two families help you read arguments safely.
to_* — lenient. Use a default when the value is not what you expect:
check_* — strict. Throw a LRuntimeException on mismatch:
opt_* — treat nil as the default, otherwise throw on mismatch:
Globals
Tables
Read and write with the get_field / set_field helpers (which respect __index / __newindex), or use raw_get / raw_set to bypass metatables:
Iterate a table with a C++ range loop:
Length and concatenation:
Calling functions
Or use pcall to catch errors instead of throwing:
A variadic form accepts native C++ values directly:
Coroutines
resume returns {true, ...results} or {false, error_message}.
Errors
Throw errors as exceptions:
There are helpers for common argument errors:
Writing a module
A native module is a C++ source file that exports one function (luaopen_<name>) which returns a table. See Modules for the full worked example and how to compile and link it.
A complete example using lazy registration:
Compile this into an object or library, then link it with:
Your module then becomes available as require("mylib").
Porting an existing Lua C module?
The migration guide (doc/migration-guide.md) walks you through converting a module written against the classic Lua C API. If you would rather keep it as C, link it with --modules as described in Lua C API.
The Lua C API
clx ships the standard Lua 5.5 headers — lua.h, lauxlib.h and lualib.h — plus a bridge that implements them on top of the clx runtime. C modules written against the official API compile and run unchanged: no source port, no Lua library to link against.
This page is for people using the C API from C or C++. It covers how to write a module, what the bridge implements, and where it behaves differently from stock Lua. For compiling, finding and linking archives, see Modules. If you would rather write native code in C++, see the C++ API section.
A first module
Three things follow from how clx links modules:
- The archive must be named after the module — demo.a (demo.lib on Windows) — and export a plain extern "C" int luaopen_demo(lua_State*).
- Compile the module as C, or wrap its opener in extern "C" if you compile it as C++. clx decides how to call the opener by scanning the archive's symbols: a mangled luaopen_demo is treated as a C++ module and expected to have the signature clx::LValue luaopen_demo(clx::LState*), which a stock Lua module does not have.
- The opener runs once, eagerly, when the program starts — before your entry chunk runs. require("demo") then returns the value already cached in package.loaded.
The headers work from C99 onwards and from C++. Put clx's include/ directory on your C compiler's include path and do not link -llua — the lua_*/luaL_* symbols come from clx's own wrapper archive (see Linking).
The state
There is exactly one lua_State per program, created by clx::open() and destroyed by clx::close().
| Stock Lua | clx |
|---|---|
| lua_newstate(alloc, ud, seed) | returns NULL — you cannot create a state |
| luaL_newstate() | returns NULL |
| lua_close(L) | does nothing; the runtime owns the lifetime |
Everything else about the state is real. The registry, the global table and package.loaded / package.preload are the same tables the compiled Lua code uses:
- lua_pushglobaltable(L) gives you _G;
- registry[LUA_RIDX_GLOBALS] is _G, registry[LUA_RIDX_MAINTHREAD] is the main thread;
- registry["_LOADED"] is package.loaded and registry["_PRELOAD"] is package.preload, so luaL_requiref() and Lua's require() see each other's entries;
- lua_getextraspace(L) works — clx reserves LUA_EXTRASPACE bytes in front of every lua_State.
What is implemented
Every function in lua.h, lauxlib.h and lualib.h is declared and has a definition (147 API names in total). The list below is by area; the Not implemented section names every stub explicitly.
| Area | Status |
|---|---|
| Stacklua_gettop, lua_settop, lua_pushvalue, lua_rotate, lua_copy, lua_absindex, lua_pop, lua_insert, lua_remove, lua_replace | Supported |
| lua_checkstack | Always returns 1 — see Other stubs |
| Types and conversionlua_type, lua_typename, lua_is*, lua_tonumberx, lua_tointegerx, lua_toboolean, lua_tolstring, lua_rawlen, lua_tocfunction, lua_touserdata, lua_tothread, lua_topointer | Supported |
| Pushinglua_pushnil, lua_pushnumber, lua_pushinteger, lua_pushlstring, lua_pushstring, lua_pushfstring, lua_pushvfstring, lua_pushboolean, lua_pushlightuserdata, lua_pushcclosure, lua_pushthread, lua_pushexternalstring | Supported |
| Tableslua_createtable, lua_newtable, lua_get{table,field,i,global}, lua_set{table,field,i,global}, lua_raw{get,geti,getp,set,seti,setp}, lua_getmetatable, lua_setmetatable, lua_next, lua_len | Supported |
| Userdatalua_newuserdatauv, lua_getiuservalue, lua_setiuservalue | Supported |
| Arithmetic and comparisonlua_arith, lua_compare, lua_rawequal, lua_concat | Supported |
| Callslua_callk / lua_call, lua_pcallk / lua_pcall | Supported — the continuation runs when the call yields |
| Coroutineslua_newthread, lua_closethread, lua_xmove, lua_pushthread, lua_resume, lua_yieldk / lua_yield, lua_status, lua_isyieldable | Supported — see Coroutines |
| Registry and referencesluaL_ref, luaL_unref, luaL_getsubtable, luaL_requiref | Supported |
| Auxiliary libraryluaL_check*, luaL_opt*, luaL_argerror, luaL_typeerror, luaL_error, luaL_where, luaL_tolstring, luaL_newmetatable, luaL_testudata, luaL_checkudata, luaL_setfuncs, luaL_setmetatable, luaL_callmeta, luaL_getmetafield, luaL_fileresult, luaL_execresult | Supported |
| luaL_traceback | Header only — no frames |
| Strings and buffersluaL_buffinit, luaL_prepbuffsize, luaL_buffinitsize, luaL_addstring, luaL_addlstring, luaL_addvalue, luaL_addgsub, luaL_pushresult, luaL_pushresultsize, luaL_gsub, lua_numbertocstring, lua_stringtonumber, lua_pushvfstring | Supported |
| GClua_gc with LUA_GCSTOP, LUA_GCRESTART, LUA_GCCOLLECT, LUA_GCCOUNT, LUA_GCCOUNTB, LUA_GCSTEP, LUA_GCISRUNNING, LUA_GCGEN, LUA_GCINC, LUA_GCPARAM | Supported |
| Warningslua_setwarnf, lua_warning | Supported — only reachable from C code |
| Standard library openersluaopen_base, luaopen_package, luaopen_coroutine, luaopen_debug, luaopen_io, luaopen_math, luaopen_os, luaopen_string, luaopen_table, luaopen_utf8 | Supported — they push the table clx already loaded |
| Upvalueslua_upvalueindex, lua_getupvalue, lua_setupvalue, lua_upvalueid, lua_upvaluejoin | Supported |
Not implemented
These functions exist but do nothing useful. A module that relies on any of them will misbehave silently rather than fail to link.
No state creation
| Function | Behaviour |
|---|---|
| lua_newstate | returns NULL |
| luaL_newstate | returns NULL |
| lua_close | no-op — the runtime stays alive |
| lua_setallocf | no-op |
| lua_getallocf | always returns luaL_alloc, writes NULL to ud |
Loading and dumping bytecode
clx compiles Lua to native code ahead of time, so there is no runtime compiler and no bytecode.
| Function | Behaviour |
|---|---|
| lua_load | pushes "clx: lua_load is not supported (chunk '...')" and returns LUA_ERRSYNTAX |
| luaL_loadbufferx | pushes "clx: cannot load '<name>' at runtime" and returns LUA_ERRSYNTAX |
| luaL_loadstring | same as luaL_loadbufferx |
| luaL_loadfilex | returns LUA_ERRFILE if the file cannot be opened, otherwise LUA_ERRSYNTAX as above — the file is never compiled |
| lua_dump | always returns 1 (failure); writer is never called |
If you need to run Lua source at runtime, build with --dynamic and use load() from Lua — see Dynamic execution.
Debug and hooks
| Function | Behaviour |
|---|---|
| lua_getinfo | always returns 0 |
| lua_getlocal / lua_setlocal | always returns NULL |
| lua_sethook | no-op |
| lua_gethook | returns NULL |
| lua_gethookmask / lua_gethookcount | returns 0 |
| lua_getstack | returns 1 only for level == 0 on a running thread, and zeroes lua_Debug; every other level returns 0. Since lua_getinfo then returns 0, there is no usable frame inspection |
| luaL_traceback | pushes only msg (if given), a newline and the literal stack traceback: — no frames |
This matches the compiled-code side of clx: the debug library is not exposed as a compiled-code global either (see Lua 5.5 Compatibility).
To-be-closed variables
| Function | Behaviour |
|---|---|
| lua_toclose | no-op — the slot is never closed |
| lua_closeslot | no-op |
Other stubs
| Function | Behaviour |
|---|---|
| lua_checkstack | always returns 1; there is no fixed stack limit, so runaway recursion can exhaust memory instead of raising "stack overflow" |
| luaL_openselectedlibs | no-op, which makes the luaL_openlibs(L) macro a no-op. Standard libraries are already open (unless you build with --minimal) |
| luaL_register | not present — removed from Lua in 5.2 |
Coroutines
The coroutine API works: lua_newthread, lua_resume, lua_yieldk, lua_status, lua_xmove and friends are covered by clx's test suite. A minimal round trip:
As in stock Lua, lua_yield(L, n) hands its top n values to the resumer and the values passed to the next lua_resume become the values lua_yield "returns" (their count is what lua_yield evaluates to).
Notes on the contract:
- lua_resume(L, from, narg, nres) expects the arguments to already be on L's stack. Stock usage moves them there first with lua_xmove(from, L, narg); from is only used to check that both states belong to the same runtime.
- Results and error objects are pushed onto L's stack; *nres is set to their count. On LUA_ERRRUN exactly one value is pushed.
- lua_status returns LUA_OK for the main thread and for a freshly created thread, LUA_YIELD after a yield, LUA_OK for a coroutine that finished normally, and LUA_ERRRUN for one that died with an error.
- lua_isyieldable returns 0 on the main thread and inside lua_pcallk with k == NULL, matching stock's "cannot yield across a C-call boundary" rule.
- lua_yieldk(L, n, ctx, k) yields like lua_yield, then runs k(L, LUA_YIELD, ctx) after the coroutine is next resumed and returns whatever k returns. If k is NULL, it returns the number of values the resumer passed.
- lua_newthread shares the single runtime — same globals, same registry, same GC. lua_xmove between threads of different states is impossible because there is only one state; it raises "moving among independent states is not supported".
- lua_closethread(L, from) on a still-suspended coroutine runs clx's close_thread, which releases the fiber rather than silently dropping it; the thread ends up DEAD.
- Threads created by clx's own coroutine.create are resumable from C too: lua_tothread on a Lua thread value gives you a lua_State* whose function is already set.
Known deviations
| Behaviour | Stock Lua | clx |
|---|---|---|
| lua_yieldk with a continuation | never returns to the caller of lua_yieldk | returns k(L, LUA_YIELD, ctx)'s value; with k == NULL it returns the number of resumed values, so it "returns" like lua_yield does in Lua code |
| Resuming the main thread | runs the main function if not yet started | "cannot resume non-suspended coroutine" |
| lua_resume on a never-started thread with neither a stack function nor t->function | error | "cannot resume dead coroutine" |
Errors
Errors are C++ exceptions (clx::LRuntimeException) unwinding through your C frames, not longjmp:
- lua_error(L) and luaL_error(L, fmt, ...) never return — they throw. Both return luaL_error(...) and bare luaL_error(...) work.
- lua_pcallk / lua_pcall catch the exception, run your error handler if you passed one, and return LUA_ERRRUN (or LUA_ERRERR if the handler itself fails). Use them exactly as you would in stock Lua.
- An error escaping an unprotected lua_call propagates out to the nearest pcall in your Lua code, or to the program's top level if there is none.
- lua_atpanic is stored but never called. clx has no panic path: an unrecoverable error prints the message and exits with status 1. Do not write a panic function that is expected to longjmp or exit for you.
- luaL_where(L, 1) prefixes the message with file:line: taken from the most recent Lua call site, when clx knows it; otherwise it pushes "". Inside a luaopen_* at startup there is usually no position yet, so luaL_error messages come out unprefixed.
- Error objects behave as in stock: any Lua value can be raised, and lua_tolstring on a raised value gives you the message. lua_pushvfstring supports the same conversions as stock — %s %c %d %I %f %p %U %% — and copies unknown specifiers through unchanged.
What differs from stock Lua
You must rebuild against clx's headers
lua.h renames every lua_* and luaL_* symbol to clx_lua_* through #defines placed above the declarations. That keeps the bridge from colliding with a real Lua library that some other dependency might link.
The consequence: an object file compiled against stock Lua's headers cannot be linked into a clx program. Rebuild every C module from source with -I/path/to/clx/include. Your own luaopen_<name> symbols are not renamed, which is how --modules finds them.
luaopen_base, luaopen_math and friends are renamed (clx_luaopen_math) because clx defines them itself; include lualib.h and call them normally.
The stack is per-bundle and per-frame
Each lua_State* owns its own value stack. Inside a lua_CFunction you see only your own arguments, as in stock Lua. Strings returned by lua_pushlstring/lua_pushstring/lua_tolstring point into clx's interned string arena and stay valid as long as the value is reachable — and lua_tolstring converts numbers in place, exactly like stock.
Short strings (six bytes or fewer) are stored inline in the value. lua_tolstring re-interns them before returning a pointer, so the usual "the pointer is valid while the value stays on the stack" guarantee holds.
Notes and differences
| Area | Note |
|---|---|
| luaL_checkoption | returns the int index of the chosen option, not a pointer — this is a Lua 5.5 change, not a clx quirk |
| lua_callk / lua_pcallk | the lua_KFunction continuation runs exactly when the call yields: after the call completes, clx runs k(L, LUA_YIELD, ctx) (or k(L, status, ctx) if it failed after the yield) and abandons the C function, using k's return value as its result count — as stock does. If the call never yields, k is not called and lua_pcallk returns normally |
| yield through lua_callk | stock allows it only when k != NULL; clx always allows it |
| yield through lua_pcallk | like stock: allowed when k != NULL (the continuation runs on resume), refused with k == NULL — you get "attempt to yield across a C-call boundary" |
| luaL_pushfail | a header macro, so identical to stock: pushes nil, or false if you define LUA_FAILISFALSE |
| lua_newuserdatauv(L, sz, nuvalue) | nuvalue is ignored — you can store any number of user values, and out-of-range lua_getiuservalue/lua_setiuservalue indices return LUA_TNONE / 0 instead of failing |
| lua_pushexternalstring(L, s, len, falloc, ud) | clx copies the bytes into its own arena and calls falloc(ud, s, len+1, 0) immediately, so your buffer must be freeable right away and must not be reused |
| luaL_testudata(L, idx, "FILE*") | returns a pointer to a single shared scratch slot in the state, overwritten by the next such call. Copy the FILE* out before calling it again |
| luaL_openlibs | no-op; libraries are opened by the runtime. With --minimal they are absent and the C API cannot bring them back |
| Warnings | lua_setwarnf/lua_warning work for C-to-C calls, but clx's own runtime warnings do not route through them |
| Module initialisation | openers run at program startup, before the entry chunk, and all of them run regardless of whether anything calls require |
| lua_getextraspace | backed by real reserved bytes in front of every lua_State, so the macro behaves as in stock |
Checking your module
A quick smoke test is enough to catch most wiring problems:
If clx reports input module "lfs" collides with the precompiled C module, your .lua file shares its stem with the archive — rename one of them. If the archive is treated as a C++ module, check that luaopen_lfs was not mangled (nm -g lfs.a | grep luaopen).
See also
- Modules — building, finding and linking module archives, and the libclx_capi wrapper archive
- C++ API — the native, stack-free C++ API for new code
- doc/migration-guide.md — porting a C module to the C++ API
- Lua 5.5 Compatibility — language and library support
- Dynamic execution — running Lua source at runtime
Lua 5.5 Compatibility
clx targets Lua 5.5 compatibility. The entire core language, control flow, tables, metatables, coroutines, and standard libraries are supported out of the box — the items below are grouped by area.
Every library above ships in the AOT runtime except debug, which is only available in the embedded VM via --dynamic.
Conditional & Unsupported in AOT
These run in the embedded Lua VM rather than the AOT-compiled binary, so they require --dynamic: