今天要讲的如何让Lua打印到Unity控制台?
相信用过tolua或者xlua的人都知道,在lua脚本里面只要写一行print就能打印到unity控制台效果类似Debug.Log。 如下:
print("This is a script from a utf8 file")
print("tolua: 你好! こんにちは! 안녕하세요!")
那么它们背后的原理是什么呢?如果自己实现一个类似的函数替换功能又该如何实现?
首先来看核心代码如下:
_L = LuaDLL.luaL_newstate();
LuaDLL.luaL_openlibs( _L );
LuaDLL.lua_pushcfunction( _L, Print );
LuaDLL.lua_setglobal( _L, "print" );
1. luaL_newstate
新建一个lua状态机,没什么好说的
2. luaL_openlibs
打开lua标准库,把库里的函数放到全局变量里
什么是lua标准库?
lua标准库指的是一些lua源码自带的库函数,包括debug、package、string、math等。相关代码在lua源码里的linit.c文件。如果没有这一步,后面调用setglobal时就会失败,因为在全局里面没有print这个函数。
3. lua_pushcfunction
将c#里的方法压入栈顶。这个方法在lua源码里是一个宏。
#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0)
所以如果直接build lua源码的dll,是调用不了这个方法的。在c#端需要做以下处理,或者自行修改lua源码。
[DllImport( LUADLL, CallingConvention = CallingConvention.Cdecl )]
public static extern void lua_pushcclosure( IntPtr L, LuaCSFunction f, int n );
public static void lua_pushcfunction( IntPtr L, LuaCSFunction f )
{
lua_pushcclosure( L, f, 0 );
}
LuaCSFuncton是一个参数为Intptr,返回值为int的委托。
我们需要把c#端另外实现一个Print方法,并且把lua端的print替换掉。
[MonoPInvokeCallbackAttribute( typeof( LuaCSFunction ) )]
private static int Print( IntPtr L )
{
try
{
int n = LuaDLL.lua_gettop( L );
var sb = new StringBuilder();
//获得当前运行的函数的上一个调用层的信息,返回行数,把调用层的名称入栈 int line = LuaDLL.jlua_where( L, 1 );
string filename = LuaDLL.lua_tostring( L, -1 );
LuaDLL.lua_settop( L, n );
int offset = filename[0] == '@' ? 1 : 0;
sb.Append( '[' ).Append( filename, offset, filename.Length - offset ).Append( ':' ).Append( line ).Append( "]:" );
for( int i = 1; i <= n; i++ )
{
if( i > 1 ) sb.Append( " " );
if( LuaDLL.lua_isstring( L, i ) == 1 )
{
sb.Append( LuaDLL.lua_tostring( L, i ) );
}
else if( LuaDLL.lua_isnil( L, i ) )
{
sb.Append( "nil" );
}
else if( LuaDLL.lua_isboolean( L, i ) )
{
sb.Append( LuaDLL.jlua_toboolean( L, i ) ? "true" : "false" );
}
else
{
IntPtr p = LuaDLL.lua_topointer( L, i );
if( p == IntPtr.Zero )
{
sb.Append( "nil" );
}
else
{
sb.Append( LuaDLL.luaL_typename( L, i ) ).Append( ":0x" ).Append( p.ToString( "X" ) );
}
}
}
Debug.Log( sb.ToString() );
return 0;
}
catch(Exception e )
{
throw e;
}
}
Print 函数需要打印出lua的调用栈信息。如一开始展示的那样,它能够打印出是哪个lua脚本的哪一行调用的print。
这里我的实现方式基本照抄tolua。唯一的区别是我使用的stringbuilder进行字符串拼接,tolua使用的CString,效果是一样的。
(1) 首先调用lua_gettop知道当前lua栈里有多少层
(2) 接着调用了一个lua_where,这是tolua在c端自己写的方法,我也模仿(照抄)了一个,源码如下:
LUA_API int jlua_where(lua_State *L, int level) {
lua_Debug ar;
if (lua_getstack(L, level, &ar)) {
lua_getinfo(L, "Sl", &ar);
if (ar.currentline > 0) {
lua_pushstring(L, ar.source);
return ar.currentline;
}
}
lua_pushliteral(L, "");
return -1;
}
这里有涉及到几个lua的API:int lua_getstack (lua_State L, int level, lua_Debugar):获取栈信息,level表示第几层
int lua_getinfo (lua_State L, const charwhat, lua_Debug *ar):根据what字符串的内容给ar填充信息'n': 填充 name 及 namewhat 域;
'S': 填充 source , short_src , linedefined , lastlinedefined ,以及 what 域;
'l': 填充 currentline 域;
't': 填充 istailcall 域;
'u': 填充 nups, nparams,及 isvararg 域; -
'f': 把正在运行中指定层次处函数压栈;
'L': 将一张表压栈,这张表中的整数索引用于描述函数中哪些行是有效行。 (有效行指有实际代码的行,即你可以置入断点的行。 无效行包括空行和只有注释的行。)
const char lua_pushliteral (lua_StateL, const char *s): 等同于pushstring
在Print里面还有一个方法lua_tostring也值得一讲。lua_tostring在lua源码里也是一个宏。不能被打到dll里面。
#define lua_tostring(L,i) lua_tolstring(L, (i), NULL)LUA_API const char *lua_tolstring (lua_State *L, int idx, size_t *len){...}
在c#端需要这么处理:
[DllImport( LUADLL, CallingConvention = CallingConvention.Cdecl )]
public static extern IntPtr lua_tolstring( IntPtr luaState, int index, out int len );
public static string lua_tostring( IntPtr luaState, int index )
{
int len = 0;
IntPtr str = lua_tolstring( luaState, index, out len );
if( str != IntPtr.Zero )
{
string result = Marshal.PtrToStringAnsi( str, len );
if( result == null )
{
byte[] buffer = new byte[len];
Marshal.Copy( str, buffer, 0, len );
return Encoding.UTF8.GetString( buffer );
}
return result;
}
return null;
}
lua_tolstring之所以需要out是因为lua源码里对应的方法会给len赋值。还有在lua_tostring将指针Intptr转成了字符串是因为c里面的char类型不能直接转成c#里的string类型。
(3)调用lua_settop把调用lua_where的时候往栈里放的东西扔掉。lua_settop是一个非常常用的API,作用是把第n层往上的栈里存的东西都舍弃。
(4)遍历,判断一个栈里的从1到n层存的都是什么数据类型
(5)异常处理,我这里因为偷懒,直接抛出异常。在tolua里面有一个luaExection类专门用来处理lua端的异常的。
4. lua_setglobal
最后一步调用lua_setglobal替换全局函数print就大功告成了。
总结,这次依旧讲的的lua的api调用以及lua和c#端进行交互的问题。并且从这次开始不再使用tolua等别人打包好的dll了。而是我自己拿lua源码打包。这些知识属于比较底层的东西,可能很多人觉得没有什么用,我之前也是这么觉得的。别人都已经造好轮子了,我还去研究轮子怎么造有用吗?最近也是因为工作需要不得不研究lua才发现,自己以前懂的连皮毛都算不上,都是停留在tolua,xlua怎么用的程度,一旦脱离了这些工具,自己什么也不会了。
单纯的使用工具并不能提高自己的技术。因为工具总是会变的,只有掌握好底层,才能以不变应万变。
关于作者:水曜日鸡,简称水鸡,ACG宅。曾参与索尼中国之星项目研发,具有2D联网多人动作游戏开发经验。
游戏行业交流学习群:891809847