Lua是用C编写的,C#这边操作的最底层,实际上是调用了DLL封装好的Lua API

Lua中最关键的概念就是Lua栈,所有交互都通过这个栈进行,这个栈的核心概念只有两点

  • 1是栈底,往上递增
  • -1是栈顶,往下递减

image-20240513221030202

C调用Lua

取值

/*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));
}

函数

假设有如下Lua代码

--game.lua
name = "Bob"
age = 18
player = { name = "bob", sex = "boy"}
function getCoin (curCoin,change)
    return curCoin+change
end
//读取参数,压入栈
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));

其实关键的是最后几行,我们先压入一个函数,再压入两个参数,最后调用,调用结果也会被压入栈,所以我们可以通过-1获取(栈顶)

注意,执行完之后,Lua栈会把相关函数和参数都弹出,什么意思?看一下执行完之后的栈情况你就懂了

image-20240513223257939

到index5应该是很好理解的, 只是按照顺序压入了一些数据而已,而再调用函数前,index6应该是压入的函数,index7和index8则是压入的两个参数

调用函数后index678都被弹出了,然后压入一个返回值

lua_getglobal(L,”name”);

  1. 先把name压入栈
  2. 通过栈顶元素(也就是name)去Lua寻找对应的值,弹出栈顶元素,并压入对应的值(此时栈顶就是Bob了)

lua_getfield(L, -1 ,”name”);

这个想对来说会烧脑一点

  1. lua_pushstring(L,name),会先把name字符串压入栈,此时index-1=name,因为上面一行我们提前压入过一个table,所以现在index-2是table(Player)

  2. lua_gettable(L,-2),这个API的意思是,把栈顶元素当做key,去指定元素(-2)里取值,现在-2是Player,所以此时就等于是Player[name](栈顶元素是name,-2对应的元素是Player)

    然后回弹出栈顶元素,并压入返回值,所以index-1变成了bob

Lua调用C

这个相对简单很多,只要把符合规范的C函数注册到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内部,其实是分两步进行的

lua_pushcfunction(L, getMoney); //将函数放入栈中
lua_setglobal(L, "getMoney");   //设置lua全局变量getMoney

先把函数压入栈,然后setglobal,弹出栈顶元素

C#调用C/Lua

C#通过P/Invoke间接调用C的API,间间接调用Lua

对应string,int等等简单值类型没啥好说的,有对应的push_string等方法,但是C#的类对象Lua可识别不了,所以对于Class,C#传递的肯定是一个指针

public void Push(RealStatePtr L, object o)
{
    bool is_first;
    int type_id = getTypeId(L, type, out is_first);
   	...
    // C#侧进行缓存
    index = addObject(o, is_valuetype, is_enum);
    // 将代表对象的索引push到lua
    LuaAPI.xlua_pushcsobj(L, index, type_id, needcache, cacheRef);
}

首先把对象在C#侧缓存起来

int addObject(object obj, bool is_valuetype, bool is_enum)
{
    int index = objects.Add(obj);
    //如果是枚举
    if (is_enum)
    {
        enumMap[obj] = index;
    }
    //如果不是枚举也不是值类型
    else if (!is_valuetype)
    {
        reverseMap[obj] = index;
    }
    
    return index;
}

然后会把index作为key传递给Lua,传递的是index哦,代表对象的索引号

type_id作为meta_ref,代表的是对象类型的索引号

// xlua.c
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
    ...
}

到Lua侧后,会创建一张UserData,并把UserData和key,meta_ref关联起来

到这里,我们在Lua侧可以通过索引拿到C#对象,但是我们不知道对象里的属性和方法啊!

所以还得给这个类对象注册他的元表,通过meta_ref!

元表在C#侧,是通过框架(xLua/toLua)生成的一大堆warp文件,都是静态的,会在Lua虚拟机启动的时候注册到Lua虚拟机内

那么怎么找到这些元表呢?没错,就是通过type_id,每个类型注册元表的时候都会对应一个type_id