【Lua与C#交互④】如何让Lua打印到Unity控制台

3 篇文章 2 订阅

今天要讲的如何让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替换掉。

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自己写的方法,我也模仿(照抄)了一个,源码如下:

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_Debug *ar):获取栈信息,level表示第几层
  • int lua_getinfo (lua_State *L, const char *what, 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_State *L, 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联网多人动作游戏开发经验。

CSDN博客:https://blog.csdn.net/j756915370
知乎专栏:https://zhuanlan.zhihu.com/c_1241442143220363264
交流学习群:891809847

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值