跳到主要内容
C++嵌入 Lua 脚本完整示例项目实战 | 极客日志
C++
C++嵌入 Lua 脚本完整示例项目实战 综述由AI生成 如何在 C++ 项目中集成 Lua 解释器,实现混合编程。内容涵盖从 Lua 5.4 源码编译(Windows Visual Studio 及 Linux Makefile)到静态库链接的全过程。重点讲解了 Lua 状态机的创建与管理、内存安全(RAII 封装)、标准库的安全加载策略以及沙箱机制。此外,还深入剖析了 C++ 与 Lua 之间的数据交互方式(栈操作),以及如何通过元表将 C++ 类暴露给 Lua 脚本调用。文章提供了完整的代码示例和跨平台构建建议,帮助开发者掌握高性能嵌入式脚本系统的核心技术与最佳实践。
字节跳动 发布于 2026/3/24 更新于 2026/5/9 14K 浏览C++嵌入 Lua 脚本完整示例项目实战
简介
在 IT 开发中,C++ 凭借高性能广泛应用于系统级编程,而 Lua 作为轻量级脚本语言常用于游戏逻辑、配置管理与嵌入式场景。将 Lua 集成到 C++ 程序中,可兼顾性能与灵活性,提升项目的可扩展性与可维护性。本文通过'LuaInC++'示例工程,介绍如何在 C++ 项目中引入 Lua 解释器、加载执行脚本、调用 Lua 函数,并实现 C++ 与 Lua 之间的数据交互与对象暴露。
Lua 与 C++ 混合编程深度实践:从环境搭建到类绑定的完整技术路径
在现代高性能系统开发中,我们常常面临一个两难选择:既要极致的运行效率,又要灵活的逻辑热更新能力。尤其是在游戏引擎、仿真平台或服务中间件这类长期维护、频繁迭代的大型项目里,每一次重启调试都意味着宝贵的时间成本。
而就在这个平衡点上,Lua 以其轻量级、高可嵌入性的特质脱颖而出。想象一下这样的场景——你正在调试一款 3A 大作的角色行为树,只需修改几行 Lua 脚本并保存,角色立刻就能以全新的 AI 策略行动,无需重新编译整个庞大的 C++ 工程。
今天我们要做的,不是泛泛而谈'Lua 有多好',而是手把手带你走完一条完整的实战路线图:从零开始构建跨平台 Lua 运行时,深入剖析状态机生命周期管理,再到最终实现 C++ 类的安全暴露与调用。
Lua 运行时环境的构建
说到集成 Lua,很多人第一反应是:'不就是下载个库然后 #include <lua.h> 嘛?'但当你真正动手时就会发现,不同版本之间的差异、编译选项的冲突、静态库动态库的选择……每一个细节都在考验你的工程素养。
该选哪个 Lua 版本?
目前主流稳定版有三个——5.1、5.3 和 5.4。别小看这几个数字的区别,它们之间可是藏着不少'坑'。
版本 发布时间 关键特性 适用场景 Lua 5.1 2006 年 极简设计,社区生态成熟;所有数值统一为 double 老项目维护、兼容 tolua++ 等旧绑定工具 Lua 5.3 2015 年 引入 int64 支持,位运算性能提升;保留良好兼容性 需要精确整数计算的应用(如金融模拟) Lua 5.4 2020 年 字符串去重、闭包优化、JIT 候选增强 新项目首选,追求性能与现代语言特性
如果你是从头启动一个新项目,我强烈建议直接上 Lua 5.4 。它不仅带来了显著的性能改进,还引入了像 const 变量、局部延续(continuation)优化等现代化语言特性。
📦 下载地址:https://www.lua.org/ftp/
推荐下载完整源码包,例如 lua-5.4.6.tar.gz
解压后你会发现,整个 Lua 核心就这么点东西:
lua-5.4.6/
├── src/
├── Makefile
├── README
└── etc/
总共才 20 个左右的 .c 文件,总行数不到两万!这种极简内核的设计,正是它能被轻松集成进各种宿主程序的根本原因。
在 Windows 上用 Visual Studio 编译 Lua 静态库
咱们先从最常见的开发环境说起——Windows + Visual Studio。
创建一个空项目,命名为 LuaStaticLib ,然后把 src/ 目录下的所有 .c 文件都复制进去。注意啊,有两个文件千万别加进来: 和 。为什么?
lua.c
luac.c
因为这两个是独立可执行程序的入口(分别是解释器和编译器),我们是要做静态库供别人调用的,不需要这些'外壳'。如果你不小心加上去了,编译时可能会遇到类似这样的错误:
error LNK2019: unresolved external symbol _main referenced in function ...
配置类型 → 设置为'静态库 (.lib)'
C/C++ → 运行库 → 根据主程序决定用 /MT 还是 /MD
预处理器定义 → 删除 LUA_BUILD_AS_DLL (否则会导出符号)
附加包含目录 → 指向 ../include ,方便后续使用
正在创建库 D:\Projects\LuaInC++\lib \build_vs\Release\lua54.lib
可以用 dumpbin /symbols lua54.lib 验证一下是否包含了必要的符号,比如 lua_pcall 、 lua_getglobal 等。
💡 小贴士:建议把生成的 .lib 文件按平台分类存放,比如放在 lib/win32/lua54.lib ,这样以后做跨平台项目时结构更清晰。
Linux 环境下通过 Makefile 一键生成 liblua.a 转战 Linux 就简单多了,官方自带 Makefile 简直是贴心到家。
tar -zxvf lua-5.4.6.tar.gz
cd lua-5.4.6
make linux
liblua.a —— 我们需要的静态库
lua —— 可执行解释器
luac —— 字节码编译器
sudo make install INSTALL_TOP=/usr/local
但这对嵌入式项目来说其实不太推荐,毕竟我们希望依赖尽可能封闭可控。更好的做法是在主项目的 Makefile 中自动拉取并构建:
LUA_SRC = ./thirdparty/lua-5.4.6
LUA_LIB = $(LUA_SRC) /src/liblua.a
$(LUA_LIB) : $(MAKE) -C $(LUA_SRC) linux
%.o: %.cpp
g++ -c $< -o $@ -I$(LUA_SRC) /src -DLUA_USE_LINUX
main: $(LUA_LIB)
g++ main.o $(LUA_LIB) -lm -ldl -o main
看到了吗?这里有个容易忽略的点: 即使你是静态链接,也得加上 -lm -ldl 。因为 Lua 底层用了数学库和动态加载相关函数(比如 dlopen ),哪怕你自己没显式调用,编译器也会报'undefined reference'。
跨平台集成自动化流程 为了让这套机制更具通用性,我们可以画个 Mermaid 流程图来理清思路:
graph TD
A[下载 lua-5.4.6.tar.gz] --> B[解压至项目目录]
B --> C[进入 src/ 目录]
C --> D[执行 make linux]
D --> E[生成 liblua.a]
E --> F[拷贝至项目 lib/ 文件夹]
F --> G[C++ 项目链接 liblua.a]
这套流程最大的好处是什么? 完全可控且可复现 。无论是本地开发还是 CI/CD 流水线,只要执行相同步骤,就能得到一致的结果。
接入 C++ 主程序 现在我们已经拿到了 .lib 或者 .a ,下一步就是让它真正'活'起来——接入我们的 C++ 主程序。
LuaInC++/
├── include/
├── lib/
│ ├── win32/lua54.lib
│ └── linux/liblua.a
├── lua_scripts/
│ └── test.lua
├── src/
│ └── main.cpp
└── build/
在 Visual Studio 里,你需要设置三处关键路径:
C/C++ → 附加包含目录 → $(ProjectDir).. include
链接器 → 附加库目录 → $(ProjectDir).. lib\win32
链接器 → 附加依赖项 → lua54.lib
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <iostream>
int main () {
lua_State* L = luaL_newstate ();
if (!L) {
std::cerr << "Failed to create Lua state!" << std::endl;
return -1 ;
}
std::cout << "Lua state created successfully!" << std::endl;
luaL_openlibs (L);
luaL_dostring (L, "print('Hello from Lua!')" );
lua_close (L);
return 0 ;
}
Lua state created successfully!
Hello from Lua!
必须用 extern "C" 包住头文件,防止 C++ 名称修饰导致链接失败;
头文件顺序无所谓,但最好养成按 lua.h → lualib.h → lauxlib.h 的习惯;
如果出现 LNK2019: unresolved external symbol ,八成是库没连上或 MT/MD 不匹配。
说到 MT/MD,这是 Windows 下另一个著名'深坑'。
MT vs MD:运行时库冲突的那些事儿
/MT :多线程静态版
/MTd :调试版静态
/MD :多线程 DLL 版(发布推荐)
/MDd :调试版 DLL
问题来了: 主程序用 /MD ,Lua 库却用 /MT ,会发生什么?
答案是:堆空间分裂(heap split)。也就是说,你在 Lua 里 malloc 的内存,回到 C++ 这边 free 时可能出错,轻则崩溃,重则数据损坏。
dumpbin /directives lua54.lib
看看有没有 /failifmismatch:"RuntimeLibrary=MT_StaticRelease" 这种字样。
解决办法也很简单—— 保持一致就行 。要么全用 /MD ,要么全用 /MT 。我个人建议生产环境统一用 /MD ,这样可以减少最终二进制体积,并共享系统 CRT。
:: build_lua_md.bat
nmake /f Makefile.msc MYCFLAGS="-MD"
封装一层抽象 接下来我们玩点高级的—— 封装一层抽象,让跨平台变得更优雅 。
#pragma once
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM_WINDOWS
#elif defined(__linux__)
#define PLATFORM_LINUX
#else
#error "Unsupported platform"
#endif
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#ifdef PLATFORM_WINDOWS
#pragma comment(lib, "lua54.lib" )
#endif
这样一来,主程序只需要 #include "lua_wrapper.h" ,连手动加依赖库都省了。是不是舒服多了?
#define USE_LUA_STATIC_LIB
配合 CMake 之类的构建系统,就能实现全自动适配。
Lua 状态机的管理与数据交互 现在我们终于可以进入真正的核心环节了—— Lua 状态机的管理与数据交互 。
每一个 lua_State* 实例都是一个完全隔离的虚拟机环境。你可以把它理解为一个沙盒,里面有独立的栈、注册表、GC 机制和全局变量表。
lua_State* L = luaL_newstate ();
if (!L) {
}
栈空间(默认大小可通过 LUAI_MAXSTACK 调整)
字符串常量池
表结构哈希桶
垃圾回收器上下文
所以必须记住一点: 每次创建都要配对 lua_close(L) ,否则每运行一次就泄露几 MB 内存,很快就会 OOM。
struct LuaDeleter {
void operator () (lua_State* L) {
if (L) lua_close (L);
}
};
using LuaStatePtr = std::unique_ptr<lua_State, LuaDeleter>;
LuaStatePtr CreateLuaState () {
return LuaStatePtr (luaL_newstate ());
}
这样哪怕中途抛异常,也能自动释放资源,彻底告别内存泄漏。
标准库的安全加载 关于标准库的加载,这里有个重要提醒: 慎用 luaL_openlibs() !
它一口气开了七个库,其中 io.* 和 os.* 危险系数极高。试想一下,如果用户脚本能执行 os.execute("rm -rf ~") ,你的服务器还能安稳吗?
static const struct luaL_Reg loadedlibs[] = {
{"_G" , luaopen_base},
{LUA_TABLIBNAME, luaopen_table},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{NULL , NULL }
};
然后逐个注册,排除 io 和 os 。甚至还可以进一步禁用 debug.getinfo 这类敏感函数。
local safe_env = {
print = print ,
string = string ,
math = math
}
setmetatable (safe_env, {
__index = function (_, k)
error ("Attempt to access forbidden global '" .. k .. "'" , 2 )
end
})
_ENV = safe_env
执行模式与数据交互
luaL_dostring(L, code) :简单粗暴,适合快速原型
luaL_loadstring + lua_pcall :分步执行,便于错误捕获
if (luaL_dostring (L, "syntax error" )) {
handle_error (lua_tostring (L, -1 ));
}
if (luaL_loadstring (L, "syntax error" ) == LUA_OK) {
lua_pcall (L, 0 , 0 , 0 );
} else {
handle_compile_error ();
}
后者能精准定位问题是语法错误还是运行时异常,还能获取完整的栈回溯信息,更适合正式项目。
数据交互方面,核心就是那个'万能中介'—— Lua 栈 。
lua_getglobal (L, "config_level" );
if (lua_isnumber (L, -1 )) {
int level = lua_tointeger (L, -1 );
}
lua_pop (L, 1 );
lua_pushinteger (L, 42 );
lua_setglobal (L, "answer" );
至于 C++ 对象传递, lightuserdata 虽然快,但风险也大——指针悬空、GC 失控、跨状态机无效等问题层出不穷。
推荐做法是用 full userdata + 元表:
void * ud = lua_newuserdata (L, sizeof (Person*));
*(Person**)ud = new Person ("Alice" , 25 );
luaL_getmetatable (L, "PersonMeta" );
lua_setmetatable (L, -2 );
static int person_gc (lua_State* L) {
Person* self = *(Person**)lua_touserdata (L, 1 );
delete self;
return 0 ;
}
C++ 类暴露给 Lua 最后,我们来看看如何把 C++ 类完整暴露给 Lua。
local p = Person.new("Bob" , 20 )
p:introduce()
p.age = 25
print (p.name)
创建元表,设置 __index 拦截属性访问
注册构造函数 new
成员方法包装成 C 函数,通过 lua_touserdata 获取 this 指针
添加 __gc 确保对象自动销毁
具体代码就不展开了(前面已有详细示例),但我想强调一点: 不要怕麻烦 。手工绑定确实繁琐,但换来的是极致的控制力和性能表现。
当然,如果你追求开发效率,也有像 Sol2 这样的现代 C++ 绑定库,只需几行模板代码就能完成自动注册:
sol::state lua;
lua.open_libraries ();
lua.new_usertype <Person>(
"Person" ,
"new" , sol::constructors <Person (std::string, int )>(),
"name" , &Person::name,
"age" , &Person::age,
"introduce" , &Person::introduce
);
总结 回顾整条技术链,你会发现 Lua 的强大之处不在于某一项炫酷功能,而在于它恰到好处地站在了'简洁'与'实用'的平衡点上。
它不像 Python 那样臃肿,也不像 JavaScript 那样难以嵌入。它的 API 干净利落,文档清晰明了,几乎没有多余的抽象层级。正因如此,才能在《王者荣耀》、《原神》这类顶级游戏中默默承担着核心逻辑调度的任务。
而当我们掌握了从环境配置、状态管理到类绑定的全套技能后,实际上已经具备了解决绝大多数嵌入式脚本需求的能力。
使用 Fengari 将 Lua 编译为 WebAssembly,在浏览器中运行;
结合 Redis 的 Lua 脚本引擎,打造高性能规则引擎;
基于协程实现异步任务调度系统……
但无论如何扩展,今天的这套基础框架都会是你最坚实的起点。
所以,下次当你面对'要不要加脚本系统'的争论时,不妨微笑着说一句:
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online