PONYλM2Modula-2

Lua.CodeCompared.To/C

An interactive executable cheatsheet comparing Lua and C

Lua 5.3 C17 (GCC)
Compiling It Instead of Running It
Hello, World
A Lua chunk is a list of statements that the interpreter reads and runs, so there is nothing to declare and nothing to build. A C program is a translation unit: it needs the header that declares its output function, an entry point with a declared return type, and a status to hand back to whatever launched it.
print("Hello, World!")
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
Nothing in the C version is optional. Drop #include <stdio.h> and the compiler has never heard of printf; drop the \n and the newline never appears, because printf writes exactly the bytes you give it and nothing more.
There is no "just run the file"
This row is about the workflow rather than the syntax, because it is the difference a Lua programmer feels first. Editing a Lua file and re-running it is one action. In C, the compile is a separate step that produces a separate artifact, and the thing you run is that artifact — not your source.
-- Saved as greet.lua, this is run with: lua greet.lua -- The interpreter compiles it to bytecode in memory and executes it. print("no build step")
/* Saved as greet.c, this is built and run with two commands: cc -o greet greet.c ./greet The first produces a machine-code file; the second runs it. */ #include <stdio.h> int main(void) { printf("no build step\n"); return 0; }
The practical consequence is that a C mistake is caught at build time by a compiler that refuses to produce a binary, whereas a Lua mistake waits until the line actually executes. That trade is the whole reason the type declarations in the next section exist.
Comments
-- A single-line comment. --[[ A long comment spanning several lines. ]] print("commented")
#include <stdio.h> /* A block comment spanning several lines. */ int main(void) { // A single-line comment. printf("commented\n"); return 0; }
C block comments do not nest, so wrapping a region that already contains /* … */ ends the comment early and leaves a stray */ the compiler rejects. Lua's long comments can be given extra equals signs (--[==[ … ]==]) to nest safely, which is the closest either language comes to a comment you can wrap around anything.
Semicolons and whitespace
Lua ends statements at a newline when the parse is unambiguous, and a semicolon is legal but almost never written. C ends every statement with a semicolon and treats newlines as ordinary whitespace, so a missing one is reported on the *following* line.
local greeting = "hi" -- no semicolon needed local count = 3 -- newlines are not significant either print(greeting, count)
#include <stdio.h> int main(void) { const char *greeting = "hi"; /* semicolon required */ int count = 3; /* newlines are not significant */ printf("%s\t%d\n", greeting, count); return 0; }
The %s and %d in printf are a preview of the strings section: C has no way to print a value without being told its type, whereas Lua's print inspects each argument at run time and separates them with a tab on its own.
Declared Types
Every variable has a declared type
In Lua the value carries the type and the variable is just a name that currently refers to it, so one local keyword covers every case. In C the *variable* carries the type: it is chosen when you declare it, fixed for the variable's whole life, and the compiler enforces it.
local count = 3 local ratio = 0.5 local label = "items" print(count, ratio, label)
#include <stdio.h> int main(void) { int count = 3; double ratio = 0.5; const char *label = "items"; printf("%d\t%g\t%s\n", count, ratio, label); return 0; }
A C variable cannot later hold a different kind of value. Assigning count = "items" is not a run-time surprise as it would be in Lua — it is a compile error, and the program is never produced.
A variable cannot change type
Rebinding a Lua name to a different kind of value is so ordinary it barely registers. C has no equivalent, and the commented-out line shows what the compiler says if you try.
local value = 42 print(type(value)) value = "now a string" print(type(value)) value = { 1, 2, 3 } print(type(value))
#include <stdio.h> int main(void) { int value = 42; printf("int\n"); /* value = "now a string"; <- would not compile: incompatible types when assigning to type 'int' from type 'char *' */ value = 7; /* only another int is allowed */ printf("%d\n", value); return 0; }
This is the single largest adjustment coming from Lua. A C variable is a fixed-size, fixed-type box at a known location, and the type is how the compiler decides how many bytes to move and how to interpret them.
Uninitialized is garbage, not nil
Reading an unassigned Lua variable gives nil, and reading a missing table key gives nil too. Both are defined, useful answers. C has no such value and no such guarantee: an uninitialized local holds whatever was already in that memory.
local declared print(declared) -- nil: reading it is safe and defined local table_of_values = {} print(table_of_values.missing) -- nil as well
#include <stdio.h> int main(void) { int declared = 0; /* WITHOUT this initializer the value is indeterminate -- not zero, not "nil", just whatever bytes were on the stack. */ printf("%d\n", declared); return 0; }
Reading it is undefined behavior, which is worse than a wrong answer — the compiler is entitled to assume it never happens and optimize accordingly. Always write the initializer; there is no nil to fall back on.
Block scope, without the global default
Both languages scope a name to the enclosing block, and both let an inner block see outward. The difference is what happens when you get it wrong.
local outer = "visible" do local inner = "block only" print(outer, inner) end print(outer, inner) -- inner is nil out here
#include <stdio.h> int main(void) { const char *outer = "visible"; { const char *inner = "block only"; printf("%s\t%s\n", outer, inner); } /* printf("%s\n", inner); <- would not compile: 'inner' undeclared */ printf("%s\n", outer); return 0; }
Using inner outside its block is a compile error in C and a silent nil in Lua. Note too that C has no equivalent of Lua's global-by-default rule — there is no way to accidentally create a variable by assigning to an undeclared name, because the declaration is mandatory.
No multiple assignment, no swap
Lua evaluates the whole right-hand side before assigning any of it, which is what makes first, second = second, first work. C assigns one variable per statement, so the swap needs somewhere to put the value it is about to overwrite.
local first, second = "a", "b" print(first, second) first, second = second, first print(first, second)
#include <stdio.h> int main(void) { const char *first = "a"; const char *second = "b"; printf("%s\t%s\n", first, second); const char *temporary = first; /* the swap needs a third name */ first = second; second = temporary; printf("%s\t%s\n", first, second); return 0; }
The missing multiple assignment has a larger consequence further down this page: it is also why C functions cannot return more than one value, and why out-parameters exist.
Fixed-Width Numbers & Overflow
Integers and floats are separate types
Lua 5.3 split the single number type into integer and float subtypes, so this distinction is already familiar — but in Lua both are still "a number" and 7 == 7.0 is true. In C they are genuinely different types with different sizes and different arithmetic.
local whole = 7 local fractional = 7.0 print(math.type(whole), math.type(fractional)) print(whole == fractional)
#include <stdio.h> int main(void) { int whole = 7; double fractional = 7.0; printf("int\tdouble\n"); printf("%s\n", whole == fractional ? "true" : "false"); return 0; }
The comparison is still true in C, because the int is converted to double before comparing. That implicit conversion is convenient here and the source of real bugs elsewhere, as the division row shows.
Division truncates when both sides are integers
Lua gives division two operators so the result type is visible in the source: / always produces a float and // always floors. C has one / whose meaning depends entirely on the types of its operands.
print(7 / 2) -- 3.5: / always produces a float print(7 // 2) -- 3: floor division is a separate operator print(7.0 // 2) -- 3.0
#include <stdio.h> int main(void) { printf("%d\n", 7 / 2); /* 3: integer division truncates */ printf("%g\n", 7.0 / 2); /* 3.5: one float operand is enough */ printf("%g\n", (double)7 / 2); /* 3.5: the usual fix, a cast */ return 0; }
This is the most common numeric bug for a Lua programmer writing C. 7 / 2 silently yields 3 — no warning, no float — and the usual repair is to cast one operand, as the third line does.
Integers have a width, and it wraps
Lua integers are 64-bit and wrap on overflow, so the concept is not new. What is new is that C has many integer types of different widths, and that the width is your choice at declaration time.
local largest = math.maxinteger print(largest) print(largest + 1 == math.mininteger) -- Lua wraps too, at 64 bits
#include <stdio.h> #include <limits.h> int main(void) { unsigned int largest = UINT_MAX; printf("%u\n", largest); printf("%u\n", largest + 1u); /* wraps to 0: defined for unsigned */ return 0; }
The example uses unsigned int deliberately: unsigned overflow is defined to wrap, while signed overflow is undefined behavior in C, not a wrap. A signed INT_MAX + 1 is not merely a surprising number — it licenses the compiler to do anything at all. 🚨 Fengari, which runs the Lua column in your browser, uses 32-bit integers, so clicking run here prints 2147483647 rather than the 64-bit maximum a desktop lua reports. The language specifies 64-bit integers; this build does not provide them.
No automatic string-to-number coercion
Lua coerces a numeric string in arithmetic, which many Lua programmers rely on when reading configuration. C has no such rule, and the reason the commented line is dangerous is that it compiles.
print("10" + 5) -- 15: the string is coerced print(tonumber("10") + 5) -- 15: the explicit version
#include <stdio.h> #include <stdlib.h> int main(void) { /* printf("%d\n", "10" + 5); <- compiles, but does NOT parse "10": it advances a pointer 5 characters past the string. */ int parsed = atoi("10"); /* the explicit conversion */ printf("%d\n", parsed + 5); return 0; }
"10" + 5 in C is pointer arithmetic: it produces a pointer five bytes into a two-byte string, and using it reads past the end. Conversion must be explicit — atoi here, or strtol when you need to detect failure. One browser caveat: Lua 5.3 makes string-to-number coercion produce a float, so Fengari prints 15.0 where a 5.4-or-later desktop lua prints 15. The coercion happens either way; only the subtype differs.
Truth, nil, and the Zero Trap
🚨 In C, 0 is false
This is the one Lua habit that must be unlearned outright, and it is worth reading twice because it is the exact inverse of the rule you already know. In Lua only nil and false are falsy — 0 and "" are both true. In C, 0 is the definition of false.
if 0 then print("zero is TRUE in Lua") end if "" then print("the empty string is TRUE too") end
#include <stdio.h> int main(void) { if (0) { printf("never printed\n"); } else { printf("zero is FALSE in C\n"); } return 0; }
The trap is not the literal 0 but the function that returns a count. if (strlen(name)) is true for a non-empty name and false for an empty one, which is the opposite of what the same shape does in Lua.
There is no nil — there is NULL, for pointers only
Lua uses one value, nil, for "absent" everywhere: an unset variable, a missing table key, a function that returned nothing. C has NULL, but it is only a pointer value — an int has no way to say "no integer here."
local found = nil if not found then print("nothing found") end local numbers = { 1, 2, 3 } print(numbers[99]) -- nil, not an error
#include <stdio.h> #include <string.h> int main(void) { const char *found = strchr("abc", 'z'); /* NULL when absent */ if (found == NULL) { printf("nothing found\n"); } return 0; }
This is why C functions so often return a status separately from their result, and why an int result frequently reserves -1 to mean failure. There is no out-of-band value the way nil is out-of-band for every Lua type.
Booleans are a late addition
Lua has had a real boolean type since 5.0, distinct from numbers. C got bool only in C99, and it is a small integer type rather than a separate kind of value — which is why printing one shows 1.
local ready = true print(type(ready)) print(ready and "yes" or "no")
#include <stdio.h> #include <stdbool.h> int main(void) { bool ready = true; printf("%d\n", ready); /* prints 1: bool IS an integer */ printf("%s\n", ready ? "yes" : "no"); return 0; }
C's ? : is a genuine conditional expression and is the direct equivalent of Lua's and/or idiom — with one advantage: it works correctly when the middle value is false, which ready and false or "no" famously does not.
Strings as NUL-Terminated Bytes
Length is a walk, not a lookup
A Lua string knows its own length, so # is a field read. A C string is just the address of some bytes, with a zero byte marking the end — nothing records how many there are, so the length must be counted every time you ask.
local greeting = "hello" print(#greeting) -- O(1): the length is stored with the string
#include <stdio.h> #include <string.h> int main(void) { const char *greeting = "hello"; printf("%zu\n", strlen(greeting)); /* O(n): scans for the NUL byte */ return 0; }
The cost matters in loops. Writing for (size_t index = 0; index < strlen(text); index++) re-scans the whole string on every iteration, turning a linear loop into a quadratic one. Compute the length once, before the loop.
C strings are mutable buffers
Every Lua string operation returns a new string and leaves the old one alone, because Lua strings are immutable and interned. A C string is a region of memory you own, and uppercasing it means writing over the bytes that are there.
local greeting = "hello" local shouted = greeting:upper() print(greeting, shouted) -- the original is untouched
#include <stdio.h> #include <ctype.h> int main(void) { char greeting[] = "hello"; /* an array: a modifiable copy */ for (int index = 0; greeting[index] != '\0'; index++) { greeting[index] = toupper((unsigned char)greeting[index]); } printf("%s\n", greeting); /* the original IS modified */ return 0; }
Note char greeting[] rather than const char *greeting. The array form copies the literal into modifiable storage; writing through a pointer to a string literal is undefined behavior and typically crashes, because literals usually live in read-only memory.
Concatenation means managing a buffer
Lua's .. allocates a new string of exactly the right size and hands it back. C has no concatenation operator at all: you supply the destination, and you are responsible for it being large enough.
local first = "Ada" local last = "Lovelace" local full = first .. " " .. last print(full)
#include <stdio.h> int main(void) { const char *first = "Ada"; const char *last = "Lovelace"; char full[64]; /* you choose the size, up front */ snprintf(full, sizeof full, "%s %s", first, last); printf("%s\n", full); return 0; }
Use snprintf rather than strcat or sprintf. It takes the buffer size and refuses to write past it, which is the difference between a truncated string and a buffer overflow — historically the most exploited bug class in C.
== compares addresses, not contents
Lua compares strings by value, and because they are interned, equal strings are usually the same object anyway. In C a string is a pointer, so == asks whether two pointers hold the same address — a question that is almost never the one you meant.
local left = "abc" local right = "ab" .. "c" print(left == right) -- true: Lua compares by value
#include <stdio.h> #include <string.h> int main(void) { const char *left = "abc"; char right[] = "abc"; printf("%s\n", left == right ? "same address" : "different addresses"); printf("%s\n", strcmp(left, right) == 0 ? "same contents" : "differ"); return 0; }
Use strcmp, and remember it returns 0 for equal. Combined with the zero-is-false rule above, if (strcmp(a, b)) reads as "if equal" and means the exact opposite — a classic first-week C bug.
No patterns, no gsub
Lua's pattern library is small but built in, and gmatch makes tokenizing a one-liner. The C standard library has no pattern matching whatsoever — splitting on a delimiter is the most it offers, and even that modifies the string in place.
local sentence = "one two three" for word in sentence:gmatch("%a+") do io.write(word, ";") end print()
#include <stdio.h> #include <string.h> int main(void) { char sentence[] = "one two three"; char *rest = sentence; char *word; while ((word = strtok_r(rest, " ", &rest)) != NULL) { printf("%s;", word); } printf("\n"); return 0; }
strtok_r writes NUL bytes over the delimiters in sentence, which is why the input has to be a modifiable array rather than a literal. For anything resembling a real pattern you reach for POSIX <regex.h> or a third-party library.
A NUL byte ends the string
Lua strings are counted byte sequences, so a zero byte is just another byte and #data is 5. In C the zero byte is the terminator, so the same literal is a two-character string sitting in six bytes of storage.
local data = "ab\0cd" print(#data) -- 5: Lua strings can contain zero bytes
#include <stdio.h> #include <string.h> int main(void) { char data[] = "ab\0cd"; printf("%zu\n", strlen(data)); /* 2: the scan stops at the zero */ printf("%zu\n", sizeof data); /* 6: the storage, including both zeros */ return 0; }
This is why C cannot hold arbitrary binary data in a plain string, and why any API that deals with binary takes a pointer and a length. Lua needs no such convention, which is one reason it is a comfortable place to do binary parsing.
Arrays Are Not Tables
An array has one type and a fixed size
A Lua table grows when you assign past the end and holds a mix of types without comment. A C array is a fixed run of same-typed slots decided at compile time, and there is no mechanism anywhere to make it longer.
local values = { 10, 20, 30 } values[4] = 40 -- grows on demand values[5] = "a string" -- and holds anything print(#values, values[5])
#include <stdio.h> int main(void) { int values[3] = { 10, 20, 30 }; /* values[3] = 40; <- no bounds check, no growth: this writes past the end of the array and corrupts the stack. */ printf("%d\t%d\t%d\n", values[0], values[1], values[2]); return 0; }
Nothing checks the index. Writing values[3] compiles, runs, and silently overwrites whatever happens to sit after the array — a neighboring variable, a saved register, a return address. This is the single most important safety difference on the page.
Indices start at 0
Lua is one of the few languages that indexes from 1, and the habit is deep. C indexes from 0, which changes not just the first subscript but the shape of every loop: the bound becomes < count rather than <= count.
local values = { "first", "second", "third" } print(values[1]) -- first for index = 1, #values do io.write(index, "=", values[index], " ") end print()
#include <stdio.h> int main(void) { const char *values[] = { "first", "second", "third" }; int count = 3; printf("%s\n", values[0]); /* first */ for (int index = 0; index < count; index++) { printf("%d=%s ", index, values[index]); } printf("\n"); return 0; }
Mixing the two conventions produces the off-by-one that reads or writes one slot past the end. Because C does not check, the symptom appears somewhere else entirely — often long after the line that caused it.
An array does not know its own length
Lua's # works on any table, anywhere. In C the length is known only where the array was declared: passing it to a function converts it to a bare pointer, and the size information is gone.
local values = { 10, 20, 30 } print(#values) local function total(numbers) local sum = 0 for _, value in ipairs(numbers) do sum = sum + value end return sum end print(total(values))
#include <stdio.h> /* The length must travel alongside the pointer -- there is no way to recover it from the array once it decays. */ static int total(const int *numbers, int count) { int sum = 0; for (int index = 0; index < count; index++) sum += numbers[index]; return sum; } int main(void) { int values[3] = { 10, 20, 30 }; int count = (int)(sizeof values / sizeof values[0]); printf("%d\n", count); printf("%d\n", total(values, count)); return 0; }
The sizeof values / sizeof values[0] idiom works in main and would be wrong inside total, where sizeof numbers is the size of a pointer. This is why essentially every C function taking an array also takes a count.
No string keys, no hash part
A Lua table is an array and a hash map at once, so string keys need no thought. C has no built-in map of any kind: the nearest thing in the standard library is a sorted array plus bsearch, and most real code either writes a hash table or pulls one in.
local settings = {} settings["width"] = 80 settings.height = 24 for key, value in pairs(settings) do print(key, value) end
#include <stdio.h> #include <string.h> struct Setting { const char *key; int value; }; int main(void) { struct Setting settings[] = { { "width", 80 }, { "height", 24 } }; int count = 2; for (int index = 0; index < count; index++) { printf("%s\t%d\n", settings[index].key, settings[index].value); } return 0; }
Looking a key up here is a linear scan with strcmp. That is genuinely what small C programs do, and it is a fair reminder of how much work a Lua table is quietly doing every time you write settings.width.
Arrays decay to pointers when passed
Lua tables are reference values, so a function that mutates its argument is visible to the caller. C arrays behave the same way, but for a different reason worth knowing: the array is not passed at all — its first element's address is.
local function modify(numbers) numbers[1] = 99 -- tables are passed by reference end local values = { 1, 2, 3 } modify(values) print(values[1]) -- 99
#include <stdio.h> static void modify(int *numbers) { numbers[0] = 99; /* writes through to the caller's array */ } int main(void) { int values[3] = { 1, 2, 3 }; modify(values); /* 'values' becomes &values[0] here */ printf("%d\n", values[0]); return 0; }
That is why the parameter can be written int *numbers and why sizeof on it gives a pointer size. Everything else in C is copied on the way in, including whole structs; arrays are the one exception.
Structs Instead of Tables
Fields are declared, not added
A Lua table's shape is whatever you have put in it so far, and a typo in a field name creates a new field. A C struct's shape is fixed by its declaration, and the same typo does not compile.
local point = { x = 3, y = 4 } point.z = 5 -- a new field, any time print(point.x, point.z)
#include <stdio.h> struct Point { int x; int y; }; int main(void) { struct Point point = { .x = 3, .y = 4 }; /* point.z = 5; <- would not compile: no member named 'z' */ printf("%d\t%d\n", point.x, point.y); return 0; }
The .x = 3 form is a designated initializer, added in C99. It is worth preferring over positional { 3, 4 } for the same reason Lua programmers prefer named fields: the meaning survives a reordering of the declaration.
Structs are copied, tables are not
Assigning a Lua table copies a reference, so the two names are the same object — a fact every Lua programmer has been bitten by at least once. Assigning a C struct copies the whole value, which is the opposite surprise.
local original = { x = 1 } local alias = original -- same table alias.x = 99 print(original.x) -- 99: they are one object
#include <stdio.h> struct Point { int x; }; int main(void) { struct Point original = { .x = 1 }; struct Point copy = original; /* a genuine copy of the bytes */ copy.x = 99; printf("%d\n", original.x); /* still 1 */ return 0; }
Struct assignment and struct arguments both copy every byte, so passing a large struct by value is real work. C code therefore passes a pointer (struct Point *) when the struct is big or when the callee needs to modify it — which restores the Lua behavior you started with.
No methods and no colon syntax
Lua has no classes either, but metatables plus the colon call give you something that reads like methods. C has neither: functions live outside the struct, and the convention is a name prefix plus an explicit pointer to the instance.
local Counter = {} Counter.__index = Counter function Counter.new() return setmetatable({ count = 0 }, Counter) end function Counter:increment() self.count = self.count + 1 end local counter = Counter.new() counter:increment() print(counter.count)
#include <stdio.h> struct Counter { int count; }; /* The "self" is an explicit first parameter -- which is exactly what Lua's colon syntax is doing for you behind the scenes. */ static void counter_increment(struct Counter *self) { self->count += 1; } int main(void) { struct Counter counter = { .count = 0 }; counter_increment(&counter); printf("%d\n", counter.count); return 0; }
The -> operator is (*self).count written shorter. Seeing self spelled out as a parameter is a useful reminder of what counter:increment() desugars to in Lua — the two languages differ in notation here more than in mechanism.
No metatables, no operator overloading
Metatables are the mechanism behind most of what makes Lua feel flexible: operator overloading, inheritance, default values, read-only tables, proxies. C has no hook of any kind — an operator means exactly one thing for the types it is defined on.
local Vector = {} Vector.__index = Vector Vector.__add = function(left, right) return setmetatable({ x = left.x + right.x }, Vector) end local sum = setmetatable({ x = 1 }, Vector) + setmetatable({ x = 2 }, Vector) print(sum.x)
#include <stdio.h> struct Vector { int x; }; /* No operator can be given a new meaning, so addition is a function. */ static struct Vector vector_add(struct Vector left, struct Vector right) { struct Vector result = { .x = left.x + right.x }; return result; } int main(void) { struct Vector sum = vector_add((struct Vector){ .x = 1 }, (struct Vector){ .x = 2 }); printf("%d\n", sum.x); return 0; }
Nor is there any equivalent of __index, so there is no way to intercept a missing field, and no way to give a struct a default. What you declare is what exists.
malloc & free: No Garbage Collector
You allocate, and you free
This is the largest single change. In Lua you create values and stop referring to them; the collector does the rest, and the word "free" never appears in a Lua program. In C every allocation is a promise to release it exactly once.
local buffer = {} for index = 1, 100 do buffer[index] = index end print(#buffer) buffer = nil -- the collector reclaims it, eventually
#include <stdio.h> #include <stdlib.h> int main(void) { int *buffer = malloc(100 * sizeof *buffer); if (buffer == NULL) return 1; /* allocation can fail */ for (int index = 0; index < 100; index++) buffer[index] = index + 1; printf("%d\n", buffer[99]); free(buffer); /* mandatory, and exactly once */ return 0; }
Three failure modes have no counterpart in Lua: forgetting free leaks; calling it twice corrupts the allocator; and using the pointer afterwards reads memory that now belongs to something else. Note also that malloc can return NULL, so the check is not optional.
Stack or heap is your decision
Every Lua table is heap-allocated and reference-counted into the collector, so returning one from a function is unremarkable. In C you choose: a local variable lives on the stack and is gone the instant the function returns, and only heap memory survives.
local function make() local values = { 1, 2, 3 } -- always heap-allocated, always safe return values -- returning it is fine end print(#make())
#include <stdio.h> #include <stdlib.h> /* A local array lives on the stack and dies when the function returns, so this one is allocated on the heap in order to outlive make(). */ static int *make(int count) { int *values = malloc((size_t)count * sizeof *values); if (values == NULL) return NULL; for (int index = 0; index < count; index++) values[index] = index + 1; return values; } int main(void) { int *values = make(3); if (values == NULL) return 1; printf("%d\n", values[2]); free(values); return 0; }
Returning the address of a stack local is the classic beginner error — it compiles, and the pointer is dangling before the caller can use it. If a value must outlive its function, it must be on the heap, and then somebody must free it.
Who frees it is a convention, not a rule
Lua has no notion of owning a value — several tables can hold the same one and the collector works it out. In C, every allocated pointer has exactly one party responsible for freeing it, and the language provides no way to say who.
-- Ownership is not a concept in Lua: any number of tables -- may refer to the same value, and the collector settles it. local shared = { name = "config" } local first_holder = { data = shared } local second_holder = { data = shared } print(first_holder.data.name, second_holder.data.name)
#include <stdio.h> #include <stdlib.h> #include <string.h> /* The comment IS the contract: nothing in the language records it. */ /* Returns a newly allocated string; the caller must free it. */ static char *duplicate_name(const char *name) { size_t length = strlen(name) + 1; char *copy = malloc(length); if (copy != NULL) memcpy(copy, name, length); return copy; } int main(void) { char *name = duplicate_name("config"); if (name == NULL) return 1; printf("%s\n", name); free(name); /* because the comment above said so */ return 0; }
The convention lives in a comment and in the function's name, which is why C libraries document ownership so obsessively and why mismatched expectations between two libraries are such a common source of leaks and double frees.
Use-after-free has no Lua equivalent
A Lua value stays alive exactly as long as something refers to it, so an alias can never outlive its target. C tracks nothing: free releases the memory regardless of how many pointers still point at it.
local record = { value = 7 } local alias = record record = nil -- the table survives: alias still refers to it print(alias.value) -- 7, guaranteed
#include <stdio.h> #include <stdlib.h> int main(void) { int *record = malloc(sizeof *record); if (record == NULL) return 1; *record = 7; int *alias = record; printf("%d\n", *alias); /* fine: still allocated */ free(record); record = NULL; /* good hygiene -- but 'alias' is now dangling */ /* printf("%d\n", *alias); <- undefined behavior: use after free */ return 0; }
Setting record = NULL after freeing protects that one variable and does nothing for alias, which still holds the old address. Nothing in the language will tell you; the read simply returns whatever now occupies that memory.
Growing an array means reallocating it
Appending to a Lua table is one line and the table handles its own capacity. This is the C version of the same loop, and it is the clearest illustration on the page of how much a Lua table does for free.
local values = {} for index = 1, 5 do values[#values + 1] = index * 10 -- the table grows itself end print(#values, values[5])
#include <stdio.h> #include <stdlib.h> int main(void) { int capacity = 2; int count = 0; int *values = malloc((size_t)capacity * sizeof *values); if (values == NULL) return 1; for (int index = 0; index < 5; index++) { if (count == capacity) { /* full: grow it yourself */ capacity *= 2; int *grown = realloc(values, (size_t)capacity * sizeof *values); if (grown == NULL) { free(values); return 1; } values = grown; } values[count++] = (index + 1) * 10; } printf("%d\t%d\n", count, values[4]); free(values); return 0; }
Note that realloc's result is assigned to a new variable first. Assigning it straight back to values would leak the original block on failure, because realloc returns NULL without freeing what you gave it.
No collectgarbage to ask
Lua exposes its collector through collectgarbage: you can read the heap size, force a cycle, or switch to generational mode. This matters for game loops, where an unexpected collection is a dropped frame.
local before = collectgarbage("count") local scratch = {} for index = 1, 1000 do scratch[index] = index end scratch = nil collectgarbage("collect") print(collectgarbage("count") <= before + 100)
#include <stdio.h> #include <stdlib.h> int main(void) { /* There is no collector to interrogate, and no pause to tune. Memory is released at exactly the moment you release it. */ void *block = malloc(1024); if (block == NULL) return 1; free(block); printf("%s\n", "released"); return 0; }
C has no collector, so it has no pause and nothing to tune — which is precisely why performance-sensitive parts of a Lua application get rewritten in C. You trade the risk of a collection at a bad moment for the risk of getting the frees wrong. The Lua column is shown rather than run here because Fengari, which executes this page in your browser, does not implement collectgarbage — it reports lua_gc not implemented and leaves collection to the JavaScript engine underneath it.
Control Flow
Conditionals need parentheses, not then
The logic is identical; only the punctuation moves. C requires parentheses around the condition and uses braces instead of then/end, and else if is two words rather than Lua's single elseif.
local score = 72 if score >= 90 then print("A") elseif score >= 70 then print("B") else print("C") end
#include <stdio.h> int main(void) { int score = 72; if (score >= 90) { printf("A\n"); } else if (score >= 70) { printf("B\n"); } else { printf("C\n"); } return 0; }
Braces are optional around a single statement, and omitting them is how the notorious goto fail bug happened. Write them always; every C style guide worth following says the same.
The for loop is three expressions
Lua's numeric for takes a start, a limit and an optional step, and the loop variable is scoped to the loop and cannot be modified usefully. C's for is three arbitrary expressions — initialize, test, advance — and the body may change any of them.
for index = 1, 5 do io.write(index, " ") end print() for index = 10, 1, -3 do io.write(index, " ") end print()
#include <stdio.h> int main(void) { for (int index = 1; index <= 5; index++) { printf("%d ", index); } printf("\n"); for (int index = 10; index >= 1; index -= 3) { printf("%d ", index); } printf("\n"); return 0; }
Because the test is written out, the inclusive/exclusive choice is yours: <= 5 here matches Lua's inclusive limit, while the far more common < count pairs with zero-based indexing.
repeat/until becomes do/while
The while loops match exactly. The bottom-tested loop is the one to watch: Lua's repeat … until stops when its condition becomes true, and C's do … while continues while its condition is true.
local countdown = 3 while countdown > 0 do io.write(countdown, " ") countdown = countdown - 1 end print() local attempts = 0 repeat attempts = attempts + 1 until attempts >= 3 print(attempts)
#include <stdio.h> int main(void) { int countdown = 3; while (countdown > 0) { printf("%d ", countdown); countdown -= 1; } printf("\n"); int attempts = 0; do { attempts += 1; } while (attempts < 3); /* note: while the condition HOLDS */ printf("%d\n", attempts); return 0; }
The conditions are therefore negations of one another — until attempts >= 3 becomes while (attempts < 3). Translating one to the other without flipping the test is an easy and silent mistake.
C has switch and continue; Lua has neither
Two constructs C has and Lua does not. Lua fakes continue with a goto to a label at the end of the body — the idiom in the left column — and dispatches with an if chain because it has no switch.
for index = 1, 5 do if index % 2 == 0 then goto continue end io.write(index, " ") ::continue:: end print() local command = "stop" if command == "go" then print("moving") elseif command == "stop" then print("halted") else print("unknown") end
#include <stdio.h> #include <string.h> int main(void) { for (int index = 1; index <= 5; index++) { if (index % 2 == 0) continue; /* a real keyword */ printf("%d ", index); } printf("\n"); int command = 2; /* switch needs an integer */ switch (command) { case 1: printf("moving\n"); break; case 2: printf("halted\n"); break; default: printf("unknown\n"); break; } return 0; }
C's switch only works on integer types, which is why the string command had to become a number; dispatching on a string still means an strcmp chain. And every case needs its break, or control falls through into the next one.
Functions Without Closures
Parameter and return types are declared
A Lua function accepts anything and returns anything. A C function declares the type of every parameter and of its result, and the compiler checks every call site against that declaration.
local function add(left, right) return left + right end print(add(2, 3))
#include <stdio.h> static int add(int left, int right) { return left + right; } int main(void) { printf("%d\n", add(2, 3)); return 0; }
The static keyword limits the function to this one source file, which is C's nearest equivalent to a local function. Without it the name is visible to every other file in the program and can collide at link time.
One return value, so out-parameters
Returning a value plus an error message is the standard Lua idiom, and it depends on multiple return values. C functions return exactly one value, so the extra results are passed back through pointers the caller supplies.
local function divide(numerator, denominator) if denominator == 0 then return nil, "division by zero" end return numerator / denominator end local result, message = divide(10, 0) print(result, message)
#include <stdio.h> /* The result travels through a pointer; the return value is the status. */ static int divide(int numerator, int denominator, double *result) { if (denominator == 0) return 0; /* 0 means failure here */ *result = (double)numerator / denominator; return 1; } int main(void) { double result = 0.0; if (divide(10, 0, &result)) { printf("%g\n", result); } else { printf("division by zero\n"); } return 0; }
This inverts what the return value is for: in Lua it is the answer, in C it is very often the status, with the answer written through a pointer. Reading result without checking the status first is how uninitialized values spread.
🚨 Function pointers do not capture anything
Closures are central to Lua: iterators, callbacks, module privacy and object state all lean on a function capturing an upvalue. C has function pointers, and they carry no environment whatsoever.
local function make_counter() local count = 0 -- captured by the closure return function() count = count + 1 return count end end local next_value = make_counter() print(next_value(), next_value(), next_value())
#include <stdio.h> /* A function pointer is an address and nothing else -- there is no captured environment, so the state must be passed in explicitly. */ static int next_value(int *count) { *count += 1; return *count; } int main(void) { int count = 0; int first = next_value(&count); int second = next_value(&count); int third = next_value(&count); printf("%d\t%d\t%d\n", first, second, third); return 0; }
This is why C callback APIs almost always take a void *user_data alongside the function pointer — that parameter is the hand-rolled replacement for the captured variable. The three calls are also written as separate statements deliberately: C does not define the order in which a single call's arguments are evaluated, so putting all three inside one printf could legally print them in any order.
Passing a function as a value
Higher-order functions work in C, but the anonymous function does not: there are no function literals, so the callback must be a named top-level function declared somewhere else.
local function apply(numbers, transform) local results = {} for index, value in ipairs(numbers) do results[index] = transform(value) end return results end local doubled = apply({ 1, 2, 3 }, function(value) return value * 2 end) print(table.concat(doubled, ","))
#include <stdio.h> static int double_it(int value) { return value * 2; } static void apply(const int *numbers, int count, int (*transform)(int), int *results) { for (int index = 0; index < count; index++) { results[index] = transform(numbers[index]); } } int main(void) { int numbers[] = { 1, 2, 3 }; int results[3]; apply(numbers, 3, double_it, results); printf("%d,%d,%d\n", results[0], results[1], results[2]); return 0; }
The declaration int (*transform)(int) reads outward from the name: transform is a pointer to a function taking an int and returning an int. Note also that the results array is supplied by the caller, because apply has nowhere to allocate one without deciding who frees it.
Varargs exist but do not know their own count
Lua's ... can be packed into a table and counted with select("#", ...). C's variadic arguments are far weaker: there is no way to ask how many were passed, or what type any of them is.
local function sum(...) local total = 0 for _, value in ipairs({ ... }) do total = total + value end return total, select("#", ...) end print(sum(1, 2, 3))
#include <stdio.h> #include <stdarg.h> /* The count must be passed explicitly -- va_list cannot report it. */ static int sum(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\t%d\n", sum(3, 1, 2, 3), 3); return 0; }
That is why printf needs a format string — it is the only thing telling the function how many arguments to read and how to interpret each one. A mismatched format specifier reads the wrong number of bytes off the stack, which is why compilers special-case printf and warn about it.
No default arguments
Calling a Lua function with too few arguments is legal — the missing ones are nil — which is what makes the or idiom work. C checks the argument count at compile time, so a call with the wrong number does not build.
local function greet(name, greeting) greeting = greeting or "Hello" -- the standard Lua idiom return greeting .. ", " .. name end print(greet("Ada")) print(greet("Ada", "Welcome"))
#include <stdio.h> /* No defaults and no overloading, so the caller passes everything -- or you write a second function with a different name. */ static void greet(const char *name, const char *greeting) { printf("%s, %s\n", greeting, name); } static void greet_default(const char *name) { greet(name, "Hello"); } int main(void) { greet_default("Ada"); greet("Ada", "Welcome"); return 0; }
C also has no function overloading: two functions cannot share a name, which is why the standard library is full of families like strcpy and strncpy rather than one name with optional parameters.
Return Codes Instead of pcall
There is no pcall and no unwinding
Lua's error unwinds the stack until it meets a pcall, so a failure deep in a call chain can be handled far above it. C has no such mechanism: an error must be returned, by hand, through every frame between the failure and whoever cares.
local ok, message = pcall(function() error("something broke", 0) end) print(ok, message) print("execution continues")
#include <stdio.h> /* Nothing to catch: a failing function returns a status and the caller checks it, on every single call. */ static int risky(int input, int *output) { if (input < 0) return -1; /* the error IS the return value */ *output = input * 2; return 0; } int main(void) { int output = 0; int status = risky(-1, &output); printf("%s\t%s\n", status == 0 ? "true" : "false", status == 0 ? "ok" : "something broke"); printf("execution continues\n"); return 0; }
The consequence is that C error handling is opt-in at every level. Ignoring the return value of a function that can fail is legal and silent, which is exactly why so much C code checks nothing and then behaves strangely later.
errno: the out-of-band error channel
Lua reports why something failed by returning a second value. C's standard library reports it through errno, a global the failing call sets — which means it must be read immediately, before any other library call overwrites it.
-- Lua returns the reason as a second value. local function parse(text) local number = tonumber(text) if number == nil then return nil, "not a number: " .. text end return number end local value, reason = parse("abc") print(value, reason)
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> int main(void) { const char *text = "abc"; errno = 0; char *unparsed = NULL; long value = strtol(text, &unparsed, 10); if (unparsed == text || *unparsed != '\0') { printf("nil\tnot a number: %s\n", text); } else { printf("%ld\n", value); } return 0; }
Note that strtol does not use errno to signal "not a number" at all: it reports that through the unparsed pointer, and reserves errno for range errors. Every C function documents its own convention, and there is no rule covering all of them.
assert compiles away in release builds
Both languages have assert, and they are not the same tool. Lua's is an ordinary function that raises a catchable error and is always present, so it is perfectly reasonable in production code.
local function withdraw(balance, amount) assert(amount > 0, "amount must be positive") return balance - amount end print(withdraw(100, 30)) local ok, message = pcall(withdraw, 100, -5) print(ok, message)
#include <stdio.h> #include <assert.h> static int withdraw(int balance, int amount) { assert(amount > 0); /* aborts the process; vanishes under -DNDEBUG */ return balance - amount; } int main(void) { printf("%d\n", withdraw(100, 30)); /* withdraw(100, -5); <- would abort, not raise something catchable */ return 0; }
C's assert is a macro that calls abort() — no catching, no cleanup — and compiling with -DNDEBUG removes it entirely. It is for catching programmer mistakes during development, never for validating input that might legitimately be wrong.
The process exit status
This is one place the two languages agree closely: a program reports success or failure to whatever launched it with a small integer, where zero means success.
-- os.exit takes a status the way C's return from main does. print("about to fail") os.exit(1)
#include <stdio.h> int main(void) { printf("about to fail\n"); return 1; /* the value main returns IS the exit status */ }
In C the value returned from main is that status, so return 0 at the end of every example on this page is not decoration. exit(1) from <stdlib.h> does the same thing from anywhere in the program, matching Lua's os.exit. The Lua column here is shown rather than run: there is no process to leave in the browser, and Fengari's os.exit tears down the interpreter that the rest of the page depends on.
The Preprocessor Has No Lua Equivalent
Constants are text substitution
Lua has no constants at all — a local holding a value you agree not to reassign is the whole mechanism. C has two: a const variable, and #define, which is not a variable but an instruction to a text-substitution pass that runs before the compiler ever sees the file.
local MAX_PLAYERS = 4 -- a normal local, checked like any value print("room for " .. MAX_PLAYERS)
#include <stdio.h> #define MAX_PLAYERS 4 /* the text 4 is pasted in before compiling */ int main(void) { printf("room for %d\n", MAX_PLAYERS); return 0; }
Because it is textual, a macro has no type and no scope, obeys no block rules, and is invisible in a debugger. Modern C style prefers const int or an enum for plain constants and keeps #define for things a variable cannot express.
A macro is not a function
A Lua function evaluates its argument and receives the result. A macro pastes the argument text into the body unchanged, so the surrounding operators can bind differently than you intended.
local function double_it(value) return value * 2 end print(double_it(1 + 2)) -- 6: the argument is evaluated first
#include <stdio.h> #define DOUBLE_BAD(value) value * 2 #define DOUBLE_OK(value) ((value) * 2) int main(void) { printf("%d\n", DOUBLE_BAD(1 + 2)); /* 1 + 2 * 2 = 5, not 6 */ printf("%d\n", DOUBLE_OK(1 + 2)); /* 6 */ return 0; }
Hence the rule that every macro parameter and the whole body get their own parentheses. A second hazard has no fix at all: a macro that uses its argument twice will evaluate it twice, so DOUBLE_OK(count++) increments count two times.
Code can be removed before compiling
A disabled Lua branch is still compiled, still shipped, and still costs a test at run time. #if removes the text before compilation, so the disabled code is not merely skipped — it is not in the binary.
local DEBUG = true if DEBUG then print("debug: starting") -- the branch still exists in the chunk end print("running")
#include <stdio.h> #define DEBUG 1 int main(void) { #if DEBUG printf("debug: starting\n"); /* deleted entirely when DEBUG is 0 */ #endif printf("running\n"); return 0; }
This is how one C source file supports several platforms, and it is why C code often needs to be read twice: what the compiler sees depends on which macros were defined, so the file on screen may not be the program that was built.
Headers & Linking vs. require
#include is a paste, not a load
These look equivalent and are not. require executes a module and gives you its return value, once, at run time. #include copies a file's text into yours before compiling, every time, and produces no value.
-- require finds the module, runs it once, caches the result, -- and hands back whatever it returned. local formatter = require("string") print(formatter.format("%d items", 3))
#include <stdio.h> /* pastes the text of stdio.h in, right here */ int main(void) { printf("%d items\n", 3); return 0; }
A header carries only declarations — promises that a function exists somewhere with a given signature. The actual machine code for printf is not in stdio.h; it is in the C library, and the linker connects the two after compilation.
Declaration and definition are separate
A Lua module is one file that defines things and returns a table. C splits every public function in two: a declaration in a header that callers include, and a definition in a source file that gets compiled separately.
-- One file is the whole module. It defines its functions and -- returns a table; nothing is declared twice. local geometry = {} function geometry.area(width, height) return width * height end print(geometry.area(3, 4))
#include <stdio.h> /* In a real project the declaration would live in geometry.h and the definition in geometry.c, so every caller can be compiled against the promise before the implementation is even written. */ int area(int width, int height); /* declaration */ int area(int width, int height) { /* definition */ return width * height; } int main(void) { printf("%d\n", area(3, 4)); return 0; }
Keeping the two in agreement is the programmer's job, and a header that has drifted from its source is a familiar C bug. Nothing like it can happen in Lua, where the function and its name are the same object.
No package.path, no module cache
Lua resolves a module when require runs, using package.path, and remembers the result in package.loaded. Both are ordinary tables you can inspect and modify while the program runs.
-- Lua searches package.path at run time and caches by module name. print(type(package.path)) print(package.loaded["string"] ~= nil) -- already loaded and cached
#include <stdio.h> int main(void) { /* Header search paths (-I) and libraries (-l) are compiler flags, fixed at build time. There is no run-time search and no cache to inspect, because nothing is loaded at run time at all. */ printf("%s\n", "resolved at build time"); return 0; }
C resolves everything before the program starts: -I tells the compiler where headers are and -l tells the linker which libraries to bind in. There is no equivalent of adding a search path at run time short of dlopen, which is a separate mechanism entirely.
Writing C That Lua Can Call
What a C function looks like from the inside
This section closes the loop. Most of the Lua standard library is C, and this is what one of those functions actually looks like — the same call you already make, seen from the other side of the boundary.
-- You have called functions like this a thousand times. -- string.rep is written in C; this is the Lua you type. print(("ab"):rep(3))
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> /* Every C function callable from Lua has this one signature: it takes the state, and returns how many results it left on the stack. */ static int repeat_text(lua_State *state) { const char *text = luaL_checkstring(state, 1); lua_Integer times = luaL_checkinteger(state, 2); luaL_Buffer buffer; luaL_buffinit(state, &buffer); for (lua_Integer index = 0; index < times; index++) { luaL_addstring(&buffer, text); } luaL_pushresult(&buffer); return 1; /* one result */ } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, repeat_text); lua_setglobal(state, "repeat_text"); luaL_dostring(state, "print(repeat_text('ab', 3))"); lua_close(state); return 0; }
Note the signature: every C function Lua can call takes a lua_State * and returns an int. That integer is not a status — it is the number of return values the function left behind, which is how C manages to return the multiple values it otherwise cannot.
Arguments arrive on a stack, by position
There is no parameter list. Arguments are pushed onto a per-call stack before your function runs, and you read them by index — and pleasantly for a Lua programmer, that index starts at 1.
local function describe(...) print(select("#", ...), (select(1, ...))) end describe("a", "b", "c")
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int describe(lua_State *state) { int count = lua_gettop(state); /* how many arguments */ const char *first = luaL_checkstring(state, 1); /* index 1, like Lua */ printf("%d\t%s\n", count, first); return 0; /* no results */ } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, describe); lua_setglobal(state, "describe"); luaL_dostring(state, "describe('a', 'b', 'c')"); lua_close(state); return 0; }
lua_gettop returns how many arguments were passed, which is C's answer to select("#", ...). Negative indices count from the top, so -1 is the last argument — a convention with no parallel in either language's ordinary code.
luaL_check* raises a proper Lua error
Validating arguments by hand is normal in Lua. On the C side the luaL_check* family does it for you, and crucially it raises a real Lua error — catchable by pcall on the Lua side, with the standard "bad argument #1" wording.
local function set_volume(level) if type(level) ~= "number" then error("bad argument #1 to 'set_volume' (number expected)", 0) end print("volume " .. level) end set_volume(7) print(pcall(set_volume, "loud"))
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int set_volume(lua_State *state) { lua_Integer level = luaL_checkinteger(state, 1); /* raises if wrong */ printf("volume %lld\n", (long long)level); return 0; } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, set_volume); lua_setglobal(state, "set_volume"); luaL_dostring(state, "set_volume(7) " "print(pcall(set_volume, 'loud'))"); lua_close(state); return 0; }
These functions do not return on failure: they perform a long jump out of your C function. Anything you allocated with malloc beforehand is therefore leaked, which is why argument checking belongs at the very top of the function, before any resource is acquired.
Returning several values from C
Earlier on this page a plain C function could return only one value. Through the Lua API that limit disappears: you push as many results as you like and return the count.
local function bounds(numbers) local smallest, largest = numbers[1], numbers[1] for _, value in ipairs(numbers) do if value < smallest then smallest = value end if value > largest then largest = value end end return smallest, largest end print(bounds({ 4, 1, 9 }))
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int bounds(lua_State *state) { lua_Integer smallest = 0, largest = 0; lua_Integer length = luaL_len(state, 1); for (lua_Integer index = 1; index <= length; index++) { lua_geti(state, 1, index); lua_Integer value = lua_tointeger(state, -1); lua_pop(state, 1); if (index == 1 || value < smallest) smallest = value; if (index == 1 || value > largest) largest = value; } lua_pushinteger(state, smallest); lua_pushinteger(state, largest); return 2; /* two results */ } int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); lua_pushcfunction(state, bounds); lua_setglobal(state, "bounds"); luaL_dostring(state, "print(bounds({ 4, 1, 9 }))"); lua_close(state); return 0; }
The loop also shows the stack discipline the API demands. lua_geti pushes the element, lua_tointeger reads it at index -1, and lua_pop removes it — forget the pop and the stack grows by one on every iteration.
Building the table require returns
A Lua module is a table of functions that the file returns. A C module builds the same table from an array of name/function pairs, and this is exactly what require receives when it loads a compiled .so.
-- A Lua module returns a table of functions. local geometry = {} function geometry.area(width, height) return width * height end function geometry.perimeter(width, height) return 2 * (width + height) end -- return geometry <- a real module file would end with this print(geometry.area(3, 4), geometry.perimeter(3, 4))
#include <stdio.h> #include <lua.h> #include <lauxlib.h> #include <lualib.h> static int area(lua_State *state) { lua_pushinteger(state, luaL_checkinteger(state, 1) * luaL_checkinteger(state, 2)); return 1; } static int perimeter(lua_State *state) { lua_pushinteger(state, 2 * (luaL_checkinteger(state, 1) + luaL_checkinteger(state, 2))); return 1; } /* The array Lua uses to build your module's table. */ static const luaL_Reg geometry_functions[] = { { "area", area }, { "perimeter", perimeter }, { NULL, NULL } }; int main(void) { lua_State *state = luaL_newstate(); luaL_openlibs(state); luaL_newlib(state, geometry_functions); lua_setglobal(state, "geometry"); luaL_dostring(state, "print(geometry.area(3, 4), geometry.perimeter(3, 4))"); lua_close(state); return 0; }
The { NULL, NULL } at the end is the terminator — the array does not know its own length, the same limitation the arrays section described, showing up in a real API. In a shared library the entry point would be luaopen_geometry returning 1, and require("geometry") would find it by name.
Calling back into Lua
Traffic crosses the boundary both ways. Pushing a function and its arguments and then calling lua_pcall is how a C host runs a callback your Lua code registered — the mechanism behind every "on_update" hook you have ever written.
-- In Lua, calling a function you were handed needs no ceremony. local function apply_twice(transform, value) return transform(transform(value)) end print(apply_twice(function(number) return number + 10 end, 1))
#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 add_ten(number) return number + 10 end"); lua_getglobal(state, "add_ten"); /* push the function */ lua_pushinteger(state, 1); /* push its argument */ /* 1 argument, 1 result, no message handler */ if (lua_pcall(state, 1, 1, 0) != LUA_OK) { printf("error: %s\n", lua_tostring(state, -1)); lua_close(state); return 1; } printf("%lld\n", (long long)lua_tointeger(state, -1)); lua_close(state); return 0; }
It is lua_pcall rather than lua_call for the reason the error-handling section gave: an uncaught Lua error long-jumps, and if it crosses a plain lua_call in C it will unwind past your cleanup code. lua_pcall returns a status instead, which is C's pcall.
Gotchas for Lua Developers
= inside a condition is legal
Lua makes this impossible by grammar: assignment is a statement, not an expression, so a stray single = in a condition will not parse. In C assignment is an expression with a value, so the same typo compiles and behaves.
local value = 0 -- if value = 1 then <- a syntax error in Lua; assignment is -- a statement and cannot appear here. if value == 1 then print("one") else print("not one") end
#include <stdio.h> int main(void) { int value = 0; if (value = 1) { /* assigns 1, then tests it: always true */ printf("this always runs\n"); } printf("%d\n", value); /* value has been changed to 1 */ return 0; }
The condition becomes the assigned value, so if (value = 1) is always true and if (value = 0) is always false — and the variable is quietly modified either way. Compilers warn about it with -Wall, which is a strong argument for always building with warnings on.
Averages come out wrong
This is the integer-division rule from the numbers section, in the form it actually bites. Declaring the result double does not help, because the division has already happened in integer arithmetic before the assignment converts anything.
local scores = { 7, 8 } local average = (scores[1] + scores[2]) / 2 print(average) -- 7.5
#include <stdio.h> int main(void) { int scores[] = { 7, 8 }; double wrong = (scores[0] + scores[1]) / 2; /* 7.0 */ double right = (scores[0] + scores[1]) / 2.0; /* 7.5 */ printf("%g\t%g\n", wrong, right); return 0; }
The fix is to make one operand a floating-point value — 2.0 here — so the division itself is done in floating point. Coming from Lua, where / always produces a float, this is the easiest wrong answer on the whole page to walk past.
sizeof measures the pointer, not the data
The sizeof idiom from the arrays section is correct only in the scope where the array was declared. Passed to a function, the array has decayed to a pointer, and the same expression silently measures the wrong thing.
local function count_items(values) return #values -- works wherever the table came from end print(count_items({ 1, 2, 3, 4 }))
#include <stdio.h> static void report(int *values) { /* sizeof values is the size of a POINTER here, not the array. */ printf("%zu\n", sizeof values / sizeof values[0]); /* 2 on a 64-bit machine */ } int main(void) { int values[4] = { 1, 2, 3, 4 }; printf("%zu\n", sizeof values / sizeof values[0]); /* 4: correct here */ report(values); return 0; }
On a 64-bit machine it yields 2 — an 8-byte pointer divided by a 4-byte int — which is a plausible-looking number, not an obvious error. This is why the count is a parameter in every well-written C function that takes an array.
A wrong printf format is not caught at run time
Lua's string.format checks each argument against its specifier and raises an error if they disagree. C's printf cannot: as the varargs row explained, it has no way to know what it was actually handed.
print(string.format("%d", 42)) -- string.format("%d", "abc") <- raises a clear Lua error local ok, message = pcall(string.format, "%d", {}) print(ok)
#include <stdio.h> int main(void) { double value = 3.5; printf("%d\n", 42); /* correct */ /* printf("%d\n", value); <- reads a double's bytes as an int: undefined behavior, garbage output, no error anywhere. */ printf("%g\n", value); /* correct */ return 0; }
The format string is the only description of the arguments, so a mismatch reads the wrong number of bytes and everything after it is garbage too. Compilers do check literal format strings with -Wformat (included in -Wall), which is the only reliable defense.