Lua Userdata


(一)通過一個簡單的例子來看一下userdata的用法:

寫一個C的Lua庫,讓Lua能夠訪問C的數組,借助userdata來實現。

(1)VS中新建一個DLL工程,設置好lua庫的包含目錄、鏈接庫;

(2)新建一個源文件main.cpp,代碼如下:

#include <stdio.h>
#include <string.h>

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

typedef struct NumArray
{
    int size;
    double values[1];
}NumArray;

// lua語句:newarray(size)
extern "C" int newarray(lua_State* L)
{
    int n = luaL_checkint(L, 1);
    size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
    NumArray* a = (NumArray*)lua_newuserdata(L, nbytes);
    a->size = n;
    return 1;    // 新建的userdata會壓棧
}

// lua語句:setarray(userdata, index, value)
extern "C" int setarray(lua_State* L)
{
    NumArray* a = (NumArray*)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2);
    double value = luaL_checknumber(L, 3);

    luaL_argcheck(L, a != NULL, 1, "array excepted");
    luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");

    a->values[index - 1] = value;
    return 0;
}

// lua語句:getarray(userdata, index)
extern "C" int getarray(lua_State* L)
{
    NumArray* a = (NumArray*)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2);

    luaL_argcheck(L, a != NULL, 1, "array excepted");
    luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");

    lua_pushnumber(L, a->values[index - 1]);
    return 1;
}

// lua語句:getsize(userdata)
extern "C" int getsize(lua_State* L)
{
    NumArray* a = (NumArray*)lua_touserdata(L, 1);
    luaL_argcheck(L, a != NULL, 1, "array excepted");
    lua_pushnumber(L, a->size);
    return 1;
}

static const struct luaL_reg arraylib[] = 
{
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
    const char* libName = "array";
    luaL_register(L, libName, arraylib);
    return 1;
}

(3)編譯生成名為array.dll的文件,並將array.dll放在luaforwindows的clibs子目錄下,該目錄下都是為lua寫的c庫,或者將其放到本地注冊的Lua環境變量的某個目錄下;

(4)lua測試:

require "array"

a = array.new(100)
print(array.size(a))

for i = 1, 100 do
    array.set(a, i, i)
end

print(array.get(a, 10))

上面代碼中的關鍵函數:

  void *lua_newuserdata (lua_State *L, size_t size);
  新建full userdata。
  (1)分配一塊指定大小的內存;
  (2)將該full userdata壓棧;
  (3)返回該內存塊的地址給主機程序,主機程序能夠隨意使用這塊內存。

  void luaL_argcheck (lua_State *L,int cond,int arg,const char *extramsg);
  檢查條件是否滿足。

(二)利用metatable標識userdata來增加代碼的安全性

  上面的C庫是有缺陷的,比如我們怎么確保例子中setarray的第一個參數就是我們想要的數組userdata,而不是別的不相關的userdata呢?userdata是一種lua類型,它可以用來表示宿主語言中的各種自定義類型對象,為了區分特定類型,我們使用的方法是:
  我們單獨為該數組創建一個metatable,每次創建數組userdata時,我們設置其和metatable的關聯。每次我們訪問數組的時候,都檢查一下其是否有一個正確的metatable即可。也就是利用不同的metatable來標記不同類型的userdata。因為Lua代碼不能夠改變userdata的metatable,所以Lua不會偽造我們的代碼。

  所以我們對上面的例子進行一些改進,給數組userdata添加一個類型標識,C庫代碼如下:

#include <stdio.h>
#include <string.h>

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

typedef struct NumArray
{
    int size;
    double values[1];
}NumArray;

// lua語句:newarray(size)
extern "C" int newarray(lua_State* L)
{
    int n = luaL_checkint(L, 1);
    size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
    NumArray* a = (NumArray*)lua_newuserdata(L, nbytes);

    // 獲取預先創建好的metatable,並設置給新建的userdata
    luaL_getmetatable(L, "LuaBook.array");
    lua_setmetatable(L, -2);

    a->size = n;
    return 1;    // 新建的userdata會壓棧
}

// 輔助函數,檢查數組userdata的metatable是否為LuaBook.array(可理解為是否是LuaBook.array類型的userdat)
static NumArray* checkarray(lua_State* L)
{
    void* ud = luaL_checkudata(L, 1, "LuaBook.array");
    luaL_argcheck(L, ud != NULL, 1, "array expcected");
    return (NumArray*)ud;
}

// 輔助函數,獲取索引處的指針
static double* getelem(lua_State* L)
{
    NumArray* a = checkarray(L);
    int index = luaL_checkint(L, 2);
    luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");
    return &a->values[index - 1];
}

// lua語句:setarray(userdata, index, value)
extern "C" int setarray(lua_State* L)
{
    double newvalue = luaL_checknumber(L, 3);
    *getelem(L) = newvalue;
    return 0;
}

// lua語句:getarray(userdata, index)
extern "C" int getarray(lua_State* L)
{
    lua_pushnumber(L, *getelem(L));
    return 1;
}

// lua語句:getsize(userdata)
extern "C" int getsize(lua_State* L)
{
    NumArray* a = checkarray(L);
    lua_pushnumber(L, a->size);
    return 1;
}

static const struct luaL_reg arraylib[] = 
{
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
    // 創建數組userdata將要用到的metatable
    luaL_newmetatable(L, "LuaBook.array");

    const char* libName = "array";
    luaL_register(L, libName, arraylib);
    return 1;
}

上面代碼中的關鍵函數:

  void luaL_newmetatable (lua_State *L, const char *tname);
  創建userdata可用的metatable。
  如果registry已經有tnme鍵值,則函數返回0;
  否則,創建一個[tname, metatable],並放入registry,並返回1。
  兩種情況下,都會講tname對應的值入棧。
  堆棧+1

  void *luaL_checkudata (lua_State *L, int index, const char *tname);
  檢查在棧中指定位置的對象是否為帶有給定名字的metatable(registry中鍵tname對應的值)的usertata。是則返回userdata地址,否則返回NULL。

  void luaL_getmetatable (lua_State *L, const char *tname);
  獲取registry中的tname對應的metatable,並入棧。注意區分lua_getmetatable函數。

  void luaL_setmetatable (lua_State *L, const char *tname);
  將棧頂對象的metatable設置為registry表中鍵tname對應的值。注意區分lua_setmetatable函數。

  int lua_getmetatable (lua_State *L, int index);
  獲取index對應的table的metatable,並入棧。如果該table沒有metatable,則返回0,且堆棧不變。

  void lua_setmetatable (lua_State *L, int index);
  將棧頂的table出棧並設置給index處的值作為metatable。
  堆棧-1

 (三)將上面的代碼改造成面向對象的方式

  類型為對象的userdata,可以使用如下的語法來操作對象的實例:

require "array"

a = array.new(100)

print(getmetatable(a))

print(a:size())

for i = 1, 100 do
    a:set(i, i)
end

print(a:get(10))

  思路大致如下:

  (1)array表只包含一個方法,也就是用來生成數組對象的new方法;

  (2)數組userdata帶有metatable用於類型識別;

  (3)userdata的metatable定義__index,那么,每當訪問數組的方法時,都會觸發__index這個metamethod(對於userdata來講,每次被訪問的時候元方法都會被調用,因為userdata根本就沒有任何key);

  (4)將metatable.__index設為該表metatable本身(__index可以為函數或者表,這里使用后者);

  (5)metatable包含其余所有的數組操作函數。

  那么每當調用userdata的某個方法時,比如a:size(),它等同於a.size(a),這時會觸發userdata的名為__index的metamethod,metatable的__index就是它本身,而metatable表中有size域,所以調用metatable的size(a)函數,就ok了。

#include <stdio.h>
#include <string.h>

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

typedef struct NumArray
{
    int size;
    double values[1];
}NumArray;

// lua語句:newarray(size)
extern "C" int newarray(lua_State* L)
{
    int n = luaL_checkint(L, 1);
    size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
    NumArray* a = (NumArray*)lua_newuserdata(L, nbytes);

    // 獲取預先創建好的metatable,並設置給新建的userdata
    luaL_getmetatable(L, "LuaBook.array");
    lua_setmetatable(L, -2);

    a->size = n;
    return 1;    // 新建的userdata會壓棧
}

// 輔助函數,檢查數組userdata的metatable是否為LuaBook.array(可理解為是否是LuaBook.array類型的userdat)
static NumArray* checkarray(lua_State* L)
{
    void* ud = luaL_checkudata(L, 1, "LuaBook.array");
    luaL_argcheck(L, ud != NULL, 1, "array expcected");
    return (NumArray*)ud;
}

// 輔助函數,獲取索引處的指針
static double* getelem(lua_State* L)
{
    NumArray* a = checkarray(L);
    int index = luaL_checkint(L, 2);
    luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");
    return &a->values[index - 1];
}

// lua語句:setarray(userdata, index, value)
extern "C" int setarray(lua_State* L)
{
    double newvalue = luaL_checknumber(L, 3);
    *getelem(L) = newvalue;
    return 0;
}

// lua語句:getarray(userdata, index)
extern "C" int getarray(lua_State* L)
{
    lua_pushnumber(L, *getelem(L));
    return 1;
}

// lua語句:getsize(userdata)
extern "C" int getsize(lua_State* L)
{
    NumArray* a = checkarray(L);
    lua_pushnumber(L, a->size);
    return 1;
}

// metatable的tostring函數 
int array2string(lua_State* L)
{
    NumArray* a = checkarray(L);
    lua_pushfstring(L, "array(%d)", a->size);
    return 1;
}

// 表本身只包含一個new方法
static const struct luaL_reg arraylib_f[] = 
{
    {"new", newarray},
    {NULL, NULL}
};

// 這些方法注冊在metatable里面
static const struct luaL_reg arraylib_m[] = 
{
    {"__tostring", array2string},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
    // 創建數組userdata將要用到的metatable
    luaL_newmetatable(L, "LuaBook.array");

    // 設置metatable的__index為metatable本身
    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);
    lua_settable(L, -3);
    // 注冊metatable的函數
    luaL_register(L, NULL, arraylib_m);

    // 創建array表,只有一個new函數
    luaL_register(L, "array", arraylib_f);
    return 1;
}

(四)以數組下標的形式訪問

  怎樣實現支持下表操作的語法來訪問userdata呢,就像下面一樣:

require "array"

a = array.new(100)
a[10] = 3
print(a[10])

  可以直接在lua中通過以下代碼實現:

local metaarray = getmetatable(newarray(1))
metaarray.__index = array.get
metaarray.__newindex = array.set

  對應到C中的實現方式如下:

static const struct luaL_reg arraylib[] = 
{
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
    // 創建數組userdata將要用到的metatable
    luaL_newmetatable(L, "LuaBook.array");
    luaL_register(L,"array",arraylib);

    // 那么現在metatable在棧底,array表在其上的位置
    // metatable.__index = array.get
    lua_pushliteral(L, "__index");
    lua_pushliteral(L, "get");
    lua_gettable(L, 2);
    lua_settable(L, 1);

    // metatable.__index = array.set
    lua_pushliteral(L, "__newindex");
    lua_pushliteral(L, "set");
    lua_gettable(L, 2);
    lua_settable(L, 1);

    return 0;
}

  將metatable的__index設為array的get方法,__newindex設為set方法即可。在讀取a[i]的時候會觸發__index,並將對象本身和參數同時傳遞給__index對應的函數,寫a[i]的時候原理一致。

(五)light userdata

  light userdata不同於full userdata,它有如下特點:
  (1)full userdata代表Lua中的C對象,light userdata代表一個C指針的值(也就是一個void *類型的值)。由於它是一個值,我們不能創建他們(同樣的,我們也不能創建一個數字)。
  (2)僅僅是一個指針,像數字一樣,沒有metatables,light userdata不需要垃圾收集器來管理她。
  (3)可以用於表示不同類型的對象,我們在Lua中使用light userdata表示C對象。

  因為它是一個值,任何指向同一個C地址的light userdata都相等。

  void lua_pushlightuserdata (lua_State *L, void *p);
  將一個light userdata入棧。

 (六)userdata相關的資源釋放

  Lua以__gc元方法的方式提供了finalizers。這個元方法只對userdata類型的值有效。當一個userdata將被收集的時候,並且userdata有一個__gc域,Lua會調用這個域的值(應該是一個函數):以userdata作為這個函數的參數調用。這個函數負責釋放與userdata相關的所有資源,比如說文件描述符、窗口句柄等。

 


免責聲明!

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



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