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出棧,並將自己入棧

 

打印棧

#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個元素都出棧
    
    // 另外一種方式:設置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++交互總結(很詳細)

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM