可可西

Lua与C/C++互操作

Lua通过一个“虚拟栈”(Lua State)与C/C++程序进行数据交互。

当在Lua里面操作这个栈的时候,每次操作的都是栈的顶部。而Lua的C API则有更多的控制权,可非常灵活地操纵这个栈的任意位置。

c/c++调用lua实际上是:c/c++获取全局表中的lua变量或函数,然后把数据放入栈中,lua再去栈中取数据,然后返回数据对应的值到栈顶,再由栈顶返回c++。

lua调c/c++也一样:先将c/c++的函数注册到lua解释器中,然后lua再去调用它们。

 

栈(Lua State)

注1:绝对索引是从1开始由栈底到栈顶依次增长的

注2:相对索引是从-1开始由栈顶到栈底依次递减的(在lua API函数内部会将相对索引转换为绝对索引)

注3:上图栈的容量为7,栈顶绝对索引为5,有效索引范围为:[1, 5],可接受索引范围为:[1, 7]

注4:Lua虚拟机指令里寄存器索引是从0开始的,而Lua API里的栈索引是从1开始的,因此当需要把寄存器索引当成栈索引使用时,要进行+1

 

栈是FILO(先进后出)的。栈中每个元素为一个TValue类型。64位系统下,sizeof(TValue)=16sizeof(Value)=8

其中boolean(布尔)、integer(整型)、double(浮点)、light userdatalight c function是直接存在栈上的

TStringUdataClosureTablelua state在栈上只是一个指针,都为GC类型,当没有被引用时会被lua的GC系统自动回收,具体结构如下:

 

将不同类型的变量压栈

static int Square(lua_State* L) 
{
    double d = lua_tonumber(L, 1); /* get argument */
    lua_pushnumber(L, d*d); /* push result */
    return 1; /* number of results */
}

#include <string.h>
static int CClosureStrLen(lua_State* L)
{
    const char* upval = lua_tostring(L, lua_upvalueindex(1));// get first upvalue

    lua_pushnumber(L, (int)strlen(upval)); /* push result */
    return 1;
}

typedef struct Rect
{
    float w, h;
} Rect;

Rect g_rc;


/************** 测试代码 **************/
lua_settop(L, 0); //把栈上所有元素移除

lua_pushnil(L); // 把nil压栈

lua_pushboolean(L, 1); // 把布尔值true压栈

lua_pushinteger(L, 35);// 把整型数35压栈

lua_pushnumber(L, 12.8);// 把浮点数12.8压栈

lua_pushcfunction(L, Square);// 把c函数Square压栈

lua_pushlightuserdata(L, &g_rc);// 把全局变量Rect g_rc的指针压栈

lua_pushstring(L, "Hello!");// 把字符串Hello!压栈

lua_newtable(L);// 创建一个空表并压栈

lua_newthread(L);// 创建一个Thread并压栈

lua_newuserdata(L, sizeof(Rect)); //创建一个内存块为Rect的full userdata,并压栈

lua_pushstring(L, "cclosure upvalue");// 创建一个字符串upvalue,内容为cclosure upvalue,并压栈
lua_pushcclosure(L, CClosureStrLen, 1);// 创建有1个upvalue的c函数闭包(upvalue为栈顶元素),成功后将栈顶1个upvalue出栈,并将自己入栈

 

以压入一个int整型数来说明压栈的实现:

void PushInt(lua_State* L, int n) {
    TValue v;
    v.tt_ = LUA_TNUMINT;//19,表示整型
    v.value_.i = n;
    TValue* io = (L->top - 1); *io = v; // 将TValue v压入LuaState栈中
    L->top++; // LuaState的top指针上移
}

 

打印栈

#include "lobject.h"
#include "lstate.h"

const TValue luaO_nilobject_ = { NILCONSTANT };

/* value at a non-valid index */
#define NONVALIDVALUE        cast(TValue *, luaO_nilobject)
/* test for pseudo index */
#define ispseudo(i)        ((i) <= LUA_REGISTRYINDEX)

static TValue* index2addr(lua_State* L, int idx) {
    CallInfo* ci = L->ci;
    if (idx > 0) {
        TValue* o = ci->func + idx;
        api_check(L, idx <= ci->top - (ci->func + 1), "unacceptable index");
        if (o >= L->top) return NONVALIDVALUE;
        else return o;
    }
    else if (!ispseudo(idx)) {  /* negative index */
        api_check(L, idx != 0 && -idx <= L->top - (ci->func + 1), "invalid index");
        return L->top + idx;
    }
    else if (idx == LUA_REGISTRYINDEX)
        return &G(L)->l_registry;
    else {  /* upvalues */
        idx = LUA_REGISTRYINDEX - idx;
        api_check(L, idx <= MAXUPVAL + 1, "upvalue index too large");
        if (ttislcf(ci->func))  /* light C function? */
            return NONVALIDVALUE;  /* it has no upvalues */
        else {
            CClosure* func = clCvalue(ci->func);
            return (idx <= func->nupvalues) ? &func->upvalue[idx - 1] : NONVALIDVALUE;
        }
    }
}

void DumpLuaStack(lua_State* L)
{
    int ad = -1;
    int i = lua_gettop(L);
    printf("\n----------------  Stack Dump ----------------\n");
    while (i) {
        StkId o = index2addr(L, i);
        int t = lua_type(L, i);
        switch (t) {
        case LUA_TSTRING:
            printf("%d[%d]:'%s'\n", i, ad, lua_tostring(L, i));
            break;
        case LUA_TBOOLEAN:
            printf("%d[%d]: %s\n", i, ad, lua_toboolean(L, i) ? "true" : "false");
            break;
        case LUA_TNUMBER:
            printf("%d[%d]: %g\n", i, ad, lua_tonumber(L, i));
            break;
        case LUA_TFUNCTION:
            if (ttislcf(o)) {
                printf("%d[%d]: c %p\n", i, ad, fvalue(o)); // lua_CFunction
            }
            else if (ttisCclosure(o))
            {
                printf("%d[%d]: c closure %p\n", i, ad, clCvalue(o)->f); // CClosure
            }
            else if (ttisLclosure(o))
            {
                Proto* pinfo = clLvalue(o)->p;
                printf("%d[%d]: lua closure %s[%d,%d]\n", i, ad, getstr(pinfo->source), pinfo->linedefined, pinfo->lastlinedefined);
            }
            break;
        case LUA_TTABLE:
            printf("%d[%d]: table:%p\n", i, ad, hvalue(o)); // 等价于printf("%d[%d]: table:%p\n", i, ad, lua_topointer(L, i));
            break;
        case LUA_TLIGHTUSERDATA:
            printf("%d[%d]: light userdata:%p\n", i, ad, pvalue(o));  // 等价于printf("%d[%d]: light userdata:%p\n", i, ad, lua_topointer(L, i));
            break;
        case LUA_TUSERDATA:
            printf("%d[%d]: full userdata:%p\n", i, ad, uvalue(o));
            break;
        case LUA_TTHREAD:
            printf("%d[%d]: thread:%p\n", i, ad, thvalue(o));  // 等价于printf("%d[%d]: thread:%p\n", i, ad, lua_topointer(L, i));
            break;
        default: printf("%d[%d]: %s\n", i, ad, lua_typename(L, t)); break;
        }
        i--; ad--;
    }
    printf("---------------------------------------------\n");
}

 

上面示例的不同类型变量压栈的最终结果如下:

 

C++解释执行lua文件

Test1.lua内容如下:

print("Hello, Lua!")

LuaTest.cpp代码如下:

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open(); // 创建一个lua虚拟机
    luaL_openlibs(L);  // 打开状态机L中的所有 Lua 标准库
    luaL_dofile(L, "Test1.lua"); // 载入Test.lua并解释执行
    lua_close(L); // 关闭lua虚拟机
    return 0;
}

注:luaL_dofile函数实际上是执行了luaL_loadfile来加载lua文件,加载成功之后会编译该代码块为一个Lua闭包放置在栈顶,并返回0。由于返回值为假,会继续调用lua_pcall来执行该Lua闭包,最后把该Lua闭包弹出栈。

#define luaL_dofile(L, fn) \
    (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

 

C++访问lua

C++调用lua函数

Test2.lua内容如下:

function add(x,y)
    return x + y
end

mytable={}

function mytable.StaticFunc()
    print("mytable.StaticFunc called.")
end

function mytable:Func()
    print("mytable:Func self:", self)
end

LuaTest.cpp代码如下:

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open();
    luaL_openlibs(L);
    luaL_dofile(L, "Test2.lua");

    lua_getglobal(L, "add"); // 获取全局函数add,并压入栈顶
    lua_pushinteger(L, 30); // 将整型的值30压入栈顶
    lua_pushinteger(L, 50); // 将整型的值50压入栈顶
    lua_call(L, 2, 1); // 对栈顶的30和50执行add函数调用,执行完后将30, 50,add弹出栈,将结果80压栈  注:2为参数个数,1为返回值个数

    int sum = (int)lua_tointeger(L, -1); // 将栈顶80赋值给sum变量
    lua_pop(L, 1); // 从栈顶弹出1个元素

    printf("The sum is: %d\n", sum);

    // 调用mytable表的静态函数
    lua_getglobal(L, "mytable"); // 将名为mytable的全局table变量的值压栈
    lua_pushstring(L, "StaticFunc"); // 将函数名为StaticFunc压栈
    lua_gettable(L, -2); // 从索引为-2处的表中,读取key(在栈顶处)为StaticFunc的函数名  读取成功后,将key出栈,并将读取到的函数名入栈
    lua_call(L, 0, 0); // 执行完后将StaticFunc弹出栈  注: 第一个0表示参数个数为0,第二个0表示无返回值

    // 调用mytable表的成员函数  采用新方法获取函数名
    lua_getfield(L, -1, "Func");// 从索引为-1处的表中,读取key为Func的函数名  成功后将读取到的函数名入栈
    lua_pushvalue(L, -2); // 将索引为-2处的表复制一份并压入栈顶
    lua_call(L, 1, 0); // 执行完后将Func弹出栈  注: 1表示参数个数,即self指针,为当前table,第二个0表示无返回值

    lua_close(L);
    return 0;
} 

 

C++读写Lua中的全局变量

Test3.lua内容如下:

sayhi="Hello Lua!"
mytable={sex = "male", age=18}

LuaTest.cpp代码如下:

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open();
    luaL_openlibs(L);
    
    luaL_dofile(L, "Test3.lua");
    
    lua_settop(L, 0); //把栈上所有元素移除

    // ******读取名为sayhi的字符串全局变量的值******
    lua_getglobal(L, "sayhi");// 将名为sayhi的全局string变量的值压栈

    if (lua_isstring(L, -1) != 0)
    {
        const char* str = lua_tostring(L, -1); // 从栈顶读取字符串内容
        printf("str: %s\n", str);
    }

    // ******修改名为sayhi的字符串全局变量的值******
    lua_pop(L, 1); // 从栈顶弹出1个元素

    lua_pushstring(L, "Welcome Lua!"); // 将Welcome Lua!字符串压栈
    lua_setglobal(L, "sayhi");// 将栈顶的元素设置给全局变量sayhi

    lua_getglobal(L, "sayhi");
    const char* str2 = lua_tostring(L, -1); // str2为Welcome Lua!

    // ******读取名为mytable的table全局变量中的内容******
    lua_getglobal(L, "mytable"); // 将名为mytable的全局table变量的值压栈
    lua_pushstring(L, "sex"); // 将字符串sex压栈
    lua_gettable(L, -2); // 从索引为-2处的表中,读取key(在栈顶处)为sex的元素  读取成功后,将key出栈,并将读取到的元素入栈

    // 另外一种方式:读取table中的某个key的值
    lua_getfield(L, -2, "age");// 从索引为-2处的表中,读取key为age的元素  成功后将读取到的元素入栈
    int age = (int)lua_tointeger(L, -1); // 从栈顶读取age 为18
    const char* sex = lua_tostring(L, -2); // 从索引为-2处读取sex  为male

    // ******修改名为mytable的table全局变量中的内容******
    lua_pop(L, 2); // 从栈顶弹出2个元素
    lua_pushstring(L, "age"); // 将字符串age压栈
    lua_pushinteger(L, 20); // 将整数20压栈
    lua_settable(L, -3); // 设置索引为-3处的表的key(在栈顶下一个,即age)对应的value为20(在栈顶处)  设置成功后,将栈顶的2个元素都出栈   注:也可使用lua_rawset(L, -3)做一次直接赋值(不触发元方法)
    
    // 另外一种方式:设置table中的某个key的值
    lua_pushstring(L, "female"); // 将字符串female压栈
    lua_setfield(L, -2, "sex"); // 设置索引为-2处的表的key为sex对应的value为female(在栈顶处) 设置成功后,将栈顶的female出栈

    lua_getfield(L, -1, "sex"); // 从索引为-1处的表中,读取key为sex的元素  成功后将读取到的元素入栈
    lua_getfield(L, -2, "age"); // 从索引为-2处的表中,读取key为age的元素  成功后将读取到的元素入栈
    int age2 = (int)lua_tointeger(L, -1); // 从栈顶读取age2 为20
    const char* sex2 = lua_tostring(L, -2); // 从索引为-2处读取sex  为female

    lua_close(L);
    return 0;
}

 

Lua访问C++(无dll)

Lua调用C++全局函数

Test4.lua内容如下:

local avg, sum = average(10,20,30,40,50)
print("The average is ", avg)
print("The sum is ", sum)

LuaTest.cpp代码如下:

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

static int Average(lua_State *L)
{
    int n = lua_gettop(L);  // 获取栈上元素的个数
    double sum = 0;
    for (int i = 1; i <= n; ++i)
    {
        sum += lua_tonumber(L, i); // 依次去除索引为1到n的元素,并累加到sum
    }
    lua_pushnumber(L, sum / n); // 将sum/n计算得到平均值压栈
    lua_pushnumber(L, sum);  // 将sum压栈

    return 2;  // 表明有2个返回值
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open();
    luaL_openlibs(L);
    
    lua_register(L, "average", Average); // 将c++的Average函数注册成lua中的average方法
    
    luaL_dofile(L, "Test4.lua");

    lua_close(L);
    return 0;
}

注:lua_register先用lua_pushcfunction把在c++函数压入栈中,然后调用lua_setglobal来设置栈顶的c++函数对应lua函数名,最后弹出栈顶的c++函数。

这样就可以把lua函数和c++函数建立绑定关系,使得在后续的lua脚本中使用lua函数名来调用该c++函数。

#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n)))

 

Lua调用C++成员函数

下面示例也是MyArray的userdata的实现

Test5.lua内容如下:

function MyArrayTest(size)
  local a1 = myarray.new(size)
  myarray.set(a1, 1, 25.6)
  print(myarray.size(a1))
  print(myarray.get(a1, 1))
end

LuaTest.cpp代码如下:

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

class MyArray
{
public:
    int GetSize() { return size; }
    void SetSize(int insize) { size = insize; }

    double GetAt(int index) { return values[index]; }
    void SetAt(int index, double value) { values[index] = value; }
private:
    int size;
    double values[1];
};

static int newarray(lua_State* L)
{
    int n = (int)luaL_checkinteger(L, 1); // 检查栈上索引为1处的元素是否为整型,并返回该元素
    size_t nbytes = sizeof(MyArray) + (n - 1) * sizeof(double);
    MyArray* a = (MyArray*)lua_newuserdata(L, nbytes); // 创建一个userdata,并压栈
    a->SetSize(n);// 设置数组的大小
    return 1;//表示有1个返回值
}

static int setarray(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1);// 获取栈上索引为1处的元素,并转换为userdata指针
    int index = (int)luaL_checkinteger(L, 2);// 检查栈上索引为1处的元素是否为整型,并返回该元素
    double value = luaL_checknumber(L, 3);// 检查栈上索引为1处的元素是否为number类型,并返回该元素

    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    luaL_argcheck(L, 1 <= index && index <= a->GetSize(), 2, "index out of range");
    a->SetAt(index-1, value); // 设置数组索引为index-1处的值为value

    return 0; //表示没有返回值
}

static int getarray(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1);// 获取栈上索引为1处的元素,并转换为userdata指针
    int index = (int)luaL_checkinteger(L, 2);// 检查栈上索引为1处的元素是否为整型,并返回该元素

    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    luaL_argcheck(L, 1 <= index && index <= a->GetSize(), 2, "index out of range");
    double value = a->GetAt(index-1);// 获取数组索引为index-1处的值

    lua_pushnumber(L, value); // 将获取的值压栈

    return 1;//表示有1个返回值
}

static int getsize(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1); // 获取栈上索引为1处的元素,并转换为userdata指针
    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    lua_pushnumber(L, a->GetSize());    // 获取数组的大小并压栈

    return 1;//表示有1个返回值
}

static const struct luaL_Reg MyArrayLib[] = {
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {nullptr, nullptr}
};

int luaopen_MyArray(lua_State* L)
{
    luaL_newlib(L, MyArrayLib);
    return 1;
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open();
    luaL_openlibs(L);
    luaL_requiref(L, "myarray", luaopen_MyArray, 1); // 将MyArray相关方法注册到全局表中,lua中的名为myarray
    
    luaL_dofile(L, "Test5.lua");

    lua_getglobal(L, "MyArrayTest"); // 将函数名MyArrayTest压栈
    lua_pushinteger(L, 1000); // 传入MyArray的size为1000 压栈
    lua_call(L, 1, 0); // 执行完后将MyArrayTest弹出栈  注: 1表示参数个数,第二个0表示无返回值

    lua_close(L);
    return 0;
} 

注:luaL_newlib中一共包含3个函数

luaL_checkversion: 检查Lua版本是否一致
luaL_newlibtable: 创建一个table并压入栈顶,这其实也是一个宏,实际上调用的是lua_createtable
luaL_setfuncs: 将luaL_Reg函数列表设置给刚刚压入栈的表。luaL_Reg函数列表是一个名字(key)和函数指针(value)组成的数组。

#define luaL_newlib(L,l)  \
  (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))

 

Lua访问C++(独立dll模块)

Test6.lua内容如下:

local myarray=require "myLualib"

local avg, sum = average(10,20,30,40,50)
print("The average is ", avg)
print("The sum is ", sum)

function MyArrayTest(size)
  local a1 = myarray.new(size)
  myarray.set(a1, 1, 25.6)
  print(myarray.size(a1))
  print(myarray.get(a1, 1))
end


MyArrayTest(1000)

注1:local myarray=require "myLualib"等价于以下代码

local myarray = nil
local fnluaopen_myLualib = package.loadlib("myLualib.dll","luaopen_myLualib") -- 查找myLualib.dll中名为luaopen_myLualib函数
if fnluaopen_myLualib ~= nil then
    return myarray=fnluaopen_myLualib() -- 执行luaopen_myLualib函数
end

注2:为了保证能找到myLublib.dll,可将dll文件复制到Test5.lua文件的目录下

 

myLualib.dll模块

/*************************** myLualib.h ***************************/
#pragma once
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

#ifdef MYLUALIB_EXPORTS
#define MYLUALIB_API __declspec(dllexport)
#else
#define MYLUALIB_API __declspec(dllimport)
#endif
 
extern "C" MYLUALIB_API int luaopen_myLualib(lua_State *L);//定义导出函数


/*************************** myLualib.cpp ***************************/
#include "myLualib.h"
static int Average(lua_State *L)
{
    int n = lua_gettop(L);  // 获取栈上元素的个数
    double sum = 0;
    for (int i = 1; i <= n; ++i)
    {
        sum += lua_tonumber(L, i); // 依次去除索引为1到n的元素,并累加到sum
    }
    lua_pushnumber(L, sum / n); // 将sum/n计算得到平均值压栈
    lua_pushnumber(L, sum);  // 将sum压栈

    return 2;  // 表明有2个返回值
}

class MyArray
{
public:
    int GetSize() { return size; }
    void SetSize(int insize) { size = insize; }

    double GetAt(int index) { return values[index]; }
    void SetAt(int index, double value) { values[index] = value; }
private:
    int size;
    double values[1];
};

static int newarray(lua_State* L)
{
    int n = (int)luaL_checkinteger(L, 1); // 检查栈上索引为1处的元素是否为整型,并返回该元素
    size_t nbytes = sizeof(MyArray) + (n - 1) * sizeof(double);
    MyArray* a = (MyArray*)lua_newuserdata(L, nbytes); // 创建一个userdata,并压栈
    a->SetSize(n);// 设置数组的大小
    return 1;//表示有1个返回值
}

static int setarray(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1);// 获取栈上索引为1处的元素,并转换为userdata指针
    int index = (int)luaL_checkinteger(L, 2);// 检查栈上索引为1处的元素是否为整型,并返回该元素
    double value = luaL_checknumber(L, 3);// 检查栈上索引为1处的元素是否为number类型,并返回该元素

    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    luaL_argcheck(L, 1 <= index && index <= a->GetSize(), 2, "index out of range");
    a->SetAt(index-1, value); // 设置数组索引为index-1处的值为value

    return 0; //表示没有返回值
}

static int getarray(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1);// 获取栈上索引为1处的元素,并转换为userdata指针
    int index = (int)luaL_checkinteger(L, 2);// 检查栈上索引为1处的元素是否为整型,并返回该元素

    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    luaL_argcheck(L, 1 <= index && index <= a->GetSize(), 2, "index out of range");
    double value = a->GetAt(index-1);// 获取数组索引为index-1处的值

    lua_pushnumber(L, value); // 将获取的值压栈

    return 1;//表示有1个返回值
}

static int getsize(lua_State* L)
{
    MyArray* a = (MyArray*)lua_touserdata(L, 1); // 获取栈上索引为1处的元素,并转换为userdata指针
    luaL_argcheck(L, a != nullptr, 1, "'array' expected");
    lua_pushnumber(L, a->GetSize());    // 获取数组的大小并压栈

    return 1;//表示有1个返回值
}

static const struct luaL_Reg MyArrayLib[] = {
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {nullptr, nullptr}
};

int luaopen_myLualib(lua_State* L)
{
    lua_register(L, "average", Average); // 将c++的Average函数注册成lua中的average方法
    
    luaL_newlib(L, MyArrayLib);
    return 1;
}

注:MyLuaLib模块不要直接集成lua虚拟机的源代码,应导入lua虚拟机的dll来使用,否则会报如下错误

multiple Lua VMs detected 

 

exe宿主程序

#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State* L = lua_open();
    luaL_openlibs(L);
    
    luaL_dofile(L, "Test6.lua");

    lua_close(L);
    return 0;
}

 

参考

游戏开发实现C++与Lua交互!

lua教程(runoob) 

Lua 5.3 Reference Manual(官方英文版)

lua5.3参考手册(runoob)(中文版)

Lua和C++交互详细总结

Lua和C++交互总结(很详细)

 

posted on 2020-12-27 23:26  可可西  阅读(2105)  评论(0编辑  收藏  举报

导航