Coroutines
Lua coroutines invert the relationship between caller and callee, providing a flexible solution to the question of which routine controls the main execution flow. This generalization addresses seemingly unrelated problems like event-driven programming, iterator construction, and cooperative multitasking, offering an efficient and straightforward implementation.
From a multithreading perspective, coroutines resemble threads: each maintains its own stack, local variables, and instruction pointer while sharing global variables and most other resources. The key distinction is that multithreading enables parallel execution, whereas coroutines operate cooperatively, with only one coroutine active at any time. Execution pauses only when the running coroutine explicitly yields control.
Coroutine Fundamentals
Lua's coroutine functionality is contained within the coroutine table. The create function generates new coroutines, accepting a single parameter - the function containing the coroutine's code. This function returns a "thread" type value representing the new coroutine.
Coroutines exist in four states: suspended, running, normal, and dead. The coroutine.status function examines a coroutine's current state.
Newly created coroutines begin in the suspended state and don't execute automatically. The resume function initiates or resumes execution, transitioning the coroutine from suspended to running.
The yield function enables running coroutines to suspend themselves and later resume execution. When awakened, a coroutine executes until encountering its first yield, then returns to suspended status. Subsequent resumes continue execution from the line following the yield statement.
When coroutine A resumes coroutine B, coroutine A enters the normal state - neither suspended (since it can't be resumed) nor running (since B is executing).
Lua facilitates data exchange through resume-yield pairs. The initial resume passes all extra arguments to the coroutine's main function. coroutine.resume returns true followed by yield parameters, while coroutine.yield returns resume arguments. When a coroutine completes, its main function's return values become the final resume results.
Lua implemants asymmetric coroutines requiring two distinct functions for suspension and resumption. This differs from symmetric coroutines found in other languages, which use a single function for control transfer between coroutines. Some languages implement semi-coroutines, a restricted version that can only yield when no nested calls are pending.
Producer-Consumer Problem
The classic producer-consumer scenario demonstrates coroutine power. Both producer and consumer maintain active control loops, viewing each other as callable services. Resume-yield pairs enable coordination between these components without structural modifications.
local worker = coroutine.create(function(initial)
local result1 = initial + 1
local input2 = coroutine.yield(result1)
print(input2, result1)
local result2 = input2 + 1
return result2
end)
local main_co = coroutine.running()
local start_val = 1
local success, output1, output2
success, output1 = coroutine.resume(worker, start_val)
print("First:", main_co, success, output1)
success, output2 = coroutine.resume(worker, output1)
print("Second:", main_co, success, output2)
Lua Virtual Stack
The Lua stack stores only Lua-type values. C types must convert to Lua equivalents before storage. Each C function call creates a new stack frame independent of previous ones, with the first parameter always at index 1.
Virtual machine creation establishes a main coroutine and accompanying stack. Lua guarantees at least LUA_MINSTACK (typically 20) slots for C calls, sufficient for most operations.
Stack indexing uses positive numbers from the bottom (1) and negative numbers from the top (-1), enabling easy identification of stack boundaries without knowing the exact size.
Registry
The registry is a global table accessible exclusively to C code, typically storing data shared across multiple modules. It resides at the pseudo-index LUA_REGISTRYINDEX, which references values outside the regular stack.
Registry Applications
- Enables Lua data sharing between C libraries
- Predefined table storing Lua values for C code
- Accessed via LUA_REGISTRYINDEX
Implementation Example
// registry_example.c
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
static int registry_operation(lua_State *L) {
lua_getfield(L, LUA_REGISTRYINDEX, "shared.data");
lua_rawgeti(L, -1, 1);
lua_Integer counter = lua_tointeger(L, -1);
counter++;
lua_pop(L, 1);
lua_pushinteger(L, counter);
lua_rawseti(L, -2, 1);
const char* message = lua_tostring(L, 1);
fprintf(stdout, "Counter[%lld]: %s\n", counter, message);
return 0;
}
static const luaL_Reg lib_funcs[] = {
{"process", registry_operation},
{NULL, NULL}
};
int luaopen_registry_example(lua_State *L) {
if (lua_getfield(L, LUA_REGISTRYINDEX, "shared.data") == LUA_TNIL) {
lua_createtable(L, 2, 0);
lua_pushinteger(L, 1000);
lua_rawseti(L, -2, 1);
lua_pushinteger(L, 2000);
lua_rawseti(L, -2, 2);
lua_setfield(L, LUA_REGISTRYINDEX, "shared.data");
fprintf(stdout, "Initialized registry table\n");
}
luaL_newlib(L, lib_funcs);
return 1;
}
Common API Funcsions
lua_getfield: Pushes t[k] onto stack, where t is the table at indexlua_setfield: Assigns t[k] = v, where v is the stack top valuelua_rawgeti: Directly pushes t[n] without metatable invocationlua_rawseti: Directly assigns t[n] = v without metatable invocation
Debugging in Lua
Introspection
The primary introspection function debug.getinfo returns a table containing information about a function or stack level.
function demo_function()
print(debug.traceback("Stack trace:"))
print(debug.getinfo(1))
end
demo_function()
Local Variable Access
debug.getlocal examines active local variables using stack level and variable index parameters. It returns the variable name and current value, or nil if the index exceeds active variable count.
debug.setlocal modifies local variable values using similar parameters plus the new value.
local x = 0
local y = 0
function test()
local a = 0
local b = 0
print("Before modification:")
print(debug.getlocal(1, 1))
print(debug.getlocal(1, 2))
debug.setlocal(1, 1, 10)
debug.setlocal(1, 2, 20)
debug.setlocal(2, 1, 30)
debug.setlocal(2, 2, 40)
print("After modification:")
print(debug.getlocal(1, 1))
print(debug.getlocal(1, 2))
end
test()
print("Outer variables:", x, y)
Upvalue Access
debug.getupvalue accesses function upvalues (closure variables), which persist even when functions are inactive. Unlike local variables, upvalues are indexed by their reference order within functions.
debug.setupvalue modifies upvalue values.
local counter1 = 0
local counter2 = 0
function increment()
counter1 = counter1 + 1
counter2 = counter2 + 1
end
print("Original upvalues:")
print(debug.getupvalue(increment, 1))
print(debug.getupvalue(increment, 2))
debug.setupvalue(increment, 1, 100)
debug.setupvalue(increment, 2, 200)
print("Modified upvalues:")
print(debug.getupvalue(increment, 1))
print(debug.getupvalue(increment, 2))
Hooks
Hook functions monitor execution events:
debug.gethook([thread]): Retrieves current hook functiondebug.sethook([thread,] hook, mask, count): Configures hook function
Event masks:
c: Function call eventsr: Function return eventsl: New line execution events- Instruction count events (when count > 0)
Hook functions receive event descriptions as first parameters, with line events including the new line number as a second parameter.