Nanfeng

Notes on software development, code, and curious ideas

Writing Better Lua C Functions: Arrays, Strings, and State

Lua arrays are tables whose keys happen to be consecutive integers. The C API provides lua_geti and lua_seti for this common case:

1
2
int type = lua_geti(L, table_index, key); /* pushes table[key] */
lua_seti(L, table_index, key); /* pops value into table[key] */

Before Lua 5.3, use lua_rawgeti and lua_rawseti. Their raw access bypasses metamethods.

Mapping an array

The function below receives a table and a Lua function, applies the function to each array element, and stores the result back in place:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int l_map(lua_State *L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
lua_Integer n = luaL_len(L, 1);

for (lua_Integer i = 1; i <= n; i++) {
lua_pushvalue(L, 2);
lua_geti(L, 1, i);
lua_call(L, 1, 1);
lua_seti(L, 1, i);
}
return 0;
}

In production code, consider lua_pcall when an error should be handled rather than propagated through the C stack.

Working with strings

Lua strings may contain zero bytes, so use length-aware functions such as luaL_checklstring instead of assuming C-style termination. To build a small result, push the fragments and call lua_concat. For larger or incremental output, use luaL_Buffer:

1
2
3
4
5
6
7
8
9
10
11
12
static int l_upper_ascii(lua_State *L) {
size_t len;
const char *s = luaL_checklstring(L, 1, &len);
luaL_Buffer b;
char *out = luaL_buffinitsize(L, &b, len);

for (size_t i = 0; i < len; i++)
out[i] = (s[i] >= 'a' && s[i] <= 'z') ? s[i] - 32 : s[i];

luaL_pushresultsize(&b, len);
return 1;
}

This example intentionally handles ASCII only; Unicode case conversion requires a Unicode-aware library.

Preserving C-side state

The registry is a Lua table reserved for C code. Store values under a unique light-userdata key when several functions need shared state. luaL_ref is useful when an integer reference is more convenient, and luaL_unref releases it.

For state owned by one exported function, a C closure is usually cleaner:

1
2
3
4
5
6
7
8
9
10
11
12
static int counter(lua_State *L) {
int value = (int)lua_tointeger(L, lua_upvalueindex(1));
lua_pushinteger(L, ++value);
lua_replace(L, lua_upvalueindex(1));
lua_pushinteger(L, value);
return 1;
}

static void push_counter(lua_State *L) {
lua_pushinteger(L, 0);
lua_pushcclosure(L, counter, 1);
}

Each closure receives its own upvalue. Push the same table as an upvalue for multiple closures when they deliberately need shared mutable state.

The key discipline in every C function is stack accounting: document what each operation pushes or pops, validate arguments with luaL_check*, and return the exact number of results left on the stack.

+