在 iOS 项目里接入 Lua 时,最常见的工作并不是“能不能调通”,而是原生方法、参数和复杂数据到底该怎么安全地传过去。下面围绕方法传递、返回值处理,以及数组和字典映射为 Lua Table 的做法展开,重点把几个容易踩坑的位置说清楚:函数签名必须匹配、返回值数量要和入栈一致、Table 写入时还要注意 Lua 的索引规则。
原生方法怎么暴露给 Lua
Lua 只能接收 C 定义的方法,而且方法声明必须符合 lua_CFunction 函数指针的定义:
int functionName (lua_State *state);
也就是说,想把一个 iOS 侧能力交给 Lua 调用,首先要提供一个符合这个签名的函数。例如下面这个最简单的例子,只做日志输出:
int printHelloWorld (lua_State *state)
{
NSLog(@"Hello World!");
return 0;
}
这里的返回值 0 不是“执行成功”的意思,而是表示这个函数没有向 Lua 返回任何结果。定义好函数后,再通过 lua_pushcfunction 把它压入 Lua 栈,并用全局名暴露出去:
lua_pushcfunction(self.state, printHelloWorld);
lua_setglobal(self.state, "funcVal");
完成后,Lua 侧就可以直接调用:
funcVal();
这一段的关键在于两步:先把原生函数压栈,再用 lua_setglobal 绑定名字。Lua 侧看到的其实不是 Objective-C 方法本身,而是一个符合 Lua C API 规范的可调用入口。
参数怎么取,返回值怎么回传
如果原生函数需要接收 Lua 传入的参数,可以从 state 中读取。通常先用 lua_gettop(state) 判断栈里传了几个参数,再按位置取值。下面的例子接收一个字符串参数:

int printHelloWorld (lua_State *state)
{
if (lua_gettop(state) > 0)
{
//表示有参数
const char *name = lua_tostring(state, 1);
NSLog(@"Hello %s!", name);
}
return 0;
}
对应的 Lua 调用方式如下:
funcVal ("vimfung");
这里有两个细节很重要。第一,Lua 传入的参数也是按栈位置读取的,示例中的 1 表示第一个参数。第二,参数是否存在最好先判断,否则直接取值会让函数行为变得不可控。
如果不只是打印字符串,而是希望把拼接后的结果返回给 Lua,就要使用 lua_pushXXXX 系列方法把返回值压回栈中。比如:
int printHelloWorld (lua_State *state)
{
if (lua_gettop(state) > 0)
{
//表示有参数
const char *name = lua_tostring(state, 1);
//入栈返回值
NSString *retVal = [NSString stringWithFormat:@"Hello %s!", name];
lua_pushstring(state, retVal.UTF8String);
return 1;
}
return 0;
}
然后在 Lua 侧接收返回值:
local retVal = funcVal("vimfung");
print(retVal);
这里最容易出错的地方是:函数最后返回的数量,必须和你实际压入栈中的返回值数量一致。 上面的代码压入了一个字符串,所以返回 1;如果没有压入值,就应该返回 0。这条规则一旦不匹配,就可能出现异常行为。
Lua 里的数组和字典,本质上都是 Table
在 Lua 中,数组(Array)和字典(Dictionary)都由 Table 表示。换句话说,数组并不是另一种独立类型,它只是 key 为整数且通常按顺序排列的 Table。
例如:
-- 定义数组
local arrayVal = {1,2,3,4,5,6};
-- 定义字典
local dictVal = {a=1, b=3, c=4, d=5};
上面分别演示了两种写法:一种不显式写 key,另一种显式指定 key。对于前者,Lua 会自动补出从 1 开始递增的整数 key,因此它等效于:
local arrayVal = {1=1, 2=2, 3=3, 4=4, 5=5, 6=6};
原文这里想表达的是“数组形式的 Table 会被解释为从 1 开始编号的整数索引结构”,虽然示意写法主要用于说明语义,但读者真正需要记住的是:Lua 的数组下标从 1 开始,而不是很多原生语言里常见的 0。
另外,这两种写法也可以混合使用:
local tbl = {1, 2, a=1, b=2, 3};
正因为 Table 同时承担数组和字典两类角色,所以在 C API 里对它的操作会相对复杂一些。下面分成数组传递和字典传递两部分来看。
iOS 数组如何传给 Lua
要把原生数组传给 Lua,第一步不是“设置元素”,而是先把一个 Table 放到栈顶。Lua 没有直接的 pushtable 方法,但可以用 lua_newtable 创建一个新的 Table,并自动压栈:

lua_newtable(self.state);
接着遍历数组,把每个元素压栈,再通过 lua_rawseti 按整数索引写入当前 Table:
NSArray *array = @[@1, @2, @3, @4, @5, @6];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSInteger value = [obj integerValue];
lua_pushinteger(self.state, value);
lua_rawseti(self.state, -2, idx + 1);
}];
lua_getglobal(self.state, "arrayVal");
这段代码的核心点在于 lua_rawseti 的三个参数含义:
lua_rawseti 的关键参数怎么看
lua_rawseti 用于把栈顶元素设置到指定 Table 的某个整数索引位置。

- 第二个参数表示目标 Table 在栈中的位置,示例里的
-2指向“栈顶元素下面那个对象”,也就是刚创建出来的 Table。 - 第三个参数表示要写入的索引位置。
- 因为 Lua 的数组索引通常从
1开始,所以示例里用了idx + 1,而不是直接使用 Objective-C 遍历出来的idx。
还有一个经常被忽略的细节:调用 lua_rawseti 之后,当前栈顶的那个值会被移除。也就是说,写入动作本身会消耗掉刚刚 lua_pushinteger 压入的元素,这一点在排查栈错位问题时尤其重要。
iOS 字典如何传给 Lua
字典传递的起点和数组一样,也要先创建一个 Table:
lua_newtable(self.state);
不同的是,字典的 key 是字符串,因此写入时不再使用 lua_rawseti,而是改用 lua_setfield:
[dict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSInteger value = [obj integerValue];
lua_pushinteger(self.state, value);
lua_setfield(self.state, -2, key.UTF8String);
}];
lua_setglobal(self.state, "dictVal");
这段逻辑和数组写入很相似:先把 value 压栈,再把它设置进目标 Table。区别只在于索引方式不同。
lua_setfield 和 lua_rawseti 的区别
lua_setfield 与 lua_rawseti 本质上都在做“把一个值写入 Table”这件事,只是前者面向字符串 key,后者面向整数索引。
- 传数组时,通常使用
lua_rawseti。 - 传字典时,通常使用
lua_setfield。 - 两者都会围绕当前栈中的 Table 工作,因此栈位置是否正确,直接决定写入是否成功。
完成遍历后,再通过 lua_setglobal(self.state, "dictVal") 把这个 Table 交给 Lua 全局变量使用,就能在脚本中按字典方式访问它的字段。
这类桥接里最容易踩的几个点
把上面的过程串起来看,iOS 调 Lua、Lua 调 iOS,真正难的不是 API 数量,而是对规则的把握是否稳定。尤其是下面几个点,基本决定了代码是否容易出问题:
- 暴露给 Lua 的原生函数必须符合
int functionName (lua_State *state);这个签名。 - 读取参数前先确认
lua_gettop(state),避免按不存在的位置取值。 - 压入几个返回值,函数就必须返回几;压了一个值就返回
1,没压值就返回0。 - Lua Table 的数组索引通常从
1开始,原生侧遍历数组时要主动补这个偏移。 lua_rawseti、lua_setfield这类写入操作都依赖栈位置,目标 Table 用错位置时,问题往往不在“数据内容”,而在“栈状态”。
如果把这些规则先理顺,再去写更多类型的桥接代码,后面的字符串、数字、数组、字典甚至更复杂结构,处理方式都会清晰很多。









