C#与Lua交互原理

     以前每一次研究一些新知识并且有所收获的时候,都想写一篇博客来分享这种自己的收获。 一直没有写主要是因为自己水平太菜了,怕写出来贻笑大方,最近想通了,想写就写下来,就算对别人毫无用处,还可以自己没事翻翻,省得自己都忘了。当然如果能对你有一丝丝的帮助,那真是再好不过了 。本文将针对C#与Lua的调用原理阐述一些个人的心得 。

 Lua开发环境搭建 

 

      有关于VS && Lua开发环境的搭建网上有很多例子 ,笔者参考 http://www.byjth.com/lua/33.html (补充一点:Lua库文件尽量自己编译)

      想知道C#如何与Lua进行交互 首先必须先了解C如何与Lua进行交互 ,C 和 Lua语言之间存在的差异,第一种是Lua使用了垃圾回收机制 ,而C语言需要显式的释放内存。第二种C语言是静态类型(在编译时可以确定变量类型,像int x这种),Lua是动态类型(编译时无法确定变量类型,不用注明变量类型)。为了解决这些问题 ,C与Lua的通信引入了虚拟栈结构。

头文件

  在完成环境的搭建之后,已经可以开始编写测试代码了,首先引入lua.hpp头文件,lua.hpp头文件内容如下 :

  C#与Lua交互原理_第1张图片

 

  lua.h:定义了Lua提供的基础函数,包括创建Lua环境 调用Lua函数、读写Lua环境的全局变量、注册Lua调用的新函数等      Lua.h定义的内容都有Lua_作为前缀 

 lualib.h:是一个辅助函数库,从lua.h的API中编写出的较高的抽象层,用于提供具体的功能(如luaL_dofile等) 它的所有定义都是luaL_作为前缀 

luaxlib.h:定义了打开标准库的函数,。为了使Lua保持灵活,小巧,所有的标准库都被组织到了不同的包中。当我们需要使用哪个标准库时,就可以调用lualib.h中定义的函数来打开对应的标准库;而辅助函数luaL_openlibs则可以打开所有的标准库

C调用Lua

介绍完头文件之后,咱们写一个简单的main.cpp,如下图所示 

#include 
#include 
int main()
{
	//Lua库中没有定义任何全局变量。它将所有的状态都保存在动态结构lua_State中,所有的C API都要求传入一个指向该结构的指针。
	// luaL_newstate函数用于创建一个新环境或状态,也就是所谓的lua虚拟机对象。
	lua_State* L = luaL_newstate();
	//将hello world 压入虚拟栈 
	lua_pushstring(L, "hello world");
	//将整数10压入虚拟栈
	lua_pushnumber(L, 10);
	if (lua_isnumber(L, -1) && lua_isstring(L, -2))
	{
		//虚拟栈对应位置的元素转换为对应类型 
		int num = lua_tonumber(L, -1);
		const char* str = lua_tostring(L, -2);
        lua_pop(L,2); // 出栈两个元素 
		printf("%d %s \n", num, str);
	}
	lua_close(L);
	system("pause");
	return 0;
}

 

print:10  Hello world

针对上面这个例子的API注释得已经很清楚了,现在我们来画图对栈结构进行分析  在执行了lua_pushnumber(L, 10)之后的栈结构如图

C#与Lua交互原理_第2张图片

API使用“索引”来引用栈中元素 ,当使用正数索引时,表示从栈底开始,一直到栈顶 ,使用负数索引时表示从栈顶开始,一直到栈底,而上面程序中 的lua_isnumber(L,-1)和lua_isstring就是通过索引来判断当前值的类型 然后通过lua_tonumber 等函数进行栈内元素类型转换,再通过lua_pop出栈对应数量的元素(类似还有lua_remove(L,index) 指定索引的元素出栈,并将该位置之上的所有元素下移填补空缺),  这就完成一次虚拟栈的入栈 、出栈操作 。相关的API还有

C#与Lua交互原理_第3张图片

上图是入栈的API,有关于检验 和 出栈的API与上图类似,这里就不放了 。

 在明白了虚拟栈之后,接下来我们在main.cpp同级下新建一个test.lua,使用C来获取lua定义的变量 和 调用lua的函数 

width = 800;
height = 600;
people = {name = "YGH",age = 13,sex = true}
function f(x ,y)
  return x+y,x*y,x-y
end

对应的C代码为

// 打印对应虚拟机的当前栈
static void viewStack(lua_State* L)
  {
    int i;
    int top = lua_gettop(L);
    for(int i = 1;i<=top;i++)
    {
      int t = lua_type(L,i);
      switch(t){
      case LUA_TSTRING:
      {   
         // string 
          printf("%s",lua_tostring(L,i));
          break; 
      }   
      case LUA_TBOOLEAN:
      {   
         //  _Bool 
         printf(lua_toboolean(L,i)?"true":"false"); 
        break;
      }
      case LUA_TNUMBER:
      {
         printf("%g",lua_tonumber(L,i));
         break;
      }
      default:
      {
        printf("%s",lua_typename(L,t));
        break;
      }
     }
     printf("     ");
  }      
   printf("\n");
}
         
const char* getStringField(lua_State* L, const char* key) {
	const char* name;
	lua_pushstring(L, key);
    viewStack(L);
	//以栈顶元素作为key ,在lua的内部协议里使用key 和 指定索引的table获取value ,并将key弹出 value压入  此时的索引为-2
	lua_gettable(L, -2);
	if (!lua_isstring(L, -1)) {
		printf("the key is not string");
	}
	name = lua_tostring(L, -1);
	lua_pop(L, 1);
	return name;
}
int main()
{
	int error;
	lua_State* L = luaL_newstate();
	luaL_openlibs(L);

	//加载test.lua文件 并进行编译 
	error = luaL_loadfile(L, "test.lua") || lua_pcall(L, 0, 0, 0);
	if (error)
	{
		//如果test.lua文件发生错误,会将错误信息入栈,取出错误信息并输出 
		fprintf(stderr, "%s", lua_tostring(L, -1));
		lua_pop(L, 1);
	}
	//将全局变量 width  height people分别 入栈 
	lua_getglobal(L, "width");
	lua_getglobal(L, "height");
	lua_getglobal(L, "people");

	// 按照固定索引取出对应的元素 ,此处应使用lua_is...函数进行判断,再取出 
	int w = lua_tointeger(L, -3);
	int h = lua_tointeger(L, -2);

	//取出 people 里key = “age”的变量值  
	//int age = lua_getfield(L, -1, "age");  // 使用age直接接收返回值是错误的, lua_getfield返回的是对应的数据类型,此处为Lua_Number,对应lua源码里的LUAT_NUMBER(3).下面是正确的做法
     lua_getfield(L,-1,"age");  //将people里key = “age”的变量值入栈,也就是说此时栈顶是13. 
     viewStack(L); // 对应输出 800 600 table 13 
     int age = lua_tonumber(L,-1); // 
     lua_pop(L,1) // 出栈一个元素 ,也就是13. 
	// 取出people 里 key = “name”的变量值 (该函数是我们自己实现了类似lua_getfield的功能)当前栈结构 800 600 table
	const char* name = getStringField(L, "name");

	printf("w =%d ,h = %d", w, h);
	printf("age is %d,name is %s ", age, name);
	system("pause");
	return 0;
}

 

print:  
800 600 table 13
800 600 table name
w=800,h=600 age is 13,name is YGH

有关于C获取lua的变量 上面这个例子已经很明白了,主要有一点就是获取people里的name属性时,自定义函数的运行情况,首先将变量名进行入栈,在  lua_pushstring(L, key)运行之后 整个栈的结构 如图C#与Lua交互原理_第4张图片

 

接着调用 lua_gettable(L, -2) 是以栈顶的name字符串作为key 和指定索引的table来获取 对应的value值,再弹出key ,压入value (理解这句话的操作过程很重要 ),也就是说 lua_gettable(L,-2)的作用就是以栈顶的值作为key来访问-2位置上的table,所以现在的栈结构为

C#与Lua交互原理_第5张图片

  这样再从栈顶取值的话 ,就是我们想要获取的值 。接下来再写调用lua的函数代码 

int CallFunc(const char* key, int x, int y) {
	int result;
	lua_State* L = luaL_newstate();
	luaL_openlibs(L);


	int error = luaL_loadfile(L, "test.lua") || lua_pcall(L, 0, 0, 0);
	if (error)
	{
		fprintf(stderr, "%s", lua_tostring(L, -1));
		lua_pop(L, 1);
	}


	//压入函数
	lua_getglobal(L, key);
	//压入参数 
	lua_pushnumber(L, x);
	lua_pushnumber(L, y);

	// 第二个参数是待调用函数的参数数量 第三个参数是 期望的返回结果数量 第四个参数是错误处理函数索引 
	//在获得结果之后 ,lua_pcall会先弹出栈中的函数 和 参数 然后将所得的结果按照返回顺序入栈
	if (lua_pcall(L, 2, 2, 0) != 0)
	{
		printf("function is error");
	}
	//获取存在栈顶的第一个返回结果 
	result = lua_tonumber(L, -1);
	// 获取第二个返回结果
	int  result2 = lua_tonumber(L, -2);
	printf("function result: result1 = %d result2 = %d", result, result2);
	return result;
}

int main()
{
	CallFunc("f", 5, 3);
	system("pause");
	return 0;
}

 

print:function result: result1 = 15 result2 = 8

相信你如果能够对照着该例子实现一遍,应该明白了c如何调用Lua函数,与上文获取name的值一样,都是通过将所需要的参数进行入栈,然后调用对应的C API对栈进行操作,再通过出栈来得到最终的结果 ,虚拟栈就是C与Lua相互调用的基础 

为了方便起见,我从网上下了 LuaFramework_UGUI这个框架 ,打开LuaDll.cs脚本,里面封装了大部分我们似曾相识的函数 

C#与Lua交互原理_第6张图片

看到这里大家应该都有所了解了,C#调用Lua是依靠C作为中间语言,通过C#调用C,C再调用Lua实现的 而框架中的tolua.dll等也就是借助LuaInterface封装的C语言动态库 

Lua调用C

对于Lua调用C函数,有所区别的是在调用之前需要进行注册,将函数地址告知Lua 。而且使用了一个与C语言调用Lua时相同的栈,C函数从栈中获取函数参数,并将结果压入栈中。为了将栈中的结果数据和其他值进行区分,C函数还要返回其压入栈中的结果数量 ,并且每一个C函数都有自己的局部私有栈,当Lua调用一个C函数时,第一个参数总是这个局部栈的索引1.

定义C函数如下 

static int testFunc(lua_State* L) {
	//从栈中获取函数的参数 
	int x = lua_tonumber(L, 1);
	int y = lua_tonumber(L, 2);

     //将函数的返回结果进行入栈 
	lua_pushnumber(L, x + y);
	lua_pushnumber(L, x - y);
	// 函数返回结果的数量 
	return 2;
}
int main()
{
	lua_State* L = luaL_newstate();
	luaL_openlibs(L);
        // 将函数注册到Lua中 
	lua_pushcfunction(L, testFunc);
	lua_setglobal(L, "func");
	if (luaL_dofile(L, "test.lua")) {
		fprintf(stderr, "ha %s", lua_tostring(L, -1));
		lua_pop(L, 1);
	}
	system("pause");
}

lua文件:

a,b = func(5,3);
print("hello world")
print("a ="..a.." b="..b);

print:hello world   a =8.0 b=2.0

如果读者仔细看过上面的例子,现在应该能够理解为何LuaFramework的框架中Lua要调用Unity自带的API或者我们自己写的脚本之前要先生成对应的Wrap文件,就是如上面例子一样,需要在lua里进行注册。这边我提供了一个例子 。

在 LuaFrameWork项目工程中 ,新建一个Test.cs ,为了方便观察,没有继承monobehavior

public class Test  {
    public int a;
    public int MyAdd(int x, int y) {
        return x + y;
    }
    public void MyPrint() {
        Debug.Log("hahha");
    }
}

然后我们再来看一下对应生成的TestWarp文件

//this source code was auto-generated by tolua#, do not modify it
using System;
using LuaInterface;

public class TestWrap
{

	[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
	static int MyAdd(IntPtr L)
	{
		try
		{
			ToLua.CheckArgsCount(L, 3);
			Test obj = (Test)ToLua.CheckObject(L, 1, typeof(Test));
			int arg0 = (int)LuaDLL.luaL_checknumber(L, 2);
			int arg1 = (int)LuaDLL.luaL_checknumber(L, 3);
			int o = obj.MyAdd(arg0, arg1);
			LuaDLL.lua_pushinteger(L, o);
			return 1;
		}
		catch(Exception e)
		{
			return LuaDLL.toluaL_exception(L, e);
		}
	}

	[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
	static int MyPrint(IntPtr L)
	{
		try
		{
			ToLua.CheckArgsCount(L, 1);
			Test obj = (Test)ToLua.CheckObject(L, 1, typeof(Test));
			obj.MyPrint();
			return 0;
		}
		catch(Exception e)
		{
			return LuaDLL.toluaL_exception(L, e);
		}
	}
}

可以看出对应生成的MyAdd函数和 MyPrint函数有返回值的返回1,没返回值的返回0,也就证明了wrap文件确实是使用C作为媒介将函数注册到lua中。

  到这一步,其实C#与Lua的基本交互原理就已经讲述的差不多了,如果想更进一步的话,可以看看lua的源码,也许会更加精进。

  

 

 

 

 

 

 

     

你可能感兴趣的:(Lua)