我正在编写一个iOS库,用于在游戏中嵌入Lua,并且遇到了有关userdata的问题。我希望用户能够将我的库对象视为普通表(在Lua脚本中)来设置属性,并使这些属性可供库中的基础对象使用。例如,用户脚本可能有
line = display.newLine
line.width = 3
然后可以从库(Objective C / C)代码中访问width
字段。
我有这个工作,但是运行了几秒后我得到一个EXC_BAD_ACCESS错误,所以显然我正在访问一个释放的对象或者有其他类型的内存损坏,但我似乎无法弄清楚这是为什么。
我已经将我的代码修剪为一个例子来重现错误。首先,我有一个实现库功能的基础Objective C对象。标题如下所示:
#import "lua.h"
#include "lualib.h"
#include "lauxlib.h"
@interface GeminiLine : NSObject {
int selfRef;
int propertyTableRef;
lua_State *L;
}
@property (nonatomic) int propertyTableRef;
-(id)initWithLuaState:(lua_State *)luaStat;
-(double)getDoubleForKey:(const char*) key withDefault:(double)dflt;
@end
此类保留对lua_State对象的引用,并对整个Lua引用相应的Lua userdata和uservalue(与userdata关联的表)。引用propertyTableRef
用于访问对象属性(uservalue表)。
实施如下:
#import "GeminiLine.h"
@implementation GeminiLine
@synthesize propertyTableRef;
-(id)initWithLuaState:(lua_State *)luaState {
self = [super init];
if (self) {
L = luaState;
}
return self;
}
-(double)getDoubleForKey:(const char*) key withDefault:(double)dflt {
lua_rawgeti(L, LUA_REGISTRYINDEX, propertyTableRef);
//lua_pushstring(L, key);
//lua_gettable(L, -2);
lua_getfield(L, -1, key);
if (lua_isnil(L, -1)) {
return dflt;
}
return lua_tonumber(L, -1);
}
-(void)dealloc {
luaL_unref(L, LUA_REGISTRYINDEX, propertyTableRef);
[super dealloc];
}
@end
此处唯一的非生命周期方法是getDoubleForKey
方法,它访问与对象的用户数据关联的Lua用户值。
这里给出了将对象绑定到Lua的C代码:
///////////// lines ///////////////////////////
static int newLine(lua_State *L){
NSLog(@"Creating new line...");
GeminiLine *line = [[GeminiLine alloc] initWithLuaState:L];
GeminiLine **lLine = (GeminiLine **)lua_newuserdata(L, sizeof(GeminiLine *));
*lLine = line;
[Gemini shared].line = line;
luaL_getmetatable(L, GEMINI_LINE_LUA_KEY);
lua_setmetatable(L, -2);
// append a lua table to this user data to allow the user to store values in it
lua_newtable(L);
lua_pushvalue(L, -1); // make a copy of the table becaue the next line pops the top value
// store a reference to this table so we can access it later
line.propertyTableRef = luaL_ref(L, LUA_REGISTRYINDEX);
// set the table as the user value for the Lua object
lua_setuservalue(L, -2);
NSLog(@"New line created.");
return 1;
}
static int lineGC (lua_State *L){
NSLog(@"lineGC called");
GeminiLine **line = (GeminiLine **)luaL_checkudata(L, 1, GEMINI_LINE_LUA_KEY);
[*line release];
return 0;
}
static int lineIndex( lua_State* L )
{
NSLog(@"Calling lineIndex()");
/* object, key */
/* first check the environment */
lua_getuservalue( L, -2 );
if(lua_isnil(L,-1)){
NSLog(@"user value for user data is nil");
}
lua_pushvalue( L, -2 );
lua_rawget( L, -2 );
if( lua_isnoneornil( L, -1 ) == 0 )
{
return 1;
}
lua_pop( L, 2 );
/* second check the metatable */
lua_getmetatable( L, -2 );
lua_pushvalue( L, -2 );
lua_rawget( L, -2 );
/* nil or otherwise, we return 1 here */
return 1;
}
// this function gets called with the table on the bottom of the stack, the index to assign to next,
// and the value to be assigned on top
static int lineNewIndex( lua_State* L )
{
NSLog(@"Calling lineNewIndex()");
int top = lua_gettop(L);
NSLog(@"stack has %d values", top);
lua_getuservalue( L, -3 );
/* object, key, value */
lua_pushvalue(L, -3);
lua_pushvalue(L,-3);
lua_rawset( L, -3 );
NSLog(@"Finished lineNewIndex()");
return 0;
}
// the mappings for the library functions
static const struct luaL_Reg displayLib_f [] = {
{"newLine", newLine},
{NULL, NULL}
};
// mappings for the line methods
static const struct luaL_Reg line_m [] = {
{"__gc", lineGC},
{"__index", lineIndex},
{"__newindex", lineNewIndex},
{NULL, NULL}
};
int luaopen_display_lib (lua_State *L){
// create meta tables for our various types /////////
// lines
luaL_newmetatable(L, GEMINI_LINE_LUA_KEY);
lua_pushvalue(L, -1);
luaL_setfuncs(L, line_m, 0);
/////// finished with metatables ///////////
// create the table for this library and popuplate it with our functions
luaL_newlib(L, displayLib_f);
return 1;
}
此处的关键方法是newLine
和lineNewIndex
。在newLine
中,我创建了对应于Lua对象的目标C GeminiLine
对象,并在Lua userdata中存储指向它的指针。我还在单个Gemini
对象中存储了一个指向新对象的指针,该对象为主程序提供了执行Lua脚本的访问权限。这个课程在这里给出:
Gemini *singleton = nil;
@interface Gemini () {
@private
lua_State *L;
}
@end
@implementation Gemini
@synthesize line;
- (id)init
{
self = [super init];
if (self) {
L = luaL_newstate();
luaL_openlibs(L);
}
return self;
}
+(Gemini *)shared {
if (singleton == nil) {
singleton = [[Gemini alloc] init];
}
return singleton;
}
-(void)execute:(NSString *)filename {
int err;
lua_settop(L, 0);
NSString *luaFilePath = [[NSBundle mainBundle] pathForResource:filename ofType:@"lua"];
setLuaPath(L, [luaFilePath stringByDeletingLastPathComponent]);
err = luaL_loadfile(L, [luaFilePath cStringUsingEncoding:[NSString defaultCStringEncoding]]);
if (0 != err) {
luaL_error(L, "cannot compile lua file: %s",
lua_tostring(L, -1));
return;
}
err = lua_pcall(L, 0, 0, 0);
if (0 != err) {
luaL_error(L, "cannot run lua file: %s",
lua_tostring(L, -1));
return;
}
}
@end
对于我的测试程序,我使用单一视图模板创建了一个应用程序。我修改了applicationDidFinishLaunching
上的AppDelegate
方法来调用测试脚本,如下所示:
-(void) update {
double width = [[Gemini shared].line getDoubleForKey:"width" withDefault:5.0];
NSLog(@"width = %f", width);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
// Override point for customization after application launch.
[[Gemini shared] execute:@"test"];
timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:@selector(update)
userInfo:nil
repeats:YES];
....
我还包括一个每秒触发100次并以update
方法为目标的计时器。 update
方法检索lua脚本中的属性集width
,并使用NSLog
进行记录。
我正在使用的test.lua
脚本如下:
display = require('display')
line = display.newLine()
line.width = 3;
现在,当我运行此代码时,它会正确执行几秒钟,打印出正确的消息和适当的行宽,但随后NSLog(@"width = %f", width);
行的update
行出现EXC_BAD_ACCESS错误而失败方法。起初我以为可能line
对象被垃圾收集了,但是lineGC
方法会记录这个,但事实并非如此。所以我确信问题在于我使用我的Lua userdata的用户值,无论是在设置还是访问中。
任何人都可以看到我实施此错误的方式出现错误吗?
修改
为了确认我的userdata没有被垃圾收集,我在使用lua_gc(L, LUA_GCSTOP, 0);
加载脚本之前禁用了GC。仍然得到完全相同的问题。
我之前忘了提到我正在使用Lua 5.2。
使用“编辑方案”打开每个内存调试标志表示在调用setsvalue2s
时,以下Lua代码库函数中发生错误,实际上是一个宏:
LUA_API void lua_getfield (lua_State *L, int idx, const char *k) {
StkId t;
lua_lock(L);
t = index2addr(L, idx);
api_checkvalidindex(L, t);
setsvalue2s(L, L->top, luaS_new(L, k));
api_incr_top(L);
luaV_gettable(L, t, L->top - 1, L->top - 1);
lua_unlock(L);
}
答案 0 :(得分:0)
我遇到过类似的问题。我的猜测是,Lua内存管理器(或ObjC管理器)正在释放该对象。它可以正常工作几秒钟,因为它不会被垃圾收集。
答案 1 :(得分:0)
我很确定我现在能够回答我自己的问题。问题出在getDoubleForKey
方法:
-(double)getDoubleForKey:(const char*) key withDefault:(double)dflt {
lua_rawgeti(L, LUA_REGISTRYINDEX, propertyTableRef);
//lua_pushstring(L, key);
//lua_gettable(L, -2);
lua_getfield(L, -1, key);
if (lua_isnil(L, -1)) {
return dflt;
}
return lua_tonumber(L, -1);
}
我是Lua的新手,并没有意识到我需要在拨打这样的电话后清空堆栈。当我的库函数被Lua调用时没有必要,但是我在这里打电话让Lua不会让我失望。
我通过打印出方法顶部的堆栈大小并看到它随着每次调用而增加而找到了这一点。最终堆栈变得如此之大,以至于发生了不好的事情。解决方案是在退出方法之前清空堆栈:
-(double)getDoubleForKey:(const char*) key withDefault:(double)dflt {
lua_rawgeti(L, LUA_REGISTRYINDEX, propertyTableRef);
lua_getfield(L, -1, key);
if (lua_isnil(L, -1)) {
lua_pop(L,2);
return dflt;
}
double rval = lua_tonumber(L, -1);
lua_pop(L, 2);
return rval;
}