C/C++和Lua混合编程

1. 概述

发布一款应用程序比较耗时,尤其是手机游戏应用还需要各种审查。一种简单方便的热更新,可以满足上述需求。静态编程语言生成框架,动态语言完成其他逻辑,这样可以达到热更新。lua由于其性能及简洁,是许多项目热更新时选择的动态开发语言。此文主要讲解C/C++和Lua的混合编程,主要针对Lua5.2及之后的版本(之前的版本接口略有调整)。

2. 编译Lua代码

2.1. Linux下编译

直接在指定目录执行以下命令即可完成编译,会生成liblua.a(静态库),lua(解释器),luac(编译器)。

curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
tar zxf lua-5.4.4.tar.gz
cd lua-5.4.4
make all test

2.2. Windows下编译

从官方路径https://www.lua.org/versions.html,下载相应Lua版本的源代码,解压到指定目录。

2.2.1. 编译静态库

打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LIB,编译生成liblua.lib静态库。

2.2.2. 编译解释器

打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LUA,编译生成lua.exe解释器。

2.2.3. 编译编译器

打开Visual Studio,新建一个C++静态库工程,将onlua.c文件(见附件)添加到工程中,并在C/C+±>Additional Include Directores框中指定Lua源代码目录。然后在C/C+±>Preprocessor->Preprocessor Definitions框中填入MAKE_LUAC,编译生成luac.exe编译器。

3. 给Lua编写扩展库

lua代码中添加my = require(“XXXX”),会按照搜索路径先搜索lua模块,再搜索名为XXXX的动态库。

3.1. 编写注册函数

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

int Add(lua_State* L)
{
	lua_Integer a = lua_tointeger(L, 1);
	lua_Integer b = lua_tointeger(L, 2);
	lua_pushinteger(L, a + b);
	return 1;
}

int Sub(lua_State* L)
{
	lua_Integer a = lua_tointeger(L, 1);
	lua_Integer b = lua_tointeger(L, 2);
	lua_pushinteger(L, a - b);
	return 1;
}

3.2. 编写导出函数

static luaL_Reg CustomLib[] = 
{ 
	{"Add", Add},
	{"Sub", Sub},
	{NULL, NULL} 
};

// Custom必须和动态库名字保持一致,必须导出C风格
extern "C" int luaopen_Custom(lua_State* L) 
{
	luaL_newlib(L, CustomLib);
	return 1;
}

3.3. 编译

  1. windows
    建立dll工程,并在C/C+±>Additional Include Directores框中指定Lua源代码目录,编译生成Custom.dll。详见附件。
  2. linux
    编译指定lua头文件目录…/src,执行以下命令生成Custom.so。详见附件。
    g++ add.cpp -I…/src -shared -o Custom.so

3.4. 测试

mylib = require("Custom")
print(mylib.Add(1, 2))
print(mylib.Sub(3, 2))

指定动态库目录

-- windows
package.cpath = "dir/?.dll;"..package.cpath
-- linux 
package.cpath = "dir\?.dll;"..package.cpath

4. C/C++调用Lua

  1. 创建lua虚拟机
lua_State* L = luaL_newstate();
  1. 载入默认全局lua库
luaL_openlibs(L);
  1. 加载lua源代码
    加载lua源代码,会扫描全部的代码,检测基本的语法。
if (0 != luaL_loadfile(L, strFilePath.c_str()))
{
	printf("%s\n", lua_tostring(L,-1)); 
	return 1;
}
  1. 执行栈上代码
int bRet = lua_pcall(L, 0, 0, 0);
if (0 != bRet)
{
	printf("%s\n", lua_tostring(L, -1)); 
	return 2;
}
  1. 执行附加代码
char* pLua = "print(123)";
if (0 != luaL_dostring(L, pLua))
{
	printf("%s\n", lua_tostring(L,-1)); 
	return 3;
}

5. C/C++和Lua互相调用

5.1. c/c++注册lua函数

C/CPP代码

// 待Lua调用的C注册函数。
int Add(lua_State* L)
{
	// 检查栈中的参数是否合法,1表示Lua调用时的第一个参数(从左到右),依此类推。
	// 如果Lua代码在调用时传递的参数不为number,该函数将报错并终止程序的执行。
	INT64 op1 = luaL_checkinteger(L,1);
	INT64 op2 = luaL_checkinteger(L,2);
	// 将函数的结果压入栈中。如果有多个返回值,可以在这里多次压入栈中。
	lua_pushinteger(L,op1 + op2);

	// 返回值用于提示该C函数的返回值数量,即压入栈中的返回值数量。
	return 1;
}

int Trace(lua_State *L) 
{
	int n = lua_gettop(L);  /* number of arguments */
	int i;
	for (i = 1; i <= n; i++) {  /* for each argument */
		size_t l;
		const char *s = luaL_tolstring(L, i, &l);  /* convert it to string */
		if (i > 1)  /* not the first element? */
			OutputDebugString(" ");
		OutputDebugString(s);
		lua_pop(L, 1);  /* pop result */
	}

	return 0;
}

// 在luaL_openlibs后调用
lua_register(L, "Add", Add);
lua_register(L, "Trace", Trace);

lua测试代码

print(Add(1, 2))
print(Sub(2, 1))

5.2. C/C++设置lua的全局变量和表

void SetValueOfVar(LPCSTR _lpcVarName, INT64 _nVal)
{
	lua_getglobal(L, _lpcVarName);
	lua_pop(L, -1);

	lua_pushinteger(L, _nVal);
	lua_setglobal(L, _lpcVarName);
}

void CLuaEngine::WriteItemOfTable(LPCSTR _lpcTableName, LPCSTR _lpcItemName, LPCSTR _lpcItem)
{
	lua_getglobal(L, _lpcTableName);
	lua_getfield(L, -1, _lpcItemName);

	lua_pushstring(L, _lpcItem);
	lua_setfield(L, 1, _lpcItemName);
	lua_pop(L, 1);
}

5.3. C/C++读取和遍历表

CString ReadItemOfTable(LPCSTR _lpcTableName, LPCSTR _lpcItemName)
{
	lua_getglobal(m_lState, _lpcTableName); 
	lua_getfield(m_lState, -1, _lpcItemName);
	const char* pName = lua_tostring(m_lState, -1);

	return pName;
}

void traverse_table(lua_State *L, int index)
{
    lua_pushnil(L); 
    // 现在的栈:-1 => nil; index => table
    while (lua_next(L, index))
    {
        // 现在的栈:-1 => value; -2 => key; index => table
        // 拷贝一份 key 到栈顶,然后对它做 lua_tostring 就不会改变原始的 key 值了
        lua_pushvalue(L, -2);
        // 现在的栈:-1 => key; -2 => value; -3 => key; index => table
        const char* key = lua_tostring(L, -1);
        const char* value = lua_tostring(L, -2);

        printf("%s => %s\n", key, value);

        // 弹出 value 和拷贝的 key,留下原始的 key 作为下一次 lua_next 的参数
        lua_pop(L, 2);
        // 现在的栈:-1 => key; index => table
    }
    // 现在的栈:index => table (最后 lua_next 返回 0 的时候它已经把上一次留下的 key 给弹出了)
    // 所以栈已经恢复到进入这个函数时的状态
}

5.4. C/C++注册lua库

  1. C/CPP代码
static luaL_Reg CustomLib[] = 
{ 
	{"Add", Add},
	{"Sub", Sub},
	{NULL, NULL} 
};

static int luaopen_algorithm(lua_State* L)
{
	// 创建导出库函数
	luaL_newlib(L, CustomLib);

	return 1;
}

void pre_loadlibs(lua_State* L)
{
	// 预加载扩展静态库
	luaL_getsubtable(L, LUA_REGISTRYINDEX, "_PRELOAD");

	lua_pushcfunction(L, luaopen_algorithm);
	lua_setfield(L, -2, "algo"); // algo为库名

	lua_pop(L, 1);   
}
  1. lua测试代码
my = require("algo")
print(my.Add(3, 8))

5.5. C/C++调用lua函数

function AddEx(x,y)
	return x+y
end
  1. 方法1
lua_getglobal(L, "AddEx");  //lua_getglobal函数负责从全局表中找到那个“AddEx”字段对应的数据,并把它送到代码块的栈顶

lua_pushnumber(L, 5);  // 把参数x压入到我们L虚拟机的栈中,至此栈顶数据是x,接着是function,也就是我们的“AddEx”函数
lua_pushnumber(L, 7);  // 同上,把参数y压入L虚拟机栈中,栈顶y,接着x,再往下就是function,也就是“AddEx”
lua_call(L, 2, 1);     // 2个参数,1个返回值   
// 取出返回值
std::cout <<"返回的值是:"<< (lua_tointeger(L,-1)) << std::endl;
  1. 方法2
    此方法不能获取返回值
if (0 != luaL_dostring(L, "AddEx(3, 4)"))
{
	printf("%s\n", lua_tostring(L,-1)); 
	return 3;
}

5.6 userdata

传递一个byte类型的Buff,并且提供下标操作。

  1. C/CPP代码

typedef struct _BYTE_ARRAY
{
    unsigned int nSize;
    char* pBuff;
}BYTE_ARRAY;

static int ByteArrayConstructor(lua_State * l)
{
    unsigned int size = static_cast<unsigned int>(luaL_checkinteger(l, 1));
    
    // We could actually allocate Foo itself as a user data but 
    // since user data can be GC'ed and we gain unity by using CRT's heap 
    // all along.
    BYTE_ARRAY * udata = (BYTE_ARRAY *)lua_newuserdata(l, sizeof(BYTE_ARRAY));
    (udata)->nSize = size;
    (udata)->pBuff = new char[size]();
    
    // Usually, we'll just use "Foo" as the second parameter, but I 
    // say luaL_Foo here to distinguish the difference:
    //
    // This 2nd parameter here is an _internal label_ for luaL, it is 
    // _not_ exposed to Lua by default.
    //
    // Effectively, this metatable is not accessible by Lua by default.
    luaL_getmetatable(l, "luaL_ByteArray");
    
    // The Lua stack at this point looks like this:
    //     
    //     3| metatable "luaL_foo"   |-1
    //     2| userdata               |-2
    //     1| string parameter       |-3
    //
    // So the following line sets the metatable for the user data to the luaL_Foo 
    // metatable
    //
    // We must set the metatable here because Lua prohibits setting 
    // the metatable of a userdata in Lua. The only way to set a metatable 
    // of a userdata is to do it in C.
    lua_setmetatable(l, -2);
    
    // The Lua stack at this point looks like this:
    //     
    //     2| userdata               |-1
    //     1| string parameter       |-2
    // 
    // We return 1 so Lua callsite will get the user data and 
    // Lua will clean the stack after that.
    
    return 1;
}

BYTE_ARRAY * ByteArrayCheck(lua_State * l, int n)
{
    // This checks that the argument is a userdata 
    // with the metatable "luaL_Foo"
    return (BYTE_ARRAY *)luaL_checkudata(l, n, "luaL_ByteArray");
}

static int ByteArraySet(lua_State * l)
{
    BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
    int nIdx = (int)luaL_checkinteger(l, 2);
    int nVal = (int)luaL_checkinteger(l, 3);
    
    luaL_argcheck(l, nIdx <= foo->nSize, 1, "index out of range");
    
    foo->pBuff[nIdx-1] = char(nVal);
    
    // The Lua stack at this point looks like this:
    //     
    //     4| result string          |-1
    //     3| metatable "luaL_foo"   |-2
    //     2| userdata               |-3
    //     1| string parameter       |-4
    //
    // Return 1 to return the result string to Lua callsite.
    
    return 0;
}

static int ByteArrayGet(lua_State * l)
{
    BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
    int nIdx = (int)luaL_checkinteger(l, 2);
    
    luaL_argcheck(l, nIdx <= foo->nSize, 1, "index out of range");
    
    lua_pushinteger(l, foo->pBuff[nIdx-1]);
    // The Lua stack at this point looks like this:
    //     
    //     4| result string          |-1
    //     3| metatable "luaL_foo"   |-2
    //     2| userdata               |-3
    //     1| string parameter       |-4
    //
    // Return 1 to return the result string to Lua callsite.
    
    return 1;
}


static int ByteArrayDestructor(lua_State * l)
{
    BYTE_ARRAY * foo = ByteArrayCheck(l, 1);
    delete[] foo->pBuff;
    
    return 0;
}
luaL_Reg sFooRegs[] =
{
    { "new", ByteArrayConstructor },
    { "set", ByteArraySet },
    { "get", ByteArrayGet },
    { "__gc", ByteArrayDestructor },
    { NULL, NULL }
};

int luaopen_my(lua_State *L) {
    luaL_newlib(L, sFooRegs);
    return 1;
}

static void RegisterFoo(lua_State * l)
{
    // Create a luaL metatable. This metatable is not 
    // exposed to Lua. The "luaL_Foo" label is used by luaL
    // internally to identity things.
    luaL_newmetatable(l, "luaL_ByteArray");
    
    // Register the C functions _into_ the metatable we just created.
    luaL_setfuncs (l, sFooRegs, 0);
    
    // The Lua stack at this point looks like this:
    //     
    //     1| metatable "luaL_Foo"   |-1
    lua_pushvalue(l, -1);
    
    // The Lua stack at this point looks like this:
    //     
    //     2| metatable "luaL_Foo"   |-1
    //     1| metatable "luaL_Foo"   |-2
    
    // Set the "__index" field of the metatable to point to itself
    // This pops the stack
    //lua_setfield(l, -1, "__index");
    
    // The Lua stack at this point looks like this:
    //     
    //     1| metatable "luaL_Foo"   |-1
    
    // The luaL_Foo metatable now has the following fields
    //     - __gc
    //     - __index
    //     - add
    //     - new
    
    // 那么现在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);
    
    // Now we use setglobal to officially expose the luaL_Foo metatable 
    // to Lua. And we use the name "Foo".
    //
    // This allows Lua scripts to _override_ the metatable of Foo.
    // For high security code this may not be called for but 
    // we'll do this to get greater flexibility.
    lua_setglobal(l, "ByteArray");
}
  1. lua测试代码
local byteArray = ByteArray.new(100);
byteArray[2] = 33;
print(byteArray[2])

6. 调试

应用程序内嵌入lua虚拟机执行lua代码时,没有太好的方法调试lua代码。通过打印信息来查看代码运行的效果,调试效率不高。有没有一种方法,可以单步调试lua代码呢?腾讯开源了一款基于VS Code调试的插件luapanda,使用简单方便。

  1. 在VS Code中安装luapanda。
  2. 安装相应lua版本的luasocket,https://github.com/lunarmodules/luasocket。
  3. 用VS Code打开需要调试的lua代码,在最开始添加require(“LuaPanda”).start(“127.0.0.1”,8818);
  4. 在调试窗口启动luapda。
  5. 启动包括lua代码的应用程序,VS Code即会停止在require(“LuaPanda”).start(“127.0.0.1”,8818);
  6. 更多信息参见:https://github.com/Tencent/LuaPanda/blob/master/Docs/Manual/access-guidelines.md

7. Lua代码的加密

7.1. 加密lua源代码

  1. 直接使用对称加密算法对源代码进行加密。
  2. 在载入lua代码之前,解密文件。
// 如果文件加密,可以在此处解密文件Buff(szLuaBuff)
if (0 != luaL_loadbuffer(L, szLuaBuff, nFileSize, NULL))
{
	printf("%s\n", lua_tostring(L,-1)); 
	return 1;
}
  1. 详情见附件。

7.2. 修改Opcode

对源代码的加密,通过OD追踪到luaL_loadbuffer,然后dump出所有Buff,即为代码明文。为了更进一步加强反破解,可以采用运行字节码,然后用修改OpCode来加强代码安全。

  1. 修改lopcodes.h中的opcode枚举顺序。
  2. 对应修改lopnames.h中的opnames顺序。
  3. 编译生成luac和lua静态库。
  4. 利用luac对lua源代码,生成字节码。
  5. 利用luaL_loadbuffer执行字节码。

7.3. 加密字节码并修改Opcde

  1. 采用修改OpCode生成字节码文件。
  2. 加密字节码文件。
  3. 在载入lua代码之前,解密文件,并调用luaL_loadbuffer执行字节码。
  4. 详见附件。

你可能感兴趣的:(C-C++,编程相关,lua,c++,c语言)