C++ 与 Lua 联合编程
在软件开发的广阔天地里,不同编程语言各有所长。C++ 以其卓越的性能、强大的功能和对硬件的直接操控能力,在系统开发、游戏引擎、服务器等底层领域占据重要地位,但c++编写的程序需要编译,这往往是一个耗时操作,特别对于大型程序而言编译可能耗费几十分钟;而Lua 则凭借其轻量级、可嵌入性和灵活的脚本特性,在游戏脚本、配置管理等方面大放异彩。当 C++ 与 Lua 携手合作,它们能够优势互补,创造出更强大、更灵活的应用程序。本文将带你逐步深入了解 C++ 和 Lua 联合编程,从基础概念到实际应用,领略这种编程方式的魅力。
一、 C++ 与 Lua 联合编程
(一)C++ 与 Lua 的特点
C++ 是一种静态类型的编程语言,它拥有丰富的特性,如面向对象编程、泛型编程等。这使得 C++ 在处理大规模、高性能的系统级任务时表现出色,像游戏引擎、操作系统组件等对性能和资源管理要求极高的场景,C++ 都是不二之选。
Lua 则是一种轻量级的脚本语言,它的语法简洁,易于学习和使用。Lua 的核心优势在于其可嵌入性,能够轻松地被集成到其他应用程序中,为程序提供灵活的脚本功能。在游戏开发中,Lua 常被用于编写游戏逻辑脚本、用户界面交互脚本,以及实现游戏的热更新功能,让开发者无需重新编译整个程序,就能修改游戏内容。
(二)联合编程
将 C++ 和 Lua 联合编程,就像是让两位高手相互配合。C++ 负责搭建坚实的底层框架,处理计算密集型任务和资源管理;Lua 则专注于实现灵活多变的业务逻辑和用户交互。这种合作方式不仅能提高开发效率,还能使应用程序更具扩展性和可维护性。比如在游戏开发中,C++ 实现游戏的核心引擎功能,包括图形渲染、物理模拟等;Lua 则用于编写游戏角色的行为逻辑、任务脚本和关卡配置,这样当需要修改游戏内容时,只需要更新 Lua 脚本,无需重新编译 C++ 代码,大大缩短了开发周期。
二、搭建联合编程环境
(一)安装 Lua
-
Windows 系统:从 Lua 官方网站下载 Windows 安装包,安装过程中记得勾选将 Lua 添加到系统环境变量。安装完成后,打开命令提示符,输入 “lua -v”,如果显示 Lua 的版本信息,就说明安装成功了。
-
Linux 系统:以 Ubuntu 为例,在终端中执行 “sudo apt-get install lua5.3” 命令,就能轻松完成安装。安装后,同样可以在终端输入 “lua -v” 来验证是否安装成功。或者通过官网安装,整个安装过程很简单,就算遇到问题网上也能找到大量解决方法。
关于lua的安装和基础可以参考:Lua 从基础入门到精通(非常详细),本文着重于联合编程,lua和c++基础不会展开。
(二)准备 C++ 开发环境
-
Windows 系统:推荐使用Visual Studio,官网点开下载,配置选择c++桌面应用
-
Linux 系统:自带
三、C++ 与 Lua 的基础交互
(一)第一个程序:hello lua
1.首先引入头文件,在代码开头,使用 extern "C"
是因为 C++ 支持函数重载,会对函数名进行修饰,而 Lua 是用 C 语言编写的,其函数名没有经过修饰。通过 extern "C"
可以确保 C++ 编译器以 C 语言的方式处理这些 Lua 头文件中的函数名,避免链接错误。lua.h
提供了 Lua 核心功能的接口,lauxlib.h
是 Lua 辅助库,提供了一些方便的函数,lualib.h
则用于打开 Lua 标准库。
extern "C"
{#include <lua.h>#include <lauxlib.h>#include <lualib.h>
}
2.main
函数是程序的入口点。lua_open()
函数用于创建一个新的 Lua 状态机,它就像是一个容器,管理着 Lua 解释器的所有状态信息。随后,luaopen_base(L)
、luaopen_string(L)
和 luaopen_table(L)
分别打开了 Lua 的基础库、字符串库和表库。这些库提供了 Lua 编程中常用的功能,比如基础的算术运算、字符串处理和表操作等。
int main(int argc, char* argv[])
{lua_State* L = lua_open(); luaopen_base(L);luaopen_string(L);luaopen_table(L);
3.luaL_loadfile(L, "main.lua")
尝试加载名为 main.lua
的 Lua 脚本文件。如果加载过程中出现错误,该函数会返回一个非零值。一旦发生错误,程序会打印 loadfile error:
提示信息,并通过 lua_tostring(L, -1)
从 Lua 栈中获取错误信息(这里的-1值读取lua栈顶,应为出现异常lua会把错误提示信息压入栈顶),将其打印出来,最后返回 -1 表示程序异常退出。
if(luaL_loadfile(L,"main.lua")){printf("loadfile error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}
4.lua_pcall(L, 0, 0, 0)
用于执行之前加载的 Lua 脚本。lua_pcall
是一个安全的调用函数,它会捕获 Lua 脚本执行过程中抛出的异常。第一个参数 L
是 Lua 状态机,第二个参数 0
表示传递给 Lua 脚本的参数数量,第三个参数 0
表示期望从 Lua 脚本获取的返回值数量,第四个参数 0
表示错误处理函数的索引。如果执行过程中出现错误,同样会把错误信息压入lua栈顶,通过 lua_tostring(L, -1)
从 Lua 栈中获取错误信息,最后返回 -1。
if(lua_pcall(L,0,0,0)){printf("pcall error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}
5。最后,lua_close(L)
用于关闭 Lua 状态机,释放相关的资源。
lua_close(L);return 0;
}
完整cpp文件:
extern "C"
{#include <lua.h>#include <lauxlib.h>#include <lualib.h>
}int main(int argc, char* argv[])
{lua_State* L = lua_open(); luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"main.lua")){printf("loadfile error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}if(lua_pcall(L,0,0,0)){printf("pcall error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}lua_close(L);return 0;
}
lua脚本(main.lua):
print("hello lua")
(二)lua调用c++实现的函数
1.基本函数调用
先来看完整代码
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int Ctest(lua_State* L)
{std::cout << "Cpp:Ctest" << std::endl;std::cout << lua_gettop(L) << std::endl;size_t len;const char* name = lua_tolstring(L,1,&len);int age = lua_tonumber(L,2);bool is = lua_toboolean(L,3);std::cout << lua_gettop(L) << std::endl;std::cout << "name: " << name << " age: " << age << " is: " << is << std::endl; return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctest",Ctest);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson1.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_close(L);return 0;
}
lua脚本(lesson1.lua):
ctest("xiaoming" , 13, nil)
我们定义的Ctest函数接收3个参数,字符串、数字、bool类型,代码运行时程序会把参数依次压入栈,压入完毕后从栈低开始从下往上算,第一个是字符串(索引1),第二个是数字(索引2),第三个是bool数据(索引3),我们分别用lua_tolstring,lua_tonumber,lua_toboolean从栈中取出这些数据并转换数据类型为CPP支持类型,这里的return 0指的是本函数的返回值个数是0个,也就是没有返回值。
在完成函数定义后,我们需要将定义的函数注册给lua脚本,使用lua_register(L,"ctest",Ctest);其中第一个参数是lua状态机,第二个参数是lua脚本中函数名,这个函数名可以和cpp中定义的函数名不同,第三个是cpp中实现函数的函数指针。
完成上述操作后,就可以在lua脚本中调用ctest函数了。
2.array类型数据作为参数
还是先来看完整代码
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int Ctestarr(lua_State* L)
{std::cout << "ctestarr" << std::endl;int len =luaL_getn(L,-1);std::cout << "len: " << len << std::endl;for(int i = 0; i < len ; i++){lua_pushnumber(L,i+1);lua_gettable(L,1);//pop index push table[index]size_t size;std::cout << lua_tolstring(L,-1,&size) << std::endl;lua_pop(L,1);}return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctestarr",Ctestarr);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson2.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_close(L);return 0;
}
脚本(lesson2.lua):
arr = {"xiaoming", "xiaohong", "xiaogang"}
ctestarr(arr)
我们定义的Ctestarr函数接收1个array数据,代码运行时程序会把参数压入栈,通过luaL_getn(L,-1)来计算array的长度,他的第一个参数是lua状态机,第二个是array在栈中位置,-1代表栈顶。获取长度后用佛如循环遍历读取array元素,每次读取先使用lua_pushnumber(L,i+1);向栈中压入一个索引值(lua索引从1开始,而不是从0开始,所以这里是i+1),然后执行lua_gettable(L,1);这个函数第二个参数是array在栈中位置,因为代码运行时程序会把参数压入栈,所以array一直在栈底(前面读取长度时用-1是因为栈内只有array,它即在栈底也在栈顶,而现在由于压入了索引,array已经不是栈顶了),lua_gettable(L,1);执行时会先对将lua栈中索引出栈,然后压入索引对于元素值,所以在tostring读取后记得执行lua_pop(L,1)恢复栈空间(lua_pop()函数第二个参数是出栈个数,出栈只从栈顶出,不是位置)。
3.带有键值对的table类型数据作为参数
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int CtestTable1(lua_State* L)
{std::cout << "ctesttable1" << std::endl;lua_pushnil(L);while(lua_next(L,1) != 0)//每次调用从先栈顶弹出一个值,然后push key, push value{std::cout << lua_tostring(L,-2) << ": " << lua_tostring(L,-1) << std::endl;lua_pop(L,1);}return 0;
}int CtestTable2(lua_State* L)
{std::cout << "ctesttable2" << std::endl;lua_getfield(L,1,"name");//会把value压入栈顶std::cout << lua_tostring(L,-1) << std::endl;lua_pop(L,1);lua_getfield(L,1,"age");//会把value压入栈顶std::cout << lua_tostring(L,-1) << std::endl;lua_pop(L,1);return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctesttable1",CtestTable1);lua_register(L,"ctesttable2",CtestTable2);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson3.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;return 0;
}
arr = {name="xiaoming",age = "18", id = "22300"}
ctesttable1(arr)
ctesttable2(arr)
两种方法可以读取,lua_next(L,1)函数第二个参数是table在栈中位置,它在执行时会先出战一个数据,然后入栈key,最后入栈value,也就是在第一次读取时我们要先手动压栈一个nil值。读取结束后再手动出栈一个值。
第二种方法是lua_getfield(L,1,"name");第二个参数是table在栈中位置,第三个参数是要读取的key值,执行结束会将输入key对于的value值压栈,读取结束后需要手动出栈以复原占空间。
4.带返回值的情况
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int CtestRe(lua_State* L)
{lua_pushstring(L,"return value");lua_pushnumber(L,100);lua_newtable(L);lua_pushstring(L,"key");lua_pushstring(L,"value");lua_settable(L,-3);lua_pushstring(L,"key2");lua_pushnumber(L,123);lua_settable(L,-3);return 3;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L, "ctestre", CtestRe);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson5.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;return 0;
}
a,b,c = ctestre()
print(a)
print(b)
for k,v in pairs(c) doprint(k,v)
end
我们在函数中执行lua_pushstring(L,"return value"); lua_pushnumber(L,100);向栈中压入一个string数据,一个number数据,执行lua_newtable(L);向栈中压入一个空table,然后一次压栈key值和value值,再执行lua_settable(L,-3);将压入的key和value加入table,这里的-3是之前压入的空table的栈中位置,因为压入了key和value,所以从占地开始从上往下第三个空间才是table,执行lua_settable后会把栈顶的key和value两个空间出栈,此时table又回到栈顶。
5.c++向lua中设置全局变量和读取lua中定义的全局变量
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);lua_pushstring(L,"value");//由c++设置全局变量lua_setglobal(L,"key");lua_pop(L,1);lua_pushnumber(L,18);//由c++设置全局变量lua_setglobal(L,"age");lua_pop(L,1);lua_newtable(L);lua_pushstring(L,"name");lua_pushstring(L,"xiaoming");lua_settable(L, -3);// 会把key,value出栈 lua_pushstring(L,"age");lua_pushnumber(L,13);lua_settable(L, -3);//还是-3lua_setglobal(L,"person");lua_pop(L,1);if(luaL_loadfile(L,"lesson6.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "1: " <<lua_gettop(L) << std::endl;lua_getglobal(L,"width");int width = lua_tonumber(L,-1);std::cout << "1: " <<lua_gettop(L) << std::endl;lua_pop(L,1);std::cout << "1: " <<lua_gettop(L) << std::endl;std::cout << "width = " << width << std::endl;lua_getglobal(L,"table");std::cout << "2: " <<lua_gettop(L) << std::endl;lua_getfield(L,-1,"age");std::cout << "2: " <<lua_gettop(L) << std::endl;std::cout << "age = " << lua_tonumber(L,-1) << std::endl;lua_pop(L,2);std::cout << "2: " <<lua_gettop(L) << std::endl;std::cout << lua_gettop(L) << std::endl;return 0;
}
print(key)
print(age)
for k,v in pairs(person) doprint(k,v)
end
table = {name = "xiaohong" , age = "16"}
width = 100
这部分很简单,要注意的就是栈空间的管理,并且设置全部变量的位置应该再pcall之前,读取应该在pcall之后,要不然读不到。
(三)c++调用lua实现的函数
1.调用函数
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"lesson7.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_getglobal(L,"event");lua_pushstring(L,"xiaoming ");std::cout << lua_gettop(L) << std::endl;if(lua_pcall(L,1,1,0) != 0){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;lua_pop(L,1);}else{std::cout << "pcall success: " << lua_tostring(L,-1) << std::endl;std::cout << lua_gettop(L) << std::endl;lua_pop(L,1);}std::cout << lua_gettop(L) << std::endl;return 0;
}
function event(a)print("event")print(a)return "lua_event"
end
lua_getglobal(L,"event");先将函数压栈,由于函数需要一个参数,lua_pushstring(L,"xiaoming ");把参数压栈,之后调用lua_pcall(L,1,1,0)执行,这个函数的第二个参数是"event"函数参数个数(1个),第二个是"event"函数返回值个数(1个)函数执行会把event和参数出栈,把"event"函数返回值入栈。
2.错误处理函数(lua_pcall第四个参数设置)
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"lesson8.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_getglobal(L,"err");int err = lua_gettop(L);lua_getglobal(L,"even");//故意少写一个t,触发错误lua_pushstring(L,"xiaoming ");if(lua_pcall(L,1,1,err) != 0){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout <<"CPP ERR: "<< error << std::endl;lua_pop(L,1);}else{std::cout << "pcall success: " << lua_tostring(L,-1) << std::endl;std::cout << lua_gettop(L) << std::endl;lua_pop(L,1);}std::cout << lua_gettop(L) << std::endl;return 0;
}
function err(e)print("LUA_ERR: "..e)return "lua_error"
endfunction event(a)print("event")print(a)return "lua_event"
end
lua_pacll第四个参数是错误处理函数在栈中位置,正常当不设置错误处理时当发生错误,撸啊会把错误提示入栈,当设置之后,lua会把错误提示作为参数出啊如错误处理函数,错误处理函数执行后再把其返回值(这里是“lua_err”)压入栈。
四、联合编程的挑战与应对策略
(一)内存管理
在 C++ 和 Lua 联合编程中,内存管理是一个重要问题。由于 C++ 需要手动管理内存,而 Lua 有自己的垃圾回收机制,在传递数据时需要特别小心。例如,当 C++ 创建一个对象并传递给 Lua 时,需要确保在 Lua 使用完该对象后,C++ 能够正确地释放内存。可以通过智能指针等方式来辅助内存管理,确保对象在不再使用时被正确释放。
(二)异常处理
C++ 和 Lua 的异常处理机制不同,在联合编程时需要统一处理异常,以确保程序的稳定性。可以在 C++ 中捕获 Lua 脚本执行过程中抛出的异常,并进行适当的处理。例如,使用 “lua_pcall” 函数代替 “lua_call” 函数,“lua_pcall” 函数可以捕获 Lua 函数执行过程中的异常,并将异常信息压入 Lua 栈,C++ 代码可以从栈中获取异常信息并进行处理。
(三)性能优化
虽然 C++ 性能较高,但在与 Lua 交互时,频繁的栈操作和数据传递可能会带来性能开销。为了优化性能,可以尽量减少不必要的栈操作,批量传递数据,而不是逐个传递。同时,对性能敏感的代码部分,可以使用 C++ 实现,而将逻辑相对简单、变化频繁的部分交给 Lua 处理。
C++ 与 Lua 联合编程为开发者提供了强大的工具,让我们能够充分发挥两种语言的优势。通过深入学习和实践,你可以利用这种编程方式开发出更具扩展性、灵活性和高性能的应用程序。无论是游戏开发、脚本化工具还是其他领域,C++ 与 Lua 的联合都能为你的项目带来新的活力。