PONY λ M2 Modula-2

C.CodeCompared.To/Lua

An interactive executable cheatsheet comparing C and Lua

C17 (GCC) Lua 5.4
Output & Running It
Hello, World
A C program needs a header for its output function, a typed entry point, and a return status. A Lua chunk is just a list of statements — the interpreter reads the file and runs it, so there is no entry point to declare and nothing to compile or link.
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
print("Hello, World!")
print is a global function that appends a newline, so there is no \n to remember. It also needs no header: Lua's standard library is already loaded into the global environment when the interpreter starts.
print takes any number of values
printf pairs a format string with matching arguments, and a mismatched conversion specifier is a bug the compiler can only sometimes catch. Lua splits the two jobs: print takes any values and converts them itself, while string.format is there when you want C-style control over the layout.
#include <stdio.h> int main(void) { const char *name = "Lua"; int year = 1993; printf("%s %d %.2f\n", name, year, 1.5); return 0; }
local name = "Lua" local year = 1993 print(name, year, 1.5) print(string.format("%s %d %.2f", name, year, 1.5))
print separates its arguments with a tab and calls tostring on each one, so it never has to be told what type anything is. string.format uses the same conversion specifiers as printf, including width and precision.
Comments
Lua uses -- to end of line, and --[[ ... ]] for a block. The block form nests by adding equals signs (--[==[ ... ]==]), which is how you comment out code that already contains a block comment — something C's /* */ famously cannot do.
#include <stdio.h> int main(void) { /* A block comment */ // A line comment printf("commented\n"); return 0; }
-- A line comment --[[ A block comment spanning several lines ]] print("commented")
The long-bracket syntax [[ ... ]] is the same one used for multi-line string literals; a comment is just a long bracket preceded by --.
Command-line arguments
C receives its arguments as parameters to main, with the program name in argv[0] and the count including it. Lua has no main to pass them to, so the standalone interpreter puts them in a global table named arg instead — and because arg[0] is the script name, #arg is already the count of real arguments.
#include <stdio.h> int main(int argc, char **argv) { printf("program: %s\n", argv[0]); printf("argument count: %d\n", argc - 1); for (int index = 1; index < argc; index++) { printf(" [%d] %s\n", index, argv[index]); } return 0; }
-- The standalone interpreter fills in the global table 'arg': -- arg[0] is the script name, arg[1..n] are the arguments, -- and negative indices hold the interpreter and its options. print("program: " .. arg[0]) print("argument count: " .. #arg) for index = 1, #arg do print(string.format(" [%d] %s", index, arg[index])) end
This example cannot run here: the browser runner hands a string straight to the interpreter rather than launching lua script.lua …, so there is no arg table to read. Run it with lua example.lua one two three locally to see the output.
Variables & Scope
Variables are global unless declared local
In C every variable is declared, and scope follows from where the declaration sits. Lua inverts the default: an assignment to an undeclared name silently creates a global, and only the local keyword gives you the block scope C hands out automatically.
#include <stdio.h> static int file_scope = 1; /* visible in this file only */ int main(void) { int block_scope = 2; /* visible in this block only */ printf("%d %d\n", file_scope, block_scope); return 0; }
implicitly_global = 1 -- goes into the global table! local properly_scoped = 2 -- visible to the end of this block print(implicitly_global, properly_scoped)
This is the single most common source of bugs in Lua code written by C developers. Always write local. Globals are entries in a table (_ENV), so they are also slower to read than locals, which the compiler resolves to a register slot.
A variable has no type — only its current value does
A C declaration binds a type to a name for the lifetime of the program, and the compiler rejects any assignment that violates it. A Lua variable is an untyped slot: the value carries the type, and the same slot can hold an integer, then a string, then a table.
#include <stdio.h> int main(void) { int count = 42; /* count = "forty-two"; <- would not compile */ printf("%d\n", count); return 0; }
local count = 42 print(count) count = "forty-two" -- perfectly legal: the variable is just a slot print(count) count = { 4, 2 } -- and now it holds a table print(count[1], count[2])
Nothing checks this at compile time, so a typo that in C would be a diagnostic becomes a runtime error in Lua — usually attempt to perform arithmetic on a string value, and only when that line is finally reached.
do ... end is Lua's bare block
C uses braces for both grouping and scope, so { } alone introduces a nested scope. Lua spells that do ... end, and shadowing inside it behaves exactly as a C developer expects.
#include <stdio.h> int main(void) { int value = 10; { int value = 20; /* shadows the outer one */ printf("inner: %d\n", value); } printf("outer: %d\n", value); return 0; }
local value = 10 do local value = 20 -- shadows the outer one print("inner: " .. value) end print("outer: " .. value)
A local is visible from its declaration to the end of the enclosing block, which means a local declared later in the same block is genuinely not in scope earlier — the same rule C uses.
Multiple assignment, and swapping without a temporary
C assigns one value per statement, so a swap needs a temporary. Lua evaluates the entire right-hand side before assigning any of it, which makes a swap a single statement and lets a list of names take a list of values.
#include <stdio.h> int main(void) { int first = 1, second = 2; int temporary = first; first = second; second = temporary; printf("%d %d\n", first, second); return 0; }
local first, second = 1, 2 first, second = second, first -- no temporary needed print(first, second) local one, two, three = 1, 2 -- three is nil; extras are simply missing print(one, two, three)
Any names left over receive nil, and any surplus values are discarded — no length check, no error. This same mechanism receives the multiple return values of a function call.
No ++, no --, no +=
None of C's increment or compound-assignment operators exist in Lua. Every update is written out in full, which also means there is no pre- versus post-increment distinction to reason about, and no sequence-point questions.
#include <stdio.h> int main(void) { int count = 0; count++; count += 5; printf("%d\n", count); return 0; }
local count = 0 count = count + 1 count = count + 5 print(count)
This is a deliberate simplification, not an oversight — Lua adds an operator only when it earns its keep in the grammar. Bitwise compound forms (&=, |=) are missing for the same reason.
Static Types vs. Dynamic Values
Types are inspected at runtime, not declared
C answers "what is this?" at compile time, and sizeof is as close as the language gets to asking at runtime. Lua answers it at runtime with type(), which returns one of exactly eight strings.
#include <stdio.h> int main(void) { int number = 42; const char *text = "hello"; double ratio = 1.5; printf("%zu %zu %zu\n", sizeof number, sizeof text, sizeof ratio); return 0; }
print(type(42)) -- number print(type("hello")) -- string print(type(true)) -- boolean print(type(nil)) -- nil print(type({})) -- table print(type(print)) -- function
The eight types are nil, boolean, number, string, function, table, userdata, and thread. userdata is the interesting one for a C developer: it is a block of raw memory owned by the host program, which is how a C struct is handed to Lua.
nil is a type, not a zero pointer
NULL in C is an address — a pointer value that compares equal to zero, and dereferencing it is undefined behavior. Lua's nil is a type in its own right whose single value means "nothing here," and reading a field that was never set simply yields it.
#include <stdio.h> #include <stdlib.h> int main(void) { int *pointer = NULL; if (pointer == NULL) { printf("NULL is the zero address\n"); } printf("as an integer: %d\n", (int) (pointer == NULL)); return 0; }
local value = nil print(value == nil) -- true print(type(nil)) -- "nil": its own type, with exactly one value local settings = {} print(settings.missing) -- nil, not a crash: absent keys read as nil
Because a missing table key reads as nil, there is no distinction between "absent" and "set to nil" — assigning nil to a key removes it. There is also no uninitialized memory in Lua: a fresh local starts as nil, never as garbage.
🚨 Zero is TRUE in Lua
This is the C habit that must be unlearned first. C treats zero as false, so if (count) means "if count is nonzero." In Lua only nil and false are falsy — 0, 0.0, and "" are all true.
#include <stdio.h> int main(void) { int count = 0; if (count) { printf("C: nonzero\n"); } else { printf("C: zero is false\n"); } return 0; }
local count = 0 if count then print("Lua: 0 is TRUE") else print("this never prints") end -- Only nil and false are falsy — everything else is truthy: if "" then print('the empty string is truthy too') end if 0.0 then print("so is 0.0") end
A ported if (count) therefore silently takes the wrong branch instead of failing loudly. Write if count ~= 0 then and if text ~= "" then explicitly. The upside: if value then is an unambiguous "is this present?" test, with no confusion between a legitimate zero and a missing value.
Booleans are values, and and/or return operands
C's bool is an integer that prints as 0 or 1, and the ternary operator picks between two values. Lua has real true/false values and no ternary operator — instead and and or return one of their operands, which covers both jobs.
#include <stdio.h> #include <stdbool.h> int main(void) { bool ready = true; printf("%d\n", ready); /* prints 1 — a bool is an int */ int chosen = ready ? 10 : 20; /* the ternary operator */ printf("%d\n", chosen); return 0; }
local ready = true print(ready) -- true, not 1 print(not ready) -- false local chosen = ready and 10 or 20 -- Lua's ternary idiom print(chosen) local name = nil print(name or "anonymous") -- 'or' supplies a default
a and b yields a when a is falsy and b otherwise; a or b yields a when a is truthy and b otherwise. Both short-circuit like C's && and ||. The one trap: condition and false or other misbehaves, because a legitimately false middle operand falls through to the or.
One number type, two subtypes — and no type zoo
C gives you a family of numeric types and asks you to pick a width, a signedness, and a representation. Lua has exactly one type — number — with two internal subtypes: a 64-bit signed integer and a double. math.type reports which one a value currently uses.
#include <stdio.h> #include <stdint.h> int main(void) { /* C offers a whole family of numeric types */ signed char tiny = 100; int normal = 42; long long big = 9000000000LL; uint32_t sized = 4000000000u; double real = 3.5; printf("%d %d %lld %u %.1f\n", tiny, normal, big, sized, real); return 0; }
local whole = 42 local real = 3.5 print(type(whole), type(real)) -- number number print(math.type(whole), math.type(real))-- integer float print(math.maxinteger, math.mininteger) -- 64-bit range print(math.tointeger(8.0)) -- 8 (exact float to integer) print(math.tointeger(8.5)) -- nil (not representable)
There is no unsigned, no short, and no size_t: an integer is always 64-bit and signed. Values move between the subtypes automatically, and arithmetic that cannot be exact in integers produces a float. Before Lua 5.3 there was only the double, which is why some older embedded builds still have no integer subtype at all.
Numbers & Arithmetic
/ always produces a float; // floors
Two divisions that look identical in C and Lua produce different values. / in Lua always yields a float, so 7 / 2 is 3.5 rather than 3, and integer division is spelled //. Worse for a porter: // and % floor, while C truncates toward zero.
#include <stdio.h> int main(void) { printf("%d\n", 7 / 2); /* 3 — integer division truncates */ printf("%.1f\n", 7 / 2.0); /* 3.5 */ printf("%d\n", -7 / 2); /* -3 — truncates toward zero */ printf("%d\n", -7 % 2); /* -1 — sign follows the dividend */ return 0; }
print(7 / 2) -- 3.5 : / is always float division print(7 // 2) -- 3 : // is floor division print(-7 // 2) -- -4 : floors, so it rounds DOWN, not toward zero print(-7 % 2) -- 1 : the result takes the divisor's sign print(7.5 // 2) -- 3.0 : floor division on floats stays a float
For negative operands the two languages genuinely disagree: C gives -7 / 2 == -3 and -7 % 2 == -1, Lua gives -4 and 1. Lua's rule is the mathematically consistent one (a == (a // b) * b + a % b always holds), but ported code that relies on C's truncation needs checking.
Integer overflow wraps — and is defined
Signed integer overflow is undefined behavior in C, which is why an optimizing compiler is allowed to assume your loop counter never wraps. Lua defines it: integer arithmetic wraps around 64 bits, exactly as unsigned arithmetic does in C.
#include <stdio.h> #include <limits.h> int main(void) { /* Signed overflow is UNDEFINED BEHAVIOR in C: the compiler may assume it never happens and optimize accordingly. */ unsigned int wraps = UINT_MAX; wraps = wraps + 1; /* unsigned wrapping IS defined */ printf("%u\n", wraps); printf("%d\n", INT_MAX); return 0; }
print(math.maxinteger) print(math.maxinteger + 1 == math.mininteger) -- true: it wraps -- Mixing in a float leaves integer arithmetic entirely: print(math.maxinteger + 0.0) -- And an exponent always produces a float, even for whole numbers: print(2 ^ 10) -- 1024.0, not 1024 print(2 ^ 0.5) -- 1.4142135623731
Note also that ^ is exponentiation in Lua, not bitwise XOR, and it always returns a float — 2 ^ 10 is 1024.0. Bitwise XOR is ~, which doubles as bitwise NOT in its unary form.
Bitwise operators, without unsigned types
Lua gained C's bitwise operators in 5.3, with one spelling change: XOR is ~ rather than ^ (which is exponentiation), and unary ~ is bitwise NOT. They operate on 64-bit integers, and a float operand with a fractional part is an error rather than a silent truncation.
#include <stdio.h> int main(void) { unsigned int flags = 0xF0; printf("%02X\n", flags & 0x3C); /* AND */ printf("%02X\n", flags | 0x0F); /* OR */ printf("%02X\n", flags ^ 0xFF); /* XOR */ printf("%02X\n", 1u << 4); /* shift left */ printf("%02X\n", flags >> 4); /* shift right */ return 0; }
local flags = 0xF0 print(string.format("%02X", flags & 0x3C)) -- AND print(string.format("%02X", flags | 0x0F)) -- OR print(string.format("%02X", flags ~ 0xFF)) -- XOR (binary ~) print(string.format("%02X", 1 << 4)) -- shift left print(string.format("%02X", flags >> 4)) -- shift right print(string.format("%016X", ~0)) -- NOT (unary ~)
Because every integer is signed, shifts are logical rather than arithmetic: >> fills with zeros, so shifting a negative value right does not preserve its sign the way C's implementation-defined >> on a signed type usually does. Shift counts of 64 or more produce 0 rather than the undefined behavior C would have.
math.h vs. the math library (no -lm)
The same functions live in a table called math, already loaded — there is no header to include and no -lm to remember at link time. math.max and math.min also accept any number of arguments, unlike C's two-argument fmax.
#include <stdio.h> #include <math.h> #include <stdlib.h> /* Needs -lm on most Unix toolchains */ int main(void) { printf("%.4f\n", sqrt(2.0)); printf("%.4f\n", fmax(3.0, 7.5)); printf("%d\n", abs(-42)); printf("%.4f\n", floor(-2.5)); return 0; }
print(string.format("%.4f", math.sqrt(2))) print(math.max(3, 7.5)) print(math.abs(-42)) print(math.floor(-2.5)) print(math.fmod(-7, 2)) -- C-style remainder: -1 print(-7 % 2) -- Lua's floored modulo: 1
math.fmod is deliberately kept alongside % because they differ: fmod truncates like C's %, while the % operator floors. Reach for math.fmod when porting C code whose remainders must keep the dividend's sign.
Strings Without a NUL Terminator
Strings are immutable values, not char buffers
A C string is a mutable array of bytes ending in a NUL, and every operation on it is your responsibility: allocate enough room, copy, terminate. A Lua string is an immutable value that knows its own length — you never mutate one, you build a new one.
#include <stdio.h> #include <string.h> int main(void) { char buffer[32]; strcpy(buffer, "hello"); buffer[0] = 'H'; /* mutate in place */ printf("%s\n", buffer); printf("%zu bytes stored\n", strlen(buffer) + 1); /* + the NUL */ return 0; }
local greeting = "hello" -- greeting[1] = "H" <- not possible: strings are immutable local capitalized = "H" .. greeting:sub(2) print(capitalized) print(#greeting, "bytes") -- length is stored, not scanned print(greeting:upper(), greeting) -- the original is untouched
Because they are immutable, Lua interns strings: two identical strings are usually the same object, so equality is a pointer comparison rather than strcmp. And because the length is stored rather than found by scanning for a NUL, a Lua string may contain embedded zero bytes and still work with every string function.
# is O(1), strlen is O(n)
C has to find the end of a string by scanning for the NUL, and sizeof on a literal counts that terminator. Lua's # operator reads a stored length, so it is constant time and counts only the bytes you put there.
#include <stdio.h> #include <string.h> int main(void) { const char *text = "embedding"; printf("%zu\n", strlen(text)); /* walks to the NUL */ printf("%zu\n", sizeof "embedding"); /* 10: includes the NUL */ return 0; }
local text = "embedding" print(#text) -- 9: no terminator counted print(string.len(text)) -- the same thing, spelled out print(#"") -- 0 local with_zero = "a\0b" -- an embedded NUL is just a byte print(#with_zero) -- 3
# counts bytes, not characters — a UTF-8 string of three accented letters has a length above three. Lua 5.3 added a utf8 library (utf8.len, utf8.char, utf8.codepoint) for when the distinction matters.
Concatenation without allocating
Joining two strings in C means sizing a buffer, allocating it, copying twice, and freeing — four chances to get it wrong. In Lua it is one operator, .., and the garbage collector owns the result.
#include <stdio.h> #include <string.h> #include <stdlib.h> int main(void) { const char *first = "Hello, "; const char *second = "World!"; char *joined = malloc(strlen(first) + strlen(second) + 1); if (joined == NULL) return 1; strcpy(joined, first); strcat(joined, second); printf("%s\n", joined); free(joined); return 0; }
local first = "Hello, " local second = "World!" print(first .. second) -- Numbers coerce into a concatenation automatically: print("answer: " .. 42) print(1 .. 2) -- "12" (note the spaces: 1..2 would parse as a malformed number)
Numbers are coerced to strings by .., which is the one place Lua does implicit conversion cheerfully. Watch the spacing though: 1..2 is a syntax error because the lexer tries to read 1. as a number, so write 1 .. 2.
snprintf vs. string.format
string.format takes the same conversion specifiers as printf — widths, precisions, flags, and %x all behave as you expect — but it returns a new string instead of filling a buffer you had to size correctly.
#include <stdio.h> int main(void) { char buffer[64]; snprintf(buffer, sizeof buffer, "%-8s|%5.2f|%04d|%x", "left", 3.14159, 42, 255); printf("%s\n", buffer); return 0; }
local formatted = string.format("%-8s|%5.2f|%04d|%x", "left", 3.14159, 42, 255) print(formatted) -- %q quotes a string so Lua itself can read it back: print(string.format("%q", 'he said "hi"\n'))
Two differences worth knowing: there is no buffer to overflow, so no snprintf-versus-sprintf decision to make; and Lua adds %q, which escapes a value so that reading it back as Lua source reproduces it exactly. A mismatched specifier raises a clear runtime error rather than reading whatever the stack happened to hold.
string.sub replaces pointer arithmetic
In C a substring is a pointer plus a length, and taking one means arithmetic on the pointer plus care not to run past the NUL. Lua returns a new string from string.sub, with 1-based inclusive bounds and negative indices that count from the end.
#include <stdio.h> #include <string.h> int main(void) { const char *text = "embedding"; /* "bed" — offset 2, length 3 */ printf("%.*s\n", 3, text + 2); /* last three characters */ printf("%s\n", text + strlen(text) - 3); return 0; }
local text = "embedding" print(text:sub(3, 5)) -- "bed": 1-based, and the end is inclusive print(text:sub(-3)) -- "ing": negative counts back from the end print(text:sub(1, -2)) -- "embeddin": all but the last byte
Note the method-call syntax: text:sub(3, 5) is sugar for string.sub(text, 3, 5), available on every string because strings share a metatable whose __index is the string table. Out-of-range bounds are clamped rather than being undefined behavior.
Lua patterns replace strstr and hand-rolled scanning
C's string library gives you strchr, strstr, and strtok, and anything structured means writing the scan yourself. Lua ships a small pattern language — not regular expressions, but enough for most parsing — with captures returned as multiple values.
#include <stdio.h> #include <string.h> int main(void) { const char *line = "port=8080"; const char *equals = strchr(line, '='); if (equals != NULL) { printf("key: %.*s\n", (int) (equals - line), line); printf("value: %s\n", equals + 1); } return 0; }
local line = "port=8080" local key, value = line:match("(%w+)=(%w+)") print("key: " .. key) print("value: " .. value) print(("a,b,c"):gsub(",", ";")) -- replacement plus a count for word in ("one two three"):gmatch("%a+") do print(word) end
Patterns use % where regular expressions use a backslash: %w is alphanumeric, %a alphabetic, %d a digit, %s whitespace, and capitals negate the class. There is no alternation (|) and no bounded repetition ({2,4}); the payoff is that the whole engine is a few hundred lines of C with no backtracking blowups.
Arrays, Structs, Hash Tables: All One Table
Arrays are tables — and they start at 1
A C array is a contiguous block whose length the language does not track, and reading past the end is undefined behavior. A Lua table is the only container the language has, and using it as an array means keys 1..n — 1-based indexing is the single most common porting bug.
#include <stdio.h> int main(void) { int numbers[5] = { 10, 20, 30, 40, 50 }; printf("%d\n", numbers[0]); /* first element */ printf("%d\n", numbers[4]); /* last element */ printf("%zu\n", sizeof numbers / sizeof numbers[0]); return 0; }
local numbers = { 10, 20, 30, 40, 50 } print(numbers[1]) -- first element: index 1, not 0 print(numbers[5]) -- last element print(#numbers) -- 5 print(numbers[99]) -- nil, not undefined behavior
Reading a key that was never set gives nil rather than garbage, so there is no out-of-bounds read to exploit. Under the hood the implementation still keeps consecutive integer keys in a real array, so numbers[i] is a bounds-checked array access, not a hash lookup.
No realloc: tables grow on their own
The grow-a-buffer dance — track capacity, double it, realloc, check for failure, remember to free — is a C ritual. A table has no capacity to manage: assigning to #numbers + 1 appends, and the implementation resizes its internal array as needed.
#include <stdio.h> #include <stdlib.h> int main(void) { size_t capacity = 2, count = 0; int *numbers = malloc(capacity * sizeof *numbers); if (numbers == NULL) return 1; for (int value = 1; value <= 5; value++) { if (count == capacity) { capacity *= 2; int *grown = realloc(numbers, capacity * sizeof *numbers); if (grown == NULL) { free(numbers); return 1; } numbers = grown; } numbers[count++] = value * 10; } printf("%zu items, last = %d\n", count, numbers[count - 1]); free(numbers); return 0; }
local numbers = {} for value = 1, 5 do numbers[#numbers + 1] = value * 10 -- idiomatic append end print(#numbers .. " items, last = " .. numbers[#numbers])
table.insert(numbers, value) does the same thing and reads more clearly when you also use the two-argument form to insert in the middle. Allocation failure is not something the script sees at all — Lua raises an error the host can catch instead of returning a null pointer.
The hash table you would otherwise write yourself
C's standard library has no hash table, so mapping strings to values means a linear scan, a third-party library, or writing your own hashing and collision handling. In Lua the same table type does it: any value except nil can be a key.
#include <stdio.h> #include <string.h> /* C has no built-in map, so a lookup is usually a linear scan over a parallel-array table like this one. */ struct entry { const char *key; int value; }; int main(void) { struct entry table[] = { { "ruby", 1995 }, { "lua", 1993 } }; size_t count = sizeof table / sizeof table[0]; for (size_t index = 0; index < count; index++) { if (strcmp(table[index].key, "lua") == 0) { printf("lua: %d\n", table[index].value); } } return 0; }
local released = { ruby = 1995, lua = 1993 } print(released.lua) -- dot syntax for string keys print(released["ruby"]) -- equivalent bracket syntax released.python = 1991 -- new keys need no resizing for language, year in pairs(released) do print(language, year) end
released.lua is exactly released["lua"] — the dot is sugar for a string key. Iteration order from pairs is unspecified and can change between runs, so never rely on it; use an array of keys and table.sort when order matters.
A struct is a table with named keys
A C struct declares its fields up front, and the compiler lays them out at fixed offsets. A Lua table has no declaration: fields appear when assigned and can be added at any time, which is convenient and also means a misspelled field name is not an error but a new key.
#include <stdio.h> struct point { double x, y; }; int main(void) { struct point origin = { .x = 1.5, .y = 2.5 }; printf("(%.1f, %.1f)\n", origin.x, origin.y); origin.x = 10.0; printf("(%.1f, %.1f)\n", origin.x, origin.y); return 0; }
local origin = { x = 1.5, y = 2.5 } print(string.format("(%.1f, %.1f)", origin.x, origin.y)) origin.x = 10.0 print(string.format("(%.1f, %.1f)", origin.x, origin.y)) origin.label = "origin" -- fields are not fixed at declaration print(origin.label)
There is no layout to reason about, no padding, and no sizeof. The cost is that field access is a hash lookup rather than a fixed offset, and that origin.lable = 1 silently creates a second field instead of failing to compile.
Inserting and removing without memmove
Inserting into the middle of a C array means shifting the tail with memmove and tracking the count yourself. table.insert and table.remove do the shifting, and table.remove hands back the value it took out.
#include <stdio.h> #include <string.h> int main(void) { int numbers[6] = { 10, 20, 40, 50, 0, 0 }; size_t count = 4; /* insert 30 at index 2: shift the tail right first */ memmove(&numbers[3], &numbers[2], (count - 2) * sizeof numbers[0]); numbers[2] = 30; count++; for (size_t index = 0; index < count; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
local numbers = { 10, 20, 40, 50 } table.insert(numbers, 3, 30) -- insert at position 3, shifting the rest print(table.concat(numbers, " ")) local removed = table.remove(numbers, 1) -- remove and return the first print(removed, table.concat(numbers, " ")) table.insert(numbers, 60) -- no position: append print(table.concat(numbers, " "))
Both functions work on the array part (keys 1..n) and keep it dense, which is what lets # stay meaningful. table.remove(numbers) with no position pops the last element — the cheap operation, since nothing has to shift.
ipairs for arrays, pairs for everything
C iterates by index and needs the length passed alongside the array. Lua has two iterators: ipairs walks 1, 2, 3… and stops at the first missing key, while pairs visits every key the table has, in unspecified order.
#include <stdio.h> int main(void) { const char *names[] = { "first", "second", "third" }; size_t count = sizeof names / sizeof names[0]; for (size_t index = 0; index < count; index++) { printf("%zu: %s\n", index, names[index]); } return 0; }
local names = { "first", "second", "third" } for index, name in ipairs(names) do -- 1..n, stops at the first nil print(index .. ": " .. name) end local settings = { debug = true, level = 3 } for key, value in pairs(settings) do -- every key, any type, no order print(key, value) end
Use ipairs (or a numeric for) whenever the sequence matters, and pairs when the table is a record or a map. pairs on an array works but gives no ordering guarantee — a subtle bug when output order is part of what you are testing.
table.sort takes a closure, not a function pointer
qsort needs a comparator that takes void *, casts them itself, and returns a three-way integer. table.sort takes a two-argument predicate that answers "does left come first?" — and because it is an ordinary function value, it can close over local state.
#include <stdio.h> #include <stdlib.h> static int by_descending(const void *left, const void *right) { int first = *(const int *) left; int second = *(const int *) right; return (second > first) - (second < first); } int main(void) { int numbers[] = { 30, 10, 50, 20 }; size_t count = sizeof numbers / sizeof numbers[0]; qsort(numbers, count, sizeof numbers[0], by_descending); for (size_t index = 0; index < count; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
local numbers = { 30, 10, 50, 20 } table.sort(numbers) -- ascending by default print(table.concat(numbers, " ")) table.sort(numbers, function(left, right) -- a "less than" predicate return left > right end) print(table.concat(numbers, " "))
The comparator must be a strict order: returning true for equal elements makes table.sort raise invalid order function for sorting rather than silently producing nonsense. The sort is in place and not stable, exactly like qsort.
Control Flow
if / elseif / else, and ~= for inequality
The structure is identical; only the punctuation changes. Braces become then ... end, else if becomes the single keyword elseif (no second end to close), the condition needs no parentheses, and inequality is ~=, not !=.
#include <stdio.h> int main(void) { int score = 72; if (score >= 90) { printf("excellent\n"); } else if (score >= 60) { printf("passing\n"); } else { printf("failing\n"); } if (score != 100) printf("not perfect\n"); return 0; }
local score = 72 if score >= 90 then print("excellent") elseif score >= 60 then print("passing") else print("failing") end if score ~= 100 then print("not perfect") end
There is no ! operator either — negation is the word not. Remember that the condition is tested for truthiness, so if score then is true even when score is 0.
No switch — dispatch through a table instead
Lua has no switch. For a couple of cases an if/elseif chain is fine; for a real dispatch the idiom is a table of functions keyed by the value, which is closer to an array of function pointers than to a jump table.
#include <stdio.h> int main(void) { char command = 'b'; switch (command) { case 'a': printf("added\n"); break; case 'b': printf("built\n"); break; default: printf("unknown\n"); break; } return 0; }
local command = "b" local actions = { a = function() print("added") end, b = function() print("built") end, } local action = actions[command] if action then action() else print("unknown") end
This is often better than the C original: there is no fallthrough to forget a break for, the cases can be any type rather than only integers and characters, and the table can be built at runtime or extended by other code.
The numeric for loop is a range, not three expressions
C's for is three arbitrary expressions, so the loop variable can be modified anywhere and the condition can be anything. Lua's numeric for is a fixed range: start, inclusive limit, and optional step, all evaluated exactly once before the loop begins.
#include <stdio.h> int main(void) { for (int index = 1; index <= 5; index++) { printf("%d ", index); } printf("\n"); for (int countdown = 10; countdown > 0; countdown -= 3) { printf("%d ", countdown); } printf("\n"); return 0; }
for index = 1, 5 do io.write(index, " ") end print() for countdown = 10, 1, -3 do -- start, limit, step io.write(countdown, " ") end print()
The control variable is a fresh local in each iteration and assigning to it does not affect the loop — closures created inside the loop each capture their own copy, which is the opposite of C where they would all share one variable. The limit is inclusive, so for index = 1, #items covers the whole array.
do/while becomes repeat/until — with the test inverted
while is the same loop in both languages. do { } while (cond) becomes repeat ... until cond, and the condition flips meaning: C's tests whether to go around again, Lua's tests whether to stop.
#include <stdio.h> int main(void) { int remaining = 3; while (remaining > 0) { printf("while: %d\n", remaining); remaining--; } int attempts = 0; do { attempts++; printf("do/while: %d\n", attempts); } while (attempts < 3); return 0; }
local remaining = 3 while remaining > 0 do print("while: " .. remaining) remaining = remaining - 1 end local attempts = 0 repeat attempts = attempts + 1 print("repeat: " .. attempts) until attempts >= 3 -- the CONDITION TO STOP, not to continue
One nicety Lua adds: locals declared inside the repeat body are still in scope in the until expression, so you can compute a value and test it without hoisting the declaration above the loop.
break exists; continue is spelled goto
Lua has break but no continue. Since 5.2 it does have goto and labels, and jumping to a label at the end of the loop body is the standard replacement — the pattern is common enough that ::continue:: reads as idiom rather than as a goto.
#include <stdio.h> int main(void) { for (int index = 1; index <= 6; index++) { if (index % 2 == 0) continue; /* skip evens */ if (index > 5) break; /* stop early */ printf("%d ", index); } printf("\n"); return 0; }
for index = 1, 6 do if index % 2 == 0 then goto continue end -- skip evens if index > 5 then break end -- stop early io.write(index, " ") ::continue:: end print()
Lua's goto is far more restricted than C's: it cannot jump into or out of a function, and it cannot jump into the scope of a local. That rules out most of the ways goto gets abused in C, including the goto cleanup pattern — which Lua does not need anyway, since errors unwind through pcall.
Functions
No prototypes, no headers — but order still matters
C separates declaration from definition so a caller can be compiled before the callee exists. Lua has no declarations at all: function creates a value and assigns it, so the definition must simply run before the call.
#include <stdio.h> /* A prototype, so main can call it before it is defined */ static int doubled(int value); int main(void) { printf("%d\n", doubled(21)); return 0; } static int doubled(int value) { return value * 2; }
local function doubled(value) return value * 2 end print(doubled(21)) -- A function is just a value, so this is the same thing: local tripled = function(value) return value * 3 end print(tripled(21))
local function doubled is sugar for local doubled; doubled = function… — declaring the local first, which is what lets the body call itself recursively. Writing local doubled = function… instead leaves the name out of scope inside the body.
Multiple return values instead of out-parameters
A C function returns one value, so anything else it needs to report comes back through a pointer parameter that the caller has to allocate and pass. Lua functions return as many values as they like, and the caller's assignment list picks up as many as it wants.
#include <stdio.h> /* C returns one value, so extras come back through pointers */ static int divide(int numerator, int denominator, int *remainder) { *remainder = numerator % denominator; return numerator / denominator; } int main(void) { int remainder = 0; int quotient = divide(17, 5, &remainder); printf("%d remainder %d\n", quotient, remainder); return 0; }
local function divide(numerator, denominator) return numerator // denominator, numerator % denominator end local quotient, remainder = divide(17, 5) print(quotient .. " remainder " .. remainder) -- Extra values are dropped when only one is wanted: print((divide(17, 5))) -- parentheses truncate to the first value
This is why string.find can return a start and an end, and why pcall can return a success flag plus the result. The rule to remember: a call in the middle of an expression list is truncated to one value, and wrapping a call in parentheses truncates it too.
Varargs without stdarg.h
stdarg.h gives you no way to know how many arguments arrived or what type they are — hence printf's format string and the leading count parameter here. Lua's ... knows its own length, retrievable with select("#", ...).
#include <stdio.h> #include <stdarg.h> /* The count must be passed in — C cannot ask how many arguments arrived */ static int sum_all(int count, ...) { va_list arguments; va_start(arguments, count); int total = 0; for (int index = 0; index < count; index++) { total += va_arg(arguments, int); } va_end(arguments); return total; } int main(void) { printf("%d\n", sum_all(4, 10, 20, 30, 40)); return 0; }
local function sum_all(...) local total = 0 for _, value in ipairs({ ... }) do total = total + value end return total, select("#", ...) -- the count is available end local total, count = sum_all(10, 20, 30, 40) print(total .. " from " .. count .. " arguments")
Packing varargs into a table with { ... } is convenient but stops at the first nil; select handles embedded nils correctly, and table.pack(...) gives a table with an explicit n field. There is no undefined behavior for a mismatched type — a bad value raises an error where it is used.
Closures replace the function-pointer-plus-void-pointer idiom
Every C callback API ends up with the same shape: a function pointer plus a void * for the caller's state, cast back inside. A Lua function closes over the locals it references, so the state travels with the function and needs no parameter, no struct, and no cast.
#include <stdio.h> /* C carries state alongside a function pointer, by hand */ struct counter { int total; }; static void add_to(struct counter *state, int value) { state->total += value; } int main(void) { struct counter running = { .total = 0 }; add_to(&running, 10); add_to(&running, 5); printf("%d\n", running.total); return 0; }
local function make_counter() local total = 0 -- captured by the closure return function(value) total = total + value return total end end local add_to = make_counter() add_to(10) print(add_to(5)) local separate = make_counter() -- its own private total print(separate(100))
The captured variable is called an upvalue, and each closure gets its own — separate above shares nothing with add_to. This is the mechanism behind iterators, callbacks, and Lua's object idioms, and it is the single largest expressive gap from C.
Functions are values you can store anywhere
C can store function pointers, but the declaration syntax is famously awkward and every entry must match one exact signature. In Lua a function is an ordinary value: put it in a table, pass it, return it, with nothing to declare.
#include <stdio.h> static int doubled(int value) { return value * 2; } static int squared(int value) { return value * value; } int main(void) { /* An array of function pointers, all of one exact type */ int (*operations[2])(int) = { doubled, squared }; for (int index = 0; index < 2; index++) { printf("%d\n", operations[index](5)); } return 0; }
local operations = { doubled = function(value) return value * 2 end, squared = function(value) return value * value end, } for _, name in ipairs({ "doubled", "squared" }) do print(name .. ": " .. operations[name](5)) end -- And a function can take another function, with no type to declare: local function apply_twice(operation, value) return operation(operation(value)) end print(apply_twice(operations.doubled, 3))
Since there is no signature to satisfy, a higher-order function like apply_twice works with anything callable — including a closure carrying its own state, or a table with a __call metamethod. Arity is not checked either: missing arguments arrive as nil and extras are discarded.
Default arguments via or
C has no default arguments, so the pattern is a second wrapper function. Lua passes nil for anything the caller omitted, and or substitutes a default in one line.
#include <stdio.h> /* C has no default arguments; the usual answer is two functions */ static void greet_with(const char *name, const char *greeting) { printf("%s, %s!\n", greeting, name); } static void greet(const char *name) { greet_with(name, "Hello"); } int main(void) { greet("World"); greet_with("World", "Howdy"); return 0; }
local function greet(name, greeting) greeting = greeting or "Hello" -- nil falls back to the default print(greeting .. ", " .. name .. "!") end greet("World") greet("World", "Howdy")
The caveat is the same one that applies to every or default: false is falsy, so a caller who genuinely passes false gets the default instead. When that matters, test if flag == nil then explicitly.
malloc/free vs. Garbage Collection
There is no malloc and no free
Every allocation in C is a decision: how much, checked for failure, freed exactly once on every path out. In Lua allocation is invisible — strings, tables, and closures are created by expressions and reclaimed by an incremental garbage collector when nothing references them.
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char *message = malloc(32); if (message == NULL) return 1; strcpy(message, "allocated by hand"); printf("%s\n", message); free(message); /* forget this and it leaks */ return 0; }
local message = "allocated by the runtime" print(message) local records = {} for index = 1, 3 do records[index] = { id = index, label = "record " .. index } end print(#records .. " tables created, none of them freed by hand")
What you give up is control over when memory is released, which matters for a hard-real-time loop; what you gain is the entire class of bugs that never happens — no leak, no double free, no use-after-free. The collector is written in the same portable C as the rest of the interpreter, and the host can even supply its own allocator via lua_newstate.
No dangling references
A C function that returns allocated memory also returns an obligation, and documenting who frees it is half of what a C API's comments are for. In Lua a value stays alive exactly as long as something can reach it, so returning a table transfers nothing.
#include <stdio.h> #include <stdlib.h> /* Returning a pointer to a freed block: the classic C bug */ static int *make_numbers(void) { int *numbers = malloc(3 * sizeof *numbers); if (numbers == NULL) return NULL; numbers[0] = 1; numbers[1] = 2; numbers[2] = 3; return numbers; /* the caller now owns it — and must free it */ } int main(void) { int *numbers = make_numbers(); if (numbers == NULL) return 1; printf("%d %d %d\n", numbers[0], numbers[1], numbers[2]); free(numbers); /* numbers is now dangling: reading it is undefined behavior */ return 0; }
local function make_numbers() return { 1, 2, 3 } -- ownership is not a concept here end local numbers = make_numbers() print(numbers[1], numbers[2], numbers[3]) numbers = nil -- the only reference is gone; the table becomes garbage print("nothing dangles: there is no way to name it any more")
Dropping the last reference (assigning nil) makes a value collectable but does not destroy it — anything else still holding it keeps it valid. That is why there is no equivalent of freeing a block that another part of the program is still using.
Structs copy; tables are references
Assigning a struct in C copies it field by field; only an explicit pointer aliases. In Lua the opposite holds: tables, functions, and coroutines are always handled by reference, so assignment shares the object and mutating through either name is visible through both.
#include <stdio.h> struct point { int x, y; }; int main(void) { struct point first = { .x = 1, .y = 2 }; struct point second = first; /* a full copy */ second.x = 99; printf("first.x = %d, second.x = %d\n", first.x, second.x); return 0; }
local first = { x = 1, y = 2 } local second = first -- both names refer to ONE table second.x = 99 print("first.x = " .. first.x .. ", second.x = " .. second.x) -- A copy has to be made explicitly: local copy = {} for key, value in pairs(first) do copy[key] = value end copy.x = 1 print("first.x = " .. first.x .. ", copy.x = " .. copy.x)
Numbers, booleans, nil, and strings behave like C values (strings are immutable, so sharing them is unobservable). Lua has no built-in copy — write the loop above for a shallow copy, and recurse for a deep one. This is the same surprise a C developer gets from passing a struct pointer where a struct was meant.
Asking the collector what it is doing
C gives you no accounting at all — if you want to know how much you are using, you track every allocation yourself. Lua's collectgarbage both reports usage and drives the collector.
#include <stdio.h> #include <stdlib.h> int main(void) { /* C has no runtime memory accounting: you count it yourself */ size_t allocated = 0; void *block = malloc(1024); if (block == NULL) return 1; allocated += 1024; printf("tracked by hand: %zu bytes\n", allocated); free(block); allocated -= 1024; printf("after free: %zu bytes\n", allocated); return 0; }
local before = collectgarbage("count") -- kilobytes currently in use local garbage = {} for index = 1, 1000 do garbage[index] = { index } end print("grew by " .. math.floor(collectgarbage("count") - before) .. " KB") garbage = nil collectgarbage("collect") -- a full collection cycle print("after collecting: " .. math.floor(collectgarbage("count") - before) .. " KB")
collectgarbage("count") returns kilobytes in use, ("collect") forces a full cycle, and ("step") runs one increment — useful in a game loop that wants to bound collector pauses. An embedding host has the same control from C through lua_gc. This is the one example on the page the browser cannot run: Fengari leaves lua_gc unimplemented, because the JavaScript engine underneath owns collection. Run it under a local lua and the numbers are real.
Structs With Methods: Metatables
A struct plus its functions becomes a table with methods
The C convention — a struct plus free functions whose first parameter is a pointer to it — is exactly what Lua's colon syntax automates. function savings:deposit(amount) declares a hidden first parameter named self, and savings:deposit(50) passes the table as it.
#include <stdio.h> struct account { double balance; }; /* The "method" is a free function taking the struct pointer */ static void account_deposit(struct account *self, double amount) { self->balance += amount; } int main(void) { struct account savings = { .balance = 100.0 }; account_deposit(&savings, 50.0); printf("%.2f\n", savings.balance); return 0; }
local savings = { balance = 100.0 } function savings:deposit(amount) -- the colon adds a hidden 'self' self.balance = self.balance + amount end savings:deposit(50.0) -- and passes it automatically print(string.format("%.2f", savings.balance))
That is the whole trick: obj:method(a) is sugar for obj.method(obj, a), and a "method" is just a function stored in a table field. Using a dot where you meant a colon is the resulting classic bug — self arrives as the first real argument instead.
__index gives you shared behavior (and inheritance)
C shares behavior by embedding one struct in another and calling the base's functions explicitly. Lua does it with a metatable: when a key is missing from a table, the runtime consults that table's __index, and if that is another table it looks there — chaining as far as needed.
#include <stdio.h> /* C reuses behavior by embedding a struct and forwarding calls */ struct shape { const char *name; }; struct circle { struct shape base; double radius; }; static void shape_describe(const struct shape *self) { printf("a %s\n", self->name); } int main(void) { struct circle round = { .base = { .name = "circle" }, .radius = 2.0 }; shape_describe(&round.base); printf("radius %.1f\n", round.radius); return 0; }
local Shape = {} function Shape:describe() print("a " .. self.name) end local Circle = setmetatable({}, { __index = Shape }) -- Circle inherits function Circle.new(radius) return setmetatable({ name = "circle", radius = radius }, { __index = Circle }) end local round = Circle.new(2.0) round:describe() -- found on Shape, through two hops print("radius " .. round.radius)
There are no classes in the language at all; every OOP system in Lua is a convention built on __index. The lookup only happens when the key is absent, so an instance can override an inherited method simply by having its own field of that name.
Operator overloading, which C simply lacks
C has no operator overloading, so vector arithmetic is a set of named functions and expressions read as nested calls. Lua lets a metatable define __add, __sub, __mul, __eq, __lt, __len, __concat and friends, so the operators work on your own types.
#include <stdio.h> struct vector { double x, y; }; /* C cannot overload +, so addition is a named function */ static struct vector vector_add(struct vector left, struct vector right) { struct vector result = { .x = left.x + right.x, .y = left.y + right.y }; return result; } int main(void) { struct vector first = { 1.0, 2.0 }, second = { 3.0, 4.0 }; struct vector sum = vector_add(first, second); printf("(%.1f, %.1f)\n", sum.x, sum.y); return 0; }
local Vector = {} Vector.__index = Vector Vector.__add = function(left, right) return setmetatable({ x = left.x + right.x, y = left.y + right.y }, Vector) end Vector.__eq = function(left, right) return left.x == right.x and left.y == right.y end local function vector(x, y) return setmetatable({ x = x, y = y }, Vector) end local sum = vector(1, 2) + vector(3, 4) -- the + operator, overloaded print(string.format("(%.1f, %.1f)", sum.x, sum.y)) print(vector(1, 2) == vector(1, 2)) -- __eq, so true
The metamethod is looked up on the operands' metatables, so it applies to every value sharing that metatable rather than to a type declaration. __eq is consulted only when both operands are tables (or both userdata) that are not already the same object — comparing a table to a number is simply false, never an error.
__tostring: printing your own type
In C, printing an aggregate means writing a formatting helper and handing it a buffer. Lua routes print and tostring through the __tostring metamethod, so your own values print as readably as built-in ones.
#include <stdio.h> struct point { int x, y; }; /* Printing a struct means a helper plus a caller-owned buffer */ static void point_format(const struct point *self, char *buffer, size_t size) { snprintf(buffer, size, "(%d, %d)", self->x, self->y); } int main(void) { struct point where = { .x = 3, .y = 4 }; char text[32]; point_format(&where, text, sizeof text); printf("%s\n", text); return 0; }
local Point = {} Point.__index = Point Point.__tostring = function(self) return string.format("(%d, %d)", self.x, self.y) end local where = setmetatable({ x = 3, y = 4 }, Point) print(where) -- print calls tostring, which finds __tostring print(tostring(where) .. " is the position")
Without __tostring a table prints as table: 0x55f3… — its address, which is occasionally what you want when checking identity. __name in the metatable improves the default and the error messages that mention the type.
__index as a function: defaults and lazy fields
When __index is a function rather than a table, Lua calls it with the table and the missing key — so a lookup can compute a value, load it on demand, or log the access. C has no hook here: a fallback has to be written into every lookup site.
#include <stdio.h> #include <string.h> /* A missing key in C means writing the fallback into every lookup */ struct setting { const char *key; int value; }; static int setting_lookup(const struct setting *settings, size_t count, const char *key, int fallback) { for (size_t index = 0; index < count; index++) { if (strcmp(settings[index].key, key) == 0) return settings[index].value; } return fallback; } int main(void) { struct setting settings[] = { { "retries", 3 } }; printf("%d\n", setting_lookup(settings, 1, "retries", 10)); printf("%d\n", setting_lookup(settings, 1, "timeout", 10)); return 0; }
local settings = setmetatable({ retries = 3 }, { __index = function(_, key) print("(computing a default for " .. key .. ")") return 10 end, }) print(settings.retries) -- present: the metamethod is never consulted print(settings.timeout) -- absent: __index runs and supplies a default
This is the mechanism behind read-only tables, proxy objects, and lazy loading. Its counterpart __newindex intercepts assignment to a missing key, which is how you make a table reject unknown fields — recovering, by convention, the compile-time error C would have given for a misspelled struct member.
Return Codes vs. error/pcall
Return codes and errno vs. raising an error
C reports failure in the return value and leaves the detail in errno, which means every caller must check and every unchecked call is a latent bug. Lua raises an error that unwinds the stack until something catches it, so the failure cannot be ignored by accident.
#include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> /* The C convention: a sentinel return plus errno for the detail */ static int parse_port(const char *text, int *out) { errno = 0; char *end = NULL; long value = strtol(text, &end, 10); if (errno != 0 || *end != '\0' || value < 1 || value > 65535) { return -1; } *out = (int) value; return 0; } int main(void) { int port = 0; if (parse_port("8080", &port) == 0) printf("port %d\n", port); if (parse_port("nope", &port) != 0) printf("parse failed\n"); return 0; }
local function parse_port(text) local value = tonumber(text) if not value or value < 1 or value > 65535 then error("not a valid port: " .. tostring(text)) end return math.floor(value) end print("port " .. parse_port("8080")) local ok, message = pcall(parse_port, "nope") print(ok, message)
Errors are values, not a separate mechanism: error("text") raises a string (with position information prepended), and error(value, 0) raises it unchanged. Library functions that fail for expected reasons still use the C-like convention of returning nil, messageio.open is the canonical example.
pcall is the try/catch
pcall calls a function in protected mode: it returns true plus the results, or false plus the error, and nothing propagates past it. That covers errors you raise and errors the runtime raises — arithmetic on nil, indexing a nil, calling a non-function.
#include <stdio.h> /* There is no unwinding in C: every step checks and returns */ static int risky(int value, int *out) { if (value == 0) return -1; *out = 100 / value; return 0; } int main(void) { int result = 0; if (risky(4, &result) == 0) printf("got %d\n", result); if (risky(0, &result) != 0) printf("caught the failure\n"); return 0; }
local function risky(value) if value == 0 then error("cannot divide by zero") end return 100 // value end local ok, result = pcall(risky, 4) print(ok, result) local ok_two, message = pcall(risky, 0) print(ok_two, message) -- Runtime errors are caught the same way, not just explicit error() calls: print(pcall(function() return nil + 1 end))
Because pcall takes the function and its arguments rather than a block, wrapping several statements means passing an anonymous function: pcall(function() … end). This is also the mechanism a C host uses at the boundary — lua_pcall is the same protected call, seen from the other side.
assert returns its value instead of aborting
C's assert is a macro that aborts the process and vanishes when NDEBUG is defined. Lua's is an ordinary function that raises a normal, catchable error — and returns its arguments when the condition holds, which is why it reads well wrapped around a call.
#include <stdio.h> #include <assert.h> int main(void) { int count = 3; assert(count > 0); /* compiled out entirely under -DNDEBUG */ printf("count is %d\n", count); return 0; }
local count = 3 assert(count > 0, "count must be positive") print("count is " .. count) -- assert passes its first argument through, so it wraps a call: local text = assert(tostring(42), "conversion failed") print(text) print(pcall(assert, false, "the message you supply"))
It is never compiled out, so an assert in a hot loop is a real check with a real cost. The idiom local handle = assert(io.open(path)) converts the nil, message convention into a raised error in one step.
Errors can be tables, and xpcall adds a traceback
An error value in Lua does not have to be a string — a table carries a code, a message, and anything else you want, which is how libraries build structured errors. xpcall additionally takes a message handler that runs before the stack unwinds, so it can capture a traceback.
#include <stdio.h> /* Richer errors in C mean an out-parameter struct or a global */ struct failure { int code; const char *message; }; static int attempt(int value, struct failure *problem) { if (value < 0) { problem->code = 22; problem->message = "value must not be negative"; return -1; } return value * 2; } int main(void) { struct failure problem = { 0, NULL }; int result = attempt(-1, &problem); if (result < 0) { printf("error %d: %s\n", problem.code, problem.message); } return 0; }
local function attempt(value) if value < 0 then error({ code = 22, message = "value must not be negative" }) end return value * 2 end local ok, problem = pcall(attempt, -1) if not ok then print("error " .. problem.code .. ": " .. problem.message) end -- xpcall runs a handler while the failed call's stack is still alive: local ok_two, report = xpcall(attempt, function(caught) return "handled code " .. caught.code end, -5) print(ok_two, report)
The classic handler is debug.traceback, giving xpcall(work, debug.traceback) — a stack trace at the point of failure rather than at the point of catching, which no pcall can reconstruct afterward. Note that a table error message means error(problem) adds no position information, since that is only prepended to strings.
#include vs. require
#include pastes text; require returns a value
#include is textual: the preprocessor pastes a header into your translation unit, and the actual code arrives later from the linker. require("geometry") instead runs geometry.lua once and hands you the value it returned — normally a table of functions — caching it in package.loaded so a second require returns the same table.
#include <stdio.h> /* #include "geometry.h" would paste the file's text right here, and the definitions would come from geometry.c at link time. */ /* Inlined here so the example stands alone: */ static double rectangle_area(double width, double height) { return width * height; } int main(void) { printf("%.1f\n", rectangle_area(3.0, 4.0)); return 0; }
-- In a real project this would be: local geometry = require("geometry") -- with the module in geometry.lua. Inlined here so the example runs: local geometry = (function() local module = {} function module.rectangle_area(width, height) return width * height end return module end)() print(geometry.rectangle_area(3, 4))
There is no separate declaration of a module's interface, so nothing can go out of sync between a header and its implementation, and no include guards are needed. A module is a value you bind to a local name of your choosing, which also means two modules can never collide in a global namespace.
There is no preprocessor at all
Lua has no #define, no #ifdef, and no conditional compilation. Constants are locals, function-like macros are functions, and a compile-time switch becomes a runtime if — everything the preprocessor did happens in the language proper.
#include <stdio.h> #define MAX_RETRIES 3 #define DOUBLED(value) ((value) * 2) /* parenthesize everything! */ #define VERBOSE 0 int main(void) { printf("%d\n", MAX_RETRIES); printf("%d\n", DOUBLED(1 + 2)); #if VERBOSE printf("this line is not even compiled\n"); #endif return 0; }
local MAX_RETRIES = 3 -- a constant is just a local local function doubled(value) return value * 2 end -- a function, not a macro local VERBOSE = false print(MAX_RETRIES) print(doubled(1 + 2)) -- 6: arguments evaluate once if VERBOSE then print("this line is compiled, just not run") end
That removes the whole macro hazard list: no double evaluation of arguments, no missing parentheses changing precedence, no token pasting to debug. The trade-off is real but small — dead code is still compiled and shipped, and there is no equivalent of building out a platform-specific branch entirely.
Where require looks, and how C modules load
C resolves dependencies with build-system flags — -I, -L, -l — and optionally dlopen at runtime. Lua resolves them at runtime through two path templates: package.path for Lua source and package.cpath for shared libraries, both editable by the program itself.
/* The C equivalents are compiler and linker flags, not code: cc -c geometry.c -o geometry.o # compile a translation unit cc main.o geometry.o -o app # static linking cc -shared -fPIC -o libgeometry.so geometry.c # a shared object -I adds header search paths, -L and -l add library search paths and libraries, and dlopen() loads a shared object at runtime. */
-- Lua's two search paths, inspectable at runtime: print(package.path) -- where require looks for .lua files print(package.cpath) -- where it looks for C extension modules -- A C module built as a shared library is loaded by the same require: -- local socket = require("socket") -- finds socket.so on package.cpath -- and its entry point is a C function named luaopen_socket.
Neither column runs here: the browser has no filesystem for module search and no compiler for the C side. What matters for an embedder is that a C extension is a shared library exporting luaopen_name, which require loads through package.loadlib and calls to get the module table.
Embedding Lua in a C Program
lua_State: an interpreter as a value in your program
Everything about the C side starts here. A lua_State is a complete, independent interpreter: its own globals, its own stack, its own garbage collector. You can hold several at once, and lua_close releases everything one of them ever allocated — which is why embedding Lua does not leak into your program's own allocations.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> int main(void) { lua_State *state = luaL_newstate(); /* one independent interpreter */ luaL_openlibs(state); /* load print, string, table, ... */ if (luaL_dostring(state, "print('hello from an embedded chunk')") != LUA_OK) { fprintf(stderr, "lua error: %s\n", lua_tostring(state, -1)); lua_close(state); return 1; } lua_close(state); /* frees every byte Lua allocated */ return 0; }
-- This is the entire script the C host is running. print("hello from an embedded chunk") -- The standalone "lua" command is itself a ~500-line C program that does -- exactly what the C column does: make a state, open the libraries, run -- your file, close the state. print("there is no privileged 'main' interpreter — just states")
luaL_newstate uses realloc/free; lua_newstate takes your own allocator instead, which is how game engines route Lua through their pool allocators. luaL_openlibs is optional — leave it out and the script gets no libraries at all, a cheap first step toward sandboxing untrusted scripts.
The virtual stack: how values cross the boundary
C and Lua never pass values directly — they meet on a per-state stack of Lua values. Your C code pushes arguments on, asks Lua to do something, and reads results back off. Positive indices count from the bottom (1 is the first), negative from the top (-1 is the last pushed), which is why API code is full of -1 and -2.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushinteger(state, 42); /* index 1, or -3 */ lua_pushstring(state, "text"); /* index 2, or -2 */ lua_pushboolean(state, 1); /* index 3, or -1: the top */ printf("height: %d\n", lua_gettop(state)); printf("index 1: %s = %lld\n", luaL_typename(state, 1), (long long) lua_tointeger(state, 1)); printf("index -1: %s = %d\n", luaL_typename(state, -1), lua_toboolean(state, -1)); lua_pop(state, 2); /* drop the top two */ printf("height after popping: %d\n", lua_gettop(state)); lua_close(state); return 0; }
-- Nothing in Lua can see the stack: it belongs entirely to the C API. -- The closest a script comes is a varargs list, also addressed by -- position and also aware of its own height: local function inspect(...) print("height: " .. select("#", ...)) print("first: " .. tostring((select(1, ...)))) print("last: " .. tostring((select(select("#", ...), ...)))) end inspect(42, "text", true)
The stack is why the C API needs no lua_Value type and no reference counting: a value sitting on the stack is reachable, so the collector will not move it out from under you. The discipline it demands is balance — leave the stack as you found it, and use lua_gettop/lua_settop when a function has several exits.
Reading a script's globals — Lua as a config format
This is the oldest reason to embed Lua: a configuration file that can compute. The host runs the chunk, then reads the globals it left behind with lua_getglobal — and can push values back with lua_setglobal so the script sees data the host chose.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); /* A "config file", which is really just a Lua chunk */ if (luaL_dostring(state, "window_width = 1280\n" "window_title = 'demo'") != LUA_OK) { fprintf(stderr, "config error: %s\n", lua_tostring(state, -1)); lua_close(state); return 1; } lua_getglobal(state, "window_width"); lua_getglobal(state, "window_title"); printf("%lld wide, titled %s\n", (long long) lua_tointeger(state, -2), lua_tostring(state, -1)); lua_pop(state, 2); /* C can write globals too */ lua_pushinteger(state, 720); lua_setglobal(state, "window_height"); luaL_dostring(state, "print('height set by C: ' .. window_height)"); lua_close(state); return 0; }
window_width = 1280 window_title = "demo" print(window_width .. " wide, titled " .. window_title) -- Globals are entries in a real table, which is exactly the table the -- host reads with lua_getglobal: print(_G.window_width) _G.window_height = 720 print(window_height)
lua_getglobal pushes the value and returns its type, so a host that cares about a missing or misspelled setting should check for LUA_TNIL instead of silently taking lua_tointeger's 0. Reading a global from C is a table lookup in _G, the same operation the script performs.
Calling a Lua function from C with lua_pcall
A call from C is the stack protocol in miniature: push the function, push its arguments in order, then say how many of each to expect. lua_pcall(state, 2, 2, 0) pops the function and its two arguments and leaves two results in their place.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); luaL_dostring(state, "function scale(value, factor)\n" " return value * factor, value\n" "end"); lua_getglobal(state, "scale"); /* push the function */ lua_pushnumber(state, 3.0); /* push argument 1 */ lua_pushnumber(state, 4.0); /* push argument 2 */ /* 2 arguments in, 2 results out, no message handler */ if (lua_pcall(state, 2, 2, 0) != LUA_OK) { fprintf(stderr, "call failed: %s\n", lua_tostring(state, -1)); lua_close(state); return 1; } printf("scaled %g, original %g\n", lua_tonumber(state, -2), lua_tonumber(state, -1)); lua_pop(state, 2); lua_close(state); return 0; }
function scale(value, factor) return value * factor, value end local scaled, original = scale(3, 4) print("scaled " .. scaled .. ", original " .. original) -- The host calls it the same way a script does, including protection: print(pcall(scale, 3, 4))
Use lua_pcall, not lua_call: an uncaught error in a plain lua_call propagates past your C frame (a longjmp) and never returns, so any cleanup after it is skipped. Pass LUA_MULTRET as the result count to accept however many the function returns, then use lua_gettop to see how many arrived.
Exposing a C function to Lua
The whole extension mechanism is one signature: int (*)(lua_State *). Read arguments from stack positions 1..n, push results, return their count. Register it with lua_pushcfunction plus lua_setglobal and scripts can call it like any other function.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> /* Every function callable from Lua has this one signature: it reads its arguments off the stack and returns how many results it pushed. */ static int clamp(lua_State *state) { lua_Number value = luaL_checknumber(state, 1); lua_Number lowest = luaL_checknumber(state, 2); lua_Number highest = luaL_checknumber(state, 3); if (value < lowest) value = lowest; if (value > highest) value = highest; lua_pushnumber(state, value); return 1; /* one result */ } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, clamp); lua_setglobal(state, "clamp"); /* now visible to every script */ luaL_dostring(state, "print(clamp(42, 0, 10))\n" "print(clamp(-3, 0, 10))\n" "print(type(clamp))"); lua_close(state); return 0; }
local function clamp(value, lowest, highest) return math.max(lowest, math.min(value, highest)) end print(clamp(42, 0, 10)) print(clamp(-3, 0, 10)) print(type(clamp)) -- "function": a C function is indistinguishable
From Lua's side there is no difference: type(clamp) is "function" either way, and the script cannot tell that this one is native. This is how an engine exposes its own systems — rendering, physics, entity queries — without generating bindings or writing FFI declarations.
Checking arguments and raising errors from C
The luaL_check* family is the reason C extensions have good error messages: luaL_checkinteger either returns the value or raises bad argument #1 to 'factorial' (number expected, got string), naming the position and the function. luaL_error raises your own message with printf-style formatting.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int factorial(lua_State *state) { lua_Integer count = luaL_checkinteger(state, 1); /* raises on bad input */ if (count < 0) { return luaL_error(state, "expected a non-negative integer, got %d", (int) count); } lua_Integer product = 1; for (lua_Integer index = 2; index <= count; index++) product *= index; lua_pushinteger(state, product); return 1; } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, factorial); lua_setglobal(state, "factorial"); luaL_dostring(state, "print(factorial(5))\n" "print(pcall(factorial, -1))\n" /* our own error */ "print(pcall(factorial, 'nope'))"); /* luaL_checkinteger's */ lua_close(state); return 0; }
local function factorial(count) if math.type(count) ~= "integer" then error("bad argument #1 to 'factorial' (number expected)", 2) end if count < 0 then error("expected a non-negative integer, got " .. count, 2) end local product = 1 for index = 2, count do product = product * index end return product end print(factorial(5)) print(pcall(factorial, -1)) print(pcall(factorial, "nope"))
These functions do not return on failure — they longjmp out of your C frame — so any resource you acquired before calling them leaks. Acquire nothing until the arguments are checked, or use luaL_optinteger for optional arguments with a default. Note that the script catches a C-raised error with pcall exactly as it would a Lua one.
Returning several values from a C function
A C function's return value is not its result — it is the count of results it left on the stack. That is what lets a native function return two values as naturally as a Lua one, and it is how the standard library implements the nil, message failure convention.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int divide(lua_State *state) { lua_Integer numerator = luaL_checkinteger(state, 1); lua_Integer denominator = luaL_checkinteger(state, 2); if (denominator == 0) { lua_pushnil(state); lua_pushstring(state, "division by zero"); return 2; /* the nil, message convention */ } lua_pushinteger(state, numerator / denominator); lua_pushinteger(state, numerator % denominator); return 2; /* quotient and remainder */ } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, divide); lua_setglobal(state, "divide"); luaL_dostring(state, "print(divide(17, 5))\n" "local value, problem = divide(1, 0)\n" "print(value, problem)"); lua_close(state); return 0; }
local function divide(numerator, denominator) if denominator == 0 then return nil, "division by zero" end return numerator // denominator, numerator % denominator end print(divide(17, 5)) local value, problem = divide(1, 0) print(value, problem)
Returning 0 means the call evaluates to nothing (not to nil — the difference shows up in select("#", …)). The count must match what you actually pushed: claiming more than you pushed hands the script whatever was underneath on the stack.
Registering a whole library with luaL_Reg
Registering functions one at a time gets tedious, so the API takes a luaL_Reg array — name and function pointer pairs, NULL-terminated. luaL_newlib creates a table pre-sized for them and fills it in, which is precisely how string, table, and math are built.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int rectangle_area(lua_State *state) { lua_pushnumber(state, luaL_checknumber(state, 1) * luaL_checknumber(state, 2)); return 1; } static int rectangle_perimeter(lua_State *state) { lua_pushnumber(state, 2 * (luaL_checknumber(state, 1) + luaL_checknumber(state, 2))); return 1; } /* A NULL-terminated name-to-function table, exactly like the standard libraries use internally */ static const luaL_Reg rectangle_functions[] = { { "area", rectangle_area }, { "perimeter", rectangle_perimeter }, { NULL, NULL }, }; int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); luaL_newlib(state, rectangle_functions); /* one table, both functions */ lua_setglobal(state, "rectangle"); luaL_dostring(state, "print(rectangle.area(3, 4))\n" "print(rectangle.perimeter(3, 4))"); lua_close(state); return 0; }
local rectangle = {} function rectangle.area(width, height) return width * height end function rectangle.perimeter(width, height) return 2 * (width + height) end print(rectangle.area(3, 4)) print(rectangle.perimeter(3, 4)) -- In a real project this table would be the module's return value, and a -- C library would be loaded by the same require that loads a .lua file.
Shipping this as a loadable module means compiling it as a shared library whose one exported symbol is luaopen_rectangle, doing the luaL_newlib and returning 1. Then require("rectangle") finds it on package.cpath and the script cannot tell it from a Lua module.
Handing a C struct to Lua as userdata
Userdata is how a C struct becomes a Lua value: lua_newuserdatauv allocates a block that Lua owns and collects, and attaching a metatable gives it methods so scripts write tally:add(5). luaL_checkudata then verifies on the way back in that the argument really is your type.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> struct counter { int total; }; static const char *COUNTER_TYPE = "cheatsheet.counter"; static int counter_new(lua_State *state) { /* Lua allocates the block and owns its lifetime */ struct counter *self = lua_newuserdatauv(state, sizeof *self, 0); self->total = 0; luaL_setmetatable(state, COUNTER_TYPE); /* gives it methods */ return 1; } static int counter_add(lua_State *state) { struct counter *self = luaL_checkudata(state, 1, COUNTER_TYPE); self->total += (int) luaL_checkinteger(state, 2); lua_pushinteger(state, self->total); return 1; } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); luaL_newmetatable(state, COUNTER_TYPE); lua_pushvalue(state, -1); lua_setfield(state, -2, "__index"); /* metatable.__index = itself */ lua_pushcfunction(state, counter_add); lua_setfield(state, -2, "add"); /* so counter:add(n) works */ lua_pop(state, 1); lua_pushcfunction(state, counter_new); lua_setglobal(state, "counter_new"); luaL_dostring(state, "local tally = counter_new()\n" "tally:add(10)\n" "print(tally:add(5))\n" "print(type(tally))"); lua_close(state); return 0; }
-- The pure-Lua shape of the same object: a table plus a metatable. local Counter = {} Counter.__index = Counter function Counter.new() return setmetatable({ total = 0 }, Counter) end function Counter:add(amount) self.total = self.total + amount return self.total end local tally = Counter.new() tally:add(10) print(tally:add(5)) print(type(tally)) -- "table" here; "userdata" when C owns the memory
The type check matters — without it a script could pass any userdata and your C code would reinterpret unrelated memory. If the struct owns resources Lua does not know about (a file handle, a socket, a malloc'd buffer), add a __gc metamethod to release them when the userdata is collected; that is the one place an embedder still writes cleanup code.
Errors crossing the boundary in both directions
Errors are the one place the boundary is genuinely two-way. luaL_dostring and lua_pcall return a status and leave the message at the top of the stack for C to read; luaL_error from a C function raises into Lua, where an ordinary pcall catches it.
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int always_fails(lua_State *state) { return luaL_error(state, "this C function refuses to work"); } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); /* A script error surfaces as a return code plus a message on the stack */ if (luaL_dostring(state, "error('the script gave up')") != LUA_OK) { printf("C caught: %s\n", lua_tostring(state, -1)); lua_pop(state, 1); } /* A C error is catchable by the script, like any other */ lua_pushcfunction(state, always_fails); lua_setglobal(state, "always_fails"); luaL_dostring(state, "local ok, message = pcall(always_fails)\n" "print('Lua caught:', message)"); /* A syntax error is reported the same way, before anything runs */ if (luaL_dostring(state, "this is not Lua") != LUA_OK) { printf("C caught: %s\n", lua_tostring(state, -1)); lua_pop(state, 1); } lua_close(state); return 0; }
-- From the script's side there is one mechanism, whichever side raised: local ok, message = pcall(function() error("the script gave up") end) print("Lua caught: " .. message) -- A syntax error is caught at load time rather than at call time: local chunk, syntax_problem = load("this is not Lua") print(chunk, syntax_problem)
Under the hood a raised error is a longjmp to the nearest protected call, so C code between the two is skipped entirely — no destructors, no cleanup. That is why the API discourages holding resources across a call that can raise, and why the standard advice is to keep C functions short and let Lua own the control flow.
Building and linking against liblua
The entire build integration is a header include path and a library. Lua deliberately has no build-system requirements of its own — it is ANSI C with no dependencies beyond libm, which is why it turns up inside so many programs that would not tolerate a heavier runtime.
/* Compiling a host program that embeds Lua — build commands, not C: # headers and library from pkg-config (portable across distributions) cc -std=c17 host.c $(pkg-config --cflags --libs lua) -o host # or spelled out, when pkg-config is not available cc -std=c17 -I/usr/include/lua5.4 host.c -llua5.4 -lm -ldl -o host -llua is the interpreter itself, -lm is needed because Lua's numeric code uses libm, and -ldl only matters if the interpreter should be able to load C extension modules at runtime. Lua also builds as a handful of .c files you can drop straight into your own tree — which is how most games ship it. */
-- Nothing to build on this side: Lua source needs no compilation step. -- What a script can check is the version it is running under, since the -- C API is only source-compatible across minor releases: print(_VERSION) -- A precompiled chunk is available if load time matters (luac, or -- string.dump), but it is an optimization, never a requirement.
The C column here is a set of shell commands rather than a program, so it does not run. Worth knowing: the C API is source compatible across minor versions but not binary compatible, so a host built against 5.3 must be recompiled for 5.4 — and lua_newuserdatauv in the userdata example above is one of the 5.4 additions that will not compile against 5.3.
Coroutines: No C Equivalent
Suspending and resuming a function
A C function cannot pause: to resume where you left off you hoist every local into a struct and re-enter from the top, which is why iterators and parsers in C are written as explicit state machines. A Lua coroutine keeps its own stack, so coroutine.yield suspends mid-function and resume continues exactly there.
#include <stdio.h> /* C has no way to pause a function, so state lives in a struct and the function is re-entered from the top every time. */ struct stepper { int step; }; static const char *advance(struct stepper *self) { switch (self->step++) { case 0: return "first"; case 1: return "second"; case 2: return "third"; default: return NULL; } } int main(void) { struct stepper progress = { .step = 0 }; const char *label; while ((label = advance(&progress)) != NULL) { printf("%s\n", label); } return 0; }
local stepper = coroutine.create(function() coroutine.yield("first") -- pause here, keeping all local state coroutine.yield("second") return "third" -- returning finishes the coroutine end) while true do local ok, label = coroutine.resume(stepper) if not ok or label == nil then break end print(label) if coroutine.status(stepper) == "dead" then break end end
This is cooperative and single-threaded — no preemption, no locks, no data races, and nothing resembling pthread_create. The nearest C equivalents are setjmp/longjmp (which cannot resume a frame it left) or a real thread per task (vastly more expensive). coroutine.status reports suspended, running, normal, or dead.
Generators: an iterator without an iterator struct
coroutine.wrap turns a coroutine into a plain function that yields the next value each time it is called — which is exactly the shape Lua's generic for wants. The loop body then reads like a range loop, with no iterator struct and no out-parameter.
#include <stdio.h> /* An iterator in C is a struct plus a next() function */ struct range_iterator { int current, limit, step; }; static int range_next(struct range_iterator *self, int *out) { if (self->current > self->limit) return 0; *out = self->current; self->current += self->step; return 1; } int main(void) { struct range_iterator evens = { .current = 2, .limit = 10, .step = 2 }; int value; while (range_next(&evens, &value)) { printf("%d ", value); } printf("\n"); return 0; }
local function evens_up_to(limit) return coroutine.wrap(function() for value = 2, limit, 2 do coroutine.yield(value) end end) end for value in evens_up_to(10) do -- a generic for over the generator io.write(value, " ") end print()
The difference from the C version is where the state lives: in the coroutine's own suspended stack rather than in a struct you defined and advanced by hand. wrap also propagates errors instead of returning a status the way resume does, so a failure inside the generator surfaces at the loop.
Producer and consumer without threads
In C, one of the two sides has to be a callback: either the producer calls into the consumer, or the consumer polls the producer. Coroutines let both be written as straight-line loops, because either side can suspend while the other runs.
#include <stdio.h> /* Without threads, the consumer has to drive the producer through a callback or poll it — the producer cannot simply "wait". */ static void produce_into(void (*consume)(const char *), int count) { const char *items[] = { "alpha", "beta", "gamma" }; for (int index = 0; index < count && index < 3; index++) { consume(items[index]); } } static void print_item(const char *item) { printf("consumed %s\n", item); } int main(void) { produce_into(print_item, 3); return 0; }
local producer = coroutine.create(function() for _, item in ipairs({ "alpha", "beta", "gamma" }) do coroutine.yield(item) -- hand it over and wait end end) -- The consumer is in charge, and the producer resumes where it stopped: while coroutine.status(producer) ~= "dead" do local ok, item = coroutine.resume(producer) if ok and item then print("consumed " .. item) end end
Values pass in both directions — coroutine.resume(co, value) supplies the result of the yield inside, which is how a coroutine-based scheduler feeds work back to a task. This is the same mechanism behind Lua's use in async I/O libraries and in game engines that suspend a script mid-behavior for several frames.
Standard Library
time.h vs. os.time and os.clock
os.time is time(NULL) and os.clock is clock() with the CLOCKS_PER_SEC division already applied. Both return plain numbers, so the arithmetic that C needs difftime for is just subtraction.
#include <stdio.h> #include <time.h> int main(void) { time_t wall = time(NULL); /* seconds since the epoch */ clock_t ticks = clock(); /* CPU time, in CLOCKS_PER_SEC */ printf("epoch seconds look plausible: %d\n", wall > 1600000000); printf("cpu seconds: %.3f\n", (double) ticks / CLOCKS_PER_SEC); return 0; }
local wall = os.time() -- seconds since the epoch, as an integer local cpu = os.clock() -- CPU seconds, already divided for you print("epoch seconds look plausible: " .. tostring(wall > 1600000000)) print("cpu seconds is a number: " .. tostring(type(cpu) == "number")) -- Differences, which is what timing code actually wants: local started = os.clock() local total = 0 for index = 1, 200000 do total = total + index end print("measured a loop: " .. tostring(os.clock() - started >= 0))
The resolution of os.clock is whatever the C library provides, which is not enough for microbenchmarks — embedders usually expose a high-resolution timer as a C function instead. os.time also accepts a table of date fields, filling the role of mktime.
strftime vs. os.date
os.date covers gmtime, localtime, and strftime in one function: pass a format string to get text, or "*t" to get a table of components. A leading ! selects UTC, which is the equivalent of choosing gmtime over localtime.
#include <stdio.h> #include <time.h> int main(void) { time_t when = 1000000000; /* a fixed instant, for stable output */ struct tm parts; gmtime_r(&when, &parts); /* POSIX; gmtime() on other platforms */ char text[64]; strftime(text, sizeof text, "%Y-%m-%d %H:%M:%S", &parts); printf("%s\n", text); printf("year %d, day of year %d\n", parts.tm_year + 1900, parts.tm_yday + 1); return 0; }
local when = 1000000000 -- a fixed instant, for stable output -- The leading "!" means UTC; the specifiers are strftime's own: print(os.date("!%Y-%m-%d %H:%M:%S", when)) -- "*t" returns a table of components instead of a formatted string: local parts = os.date("!*t", when) print("year " .. parts.year .. ", day of year " .. parts.yday)
The table it returns uses natural values rather than C's offsets — year is 2001, not 101, and month is 1..12 rather than 0..11 — so tm_year + 1900 has no counterpart. There is no buffer to size, and an unknown specifier raises an error instead of being silently dropped.
stdio FILE* vs. the io library
The io library is a thin, friendlier layer over stdio: file handles are objects with methods, io.lines is an iterator that strips the newline and needs no buffer, and read("a") slurps the whole file without a fseek/ftell dance.
#include <stdio.h> int main(void) { FILE *handle = fopen("example.txt", "w"); if (handle == NULL) { perror("fopen"); return 1; } fprintf(handle, "first line\nsecond line\n"); fclose(handle); handle = fopen("example.txt", "r"); if (handle == NULL) { perror("fopen"); return 1; } char line[128]; while (fgets(line, sizeof line, handle) != NULL) { printf("read: %s", line); } fclose(handle); remove("example.txt"); return 0; }
local handle = assert(io.open("example.txt", "w")) handle:write("first line\nsecond line\n") handle:close() for line in io.lines("example.txt") do -- an iterator, no buffer to size print("read: " .. line) end local whole = assert(io.open("example.txt")):read("a") -- the entire file print(#whole .. " bytes") os.remove("example.txt")
This Lua example cannot run in the browser: Fengari has no filesystem, so it omits the io library entirely apart from a shim for io.write. Run it under a local lua to see it work. Also note io.open returns nil, message on failure rather than raising, which is what assert is converting here.
Building a string: table.concat is the buffer
Appending into a fixed buffer with snprintf means tracking how much you used, checking for truncation, and getting the return-value semantics right. In Lua you collect the pieces in a table and join them once with table.concat — the idiomatic string builder.
#include <stdio.h> int main(void) { char buffer[256]; size_t used = 0; for (int index = 1; index <= 5; index++) { int written = snprintf(buffer + used, sizeof buffer - used, "%s%d", used ? ", " : "", index * index); if (written < 0 || (size_t) written >= sizeof buffer - used) break; used += (size_t) written; } printf("%s\n", buffer); return 0; }
local pieces = {} for index = 1, 5 do pieces[#pieces + 1] = tostring(index * index) end print(table.concat(pieces, ", ")) -- Why not repeated concatenation: each .. builds a whole new string, so a -- loop over n items copies O(n²) bytes. Collect, then concat once.
This matters for performance, not just tidiness: strings are immutable, so result = result .. piece in a loop allocates and copies the whole accumulated string every iteration. table.concat walks the pieces once, sums their lengths, and allocates the result exactly once.
Gotchas for C Developers
Every loop bound shifts by one
Lua indexes from 1 and its numeric for limit is inclusive, so the C idiom for (i = 0; i < n; i++) ports to for index = 1, n. Getting this wrong does not crash — it reads nil at index 0 and skips the last element.
#include <stdio.h> int main(void) { const char *items[] = { "a", "b", "c" }; size_t count = sizeof items / sizeof items[0]; for (size_t index = 0; index < count; index++) { /* 0 to count-1 */ printf("%zu:%s ", index, items[index]); } printf("\n"); return 0; }
local items = { "a", "b", "c" } for index = 1, #items do -- 1 to #items, inclusive io.write(index, ":", items[index], " ") end print() -- The literal port of the C loop silently reads one nil and misses "c": for index = 0, #items - 1 do io.write(index, ":", tostring(items[index]), " ") end print()
The convention runs through the whole standard library: string.sub, table.insert, string.find, and %1 captures all count from 1. Tables can hold a key of 0 perfectly well, but it is outside the array part that # and ipairs consider.
A misspelled name is a new global, not an error
The compiler catches an undeclared identifier in C. In Lua an unknown name is simply a global that has not been set, so reading a typo gives nil and assigning to one creates a global — no diagnostic either way.
#include <stdio.h> int main(void) { int retry_count = 3; /* printf("%d\n", retry_conut); <- compile error: undeclared */ printf("%d\n", retry_count); return 0; }
local retry_count = 3 print(retry_conut) -- nil: a typo reads as an unset global retry_conut = 5 -- and this creates one, silently print(retry_count, retry_conut)
The usual defenses are a linter (luacheck) and, at runtime, a __index/__newindex metatable on _G that raises on unknown globals — a few lines that turn this class of typo back into a loud failure. Embedders often install exactly that in the state they hand to scripts.
The if (count) idiom breaks silently
This is the same rule as the truthiness section above, repeated here because it is the mistake that survives a port: the code compiles, runs, and takes the wrong branch forever. Any C conditional that relied on a numeric or pointer value being zero needs an explicit comparison in Lua.
#include <stdio.h> int main(void) { int remaining = 0; if (remaining) { /* false when zero */ printf("still working\n"); } else { printf("done\n"); } return 0; }
local remaining = 0 if remaining then -- TRUE: only nil and false are falsy print("still working") -- so this branch runs else print("done") end if remaining ~= 0 then -- the correct port print("still working") else print("done") end
The same applies to while (remaining) loops and to if (pointer) null checks — the latter becomes if value ~= nil then, or just if value then once you know the value is never false.
# is undefined when the array has holes
Assigning nil to a table key removes it, so it does not leave a zero behind the way a C array would — it leaves a gap. # is only defined for a sequence (keys 1..n with no gaps); with a hole, the manual permits it to return any border, and real implementations return different answers depending on the table's internal layout.
#include <stdio.h> int main(void) { /* A C array's length is a property of the declaration, so a zeroed-out element changes nothing about it. */ int numbers[5] = { 1, 2, 0, 4, 5 }; printf("%zu\n", sizeof numbers / sizeof numbers[0]); return 0; }
local numbers = { 1, 2, 3, 4, 5 } print(#numbers) -- 5 numbers[3] = nil -- this is a HOLE, not a zero print(#numbers) -- 5 or 2: either answer is valid -- Count explicitly when the sequence may have gaps: local count = 0 for _ in pairs(numbers) do count = count + 1 end print(count) -- 4
The practical rules: remove from the middle with table.remove (which closes the gap) rather than by assigning nil, and count with pairs when gaps are possible. This is also why ipairs stops at the first missing index — it is walking a sequence, and the sequence has ended.
The key 1 and the key "1" are different keys
Because any value can be a table key, indexing with a string that looks like a number is legal and gives a different slot than the number would. A value that arrived as text — from a config file, a command line, a pattern capture — must go through tonumber before it indexes an array.
#include <stdio.h> int main(void) { /* A C array index is an integer, full stop — there is no way to accidentally index it with the string "1". */ int numbers[3] = { 10, 20, 30 }; printf("%d\n", numbers[1]); return 0; }
local mixed = {} mixed[1] = "integer key" mixed["1"] = "string key" print(mixed[1], mixed["1"]) -- two separate entries -- But 1 and 1.0 ARE the same key: a float with an exact integer value -- is converted before it is used as a key. mixed[2.0] = "float two" print(mixed[2]) -- So a value read from text needs converting before it indexes an array: local from_text = "1" print(mixed[tonumber(from_text)])
The one normalization Lua does perform is float-to-integer: t[2.0] and t[2] are the same key, since the float has an exact integer value (and t[2.5] is simply its own key). nil and NaN are the only values that cannot be keys at all.