Lua

Lua虚拟栈交互流程

分析Lua、C、C#如何交互的

Posted by Bob on July 28, 2018

当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));
	}

image

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);

下图说明执行完毕后虚拟栈的结果:

image

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”,最后把找到的值放到虚拟栈顶。

image

lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。

为了方便,lua 在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。

lua5.0后基于寄存器的虚拟机,lua的编译器将local变量存储至寄存器,对local变量的操作就相当于直接对寄存器进行操作,对global变量的操作要先获取变量,然后才能对其进一步操作,自然局部变量比全局变量快。

Lua 调用 C

  1. 将C的函数包装成Lua环境认可的函数
  2. 将包装好的函数注册到Lua环境中
  3. 像使用普通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时脚本的编译运行过程,如下图: image 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),基本模型如下图。

image

新的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脚本的编译运行如下图: image

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 只会上升不会下降。