news 2026/8/19 8:19:55

c++游戏后端开源框架学习——wukong(十、lua热更新)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
c++游戏后端开源框架学习——wukong(十、lua热更新)

Lua 热更新原理详解

澄清一个常见误解:Lua热更新不是"C++里有一段Lua逻辑在跑",而是"C++在运行时动态切换消息处理函数的调用入口"。


一、你的理解哪里对了,哪里错了

你的原理解

“在C++中有一段是利用lua运行的逻辑,才能使用lua热更”

这个理解部分正确:C++确实嵌入了Lua虚拟机来执行Lua脚本。

但关键不在"有没有Lua在跑",而在于C++如何决定走哪条代码路径

消息到达 │ ├── needHotfix == false → 调用C++编译好的handler函数(编译时就固定了) │ └── needHotfix == true → 调用Lua函数 fix_<消息号>() (运行时从文件加载,可替换)

热更新的本质是:C++在消息分派时,有一个if/else分支决定走C++原函数还是走Lua函数。这个分支标志needHotfix可以在运行时通过Redis动态修改

更准确的理解

不是:C++代码 ← → Lua代码(互相调用,模糊边界) 而是:C++是宿主,Lua是被嵌入的脚本引擎,C++决定何时调用Lua

二、整体架构

┌─────────────────────────────────────────────────────────────────┐ │ Lobby Server (C++进程) │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 1. 消息分派层 (MessageTarget::handleMessage) │ │ │ │ │ │ │ │ 消息到达 → 查注册表 → needHotfix? │ │ │ │ ├── false → 调用C++ handler(编译时绑定) │ │ │ │ └── true → 调用 callHotfix() │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 2. C++→Lua桥接层 (MessageTarget::callHotfix) │ │ │ │ │ │ │ │ 从Lua VM池取一个lua_State │ │ │ │ lua_getglobal(L, "fix_1000") ← 找Lua函数 │ │ │ │ lua_pushlightuserdata(L, this) ← 压入C++对象指针 │ │ │ │ lua_pcall(L, ...) ← 调用Lua函数 │ │ │ │ 归还lua_State到池中 │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 3. Lua脚本层 (hotfix/fix_1000.lua) │ │ │ │ │ │ │ │ local fixecho = require "libfixecho" ← 加载C++库 │ │ │ │ function fix_1000(a, b, c) │ │ │ │ return fixecho.lua_echo(a, b, c) │ │ │ │ end │ │ │ └───────────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌───────────────────────────▼───────────────────────────┐ │ │ │ 4. C++动态库层 (libfixecho.so) │ │ │ │ │ │ │ │ lua_echo(L) { │ │ │ │ // 从Lua栈取回C++指针 │ │ │ │ MyLobbyObject *gobj = (MyLobbyObject*) │ │ │ │ lua_touserdata(L, 1); │ │ │ │ gobj->send(...); ← 直接操作C++对象 │ │ │ │ } │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘

三、逐层拆解

第一层:C++消息分派——决定走哪条路

文件:share/msghdl/message_target.cpp:30-75

voidMessageTarget::handleMessage(int32_tmsgType,uint16_ttag,conststd::string&rawMsg){// 查注册表,获取这个消息的处理信息boolneedHotfix;MessageHandle handle;// C++处理函数指针g_MessageHandleManager.getMessageInfo(msgType,proto,needCoroutine,needHotfix,handle);// 解析protobuf消息google::protobuf::Message*msg=proto->New();msg->ParseFromString(rawMsg);#ifdefUSE_MESSAGE_HOTFIXif(needHotfix){callHotfix(msgType,tag,targetMsg);// ← 走Lua路径}else{handle(getPtr(),tag,targetMsg);// ← 走C++原路径}#elsehandle(getPtr(),tag,targetMsg);// ← 没有编译热更功能#endif}

关键点

  • needHotfix标志是运行时可变的,存在MessageHandleManager的注册表中
  • 这个标志由MessageHotfixManager通过Redis动态修改
  • #ifdef USE_MESSAGE_HOTFIX控制是否编译热更代码(只有Lobby服开启)

第二层:热更新管理器——运行时修改标志

文件:share/msghdl/message_hotfix_manager.cpp

voidMessageHotfixManager::init(){// 1. 启动时从Redis加载初始热更配置RoutineEnvironment::startCoroutine([](void*arg)->void*{auto*manager=(MessageHotfixManager*)arg;manager->resetHotfix();returnNULL;},this);// 2. 订阅Redis的"WK_Hotfix"主题,收到通知就重新加载PubsubService::Subscribe("WK_Hotfix",true,[this](conststd::string&topic,conststd::string&msg){resetHotfix();// ← 任何时候收到PubSub消息都会触发});// 3. 启动清理协程,定期回收空闲的Lua VMRoutineEnvironment::startCoroutine(updateRoutine,this);}

resetHotfix()做了什么:

voidMessageHotfixManager::resetHotfix(){// 从Redis读取热更消息列表(JSON数组,如 [1000, 1001])redisContext*cache=g_RedisPoolManager.getCoreCache()->take();std::string hotfixData;RedisUtils::GetHotfixData(cache,hotfixData);// GET "HotfixMsgs"// hotfixData = "[1000]"Document doc;doc.Parse(hotfixData.c_str());// 先清除所有消息的hotfix标志g_MessageHandleManager.clearNeedHotfix();// 遍历需要热更的消息列表for(SizeType i=0;i<doc.Size();i++){intmsgType=doc[i].GetInt();// 如 1000// 只处理本服注册了的消息if(!g_MessageHandleManager.isRegistedMessage(msgType)){continue;}// 设置这个消息的needHotfix=true ← 核心操作g_MessageHandleManager.setNeedHotfix(msgType);// 加载对应的Lua脚本文件// 约定文件名:hotfix/fix_<消息号>.luastd::ostringstream oss;oss<<"hotfix/fix_"<<msgType<<".lua";std::string content;Utility::loadFileToString(oss.str().c_str(),content);hotfixMap_[msgType]=content;// 存到内存}// 版本号+1,旧的Lua VM全部关闭hotfixVersion_++;for(autoinfo:luaStates_){lua_close(info.L);}luaStates_.clear();}

这个函数是热更新的核心:它把"哪些消息走Lua"这个决策动态写入了C++的内存,不需要重新编译。

第三层:C++调用Lua——桥接层

文件:share/msghdl/message_target.cpp:103-151

voidMessageTarget::callHotfix(int32_tmsgType,uint16_ttag,std::shared_ptr<google::protobuf::Message>msg){// 1. 从池中取一个Lua VMLuaStateInfo lsInfo;g_MessageHotfixManager.getLuaStateInfo(lsInfo);// 用unique_ptr保护,确保异常时也能归还或关闭std::unique_ptr<lua_State,decltype(&lua_close)>L(lsInfo.L,lua_close);// 2. 构造函数名:fix_<消息号>,如 fix_1000std::ostringstream oss;oss<<"fix_"<<msgType;std::string luaFun=oss.str();// 3. 从Lua全局表中找这个函数lua_getglobal(L.get(),luaFun.c_str());if(lua_isnil(L.get(),-1)){ERROR_LOG("Lua function %s not found\n",luaFun.c_str());return;}// 4. 压入参数(3个)lua_pushlightuserdata(L.get(),this);// 参数1: C++对象指针lua_pushlightuserdata(L.get(),msg.get());// 参数2: protobuf消息指针lua_pushnumber(L.get(),tag);// 参数3: 消息tag// 5. 调用Lua函数!if(lua_pcall(L.get(),3,1,0)!=LUA_OK){ERROR_LOG("Lua call failed: %s\n",lua_tostring(L.get(),-1));return;}// 6. 读取返回值intresult=lua_tonumber(L.get(),-1);lua_pop(L.get(),1);// 7. 归还Lua VM到池中L.release();// 阻止unique_ptr关闭lua_Stateg_MessageHotfixManager.backLuaStateInfo(lsInfo);}

关键理解

  • lua_getglobal:从Lua的全局变量表中查找名为fix_1000的函数
  • lua_pushlightuserdata:把C++的裸指针压入Lua栈,Lua侧可以取回
  • lua_pcall:实际调用Lua函数,期间Lua虚拟机执行Lua字节码
  • C++对象指针直接传给Lua,没有序列化/反序列化开销

第四层:Lua脚本——胶水层

文件:demo/hotfix/fixEcho/lua/fix_1000.lua

-- 设置C++动态库的搜索路径package.cpath='hotfix/?.so;'-- 加载C++编译的动态库localfixecho=require"libfixecho"-- 定义消息处理函数(名字必须叫 fix_<消息号>)functionfix_1000(a,b,c)-- a = C++的MessageTarget指针(lightuserdata)-- b = C++的protobuf::Message指针(lightuserdata)-- c = tag数字returnfixecho.lua_echo(a,b,c)end

关键理解

  • Lua脚本本身可以很简单,只是把参数转发给C++动态库
  • 也可以在Lua中写纯逻辑(如修改数值、做条件判断),不需要C++库
  • Lua脚本是文本文件,修改后不需要编译,重启Lua VM即可生效

第五层:C++动态库——真正的业务逻辑

文件:demo/hotfix/fixEcho/src/fix_echo.cpp

// 这个函数注册为Lua可调用的C函数staticintlua_echo(lua_State*L){// 从Lua栈取回C++指针(就是callHotfix压入的那些)MyLobbyObject*gobj=(MyLobbyObject*)lua_touserdata(L,1);wukong::pb::StringValue*msg=(wukong::pb::StringValue*)lua_touserdata(L,2);uint16_ttag=(uint16_t)lua_tonumber(L,3);// 直接操作C++对象!gobj->setExp(gobj->getExp()+1);gobj->send(S2C_MESSAGE_ID_ECHO,tag,*msg);lua_pushnumber(L,0);// 返回值return1;}// 注册函数表staticconststructluaL_RegmyLib[]={{"lua_echo",lua_echo},{NULL,NULL}};// Lua require时的入口extern"C"intluaopen_libfixecho(lua_State*L){luaL_newlib(L,myLib);return1;}

关键理解

  • 动态库.so文件可以被Lua的require在运行时加载
  • 替换.so文件后,重启Lua VM就会加载新版本
  • 动态库中可以直接操作C++对象(通过传入的指针),性能与原生C++几乎一样
  • 这就是"热更新C++代码"的真正含义:不是修改正在运行的C++代码,而是替换被Lua加载的动态库

四、热更新的完整流程

触发热更新

运维人员修改Redis: SET "HotfixMsgs" "[1000]" ← 告诉服务器:1000号消息走Lua PUBLISH "WK_Hotfix" "reload" ← 通知所有服务器重新加载

服务器内部流程

1. Redis PubSub收到"WK_Hotfix"消息 └→ MessageHotfixManager::resetHotfix() 被调用 2. 从Redis读取热更消息列表 └→ hotfixData = "[1000]" 3. 遍历消息列表 ├── 对消息1000:setNeedHotfix(1000, true) ├── 加载文件 hotfix/fix_1000.lua 到内存 └── 存入 hotfixMap_[1000] = "<Lua文件内容>" 4. hotfixVersion_++ (版本号递增) └── 关闭所有旧的Lua VM 5. 下一条消息1000到达时 ├── handleMessage() 查注册表 → needHotfix=true ├── callHotfix() 被调用 ├── 创建新的Lua VM(因为旧的都关了) ├── 在新VM中加载 fix_1000.lua ├── 调用 fix_1000(this, msg, tag) └── Lua内部调用 libfixecho.so 的 lua_echo()

取消热更新

运维人员修改Redis: SET "HotfixMsgs" "[]" ← 空数组,没有消息需要热更 PUBLISH "WK_Hotfix" "reload" ← 通知重新加载

服务器收到后,clearNeedHotfix()清除所有标志,消息1000重新走C++原函数。


五、Lua VM池化机制

每次调用Lua函数都创建/销毁Lua VM开销很大,框架用了对象池

// message_hotfix_manager.hstructLuaStateInfo{lua_State*L;// Lua虚拟机time_t lastUsedAt;// 最后使用时间intversion;// 创建时的hotfixVersion};std::list<LuaStateInfo>luaStates_;// VM池inthotfixVersion_;// 当前版本号

工作流程

取VM: ├── 池非空 → 取最后一个返回 └── 池为空 → 新建lua_State,加载所有hotfix脚本 归还VM: ├── 版本号匹配 → 放回池中(可复用) └── 版本号不匹配 → lua_close关闭(是旧版本,丢弃) 定期清理(每60秒): └── 关闭超过1分钟未使用的VM,但最少保留10个

版本号的作用:热更后hotfixVersion_++,池中所有旧VM的版本号都不匹配了。归还时检测到版本号不一致,直接关闭,不会复用旧逻辑的VM。这保证了热更后不会有残留的旧Lua逻辑在跑


六、关键问题解答

Q1:为什么不能直接修改C++代码来热更?

C++是编译型语言,代码编译成机器码后无法在运行时替换。要修改C++逻辑必须重新编译并重启进程。

Lua是解释型语言,代码在运行时由Lua虚拟机解释执行。修改Lua文件后,重新加载到Lua VM即可生效,不需要重启进程。

Q2:Lua热更后,C++原函数还在吗?

还在。C++原函数是编译进二进制文件的,永远不会消失。needHotfix标志只是决定了调用哪个函数

if(needHotfix){callHotfix(msgType,tag,msg);// 走Lua}else{handle(obj,tag,msg);// 走C++原函数}

取消热更(needHotfix=false)后,消息重新走C++原函数。

Q3:Lua不经过C++能直接操作游戏对象吗?

不能直接操作,必须通过C++提供的接口。在本框架中,有两种方式:

方式一:lightuserdata传递指针

// C++侧:把指针压入Lua栈lua_pushlightuserdata(L,this);// C++对象指针lua_pushlightuserdata(L,msg.get());// protobuf消息指针// Lua侧:转发给C++动态库functionfix_1000(a,b,c)returnfixecho.lua_echo(a,b,c)--a,b是指针,c是数字 end// C++动态库侧:从Lua栈取回指针,直接操作C++对象MyLobbyObject*gobj=(MyLobbyObject*)lua_touserdata(L,1);gobj->setExp(gobj->getExp()+1);// 直接调用C++方法

方式二:在Lua中写纯逻辑

functionfix_1000(a,b,c)-- 不调C++库,纯Lua逻辑-- 但a是指针,Lua不能直接操作它-- 只能做数值计算、条件判断等return0end

所以:Lua热更新的性能取决于"Lua做多少,C++动态库做多少"。纯Lua做复杂逻辑会慢,但修改简单参数/条件分支就很快。

Q4:C++动态库(.so)怎么热更新?

  1. 编译新的.so文件,替换服务器上的旧文件
  2. 触发Redis PubSub的WK_Hotfix消息
  3. 服务器关闭所有旧Lua VM(hotfixVersion_++
  4. 下次消息到达时创建新Lua VM
  5. 新Lua VM执行require "libfixecho"时加载新的.so文件

注意:Linux的.so文件如果被进程映射(已加载),直接覆盖可能失败。需要:

  • dlclose旧库(通过关闭旧Lua VM间接实现)
  • 或者用不同的文件名(如libfixecho_v2.so),修改Lua脚本中的require路径
  • 或者用mv替换(Linux允许mv覆盖已加载的so,旧进程仍用旧的,新加载用新的)

Q5:这个框架的热更新和传统Lua热更新有什么区别?

对比项传统Lua框架(如skynet)本框架
业务逻辑全部用Lua写C++为主,Lua仅做热更补丁
热更粒度整个Lua文件/模块单个消息处理函数
性能Lua执行,较慢Lua调C++动态库,接近原生
触发方式修改文件+信号Redis PubSub
适用场景快速迭代开发线上紧急修复

本框架的设计思路是C++为主,Lua为补丁:正常运行时所有逻辑走C++,只有需要紧急修复的消息才走Lua。修复完后可以取消热更标记,重新走C++。


七、数据流完整路径

以消息1000(ECHO)为例,展示从客户端到响应的完整路径:

1. 客户端发送 C2S_MESSAGE_ID_ECHO (消息号1000) │ ▼ 2. Gateway收到,根据消息ID高16位判断是LOBBY消息 └→ forwardIn RPC 转发到 Lobby服 3. Lobby服的 MessageTarget::handleMessage(1000, tag, rawMsg) │ ├── 查注册表:消息1000的 needHotfix = true │ ▼ 4. callHotfix(1000, tag, msg) │ ├── 从VM池取lua_State ├── lua_getglobal(L, "fix_1000") ├── lua_pushlightuserdata(L, this) ← LobbyObject指针 ├── lua_pushlightuserdata(L, msg.get()) ← StringValue指针 ├── lua_pushnumber(L, tag) ← tag值 │ ▼ 5. Lua执行 fix_1000(a, b, c) │ ├── require "libfixecho" ← 加载C++动态库 └── return fixecho.lua_echo(a, b, c) │ ▼ 6. C++动态库执行 lua_echo(L) │ ├── lua_touserdata(L, 1) → MyLobbyObject* gobj ├── lua_touserdata(L, 2) → StringValue* msg ├── lua_tonumber(L, 3) → tag │ ├── gobj->setExp(gobj->getExp() + 1) ← 修改玩家经验值 └── gobj->send(S2C_MESSAGE_ID_ECHO, tag, *msg) ← 发回包给客户端 │ ▼ 7. lua_echo返回1,push返回值到Lua栈 │ ▼ 8. Lua fix_1000 返回 │ ▼ 9. callHotfix 读取返回值,归还lua_State到池 │ ▼ 10. gobj->send() 通过Gateway的forwardOut发给客户端

八、安全注意事项

8.1 指针安全

// C++把裸指针传给Lualua_pushlightuserdata(L,this);// LobbyObject*lua_pushlightuserdata(L,msg.get());// protobuf::Message*

风险

  • 如果Lua VM存活时间超过C++对象生命周期,指针就悬空了
  • 如果Lua脚本把指针存到全局变量,下次调用时对象可能已销毁

框架的防护

  • Lua VM池化,每次调用后立即归还,不会长期持有指针
  • hotfixVersion_机制保证热更后旧VM全部关闭
  • 但如果Lua脚本自己存指针到全局表,框架无法防护

8.2 Lua脚本安全

Lua脚本可以执行任意Lua代码,包括:

  • os.execute()执行系统命令(如果加载了os库)
  • 文件读写
  • 死循环导致协程卡死

框架的防护

  • 只加载了package库(luaopen_package),没有加载os/io等库
  • lua_pcall保护模式调用,错误不会崩溃进程
  • 但Lua脚本中的死循环仍会卡住协程

8.3 动态库安全

C++动态库.so有和C++主程序相同的权限:

  • 可以访问进程内存
  • 可以调用系统API
  • 一个有bug的动态库可以导致进程崩溃

实际建议:动态库代码必须经过完整测试,不能随意替换。


九、总结

你的理解修正

原理解正确理解
C++中有一段Lua逻辑在跑C++嵌入了Lua VM,按需调用Lua函数
Lua热更需要C++配合C++代码编译时就预留了if(needHotfix)分支,运行时通过Redis切换
Lua直接操作游戏对象Lua通过lightuserdata拿到C++指针,转发给C++动态库操作
热更=修改C++代码热更=替换消息处理入口(从C++函数切换到Lua函数)

热更新的本质

编译时:C++代码中预埋了 if(needHotfix) { callLua(); } else { callCpp(); } 运行时:通过Redis动态修改 needHotfix 标志 通过Redis PubSub通知所有服务器实例 替换Lua脚本文件和C++动态库 效果: 特定消息的处理逻辑在运行时被替换,无需重启进程

三层热更能力

层次修改内容是否需要编译生效方式
Lua脚本fix_1000.lua不需要重新加载Lua VM
C++动态库libfixecho.so需要(编译.so)Lua require时加载新.so
C++主程序Lobby服二进制需要(重新编译)重启进程

框架的热更新主要覆盖前两层,第三层(主程序)无法热更,只能重启。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 8:19:33

AI智能体概率验证:从MDP/POMDP模型到工程实践

1. 从“确定性”到“概率性”&#xff1a;为什么AI智能体需要新的验证范式&#xff1f;在AI智能体&#xff08;AI Agents&#xff09;的开发与应用中&#xff0c;我们正面临一个根本性的范式转变。过去&#xff0c;我们验证一个软件系统&#xff0c;无论是传统的业务逻辑还是简…

作者头像 李华
网站建设 2026/8/19 8:16:19

Arduino连接PS/2鼠标:从协议解析到交互控制实战指南

1. 项目概述&#xff1a;从“点灯”到“点鼠标”的跨越 玩过Arduino的朋友&#xff0c;第一步大概率是让板子上的LED灯闪烁起来&#xff0c;这算是嵌入式世界的“Hello World”。但当你点亮了灯&#xff0c;驱动了电机&#xff0c;甚至让屏幕显示出了字符&#xff0c;是不是觉得…

作者头像 李华
网站建设 2026/8/19 8:16:02

TEMU上架软件:C++底层指纹伪装,抹除自动化特征

TEMU上架软件&#xff1a;C底层指纹伪装&#xff0c;抹除自动化特征 电商这行&#xff0c;谁的速度快谁吃肉。TEMU的自动化上架&#xff0c;是店群运营中最耗人力也最容易出错的环节。 手动上架一个商品从填写标题、上传主图、设置SKU、填写详情到发布&#xff0c;熟练操作也…

作者头像 李华
网站建设 2026/8/19 8:14:29

基于Raspberry Pi Pico W与W5100S的PoE受电设备测试板设计与实现

1. 项目缘起与核心目标最近在折腾一个物联网边缘数据采集的小项目&#xff0c;需要把几个分布在车间不同角落的传感器数据汇总到一个中心节点。布线成了大问题&#xff1a;既要拉网线传数据&#xff0c;又得单独给这些边缘设备供电&#xff0c;线缆纵横交错&#xff0c;既不美观…

作者头像 李华
网站建设 2026/8/19 8:14:24

CliffSearch:基于智能体协同进化的科学算法自动化发现框架

1. CliffSearch项目概述&#xff1a;当理论与代码开始“对话” 最近在AI驱动的科学发现领域&#xff0c;一个名为CliffSearch的项目引起了我的注意。这个项目标题“Structured Agentic Co-Evolution over Theory and Code for Scientific Algorithm Discovery”听起来有点拗口&…

作者头像 李华
网站建设 2026/8/19 8:11:27

嵌入式时序编排新范式:Sequino如何实现高精度确定性事件处理

1. 项目概述&#xff1a;当“Sequino”不只是个名字 如果你在科技圈&#xff0c;尤其是开源硬件和嵌入式开发领域混迹过一段时间&#xff0c;大概率听说过“Arduino”这个名字。它几乎成了创客和快速原型开发的代名词。但今天我想聊的&#xff0c;是一个听起来有点类似&#xf…

作者头像 李华