当前位置: 首页 > news >正文

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

  1. Windows 系统:从 Lua 官方网站下载 Windows 安装包,安装过程中记得勾选将 Lua 添加到系统环境变量。安装完成后,打开命令提示符,输入 “lua -v”,如果显示 Lua 的版本信息,就说明安装成功了。

  2. Linux 系统:以 Ubuntu 为例,在终端中执行 “sudo apt-get install lua5.3” 命令,就能轻松完成安装。安装后,同样可以在终端输入 “lua -v” 来验证是否安装成功。或者通过官网安装,整个安装过程很简单,就算遇到问题网上也能找到大量解决方法。

关于lua的安装和基础可以参考:Lua 从基础入门到精通(非常详细),本文着重于联合编程,lua和c++基础不会展开。

(二)准备 C++ 开发环境

  1. Windows 系统:推荐使用Visual Studio,官网点开下载,配置选择c++桌面应用

  2. 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 的联合都能为你的项目带来新的活力。

相关文章:

  • [预备知识]6. 优化理论(二)
  • 如何配置NGINX作为反向代理服务器来缓存后端服务的响应?
  • 微信小程序 自定义组件 标签管理
  • [SoC]AXI总线Performance验证方案
  • 【AI面试准备】Git与CI/CD及单元测试实战指南
  • [Linux]从零开始的STM32MP157 Buildroot根文件系统构建
  • mindyolo填坑
  • 如何利用dify 生成Fine‑tune 需要的Alpaca 格式数据
  • 正则表达式与文本三剑客grep、sed、awk
  • linux指令中的竖线(“|”)是干啥的?【含实例展示】
  • 数据库系统概论|第五章:数据库完整性—课程笔记1
  • 【服务器通信-socket】——int socket(int domain, int type, int protocol);
  • DBeaver连接人大金仓数据库V9
  • 上位机知识篇---PSRAM和RAM
  • 【算法基础】三指针排序算法 - JAVA
  • Google机器学习系列 - 监督学习
  • 50.【必备】二分答案法与相关题目
  • 玩转Docker(一):基本概念
  • STM32——GPIO
  • JAVA继承详细总结
  • 辛涛任山东第一医科大学副校长,曾为“博士服务团”成员
  • 人形机器人,最重要的还是“脑子”
  • 五一假期前两日,多地党政主官暗访景点、商圈安全工作
  • 政府效率部效果不佳?马斯克有意寻求支持,含糊表态部门未来
  • 据报特斯拉寻找新CEO,马斯克财报会议上表态:把更多时间投入特斯拉
  • 神十九都带回了哪些实验样品?果蝇等生命类样品已交付科学家