Lua C API 完全指南 - C/C++ 与 Lua 交互详解

深入解析 Lua C API,涵盖栈操作、数据传递、函数调用等核心机制,是理解 ToLua 框架底层原理的基础。


一、Lua C API 概述

1.1 核心概念

概念 说明
lua_State Lua 虚拟机实例,维护独立的执行环境
Lua 栈 C 与 Lua 交换数据的中介
索引方式 正索引(从底到顶)、负索引(从顶到底)
伪索引 访问注册表、全局表等特殊位置

1.2 栈结构示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────────────────────────┐
│ Lua 虚拟机栈结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ lua_State 栈: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 索引方式: │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ 1 │ 2 │ 3 │ 4 │ ... │ -3 │ -2 │ -1 │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │ │
│ │ │ │ │ │
│ │ └── 正索引 (从底到顶) 负索引 (从顶到底) ──┘ │ │
│ │ │ │
│ │ 栈顶 (-1) 是最新压入的元素 │ │
│ │ 栈底 (1) 是最早的元素 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 伪索引 (特殊用途): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ LUA_REGISTRYINDEX → 注册表 (C 代码使用) │ │
│ │ LUA_GLOBALSINDEX → 全局表 _G (Lua 代码使用) │ │
│ │ LUA_ENVIRONINDEX → 环境表 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

二、压栈操作 (Push Operations)

2.1 压入基础类型

API 功能 栈变化
lua_pushnil 压入 nil 栈顶 +1
lua_pushboolean 压入布尔值 栈顶 +1
lua_pushnumber 压入数字 栈顶 +1
lua_pushinteger 压入整数 栈顶 +1

2.2 压入字符串

API 功能 说明
lua_pushstring 压入 C 字符串 (以 \0 结尾) 自动复制
lua_pushlstring 压入带长度的字符串 可包含 \0
lua_pushfstring 格式化字符串压栈 类似 printf
lua_pushvfstring vlist 版本格式化字符串 变参列表
lua_pushliteral 压入字符串字面量 优化版本

2.3 压入特殊对象

API 功能 栈变化
lua_pushcfunction 压入 C 函数 栈顶 +1
lua_pushcclosure 压入 C 闭包 支持上值
lua_pushlightuserdata 压入轻量用户数据 指针传递
lua_pushthread 压入线程对象 协程支持
lua_pushvalue 复制指定位置到栈顶 栈顶 +1

2.4 压栈代码示例

1
2
3
4
5
6
7
8
9
10
11
// 压入各种类型到 Lua 栈
void push_values_example(lua_State *L) {
lua_pushnil(L); // 栈: [nil]
lua_pushboolean(L, 1); // 栈: [nil, true]
lua_pushinteger(L, 42); // 栈: [nil, true, 42]
lua_pushnumber(L, 3.14); // 栈: [nil, true, 42, 3.14]
lua_pushstring(L, "hello"); // 栈: [nil, true, 42, 3.14, "hello"]
lua_pushvalue(L, -5); // 栈: [nil, true, 42, 3.14, "hello", nil]

// 此时栈顶 (-1) 是 nil,-5 位置也是 nil
}

三、类型检查 (Type Checking)

3.1 类型判断函数

API 检查类型 返回值
lua_type 获取类型枚举 LUA_TNIL, LUA_TBOOLEAN, 等
lua_typename 类型名称字符串 “nil”, “boolean”, “number”, 等
lua_isboolean 是否为布尔 int (0/1)
lua_iscfunction 是否为 C 函数 int (0/1)
lua_isfunction 是否为函数 (含 Lua 函数) int (0/1)
lua_islightuserdata 是否为轻量用户数据 int (0/1)
lua_isnil 是否为 nil int (0/1)
lua_isnone 索引是否无效 int (0/1)
lua_isnoneornil 无效或 nil int (0/1)
lua_isnumber 是否为数字 int (0/1)
lua_isstring 是否为字符串 int (0/1)
lua_istable 是否为表 int (0/1)
lua_isthread 是否为线程 int (0/1)
lua_isuserdata 是否为用户数据 int (0/1)

3.2 Lua 类型常量

1
2
3
4
5
6
7
8
9
10
11
// Lua 8 种基础类型
#define LUA_TNONE (-1)
#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8

3.3 类型检查示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void check_types_example(lua_State *L) {
// 检查栈顶元素的类型
int type = lua_type(L, -1);

switch (type) {
case LUA_TNIL:
printf("栈顶是 nil\n");
break;
case LUA_TBOOLEAN:
printf("栈顶是布尔值: %d\n", lua_toboolean(L, -1));
break;
case LUA_TNUMBER:
printf("栈顶是数字: %f\n", lua_tonumber(L, -1));
break;
case LUA_TSTRING:
printf("栈顶是字符串: %s\n", lua_tostring(L, -1));
break;
case LUA_TTABLE:
printf("栈顶是表\n");
break;
case LUA_TFUNCTION:
printf("栈顶是函数\n");
break;
case LUA_TUSERDATA:
printf("栈顶是用户数据\n");
break;
case LUA_TTHREAD:
printf("栈顶是线程\n");
break;
}

// 使用便捷检查函数
if (lua_isnumber(L, 2)) {
printf("索引 2 是数字\n");
}

if (lua_isstring(L, 3)) {
printf("索引 3 是字符串(或可转为字符串)\n");
}
}

四、取值操作 (To Operations)

4.1 获取栈元素

API 返回类型 功能
lua_toboolean int 转为布尔值
lua_tocfunction lua_CFunction 获取 C 函数指针
lua_tointeger lua_Integer 获取整数值
lua_tonumber lua_Number 获取浮点数值
lua_tolstring const char* 获取字符串(可含 \0)
lua_tostring const char* 获取字符串
lua_topointer const void* 获取指针(用于相等比较)
lua_tothread lua_State* 获取线程对象
lua_touserdata void* 获取用户数据

4.2 取值注意事项

⚠️ 重要:所有 lua_to* 函数签名:

1
type lua_toxxx(lua_State *L, int index);
  • L: 当前 Lua 状态机
  • index: 栈上的位置(正索引、负索引、伪索引)

4.3 取值示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void get_values_example(lua_State *L) {
// 假设栈上有: [nil, true, 42, 3.14, "hello"]

// 获取布尔值 (索引 -4)
int b = lua_toboolean(L, -4); // b = 1

// 获取整数值 (索引 -3)
lua_Integer i = lua_tointeger(L, -3); // i = 42

// 获取浮点数值 (索引 -2)
lua_Number n = lua_tonumber(L, -2); // n = 3.14

// 获取字符串 (索引 -1)
const char *s = lua_tostring(L, -1); // s = "hello"
size_t len;
const char *s2 = lua_tolstring(L, -1, &len); // s2 = "hello", len = 5

// 获取 C 函数
if (lua_iscfunction(L, -5)) {
lua_CFunction f = lua_tocfunction(L, -5);
}

// 获取用户数据
if (lua_isuserdata(L, some_index)) {
void *ud = lua_touserdata(L, some_index);
}
}

五、栈操作 (Stack Manipulation)

5.1 栈管理函数

API 功能 栈变化
lua_gettop 获取栈顶位置(即栈大小) 无变化
lua_settop 设置栈顶位置 可增可减
lua_remove 删除指定位置元素 栈大小 -1
lua_insert 将栈顶插入指定位置 栈大小不变
lua_replace 用栈顶替换指定位置 栈大小 -1
lua_pop 弹出 n 个元素 宏: lua_settop(L, -n-1)

5.2 栈操作详解

lua_settop

1
void lua_settop(lua_State *L, int index);
index 参数 效果
正数 设置栈顶为该位置,删除上方所有元素
大于当前栈大小 用 nil 填充到新位置
0 清空整个栈
负数 相当于 lua_gettop() + index + 1
1
2
3
4
// lua_settop 示例
lua_settop(L, 5); // 设置栈顶为 5,删除 6 及以上的元素
lua_settop(L, 0); // 清空栈
lua_settop(L, -3); // 只保留栈底 3 个元素

lua_remove

1
void lua_remove(lua_State *L, int index);
  • 删除指定有效索引处的元素
  • 上方元素下移填充空缺
  • 不能使用伪索引
1
2
3
4
// lua_remove 示例
// 栈: [1, 2, 3, 4, 5]
lua_remove(L, 2); // 栈: [1, 3, 4, 5]
lua_remove(L, -1); // 栈: [1, 3, 4]

lua_insert

1
void lua_insert(lua_State *L, int index);
  • 将栈顶元素移动到指定位置
  • 指定位置到栈顶的元素上移
  • 不能使用伪索引
1
2
3
// lua_insert 示例
// 栈: [1, 2, 3, 4, 5]
lua_insert(L, 2); // 栈: [1, 5, 2, 3, 4]

lua_replace

1
void lua_replace(lua_State *L, int index);
  • 用栈顶元素替换指定位置的值
  • 弹出栈顶(栈大小 -1)
  • 其他元素位置不变
1
2
3
// lua_replace 示例
// 栈: [1, 2, 3, 4, 5]
lua_replace(L, 2); // 栈: [1, 5, 3, 4]

5.3 栈操作宏

1
2
3
4
5
// 常用宏定义
#define lua_pop(L,n) lua_settop(L, -(n)-1)
#define lua_newtable(L) lua_createtable(L, 0, 0)
#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0)

六、函数调用 (Function Call)

6.1 lua_call 详解

1
void lua_call(lua_State *L, int nargs, int nresults);

调用协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────────────┐
│ lua_call 调用流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 调用前: │
│ ┌─────┬─────┬─────┬─────┬─────────────────────────────────────┐ │
│ │ ... │ arg1 │ arg2 │ ... │ 函数对象 │ │
│ └─────┴─────┴─────┴─────┴─────────────────────────────────────┘ │
│ ▲ │
│ │ 栈底方向 │
│ │
│ 调用 lua_call(L, nargs, nresults): │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. 弹出函数和所有参数 │ │
│ │ 2. 执行函数 │ │
│ │ 3. 将结果压入栈(按直接顺序,第一个结果先入) │ │
│ │ 4. 调整结果数量为 nresults │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 调用后 (nresults = 2): │
│ ┌─────┬─────┬─────────────────────────────────────────────────┐ │
│ │ ... │res1 │ res2 │ │
│ └─────┴─────┴─────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 栈底方向 │
│ │
└─────────────────────────────────────────────────────────────────────────┘

6.2 函数调用参数

参数 说明 特殊值
nargs 参数个数 必须匹配实际压入的参数数
nresults 期望结果数 LUA_MULTRET 表示接受所有返回值

6.3 函数调用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 调用 Lua 函数示例
void call_lua_function(lua_State *L) {
// 假设 Lua 中定义: function add(a, b) return a + b, a - b end

// 1. 获取函数并压栈
lua_getglobal(L, "add"); // 栈: [add]

// 2. 压入参数
lua_pushinteger(L, 10); // 栈: [add, 10]
lua_pushinteger(L, 5); // 栈: [add, 10, 5]

// 3. 调用函数 (2个参数, 2个返回值)
lua_call(L, 2, 2); // 栈: [15, 5]

// 4. 获取结果
lua_Integer sum = lua_tointeger(L, -2); // 15
lua_Integer diff = lua_tointeger(L, -1); // 5

// 5. 清理结果
lua_pop(L, 2); // 栈: []
}

七、全局表与注册表

7.1 表结构对比

特性 全局表 _G 注册表
用途 Lua 代码全局变量存储 C 代码私有数据存储
访问方式 LUA_GLOBALSINDEX LUA_REGISTRYINDEX
可见性 Lua 代码可见 仅 C 代码可见
生命周期 与 lua_State 相同 与 lua_State 相同

7.2 注册表使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 注册表存储 C 对象示例
void registry_example(lua_State *L) {
// 创建一个 Lua 表作为命名空间
lua_newtable(L); // 栈: [table]

// 存储 C 指针到注册表
lua_pushlightuserdata(L, (void*)my_object); // 栈: [table, pointer]
lua_setfield(L, -2, "my_object"); // 栈: [table]

// 将表存入注册表
lua_setfield(L, LUA_REGISTRYINDEX, "MyNamespace"); // 栈: []

// 从注册表读取
lua_getfield(L, LUA_REGISTRYINDEX, "MyNamespace"); // 栈: [table]
lua_getfield(L, -1, "my_object"); // 栈: [table, pointer]
void *obj = lua_touserdata(L, -1); // 获取指针
lua_pop(L, 2); // 清理
}

八、完整示例

8.1 C 调用 Lua 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main() {
// 1. 创建 Lua 状态
lua_State *L = luaL_newstate();

// 2. 加载标准库
luaL_openlibs(L);

// 3. 执行 Lua 代码
const char *lua_code = R"(
function greet(name)
return "Hello, " .. name .. "!"
end

function sum(a, b)
return a + b
end
)";

if (luaL_dostring(L, lua_code) != LUA_OK) {
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
return 1;
}

// 4. 调用 greet 函数
lua_getglobal(L, "greet"); // 压入函数
lua_pushstring(L, "World"); // 压入参数
lua_call(L, 1, 1); // 调用 (1参数, 1返回值)

const char *result = lua_tostring(L, -1);
printf("%s\n", result); // 输出: Hello, World!
lua_pop(L, 1); // 清理结果

// 5. 调用 sum 函数
lua_getglobal(L, "sum");
lua_pushinteger(L, 10);
lua_pushinteger(L, 20);
lua_call(L, 2, 1);

int sum_result = lua_tointeger(L, -1);
printf("Sum: %d\n", sum_result); // 输出: Sum: 30
lua_pop(L, 1);

// 6. 关闭 Lua 状态
lua_close(L);

return 0;
}

8.2 Lua 调用 C 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

// C 函数:计算两个数的平均值
static int average(lua_State *L) {
// 检查参数数量
int n = lua_gettop(L); // 获取参数个数
if (n != 2) {
lua_pushstring(L, "错误: 需要 2 个参数");
lua_error(L); // 抛出错误
return 0;
}

// 检查参数类型
if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2)) {
lua_pushstring(L, "错误: 参数必须是数字");
lua_error(L);
return 0;
}

// 获取参数
double a = lua_tonumber(L, 1);
double b = lua_tonumber(L, 2);

// 计算并压入结果
lua_pushnumber(L, (a + b) / 2.0);

return 1; // 返回值个数
}

// C 函数:返回多个值
static int divide_mod(lua_State *L) {
int a = lua_tointeger(L, 1);
int b = lua_tointeger(L, 2);

if (b == 0) {
lua_pushstring(L, "除数不能为 0");
lua_error(L);
return 0;
}

lua_pushinteger(L, a / b); // 商
lua_pushinteger(L, a % b); // 余数

return 2; // 返回 2 个值
}

int main() {
lua_State *L = luaL_newstate();
luaL_openlibs(L);

// 注册 C 函数到 Lua
lua_register(L, "average", average);
lua_register(L, "divide_mod", divide_mod);

// 执行 Lua 代码调用 C 函数
const char *lua_code = R"(
print("平均数:", average(10, 20))

local q, r = divide_mod(17, 5)
print("17 / 5 =", q, ", 余数 =", r)
)";

if (luaL_dostring(L, lua_code) != LUA_OK) {
fprintf(stderr, "Error: %s\n", lua_tostring(L, -1));
}

lua_close(L);
return 0;
}

九、API 速查表

9.1 栈操作速查

操作 API 备注
获取栈大小 lua_gettop 返回栈顶索引
设置栈大小 lua_settop 可填充 nil 或删除
弹出元素 lua_pop 宏: lua_settop(L, -n-1)
删除元素 lua_remove 上方元素下移
插入元素 lua_insert 栈顶插入指定位置
替换元素 lua_replace 栈顶替换,弹出栈顶

9.2 压栈速查

类型 API
nil lua_pushnil
布尔 lua_pushboolean
数字 lua_pushnumber, lua_pushinteger
字符串 lua_pushstring, lua_pushlstring, lua_pushfstring
C 函数 lua_pushcfunction, lua_pushcclosure
lua_newtable, lua_createtable
用户数据 lua_newuserdata, lua_pushlightuserdata

9.3 检查速查

类型 检查函数 获取函数
nil lua_isnil -
布尔 lua_isboolean lua_toboolean
数字 lua_isnumber lua_tonumber, lua_tointeger
字符串 lua_isstring lua_tostring, lua_tolstring
lua_istable -
函数 lua_isfunction, lua_iscfunction lua_tocfunction
用户数据 lua_isuserdata, lua_islightuserdata lua_touserdata
线程 lua_isthread lua_tothread
任意类型 lua_type -

十、总结

主题 要点
栈机制 C 与 Lua 交换数据的唯一中介
索引方式 正索引从底到顶,负索引从顶到底
压入数据 lua_push* 系列,栈顶 +1
获取数据 lua_to* 系列,不改变栈
类型检查 lua_is*lua_type
函数调用 lua_call 需按协议压入函数和参数
注册表 C 代码私有存储,与 _G 隔离

💡 学习建议

  • 熟记栈的索引方式(正/负索引)
  • 注意每次操作后栈的状态变化
  • 使用 lua_gettop() 检查栈大小进行调试
  • 注意 lua_to* 不改变栈,lua_push* 增加栈
  • 注册表用于存储 C 代码的私有数据

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com