当C和Lua互相调用的时候,Lua虚拟栈严格的按照LIFO规则操作,只会改变虚拟栈顶部分。但通过Lua的API,可以查询虚拟栈上的任何元素,甚至是在任何一个位置插入和删除元素。
虚拟栈中数据定义TValue,参见上篇文章。
虚拟栈基本操作
int lua_gettop (lua_State *L); //返回栈顶索引(即栈长度 )
void lua_settop (lua_State *L, int idx); //
void lua_pushvalue (lua_State *L, int idx);//将idx索引上的值的副本压入栈顶
void lua_remove (lua_State *L, int idx); //移除idx索引上的值
void lua_insert (lua_State *L, int idx); //弹出栈顶元素,并插入索引idx位置
void lua_replace (lua_State *L, int idx); //弹出栈顶元素,并替换索引idx位置的值
lua capi 函数后面都一个[-o, +p, x]这样说明。
- 第一个域,o, 指的是该函数会从栈上弹出多少个元素。
-
第二个域,p, 指该函数会将多少个元素压栈。 (所有函数都会在弹出参数后再把结果压栈。) x y 这种形式的域表示该函数根据具体情况可能压入(或弹出) x 或 y 个元素; 问号 ‘?’ 表示 我们无法仅通过参数来了解该函数会弹出/压入多少元素 (比如,数量取决于栈上有些什么)。 - 第三个域,x, 解释了该函数是否会抛出错误: ‘-‘ 表示该函数绝对不会抛出错误; ‘e’ 表示该函数可能抛出错误; ‘v’ 表示该函数可能抛出有意义的错误,“m”表示这个函数会抛出内存溢出异常或错误执行__gc函数。
C 调用 Lua
C获取Lua值
1.用lua_getglocal来获取值,然后将其压栈
2.用C API lua_to***函数将栈中元素取出转成相应的C类型的值
下面代码简要说明入栈和取值过程:
/*1.创建一个state*/
lua_State *L = luaL_newstate();
/*2.入栈操作*/
lua_pushstring(L, "I am Bob~");
lua_pushnumber(L,2018);
/*3.取值操作*/
/*判断是否可以转为string*/
if( lua_isstring(L,1)){
/*转为string并返回*/
printf("%s",lua_tostring(L,1));
}
if( lua_isnumber(L,2)){
printf("%g ",lua_tonumber(L,2));
}
C调用Lua函数
1.用lua_getglobal来获取函数,然后将其压入栈;
2.如果这个函数有参数的话,就依次将函数的参数也压入栈;
3.调用lua_pcall开始调用函数,调用完成以后,会将返回值压入栈中;
4.最后取返回值得,调用完毕。
下面代码简要说明入栈和取值、修改table值、调用函数过程:
--game.lua
name = "Bob"
age = 18
player = { name = "bob", sex = "boy"}
function getCoin (curCoin,change)
return curCoin+change
end
lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaL_dofile(L,"game.lua");
//读取变量
lua_getglobal(L,"name");
printf("name = %s",lua_tostring(L,-1));
//读取数字
lua_getglobal(L,"age");
printf("age = %g ",lua_tonumber(L,-1));
//读取表
lua_getglobal(L, "player");
//取表中元素
lua_getfield(L, -1 ,"name");
printf("player name = %s",lua_tostring(L,-1));
lua_getfield(L,-2,"sex");
printf("player sex = %s",lua_tostring(L,-1));
//取函数
lua_getglobal(L,"getCoin");
lua_pushnumber(L,5);
lua_pushnumber(L,3);
lua_pcall(L,2,1,0);//2-参数格式,1-返回值个数,调用函数,函数执行完,会将返回值压入栈中
printf("5 + 3 = %g",lua_tonumber(L,-1));
//关闭state
lua_close(L);
下图说明执行完毕后虚拟栈的结果:
lua_getglobal(L,"name")
会执行两步操作:
1.将name放入虚拟栈中
2.由Lua去寻找变量name的值,并将变量name的值返回栈顶(替换虚拟栈中name为“Bob”)。
lua_getfield(L,-1,"name")
会执行两步操作:
1.lua_pushstring(L,”name”) lua_pushstring可以把C中的字符串存放到Lua的虚拟栈里,栈顶现在是name。
2.lua_gettable(L,-2),table对象现在在索引为-2的栈中,因为栈顶-1现在是name。lua_gettable函数会从虚拟栈顶取得一个值,然后根据这个值name去table中寻找对应的值“bob”,最后把找到的值放到虚拟栈顶。
lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。
为了方便,lua 在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。
lua5.0后基于寄存器的虚拟机,lua的编译器将local变量存储至寄存器,对local变量的操作就相当于直接对寄存器进行操作,对global变量的操作要先获取变量,然后才能对其进一步操作,自然局部变量比全局变量快。
Lua 调用 C
- 将C的函数包装成Lua环境认可的函数
- 将包装好的函数注册到Lua环境中
- 像使用普通Lua函数那样使用注册函数
包装函数要遵循规范:
typedef int (*lua_CFunction) (lua_State *L);
下面是包装好的函数:
static int getMoney(lua_State *L)
{
// 向函数栈中压入2个值
lua_pushnumber(L, 915);
lua_pushstring(L,"Bob");
//这个函数的返回值则表示函数返回时有多少返回值被压入Lua栈。
//Lua的函数是可以返回多个值的
return 2;
}
//注册函数
lua_register(L,"getMoney",getMoney);
//在lua.h中有定义 lua_register分二步做的。
#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))
lua_pushcfunction(L, getMoney); //将函数放入栈中
lua_setglobal(L, "getMoney"); //设置lua全局变量getMoney
Lua中调用
v1,v2 = getMoney()
print(v1,v2)
C# 调用 C
在Unity中Mono 和 C 通讯使用 P/Invoke, P/Invoke又名平台调用,是.NET CLR提供的,为了使开发者从托管代码(如题主的C#)调用动态连接库中的非托管代码(通常是C)而提供的一种服务。类似的功能,JAVA中叫JNI,Python中叫Ctypes。而在这期间一个重要的工作就是marshall:让托管代码中的数据和原生代码中的数据可以相互访问。
因为不同语言,不同开发环境的数据类型、结构都是不同的,当你使用P/Invoke调用dll的时候,平台会自动给你加载这个dll,并且在托管代码和非托管代码的边界自动完成数据类型转换。
使用P/Invoke的话,一般分为3步:声明,调用,异常处理。
举例,在xlua.dll里有一个如下签名的函数:
LUALIB_API lua_State *luaL_newstate (void)
我们在C#中声明
DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr luaL_newstate();
这样cs代码中的luaL_newstate函数最终都会调用到上面原生代码的luaL_newstate函数中。
在托管代码c#层面,CLR的数据类型分为两类:blittable或者non-blittable。
blittable:如system.byte,system.int16,system.intptr,在托管(managed)代码和非托管(unmanaged)代码中,内存的结构是一致的,blittable类型数据能够直接传递给非托管代码。
Non-blittable:如system.boolean,system.string,system.array在两者中的内存表现就不一致,non-blittable类型就需要做marshaling工作:分配非托管内存、复制非托管内存块、将托管类型转换为非托管类型.
正因为marshaling是一个很重量的工作,所以考量一个Lua热更新框架是否优良的标准之一就是LuaL指令是否合理调用和数据传递的处理
c#函数调用c函数走的是P/Invoke方式,明显的效率没法与c与lua的组合相比,而这种方式不好避免。云风团队用纯 C# 实现了一个 Lua 5.2 虚拟机来避免这种marshall,但最终没有上线使用。而掌趣科技完全脱离Lua,使用内置的IL解译执行虚拟机来执行DLL中的代码,自行设计一套IL托管栈来做数据转换。
IL2CPP
先看看使用Mono时脚本的编译运行过程,如下图: Mono提供了两种编译方式,就是我们经常能看到的:JIT(Just-in-Time compilation,即时编译)和AOT(Ahead-of-Time,提前编译或静态编译),但在IOS上Mono无法使用JIT,采用Full AOT模式执行。
Mono如何跨平台
它基于通用语言运行时(Common Language Runtime,CLR)来做的跨平台,我们编写的C#程序首先会被C#编译器编译为IL(intermediate language,中间语言),然后再由CLR转换为操作系统的原生代码(Native Code),基本模型如下图。
新的Mono 3由Conservative Boehm GC 转向一个真正 gc (SGen gc)。
Xlua的C#hotfix是用Mono.Cecil库对进行C#编译出来的dll程序集进行IL代码注入,做的是静态AOP。 新版的Tolua也集成了该功能。
ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。
使用IL2CPP脚本的编译运行如下图:
AOT编译器(il2cpp.exe)将由Mono输出的中间语言(IL)代码生成为C++代码,IL2CPP vm管理Gc和metadata,这种实现使得可以提高约一到二倍性能,最重要是可以抹平一些p/invoke的消耗。早期IL2CPP二个使用场景是iOS 64-bit和WebGL,后来Android也支持了IL2CPP.
IL2CPP在堆内存分配方面和Mono 最大的不同主要是Reserved Total 是可以下降的,而 Mono的 Reserved Total 只会上升不会下降。