以前每一次研究一些新知识并且有所收获的时候,都想写一篇博客来分享这种自己的收获。 一直没有写主要是因为自己水平太菜了,怕写出来贻笑大方,最近想通了,想写就写下来,就算对别人毫无用处,还可以自己没事翻翻,省得自己都忘了。当然如果能对你有一丝丝的帮助,那真是再好不过了 。本文将针对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头文件内容如下 :
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)之后的栈结构如图
API使用“索引”来引用栈中元素 ,当使用正数索引时,表示从栈底开始,一直到栈顶 ,使用负数索引时表示从栈顶开始,一直到栈底,而上面程序中 的lua_isnumber(L,-1)和lua_isstring就是通过索引来判断当前值的类型 然后通过lua_tonumber 等函数进行栈内元素类型转换,再通过lua_pop出栈对应数量的元素(类似还有lua_remove(L,index) 指定索引的元素出栈,并将该位置之上的所有元素下移填补空缺), 这就完成一次虚拟栈的入栈 、出栈操作 。相关的API还有
上图是入栈的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)运行之后 整个栈的结构 如图
接着调用 lua_gettable(L, -2) 是以栈顶的name字符串作为key 和指定索引的table来获取 对应的value值,再弹出key ,压入value (理解这句话的操作过程很重要 ),也就是说 lua_gettable(L,-2)的作用就是以栈顶的值作为key来访问-2位置上的table,所以现在的栈结构为
这样再从栈顶取值的话 ,就是我们想要获取的值 。接下来再写调用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是依靠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的源码,也许会更加精进。