A C++17 library and command-line tool that converts Lua 5.4 programs' bytecode into native C code.
Source input is first compiled into bytecode using the bundled Lua 5.4.9 compiler, then processed through the same conversion pipeline. The conversion stage does not execute the input program.
lua/ Lua 5.4.9 dependencies and build configuration
lua/ Lua core, standard libraries, and headers (including lua2c extensions)
exec/ Entry-point sources for lua / luac
runtime/ lua2c C runtime and VM helper templates
include/lua2c/ Public C++ conversion library interface
src/ Bytecode parsing, validation, C code generation, and CLI
cmake/ VM template generation and package installation configuration
tests/ Unit, differential, and C/C++ host tests
examples/ Library usage examples
The Lua core extensions include Proto::lua2c_entry, initialization of new prototypes, native VM dispatch entry points, and symbol exports for the shared library.
Upstream fixes from Lua 5.4.9 are preserved unchanged. The runtime ABI has been updated to 0x00010009. After upgrading, regenerate and recompile the generated C files and DLLs. Do not mix generated code, headers, and runtime components from different versions. If reusing an existing build directory, rerun CMake configuration. Installation interface paths remain unchanged.
Requirements: CMake 3.20+, a C99 compiler, and a C++17 compiler.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 8
ctest --test-dir build --output-on-failure -j 8
Adjust the path to vcvars64.bat to match your Visual Studio installation. For multi-configuration generators, use --config Release and ctest -C Release.
Output artifacts:
build/bin/lua2c.exe: conversion tool.build/lib/lua2c.lib: C++ conversion library.build/bin/lualib.dll,build/lib/lualib.lib: companion C runtime and import library.build/bin/lua.exe,luac.exe: companion Lua interpreter and bytecode compiler.
Optional configuration:
| Option | Default | Description |
|---|---|---|
LUA2C_SHARED_RUNTIME |
ON |
Build the companion Lua runtime as a shared library. DLL modules must use the shared runtime. |
BUILD_SHARED_LIBS |
OFF |
Build the C++ conversion library as a shared library. |
LUA2C_BUILD_CLI |
ON |
Build the CLI. |
LUA2C_BUILD_TESTS |
Follows BUILD_TESTING |
Build and run generated-code tests. |
LUA2C_RUNTIME_ASSERTIONS |
OFF |
Enable Lua internal assertions and API checks. |
Configuration for static-runtime assertion tests:
cmake -S . -B build-static -G Ninja -DCMAKE_BUILD_TYPE=Debug -DLUA2C_SHARED_RUNTIME=OFF -DLUA2C_RUNTIME_ASSERTIONS=ON
cmake --build build-static -j 8
ctest --test-dir build-static --output-on-failure -j 8
Run lua2c --help or lua2c -h to view the complete list of options, their defaults, and usage examples.
build\bin\lua2c.exe -o program.c script.lua
build\bin\lua2c.exe --bytecode -o program.c script.luac
build\bin\lua2c.exe --module mymodule -o mymodule.c module.lua
build\bin\lua2c.exe --header --module mymodule -o mymodule.c module.lua
build\bin\lua2c.exe --main-name run_script --header -o program.c script.lua
build\bin\lua2c.exe --strip -o program.c script.lua
build\bin\lua2c.exe --strip --no-bytecode -o program.c script.lua
The input format is detected automatically by default. Use --source or --bytecode to explicitly specify the format.
--chunk-name sets the source chunk's debug name. For file input, the default is @file_path.
--strip (or -s) removes Lua debug information from source or bytecode input. Debug information is retained by default.
Like luac -s, this recursively removes source names, line mappings, local variable metadata, and upvalue names,
while retaining function definition start/end lines, instructions, constants, and execution metadata.
Tracebacks, line hooks, debug APIs, and name recovery have less information after stripping; this is not code obfuscation.
--chunk-name still applies to source compilation errors, but is omitted from stripped output.
Public entry names, module names, and generated headers are unchanged. Code compiled later using runtime load/loadfile is not stripped automatically.
--no-bytecode omits instruction arrays from generated C and does not allocate or reconstruct them at runtime.
It applies recursively to both source and bytecode input, independently of --strip; the default is to retain arrays.
Native execution and coroutine/metamethod recovery use integer positions and generated completion handlers.
Calls, returns, count/line hooks (including C-hook yields), source lines, locals and upvalue metadata remain supported
when that debug information is available. Inferred call/function names and symbolic variable descriptions in errors
are limited because they require instruction analysis. string.dump of these native functions fails with
unable to dump given function; the C API lua_dump returns failure without calling its writer.
Functions compiled later by load/loadfile retain their normal bytecode and can still be dumped.
Constants, native instruction operands and any unstripped debug names remain present: this is not an anti-reversing guarantee.
This runtime ABI revision requires regenerating and recompiling existing generated C files and modules.
If -o is omitted, the generated code is written to stdout. Module names must be valid C identifiers. The default module name is lua2c_module.
--header must be used with -o. It additionally generates a .h file in the same directory, replacing the output file's extension. No header is generated by default. For example, -o mymodule.c --header produces mymodule.c and mymodule.h.
--main-name NAME sets the actual name of the chunk entry function. The default is lua2c_main. It supports both file output and stdout output.
The specified name must be a valid, non-keyword C/C++ identifier. It must not conflict with main, the module initialization entry point, or any generated internal symbols. Avoid names that already exist in the Lua/C API as well.
Generated files provide the following entry point:
int lua2c_main(lua_State *L);
/* One file-local function for each Lua function prototype: */
static int lua2c_gen_<name>_p<id>(lua_State *L, int trap);
When debug information is available, local function names, global function names, and field names are recovered where possible. Anonymous functions and stripped bytecode use stable numeric identifiers.
The lua2c_gen_* functions are runtime execution entry points that require genuine Lua closures and Lua call frames. They are not ordinary C functions that can be registered arbitrarily using lua_pushcfunction.
lua2c_main is an ordinary Lua C entry point. Values on the stack are passed as the top-level chunk's ..., and the chunk's return values remain on the stack. The function returns the number of results.
When --main-name run_script is specified, the actual definition becomes int run_script(lua_State *L). No additional lua2c_main alias is defined. The calling convention, error behavior, and coroutine behavior remain unchanged. This option does not change the names of luaopen_<module_name> or lua2c_gen_*.
The host is responsible for creating lua_State, opening the standard libraries, and configuring the environment. A typical invocation uses lua_pushcfunction followed by lua_pcall. Errors follow Lua's normal error-handling mechanism. In a coroutine, the function can be called and suspended or resumed through lua_resume. See tests/host.c.
With --header, C or C++ hosts can include the generated header directly:
#include "mymodule.h"
/* Initialize the Lua state and standard libraries before
executing the script inside a protected call. */
lua_pushcfunction(L, lua2c_main);
if (lua_pcall(L, 0, LUA_MULTRET, 0) != LUA_OK) {
/* Handle the Lua error. */
}
The header includes the public lua.h, include guards, and C++ extern "C" protection.
It declares the configured entry-point name (default: lua2c_main). When LUA2C_API=1 is enabled, it also declares luaopen_<module_name>.
Both the generated C code and its caller must be compiled with LUA2C_API=1 when invoking the module initialization entry point.
The file-local lua2c_gen_* functions are not part of the public C API and are therefore omitted from the header.
Generating a header does not change the generated C code and does not automatically enable LUA2C_MAIN or LUA2C_API. The host must still compile and link the generated C file and the companion runtime. Including the header alone is not sufficient to execute the program.
For example, after using --main-name run_script --header, the host should call lua_pushcfunction(L, run_script).
Multiple generated C files can be linked together using different entry-point names. Their headers will also have distinct include guards based on the entry-point names. If LUA2C_API is enabled for multiple modules, specify a different module name with --module for each DLL/module entry point.
Both macros default to 0:
LUA2C_MAIN=1: Enablesmain, including standard library initialization,argsetup, chunk arguments, and error handling.LUA2C_API=1: Enables and exportsluaopen_<module_name>.- Both macros can be enabled simultaneously. When both are disabled, the generated code is suitable for embedding in a host application.
Install the headers and libraries:
cmake --install build --prefix install
Compile the generated file with MSVC using the shared runtime:
cl /nologo /O2 /MD /std:c11 /DLUA2C_MAIN=1 /DLUA_BUILD_AS_DLL /DLUA2C_RUNTIME_SHARED /Iinstall\include\lua2c\runtime program.c install\lib\lualib.lib /Fe:install\bin\program.exe
install\bin\program.exe arg1 arg2
Generated code depends only on the C runtime and the companion runtime headers and library. It does not link against the C++ conversion library.
For executables, arg[0] is the executable's own path, and arg[1..] are passed as the chunk's ..., matching the argument convention used when executing a Lua script.
build\bin\lua2c.exe --module mymodule -o mymodule.c module.lua
cl /nologo /O2 /MD /LD /DLUA2C_API=1 /DLUA_BUILD_AS_DLL /DLUA2C_RUNTIME_SHARED /Iinstall\include\lua2c\runtime mymodule.c install\lib\lualib.lib /Fe:install\bin\mymodule.dll
install\bin\lua.exe -e "package.cpath='install/bin/?.dll;'..package.cpath; local m=require('mymodule')"
The module executes the script and preserves its return behavior. The script will typically return a table, but this is not mandatory.
The host and module must link against the same companion runtime. They must not each statically link a separate copy of Lua, and the module cannot be loaded by an arbitrary stock Lua DLL.
The generated code includes ABI checks for the runtime version, structure sizes, and numeric representation.
The generated code uses ordinary C99 control flow, labels, and functions. It does not use computed gotos, nested functions, VLAs, or mandatory compiler built-ins.
The following example uses an installed shared runtime. These compilers have not been tested with this configuration.
cc -std=c99 -O2 -DLUA2C_MAIN=1 -DLUA2C_RUNTIME_SHARED \
-Iinstall/include/lua2c/runtime program.c -Linstall/lib -llualib -lm -ldl -o program
tcc -DLUA2C_MAIN=1 -DLUA2C_RUNTIME_SHARED \
-Iinstall/include/lua2c/runtime program.c -Linstall/lib -llualib -lm -ldl -o program
On Linux, configure LD_LIBRARY_PATH or an rpath. On Windows, TinyCC also requires LUA_BUILD_AS_DLL, a runtime with matching architecture, and an import library that TinyCC can understand. MSVC .lib files are not guaranteed to work directly with TinyCC. If necessary, generate a compatible import library from the DLL's export list.
Public header: include/lua2c/lua2c.hpp.
#include <lua2c/lua2c.hpp>
lua2c::Options options;
options.module_name = "example";
// Optional; lua2c_main is still generated if omitted.
options.main_name = "run_example";
// Optional; strip Lua debug information for either source or bytecode input.
options.strip = true; // Default: false.
// Optional and independent of strip; native functions can no longer be dumped.
options.no_bytecode = true; // Default: false.
std::string c = lua2c::convert("print(1 + 2)", options);
lua2c::convert_to_file("print(1 + 2)", "example.c", options);
// The fourth parameter, generate_header, defaults to false.
// When true, example.h is generated as well.
lua2c::convert_to_file("print(1 + 2)", "example.c", options, true);
// File input: supports Lua source and .luac files.
std::string from_file = lua2c::convert_file("script.lua", options);
lua2c::convert_file_to_file("script.luac", "script.c", options);
lua2c::convert_file_to_file("script.luac", "script.c", options, true); // Also generates script.h
String input uses the explicit length of std::string_view and can therefore contain embedded null bytes.
Conversion, parsing, and file errors throw lua2c::Error. Allocation errors follow standard C++ exception conventions.
The output file is not opened until conversion succeeds, so invalid input will not overwrite an existing output file. Write failures do not guarantee atomic file updates.
When header generation is enabled, the C file and header are both written after successful conversion, but the two-file write is not an atomic transaction. An I/O error may leave only one of the files behind.
In this mode, the C output path must not end in .h or .H, as this would conflict with the header path.
Each conversion uses an independent Lua state, allowing different inputs to be converted concurrently on separate threads. Execution using the same lua_State remains subject to Lua's host-side synchronization requirements.
Use the installed CMake package:
find_package(lua2c CONFIG REQUIRED)
target_link_libraries(your_cpp_target PRIVATE lua2c::lua2c)
# Generated C file:
target_link_libraries(your_c_target PRIVATE lua2c::runtime)
target_compile_definitions(your_c_target PRIVATE LUA2C_MAIN=1)
examples/library/ contains a complete example of library usage that can be built with -DCMAKE_PREFIX_PATH=installation_directory.
- Performs bounded parsing of Lua 5.4 bytecode and validates headers, counts, constants, registers, upvalues, jumps, and instruction pairs.
- Generates a separate C execution function for each
Proto. Instructions and operands become compile-time constants, and ordinary control flow uses direct jumps. The entry-pointswitchhandles only entry and resumption positions. - Directly accesses the Lua stack,
TValue, tables, and upvalues. Integer wraparound, floating-point operations, comparisons, bitwise operations, and other common operations use C fast paths that preserve Lua semantics. - Preserves genuine
LClosureobjects, Lua call frames, GC roots and barriers, while reusing the core VM's mechanisms for metamethods, strings, to-be-closed variables, and error unwinding. - Converted functions execute through native C paths. This is not an interpreter wrapper that embeds
.luacand callslua_load. - By default, retains instruction metadata to support restoration of metamethod operations and
string.dump.no_bytecodereplaces instruction metadata with generated completion handlers and disables dumping native functions. Debug metadata is retained unlessstripis enabled. Functions dumped and subsequently loaded execute through the original VM by default and are not automatically recompiled. - Runtime
load,loadfile, and dynamically loaded modules can interoperate with generated functions.
Full semantic compatibility is a design goal, not a formally proven guarantee for every Lua behavior. The C module entry point and chunk entry point (default: lua2c_main) introduce additional host C boundaries that can be observed by debug. The generated Lua functions themselves still report what="Lua".
Behavior involving collectgarbage("count"), execution time, memory usage, and similar measurements is not guaranteed to match the original interpreter.
- Bytecode instruction sizes, integer/floating-point configuration, and byte order must be compatible with the converter. Lua 5.1, 5.2, 5.3, and 5.5 formats are not accepted.
- Function names, local variable names, and line numbers removed by stripping cannot be recovered.
- The Lua core ABI has been extended internally and is not guaranteed to be compatible with the original Lua's internal structures.
- The parser performs structural validation and limits prototype nesting to 200 levels, but it is not a complete type or data-flow verifier for malicious bytecode. The conversion and execution environment must not be treated as a security sandbox for untrusted programs.
- Large programs may produce large C files. Function splitting and cross-module linking optimizations are not currently implemented.
Tested on MSVC x64:
- Release, shared runtime: 243 CTest tests.
- Debug, static runtime, with Lua internal assertions and API checks enabled: 241 CTest tests (excluding the DLL
requiretests).
The test suite covers source and bytecode paths for 24 existing scripts, including stripped bytecode, as well as coroutine and metamethod yields, tail calls, <close>, GC, dynamic loading, dump/load, exact debug hooks, numeric edge cases, C host entry points, and combinations of the two macros.
Differential testing uses normal interpretation by the companion Lua runtime as the reference.
Additional tests cover manually constructed LOADKX bytecode and large operands involving 600 elements or fields.
Header generation tests cover source and bytecode input, CLI usage, disabled-by-default behavior, path conflicts, and C/C++ host linking.
Custom entry-point tests cover source and bytecode input, C/C++ calls, coroutine resumption, module loading, enabling both macros, and linking multiple generated files together.
Strip tests compare API and CLI output with Lua's stripped dump, including nested closures and absolute line info.
Every differential script also runs with --strip on source and -s on unstripped bytecode, using luac -s as the reference.
Additional coverage checks stripped debug APIs and dump/load, unchanged public headers, malformed inputs, and stripped custom-entry hosts/modules.
No-bytecode tests cover source/bytecode input, combining with strip, absence of runtime instruction arrays, coroutine and metamethod recovery, exact hooks on vararg/open-result calls, C-hook yields, GC, dump rejection before writer invocation, interpreted dump/load, large operands, and a shared no-bytecode module.
Performance depends on the workload and the C compiler's optimization level. Release builds are recommended. No speedup is guaranteed with unoptimized Debug builds or TinyCC.
Historical measurements (before upgrading to Lua 5.4.9): on the local machine, using MSVC x64 Release, the median of seven process-level runs of 24_arith_bench.lua was:
- Standard Lua VM: 106.82 ms
- Generated C: 58.63 ms
- Speedup: approximately 1.82×
These measurements include process startup and represent only this arithmetic workload.
The bundled Lua distribution and instruction-handling code derived from its lvm.c follow the Lua MIT License. Copyright notices are preserved in lua/lua/lua.h.
All new code in this project is licensed under the MIT License specified in LICENSE.
When distributing generated code, retain the Lua license.