找回密码
 立即注册
查看: 375|回复: 0

lua源码编译及与C/C++交互调用细节剖析

[复制链接]
发表于 2021-8-9 19:38 | 显示全部楼层 |阅读模式
概要

本文我们主要讨论:
    Lua源代码以及与C/C++交互工程搭建编译,LUA的栈和实现原理?LUA如何调用C函数?C/C++如何调用lua函数?C/C++访问与修改lua变量?lua与C++的类交互的细节?
最近搞一下脚本,以前用过一点点lua,但没有关注细节,这两天下载了源码研究一下lua和上层语言交互的原理,深究了一些细节,算是一步一步复现整个流程,也理解xlua,unLua,toLua这些开源方案解决的关键问题点。
补充:最近补了一下与c++交互userdata的更多细节,可以参考
lua编译与工程搭建

这一步比较熟悉的可以直接跳过。
    先去官网下载源码,我下载Lua 5.3.6 released版本,200kb左右的压缩包,并解压
    链接如下所有代码都在src中文件中,源代码中可以分为三块,一是lua的基本库,一个lua.c文件提供了解释器示例 ,luac.c提供编译器示例,后面两个不是我们关注的重点。我们选VS2019最新版本,新建一个工程,然后再建4个项目,分别是:LuaExe控制台项目,LuacExe控制台项目,LuaLib DLL动态库,TestLua控制台项目。

    LuaLib:将src文件夹中除去lua.c和luac.c外的所有程序文件都加到本工程中,为了方便与其它工程复用与动态加载,我们选择生成的目标为DLL动态库。
      在本项目属性-》General-》Configuration Type:Dynamic Library .dll选项。



      在本项目属性-》C/C++-》PreProcessor-》Preprocessor Definitions中添加:LUA_BUILD_AS_DLL预编译开关宏。




      为了进一步与C工程兼容,我们在本项目属性-》C/C++-》Advanced-》Compile As:**Compile as C Code (/TC)**选项。



    TestLua:由于我们要使用LuaLib工程生成的动态库,采用静态链接,所以我们要
      在本项目属性-》General-》Configuration Type:Application .exe选项。在本项目属性-》Linker-》General-》Additional Include Directories中添加:..\x64\Debug;让我们工程能顺利找到LuaLib工程产生的库文件,具体路径按需自己设定。在本项目属性-》Linker-》Input-》Additional Dependencies中添加:LuaLib.lib;让我们工程能顺利链接找到LuaLib工程动态库相关符号。本工程由于是C++写的,但LuaLib是C工程生成的,所以我们对于LuaLib工程的头文件,#include时要加上extern "C",告知编译器链接时不要按C++方法查找符号,这一步很重要。

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


    LuaExe:只需要将lua.c单个文件添加到工程中,然后设定同TestLua走一遍,不想调试的这个工程可以不加LuacExe:只需要将luac.c单个文件添加到工程中,然后设定同TestLua走一遍,不想调试的这个工程可以不加。如果加上,由于我们的是动态库,官方并非所有符号导出,这里应该编译不过了,少一个函数与两个变量导出。可以把LuaLib生成静态库,也可以src全部文件除lua.c直接全部加过来,当然也可以选择我们最后的,修改导出符号,这个要改动源码。
    为了代码共用,我选择最后一种,LuaLib工程加入自定义DLL_SUPPORT_LUAC_WIN宏。在luaconf.h文件中新加
    #if defined(DLL_SUPPORT_LUAC_WIN)
    #define LUAC_WIN_API_DLL __declspec(dllexport)
    #define LUAC_WIN_DATA_DLL __declspec(dllexport) extern
    #else
    #define LUAC_WIN_API_DLL __declspec(dllimport)
    #define LUAC_WIN_DATA_DLL __declspec(dllimport) extern
    #endif
在lopcodes.h文件中,修改两个全局变量的修饰符号
    LUAC_WIN_DATA_DLL const lu_byte luaP_opmodes[NUM_OPCODES];
    LUAC_WIN_DATA_DLL const char *const luaP_opnames[NUM_OPCODES+1];
在lundump.h文件中,修改一个函数的修饰符号
    LUAC_WIN_API_DLL int luaU_dump (lua_State* L, const Proto* f, lua_Writer w,
                         void* data, int strip);
然后编译即可,应该全部OK,Release版本也做同样设定。
LUA的栈和实现原理

lua之所以能够与上层语言交互,主要是用lua的虚拟栈进行了数据交互。机制上基本等同C/C++的栈,实质是一个struct,在非常重要的luaL_newstate函数调用时创建,满足先进后出。但其索引方式可以从1到n,也可以有-1到-n,正1永远表示栈底,负1永远表示栈顶,如下图所示。
当C/C++想调用lua中一个值时,lua将数值压入lua虚拟栈中,然后通过lua提供api来读取,当C/C++想向lua传入一个值时,C++通过lua提供的api将数值压入lua虚拟栈中,lua便可进行调用,通过这样交互完成相互调用。也就是说,C/C++并没有直接和lua数据完全的绝对互通,而是通过这个lua虚拟栈作桥梁。一旦数据进入lua那边,就是lua自己维护了,包含lua自己产生的变量或者管理的数据,,C++要想访问只能先让lua将其放到栈上再通过lua api来访问
lua栈内部也是分配TValue结构体数组作为栈,随着版本不同,这个有点变化,以目前我看的5.3.6来说,在lobject.h文件中有其定义。
typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;
#define TValuefields        Value value_; int tt_
typedef struct lua_TValue {
  TValuefields;
} TValue;
可以看出TValue包含数据value_与类型tt_两个部分,其中value_是Value联合体,这个联合体包括gc指针,bool,userdata,整数,及符点数,在64平台下占用8个字节。
    gc对象来说,比如string,table,thread等还需要堆上再分配内存,这里只是保存指针,生合周期结束后会被垃圾回收,有点像STL的容器。非gc对象来说,存储的类型也就是它面字面意思,lua_Integer实际是long long,lua_Number实际是double,只有bool类型用了int来存了。它们无需再单独向堆上分配内存,存在栈上,不用进行垃圾回收。
LUA调用C函数

lua规定调用注册过的c函数的形式是typedef int (*lua_CFunction) (lua_State *L);也就是普通C函数,还要经过变形转成lua_CFunction这种形式,才可以注册到lua中,然后被调用,反过来想想也是,普通C函数可没有统一形式,也无法直接用一种方式描式。
    普通C函数转成lua_CFunction形式转成lua_CFunction形式注册到lua全局表中,并分配一个函数名称,一般保留和C一样的名称在lua代码中调用注册过的函数,最终就会调用到普通C函数调用过程实际:lua先把注册的函数压入虚拟栈中,再将参数压入,执行C函数,然后将它们弹出栈,将返回值压入到虚拟栈顶
那么可以在TestLua工程中加入下面的代码,观看具体实现。
// rayhunter
// 2021/8/1
#include <iostream>
#include <string>
using namespace std;

// 必加上extern C,因为另一个工程的LuaLib.lib库我们C编译
extern "C"
{
        #include "lua.h"
        #include "lauxlib.h"
        #include "lualib.h"
}

//-------------------------  c code -----------------------------
//---------------------------------------------------------------
extern "C" long long CCode_MyAdd(long long a, long long b)
{
        return a + b;
}

extern "C" int pcf_CCode_MyAdd(lua_State* lua)
{
        long long n1 = lua_tointeger(lua, -1);
        long long n2 = lua_tointeger(lua, -2);
        long long iRet = CCode_MyAdd(n2,n1);
        lua_pushinteger(lua, iRet);

        return 1;
}

void Test_CCode_For_Lua()
{
        cout << "---------------test c code for lua--------------------" << endl;
        lua_State* lua = luaL_newstate();
        luaL_openlibs(lua);
        lua_register(lua, "CCode_MyAdd", pcf_CCode_MyAdd);
        luaL_dostring(lua, "print(\"lua add:\"..CCode_MyAdd(100,1000))");

        lua_close(lua);
        cout << "---------------test c code for lua--------------------" << endl;
}
    1)普通的CCode_MyAdd两个数字相加函数无法直接注册到lua中,转成lua_CFunction形式为pcf_CCode_MyAdd.2)通过lua_register将pcf_CCode_MyAdd注册成CCode_MyAdd加到全局表中,也就是在lua中调用“CCode_MyAdd”实际就是调用pcf_CCode_MyAdd,名称我们是可以随便取,只是为了显得调用和C一致,我们尽量取同名或者固定的变种,这里注册实际是个宏定义,#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))),3)在luaL_dostring,会将字符串转为lua文件并解析执行,里面有lua对CCode_MyAdd(100,1000)调用,最终打印:lua add:11004)调用过程会从lua库的代码,转回pcf_CCode_MyAdd代码执行,其真正执行还是C++代码,C++会先从lua虚执栈中取过lua_tointeger取出两个整数参数100和1000,注意顺序,从右到左压入的。然后调用我们普通C函数CCode_MyAdd(n2,n1);计算出结果,再将结果压入lua栈中,因为lua要用返回值。
C/C++调用lua函数

调用lua函数相对简单一些,但也有一些步骤,也可以像上面一样封装一下,显得和lua函数一样的名称和格式。
    通过lua_getglobal这个API获取lua函数,并将其压入虚拟栈如果有参数的话,通过类似lua_pushnumber,lua_pushstring这样的API,将参数压入虚拟栈调用lua_call(L,n,r)这样的宏接口,进行调用,告知虚拟机,函数有n个参数,r个返回值调用完成后,lua会将返回值压入栈,C/C++那端就可以通过类似lua_tonumber这样的API来读取函数返回值
下面给一个示例,lua中提供一个LuaCode_MyAdd两个数字相加的函数,C/C++进行调用,只需将下面代码放到上面Test_CCode_For_Lua()中lua_close前面就可以。
// lua加载字符,生成lua全局函数LuaCode_MyAdd
luaL_dostring(lua, "function LuaCode_MyAdd (x,y) return x+y end");
// lua栈和压入数据
lua_getglobal(lua, "LuaCode_MyAdd");
lua_pushinteger(lua, 100);
lua_pushinteger(lua, 200);
// C调用lua中的函数,2个传入参数,1个返回参数
lua_call(lua, 2, 1);
cout << "lua function ret:" << lua_tointeger(lua, -1) << endl;
// 栈回到原始状态
lua_pop(lua, 1);
C/C++访问与修改lua变量

调用lua变量跟函数差不多,也是先查找压栈,然后再从栈顶转换。
    通过lua_getglobal/lua_getlocal等这样API获取lua变量,并将其压入虚拟栈C/C++那端就可以通过类似lua_tonumber这样的API来转换读取lua的变量值如果lua值为table类型,通过lua_getfield和lua_setfield获取和修改表中元素的值
下面是一个示例,这次我们将lua文件不用字符串了,直接写入文本文件ccode.lua中,放到TestLua工程能直接访问的目录中,文本提供lua变量与lua函数。
--ccode.lua 文件内容
LuaFileStr = "hello lua string"
LuaFileTable = {name = "HanMei", age = 18}
function LuaFileAdd(n1, n2)
return (n1 + n2);
end;
function ShowTable()
        print("Name:"..LuaFileTable.name);
        print("Age:"..LuaFileTable.age);
end在我们的Test_CCode_For_Lua()代码中,只需再加入下面代码,对lua文本文件中的变量访问与修改,及函数调用:
// c访问lua文件
if (luaL_dofile(lua, "ccode.lua"))
    cout << lua_tostring(lua, -1) << endl;
// lua中的函数
lua_getglobal(lua, "LuaFileAdd");
lua_pushnumber(lua, 300);
lua_pushnumber(lua,400);
lua_call(lua, 2, 1);
cout << "lua file function ret:" << lua_tonumber(lua, -1) << endl;
lua_pop(lua, 1);

// lua中的数据
lua_getglobal(lua, "LuaFileStr");
cout << "lua file str:" << lua_tostring(lua, -1) << endl;
lua_getglobal(lua, "LuaFileTable");
lua_getfield(lua, -1, "name");
cout << "lua file table name:" << lua_tostring(lua, -1) << endl;
lua_getfield(lua, -2, "age");
cout << "lua file table age:" << lua_tonumber(lua, -1) << endl;
lua_getglobal(lua, "ShowTable");
lua_call(lua, 0, 0);

// 修改
lua_pushstring(lua, "Lilei");
lua_setfield(lua, 2, "name");
lua_getfield(lua, 2, "name");
cout << "lua file new table name:" << lua_tostring(lua, -1) << endl;
lua_getglobal(lua, "ShowTable");
lua_call(lua, 0, 0);
<hr/>LUA与C++类交互

有了前面的知识,我们再搞这个就比较容易了,先看C++的类代码,我们用Student为例。
//-------------------------  cpp code -----------------------------
//---------------------------------------------------------------
class Student
{
public:
        Student(int age, const std::string& name, int schoolNum, int classNum) :
                iAge(age), sName(name), iSchoolNum(schoolNum), iClassNum(classNum) {
                cout << "Constructor Student" << endl;
        };
        ~Student() { cout << "Destructor Student" << endl; }

        void SetAge(int age) { iAge = age; }
        const int GetAge() const { return iAge; }
        void SetName(const std::string& name) { sName = name; }
        const std::string& GetName() const { return sName; }

        virtual void ShowSelfInfo() {
                cout << "Age:" << iAge <<
                        " Name:" << sName <<
                        " School:" << iSchoolNum <<
                        " Class:" << iClassNum << endl;
        };

private:
        int iAge;
        std::string sName;
        int iSchoolNum;
        int iClassNum;
};
C++中类与对象到底是什么?广义上说类不过就是公共数据+公共方法,对象就是一些私有数据+类中的公有数据和公共方法,其中公共方法还要细分为对象有关的成员方法以及与对象无关的静态成员方法。再翻译一层就是类的公共数据可以用全局变量表示,公共方法也就是全局函数,只是成员函数第一个参数是隐式的this指针,这点搞反汇编比较了解,反汇编层次可以没有什么C++对象,对象不过就是带有函数指针的结构体。仔细想想,有了这些是不是就可以和lua中Table对应起来了。我们在元表中创建方法,然后创建使用该元表的table。只要创建一个元表与C++的公共方法映射起来,在Lua中创建一个使用该元表的table时就相当于创建一个C++类,当我们在lua中调用元表中的元函数时就跳转到C++类中方法,回收table的时候我们就销毁C++类,所以我们元表里key为"__index"的值的为metatable本身,"__gc"的值为释放方法,然后将成员操作方法添加到元表metatable,。
    类对象C++: 私有数据 + 类(公共数据+公共方法)Table Lua: 私有数据 + 元表(元数据+元函数)
看一下关键代码:
static const luaL_Reg method[] = {
        {"SetAge",pcf_SetAge},
        {"GetAge",pcf_GetAge},
        {"SetName",pcf_SetName},
        {"GetName",pcf_GetName},
        {"ShowSelfInfo",pcf_ShowSelfInfo},
        {"Student", pcf_CreateStudentClassToLua },
        {"__gc",pcf_DeleteStudentClassToLua},
        {NULL,NULL}
};

int luaOpenStudent(lua_State* lua)
{
        luaL_newmetatable(lua, "Student");
        lua_pushvalue(lua, -1);
        lua_setfield(lua, -2, "__index");
        luaL_setfuncs(lua, method, 0);
        return 1;
}
    lua中luaL_newmetatable创建名为Student元表metatable通过lua_setfield将__index将其元表设为自己通过luaL_setfuncs将method公共方法添加到元表中,这些函数都是我们前面提到lua_CFunction方法。
我们要搞一个pcf_CreateStudentClassToLua创建函数,我们先将lua栈中构造参数提取出来,通过lua_newuserdata创得一个对象指针,将指针来new出来一个C++对象,再将Student元表与其关联,后面再lua通过:就可以找到对应方法了。
int pcf_CreateStudentClassToLua(lua_State* lua)
{
        int age = (int)luaL_checkinteger(lua, 1);
        std::string name = luaL_checkstring(lua, 2);
        int schoolNum = (int)luaL_checkinteger(lua, 3);
        int classNum = (int)luaL_checkinteger(lua, 4);
        // 创建userdata,搞到对象指针
        Student** ppStu = (Student**)lua_newuserdata(lua, sizeof(Student));
        (*ppStu) = new Student(age,name, schoolNum, classNum);
        // 获取元表
        luaL_getmetatable(lua, "Student");
        lua_setmetatable(lua, -2);

        return 1;
}
我们将这些C++剩余的方法和其它代码补充完整
Student* GetStudent(lua_State* lua, int arg)
{
        // 从栈顶取userdata,这个是C++的对象指针
        luaL_checktype(lua, arg, LUA_TUSERDATA);
        void* userData = luaL_checkudata(lua, arg, "Student");
        luaL_argcheck(lua, userData != NULL, 1, "user data error");
        return *(Student**)userData;
}

int pcf_DeleteStudentClassToLua(lua_State* lua)
{
        Student* pStu = GetStudent(lua,1);
        if(pStu)
                delete pStu;

        return 1;
}

int pcf_SetAge(lua_State* lua)
{
        Student* pStu = GetStudent(lua, 1);
        luaL_checktype(lua, -1, LUA_TNUMBER);
        int age = (int)luaL_checkinteger(lua, -1);
        pStu->SetAge(age);
        return 0;
}

int pcf_GetAge(lua_State* lua)
{
        Student* pStu = GetStudent(lua, 1);
        const int age = pStu->GetAge();
        lua_pushinteger(lua, age);
        return 1;
}

int pcf_SetName(lua_State* lua)
{
        Student* pStu = GetStudent(lua, 1);
        luaL_checktype(lua, -1, LUA_TSTRING);
        std::string name = luaL_checkstring(lua, -1);
        pStu->SetName(name);
        return 0;
}

int pcf_GetName(lua_State* lua)
{
        Student* pStu = GetStudent(lua, 1);

        const std::string& name = pStu->GetName();
        lua_pushstring(lua, name.c_str());
        return 1;
}

int pcf_ShowSelfInfo(lua_State* lua)
{
        Student* pStu = GetStudent(lua, 1);
        pStu->ShowSelfInfo();
        return 0;
}

static const luaL_Reg libs[] = {
        {"Student",luaOpenStudent},
        {NULL,NULL}
};

void luaRegisterCppStudentLibs(lua_State* lua)
{
        const luaL_Reg* lib = libs;
        for (; lib->func; lib++)
        {
                luaL_requiref(lua, lib->name, lib->func, 1);
                lua_pop(lua, 1);
        }
}

void Test_CPP_Code()
{
        cout << endl << endl << endl << "---------------- test cpp code for lua--------------------" << endl;

        lua_State* lua = luaL_newstate();
        luaopen_base(lua);
        luaRegisterCppStudentLibs(lua);
        luaL_dofile(lua, "student.lua");
        lua_close(lua);

        cout << "---------------- test cpp code for lua--------------------" << endl;
}
//---------------------------------------------------------------
//------------------------  cpp code -----------------------------
int main()
{
#ifdef _DEBUG
        _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
        _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
        _CrtSetDebugFillThreshold(0);
#endif

        cout << "hello lua and c/c++" << endl;
        Test_CCode_For_Lua();
        Test_CPP_Code();
        return 0;
}
为了测试我们将下面代码,直接写入文本文件student.lua中,放到TestLua工程能直接访问的目录中。
print("begin hello lua");
local stu = Student.Student(19,"Li Lei",111,22);
print(stu:GetName());
stu:ShowSelfInfo();
stu:SetAge(18);
stu:SetName("Han Meimei");
stu:ShowSelfInfo();
print("end hello lua");编译TestLua工程,直接运行测,我们会得到下面的打印结果
结语

到此我们已经全部分析完毕,由于才研究,可能有纰漏,欢迎指证。如果喜欢的麻烦点赞,收藏加关注,谢谢。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 04:19 , Processed in 0.128086 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表