零、前语

经过之前的文章共享,咱们现已知道怎么经过扩展函数来扩展 Lua 。但这儿涉及到一个问题,Lua 脚本中怎么运用 C/C++ 中的类型,怎么像操作目标相同操作 C/C++ 类型的实例。处理这一问题就需求用到 userdata 类型(用户数据类型)。

userdata 类型,分为两种:

  1. 彻底用户数据(full userdata)
  2. 轻量级用户数据(light userdata)

一、full userdata(彻底用户数据)

full userdata 为 Lua 言语供给了能够用来存储任何数据的原始内存区域,没有预界说的操作。 运用彻底用户数据的 C-API 为:lua_newuserdata

运用 full userdata 创立的内存不需求进行开释,Lua 会进行办理和开释。 而假如 full userdata 相关的目标需求开释,能够经过给 full userdata 设置元表,元表中设置 __gc 进行监听开释,调用相关的目标的开释办法进行开释,下面的第五小节会进行展现这一过程。

1、lua_newuserdata

#define lua_newuserdata(L,s)	lua_newuserdatauv(L,s,1)

描绘:

分配一块指定巨细的内存,然后将该内存以 userdata 的类型进行压栈,并回来该内存的指针。

参数:

  • 参数 L: Lua 状态的指针。
  • 参数 size: 要分配的 userdata 目标的字节巨细。

回来值:

回来指向该内存的指针,能够经过这个指针进行目标的操作。

2、举个比如

假设咱们需求向 Lua 露出一个 C++ 的类型:User ,在 Lua 脚本中能够创立该类型,而且调用该类型的办法。

第一步,界说一个 User 类。

class User {
private:
    std::string name;
    long long age;
public:
    std::string introduce() {
        std::stringstream info;
        info << "大家好。我叫" << name << "。本年" << age << "岁。请重视我。";
        return info.str();
    }
    void setName(std::string name) {
        this->name = std::move(name);
    }
    void setAge(long long age) {
        this->age = age;
    }
    std::string getName() {
        return this->name;
    }
    long long getAge() {
        return this->age;
    }
};

第二步,界说对 User 操作相关的办法,并将这些办法用作库办法,向 Lua 露出一个 user 库,能够经过该库创立 User 类型的 full userdata ,并操作背后真实 C++ 实例。

static int newUser(lua_State *L) {
    std::string name = luaL_checkstring(L, 1);
    long long age = luaL_checkinteger(L, 2);
    // 生成一个 User Data 并压入栈中
    auto *user = (User *) lua_newuserdata(L, sizeof(User));
    user->setName(std::string(name));
    user->setAge(age);
    return 1;
}
static int introduce(lua_State *L) {
    User *user = (User *) lua_touserdata(L, 1);
    lua_pushstring(L, user->introduce().c_str());
    return 1;
}
static int setName(lua_State *L) {
    User *user = (User *) lua_touserdata(L, 1);
    std::string name = luaL_checkstring(L, 2);
    user->setName(std::string(name));
    return 0;
}
static int setAge(lua_State *L) {
    User *user = (User *) lua_touserdata(L, 1);
    long long age = luaL_checkinteger(L, 2);
    user->setAge(age);
    return 0;
}
static int getName(lua_State *L) {
    User *user = (User *) lua_touserdata(L, 1);
    lua_pushstring(L, user->getName().c_str());
    return 1;
}
static int getAge(lua_State *L) {
    User *user = (User *) lua_touserdata(L, 1);
    lua_pushinteger(L, user->getAge());
    return 1;
}
static const struct luaL_Reg userlib[] = {
        {"new",       newUser},
        {"introduce", introduce},
        {"setName",   setName},
        {"setAge",    setAge},
        {"getName",   getName},
        {"getAge",    getAge},
        {nullptr,     nullptr}
};
int luaopen_user(lua_State *L) {
    luaL_newlib(L, userlib);
    return 1;
}

这儿预备了六个办法,下一步会将这六个办法作为 user 模块的办法。

  • newUser 办法:该办法会接收 Lua 传递过来的两个参数:名字和年纪,然后运用 lua_newuserdata 进行创立一个 full userdata ,将指针转为 User 类型的指针后进行赋值操作,最终回来给 Lua , Lua 便会获取到一个 userdata 类型的数据。
  • introduce 办法:会经过 lua_touserdata 获取并检查 Lua 传递的第一个参数是否为 userdata ,假如是则将其转为 User 类型指针,并调用 User 的 introduce 办法,并将 introduce 的回来值入栈回来给 Lua 调用点。
  • setName 办法和 setAge 办法:和 introduce 相同,会检测并获取到 User 指针,然后别离调用 setName 和 setAge 办法进行设置值。
  • getName 办法和 getAge 办法:和 introduce 相同,会检测并获取到 User 指针,然后别离调用 getName 和 getAge 办法进行获取值,并将其回来到 Lua 调用点。

能够经过 newUser 的办法,得知 lua_newuserdata 办法仅仅为咱们在 Lua 中开辟了咱们所需求尺度的内存块,至于这块内存详细是什么类型的,则由后续的 C 代码进行操作了。

第三步,将库加载到 Lua 中,并调用 Lua 脚本。

lua_State *L = luaL_newstate();
luaL_openlibs(L);
luaopen_user(L);
lua_setglobal(L, "user");
std::string fileName = PROJECT_PATH + "/10、userdata/user/1一般版别/user.lua";
if (luaL_loadfile(L, fileName.c_str()) || lua_pcall(L, 0, 0, 0)) {
    printf("can't run config. file: %sn", lua_tostring(L, -1));
}
lua_close(L);

第四步,编写 Lua 脚本,脚本中经过 user 库,就能够调用到第二步的办法,经过 newUser 进行创立一个 User 类型的 full userdata,然后经过露出的办法对其进行操作。

local myUser = user.new("江澎涌", 29)
print("type(user) =>>", type(user))
print("type(myUser) =>>", type(myUser))
print(user.introduce(myUser));
user.setName(myUser, "jiang peng yong");
user.setAge(myUser, 28);
print(user.introduce(myUser));
print("名字 -->> ", user.getName(myUser));
print("年纪 -->> ", user.getAge(myUser));

运行后输出的结果

type(user) =>>	table
type(myUser) =>>	userdata
大家好。我叫江澎涌。本年29岁。请重视我。
大家好。我叫jiang peng yong。本年28岁。请重视我。
名字 -->> 	jiang peng yong
年纪 -->> 	28

3、lua_touserdata

void *(lua_touserdata) (lua_State *L, int idx);

描绘:

用于从 Lua 栈上获取一个指向用户数据(userdata)的指针。

参数:

  • 参数 L: 指向 Lua state 的指针。
  • 参数 idx: 要获取的 userdata 在栈上的索引值。

回来值:

函数回来一个 void* 类型的指针,指向用户数据的实际内存地址。假如给定索引的 value 不是 userdata 类型,或者索引无效,则函数回来 NULL

4、怎么在 C/C++ 确保 full userdata 参数的正确性

4-1、代码的缝隙

从第二小点你会感受到,这段代码有两处缝隙:

  1. lua_touserdata 没有做 NULL 判别,直接将其转为 User* ,假如运用者传递的参数有问题时,会导致程序直接奔溃,没有收到任何的过错提示,花费大量时间排查。
  2. full userdata 是一个 void* 类型,关于 Lua 来说并不知道指针真实指向的类型数据,所以他能够传递任何 full userdata 给 C/C++ 函数库,但函数库中的函数并不知道此时的 full userdata 详细类型是有问题的,此时 full userdata 也不是 NULL 。进行转换为 User* 进行运用,这时便会出现问题,可能此时指向内存块正好能够获取到咱们想要的数据类型但数据是过错的,可能是直接奔溃。调用者此时又需求大量的时间排查,为什么出现这种偶发的奔溃和夹杂着数据过错的问题。

这些问题的首要问题点在于怎么差异不同类型的 full userdata (NULL 也就不算 full userdata 了)。

常用的处理办法是为每种类型创立唯一的元表。 每次创立 full userdata 时,用相应的元表进行标记,然后每逢获取 full userdata 时,检查其是否具有正确的元表。

由于 Lua 脚本代码中不能修改 full userdata 的元表,因此这不能绕过这检查。

4-2、怎么存储元表

在每次创立新的 full userdata 的时分,都需求带上同一个元表,这样后续才干进行检查,所以这个元表的存储需求借助 “注册表” 或 “上值” 。

在 Lua 言语中,惯例是将一切新的 C 言语类型注册到注册表中,用类型名作为索引,以元表作为值。 这儿需求谨慎的挑选类型名,以防止冲突。

4-3、怎么在 C 言语中运用元表

首要运用三个 C-API:

  1. luaL_newmetatable
  2. luaL_getmetatable
  3. lua_setmetatable
  4. luaL_checkudata

(1)luaL_newmetatable

int   (luaL_newmetatable) (lua_State *L, const char *tname);

描绘:

会创立一张新表(后续用作元表),然后将其压入栈顶,并将该表与 tname 以 key-value 的形式存入注册表中。

参数:

  • 参数 L: 指向 Lua 状态机(Lua state)的指针。
  • 参数 tname: 用于标识元表的字符串,这一字符串作为 key 和创立的元表相关存入注册表。

回来值:

该函数回来一个整数值,表明是否成功创立元表。

  • 假如元体现已存在,函数回来 0 。
  • 假如成功创立了一个新的元表,函数回来非 0 值。

(2)luaL_getmetatable

#define luaL_getmetatable(L, tname)	(lua_getfield(L, LUA_REGISTRYINDEX, (tname)))

描绘:

从注册表中获取与 tname 相关的元表,并将元表压入栈顶。

参数:

  • 参数 L: Lua 状态机(Lua state)指针。
  • 参数 tname: 要获取的元表称号。

回来值:

没有回来值。但运行了该函数后会将 tname 对应的元表压入栈顶,假如 tname 没有对应的元表,则会导致压入 nil 。

(3)lua_setmetatable

int   (lua_setmetatable) (lua_State *L, int objindex);

描绘:

用于为栈中索引 objindex 的 Lua 值设置元表。

参数:

  • 参数 L: Lua 状态机(Lua state)指针。
  • 参数 index: 需求设置元表的 Lua 变量,在栈中的索引值。

(4)luaL_checkudata

void *(luaL_checkudata) (lua_State *L, int ud, const char *tname);

描绘:

检查栈中指定位置 ud 上的目标是否与指定称号 tname 的元表相匹配。

参数:

  • 参数 L: Lua 状态机(Lua state)指针。
  • 参数 ud: 需求检查的 full userdata 的索引位置。
  • 参数 tname: 需求与 full userdata 比较的元表在注册表中存储的 key 称号。

回来值:

假如 ud 索引位置的元素不是用户数据,或该用户数据没有正确的元表,则 luaL_checkudata 会抛出异常至 Lua 脚本中。不然,luaL_checkudata 会回来这个 full userdata 的地址。

4-4、举个比如

将上面 User 的比如改造一下。

第一步,在库加载的函数中增加元表的创立。

static const std::string META = "Jiang.user";
int luaopen_userForMetatable(lua_State *L) {
    // 创立元表,存储在注册表中
    luaL_newmetatable(L, META.c_str());
    luaL_newlib(L, userlib);
    return 1;
}

第二步,在创立每个 full userdata 实例时,加上元表。

static int newUserForMetatable(lua_State *L) {
    std::string name = luaL_checkstring(L, 1);
    long long age = luaL_checkinteger(L, 2);
    // 生成一个 User Data 并压入栈中
    auto *user = (User *) lua_newuserdata(L, sizeof(User));
    user->setName(std::string(name));
    user->setAge(age);
    // 将 META.c_str() 的对应表入栈,然后相关到 -2 的表做元表
    luaL_getmetatable(L, META.c_str());
    lua_setmetatable(L, -2);
    return 1;
}

第三步,界说一个检测的元表的宏,然后替换每一处需求获取 full userdata 的代码。

// 界说检测元表宏
#define checkUser(L) (User *)luaL_checkudata(L, 1, META.c_str())
static int introduceForMetatable(lua_State *L) {
    // 替换获取 full userdata 的办法,进行检查是否合法的目标
    User *user = checkUser(L);
    lua_pushstring(L, user->introduce().c_str());
    return 1;
}
static int setNameForMetatable(lua_State *L) {
    // 替换获取 full userdata 的办法,进行检查是否合法的目标
    User *user = checkUser(L);
    std::string name = luaL_checkstring(L, 2);
    user->setName(std::string(name));
    return 0;
}
static int setAgeForMetatable(lua_State *L) {
    // 替换获取 full userdata 的办法,进行检查是否合法的目标
    User *user = checkUser(L);
    long long age = luaL_checkinteger(L, 2);
    user->setAge(age);
    return 0;
}
static int getNameForMetatable(lua_State *L) {
    // 替换获取 full userdata 的办法,进行检查是否合法的目标
    User *user = checkUser(L);
    lua_pushstring(L, user->getName().c_str());
    return 1;
}
static int getAgeForMetatable(lua_State *L) {
    // 替换获取 full userdata 的办法,进行检查是否合法的目标
    User *user = checkUser(L);
    lua_pushinteger(L, user->getAge());
    return 1;
}

编写 Lua 脚本进行运行,检测是否有检查 full userdata 的合法性。

local myUser = user.new("江澎涌", 29)
print(pcall(function()
    print(user.introduce(io.stdio))
end))
print(user.introduce(myUser))

会有以下输出

false	...CPP_2022/10、userdata/user/2增加元表检查/user.lua:10: bad argument #1 to 'introduce' (Jiang.user expected, got nil)
大家好。我叫江澎涌。本年29岁。请重视我。

5、怎么对 userdata 进行目标化操作

在编写 Lua 脚本的时分,会发现 user 的调用并不是目标的调用办法。从之前 《Lua 面向目标》 文章中得知,能够经过元表来完成这一目标。

Lua 会在找不到指定键时调用元表的 __index 办法;而 full userdata 中并没有键,所以 Lua 在每次访问都会调用元表。 基于这一点,能够给 full userdata 设置元表(在上一小节中现已完成),然后将需求调用的办法设置到该元表中,最终将元表的 __index 办法设置成自身,这样就能够在找不到办法的时分就从元表中进行查询办法。

5-1、举个比如

继续改造 User 的比如,首要的改造点在于加载库的办法。

luaopen_userForObj 办法中,别离履行以下两点:

  1. 复制一份元表,然后将其设置到 key 为 __index 中,这样达到上述的 full userdata 查找不到办法时,能够在元表中查找。
  2. 将 userlib_function 办法列表设置到元表中。
static const struct luaL_Reg userlib[] = {
        {"new",   newUserForObj},
        {nullptr, nullptr}
};
static const struct luaL_Reg userlib_function[] = {
        {"introduce", introduceForObj},
        {"setName",   setNameForObj},
        {"setAge",    setAgeForObj},
        {"getName",   getNameForObj},
        {"getAge",    getAgeForObj},
        {"__gc",      release},
        {nullptr,     nullptr}
};
int luaopen_userForObj(lua_State *L) {
    luaL_newmetatable(L, META.c_str());
    // 复制元表
    lua_pushvalue(L, -1);
    // metatable.__index = metatable
    lua_setfield(L, -2, "__index");
    // 组册元办法
    luaL_setfuncs(L, userlib_function, 0);
    luaL_newlib(L, userlib);
    return 1;
}

操作完成之后,就能够进行面向目标的编程了。

local myUser = user.new("江澎涌", 29)
print(myUser:introduce())
myUser:setName("jiang")
print("名字:", myUser:getName())
myUser:setAge(28)
print("年纪:", myUser:getAge())
myUser = nil;
collectgarbage();

最终输出

大家好。我叫江澎涌。本年29岁。请重视我。
名字:	jiang
年纪:	28
开释 User

值得注意

User 的析构函数是不会被 Lua 主动调用的,假如需求在 full userdata 被开释时开释 User 的相关资源,则需求像上面改造点那样,设置 __gc 元办法,办法的完成则履行 User 的开释逻辑。

二、light userdata

light userdata 数据类型是一个代表 C 言语指针的值,即是一个 void * 值。

light userdata 数据类型关于 Lua 是一个值并不是一个目标,所以 Lua 只需求经过 lua_pushlightuserdata 将指针压入栈中即可。

1、light userdata 和 full userdata 的差异

light userdata 不是缓冲区,而仅仅一个指针,而且没有元表。light userdata 不受 Lua 废物收集器办理,需求用户自行办理。

  • 由于 light userdata 没有元表,所以在 Lua 运用中无法像前面对 full userdata 相同进行类型判别。
  • full userdata 相较于 light userdata 开支并不会很大。关于给定内存巨细,full datauser 与 malloc 比较仅仅增加了一点开支。

2、light userdata 的用处

light userdata 的真实用处是持平性判别。

  • full userdata 是一个目标,只与自身持平。
  • light userdata 是一个 C 言语指针的值,因此它与一切表明相同指针的 light userdata 数据持平。

能够运用 light userdata 数据在 Lua 言语中查找 C 言语目标。

例如作为 “注册表” 的 key:

在之前 《C 函数中怎么保存 Lua 的数据》 的文章中,现已共享了 light userdata 的一种用法,将其他用作 “注册表” 的键,利用了同一 light userdata 每次压入 Lua 栈中都是相同的值,Lua 从“注册表”中获取到的 value 都相同元素。

3、举个比如

在之前 《C 函数中怎么保存 Lua 的数据》 的文章中,现已有运用过,就不再进行过多的展现运用流程。

这个比如中经过传入和取出检查 light userdata 在 Lua 中的体现。

第一步:界说一个 User 类。

class User {
private:
    std::string name;
    long long age{};
public:
    User() {
        printf("User 结构。n");
    }
    ~User() {
        printf("User 析构。n");
    }
    std::string introduce() {
        std::stringstream info;
        info << "大家好。我叫" << name << "。本年" << age << "岁。请重视我。";
        return info.str();
    }
    void setName(std::string name) {
        this->name = std::move(name);
    }
    void setAge(long long age) {
        this->age = age;
    }
    std::string getName() {
        return this->name;
    }
    long long getAge() {
        return this->age;
    }
};

第二步,创立 User 类,并将他经过 lua_pushlightuserdata 进行压入栈中,打印栈中元素和类型,最终取出元素进行强制转换为 User 指针类型(由于没有元表,所以没有办法能够判别真实类型),然后进行销毁开释。

lua_State *L = luaL_newstate();
luaL_openlibs(L);
User *user = new User();
lua_pushlightuserdata(L, user);
stackDump(L);
User *user1 = (User *) lua_touserdata(L, 1);
printf("%sn", user1->introduce().c_str());
delete user;
user = nullptr;
lua_close(L);

最终输出

User 结构。
栈顶
^ typename: userdata, value: userdata    
栈底
大家好。我叫。本年0岁。请重视我。
User 析构。

三、写在最终

Lua 项目地址:Github传送门 (假如对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支撑)

假如觉得本篇博文对你有所启示或是处理了困惑,点个赞或重视我呀

公众号查找 “江澎涌”,更多优质文章会第一时间共享与你。

Lua 中运用 C 言语的用户自界说类型——userdata