Lua 非侵入式热补丁系统开发与使用手册
1 文档概述
1.1 适用场景
本系统运行在 ibmc 闭源二进制 SDK + Lua/Skynet 环境中,无法修改闭源二进制文件,由闭源底座提供补丁加载入口,开源社区提供补丁框架与修复脚本,实现无侵入运行时热修复。
1.2 能力范围
支持三类热修复能力:
- 文件整体替换(package):劫持 Lua 模块加载,完全替换闭源文件
- 公开函数重写(public_fun):对模块导出的公开函数做包装 / 修复
- 闭包私有函数重写(private_func):通过 Lua debug 库修复闭包内未导出的私有函数
1.3 角色与分工
| 归属 | 文件 | 职责 |
|---|---|---|
| 闭源 SDK 内部 | main.lua | 补丁统一入口,安全捕获异常,启动补丁加载流程 |
| 开源社区 | fix.lua | 补丁核心调度框架,实现三种修复逻辑、目录扫描、补丁分发 |
| 开源社区 | patch_x.lua | 业务补丁脚本,按规范编写修复逻辑 |
2 整体目录结构
项目根目录/
└── src/
└── service/
├── fix.lua # 社区补丁核心框架
├── patch_a.lua # 文件全覆盖补丁示例
├── patch_b.lua # 公开函数修复补丁示例
├── patch_c.lua # 私有闭包函数修复补丁示例
└── time_manager.lua # 模块替换场景对应的修复模块(可选)
强制规则
- 所有补丁脚本必须放置在 src/service/ 目录下
- 补丁文件名必须以 patch_ 开头,后缀为 .lua
- 仅闭源底座可修改 main.lua,社区禁止改动入口文件
3 闭源 SDK 侧:补丁入口 main.lua
3.1 代码实现
该文件部署在闭源 SDK 内部,负责安全拉起整个补丁系统,使用 pcall 做异常隔离,补丁报错不会导致主程序崩溃。
local log = require 'mc.logging'
-- 异常捕获:补丁加载失败不影响主业务
local ok, err = pcall(function()
local fix = require 'service.fix'
-- 传入业务标识 bmc_time,用于日志区分
fix.load_all_patches('bmc_time')
end)
if not ok then
log:error('load patches failed, err:%s', err)
end
注意
- 必须写到main.lua最前面,提前加载补丁生效
3.2 执行逻辑
- 通过 pcall 安全执行补丁加载逻辑
- 加载社区核心框架 service.fix
- 调用统一入口 load_all_patches,传入业务名称
- 加载异常时打印错误日志,主程序继续运行
4 开源社区侧:核心框架 fix.lua
目录结构:在社区manifest添加
4.1 代码实现
社区统一提供的补丁调度核心,负责目录遍历、补丁解析、三类修复逻辑实现。
local log = require "mc.logging"
local utils_core = require 'utils.core'
local fix = {}
-- 类型1:模块整体覆盖(package.preload 劫持)
function fix.package(patch, app_name)
local mod_name = patch.target
package.preload[mod_name] = patch.func
log:notice('[%s]load package %s successfully', app_name, mod_name)
end
-- 类型2:公开导出函数修复/包装
function fix.public_fun(patch, app_name)
local mod_name, func_name = patch.target:match("^(.+)%.(.+)$")
local mod = require(mod_name)
local old_func = mod[func_name]
-- 重写函数,原生函数作为最后一个参数传入
mod[func_name] = function(...)
return patch.func(..., old_func)
end
log:notice('[%s]load public fun %s successfully', app_name, patch.target)
end
-- 类型3:闭包内私有函数修复(基于 debug 库)
function fix.private_func(patch, app_name)
local mod_name, entry_func = patch.target:match("^(.+)%.(.+)$")
local private_name = patch.private
local mod = require(mod_name)
local entry = mod[entry_func]
-- 遍历 upvalue 查找私有函数
for i = 1, 1000 do
local name, _ = debug.getupvalue(entry, i)
if not name then break end
if name == private_name then
debug.setupvalue(entry, i, patch.func)
log:notice('[%s]load private fun %s successfully', app_name, private_name)
break
end
end
end
-- 统一入口:扫描并加载所有补丁
function fix.load_all_patches(app_name)
log:notice('[%s]load all patches start', app_name)
local patch_dir = "service"
-- 拼接补丁目录绝对路径
local dir = utils_core.getcwd() .. '/src/service'
local files = utils_core.dir(dir)
if #files == 0 then
log:notice('[%s]resume file not exist, no upgrade required', app_name)
return
end
-- 遍历目录下所有 lua 文件
for _, file in pairs(files) do
if file:match("%.lua$") then
local path = patch_dir .. "." .. file:gsub("%.lua$", "")
-- 只处理 patch_ 开头的补丁文件
if not string.find(file, 'patch_') then
goto continue
end
-- 加载补丁配置表
local patch = require(path)
if type(patch) ~= "table" then
goto continue
end
-- 根据补丁类型分发执行
if patch.fix_type == 'package' then
fix.package(patch, app_name)
elseif patch.fix_type == 'public_fun' then
fix.public_fun(patch, app_name)
elseif patch.fix_type == 'private_func' then
fix.private_func(patch, app_name)
end
end
::continue::
end
log:notice('[%s]load all patches end', app_name)
end
return fix
4.2 核心流程
- 读取 src/service 目录下所有文件
- 过滤出 patch_ 开头的 Lua 补丁文件
- 加载补丁配置表,校验格式
- 根据 fix_type 调用对应修复逻辑
- 打印加载日志,标记补丁执行状态
5 开源社区侧:三类补丁开发规范与示例
所有补丁文件统一返回一张配置表,框架根据字段自动识别修复类型。
5.1 类型一:package 模块全覆盖补丁
适用场景
闭源模块整体逻辑异常、功能缺失,需要完全替换整个模块。
补丁文件:patch_a.lua
local log = require 'mc.logging'
return {
-- 固定类型标识
fix_type = 'package',
-- 目标:需要劫持的闭源模块名(与 require 名称一致)
target = "time_manager",
-- 劫持函数:返回社区修复后的完整模块
func = function(...)
log:info("package preload 模块劫持成功")
-- 加载社区编写的修复模块
return require("service.time_manager")
end
}
配套说明
需在 service 目录下提供完整修复模块 time_manager.lua。
5.2 类型二:public_fun 公开函数修复补丁
适用场景
模块整体正常,仅单个导出函数存在 Bug、参数异常、逻辑缺失,推荐优先使用该类型(最稳健)。
补丁文件:patch_b.lua
local log = require "mc.logging"
local time_intf = require 'time_intf'
return {
-- 固定类型标识
fix_type = "public_fun",
-- 目标格式:模块名.函数名
target = "time_manager.initialize",
-- 修复逻辑:最后一个参数为原生旧函数
func = function(self, old_func)
self.mdb_time.DateTime = time_intf.get_date_time()
if self.mdb_time.DateTimeLocalOffset == '+08:00' then
self:set_timezone('UTC+08:00')
end
end
}
5.3 类型三:private_func 私有闭包函数修复
适用场景
Bug 函数为闭包内局部私有函数,未导出到模块表,常规方式无法访问,依赖 Lua debug 库修改 upvalue。
补丁文件:patch_c.lua
local log = require 'mc.logging'
return {
-- 固定类型标识
fix_type = 'private_func',
-- 目标:模块.公开入口函数(私有函数所在的外层导出函数)
target = "c.get",
-- 闭包内私有函数名称
private = "set",
-- 修复后的私有函数逻辑
func = function(...)
log:notice('fix private func')
-- 此处编写私有函数修复逻辑
end
}
6 完整加载流程
- 闭源 SDK 启动,执行内部 main.lua
- main.lua 安全加载 service/fix.lua 框架
- 框架扫描 src/service/ 目录所有文件
- 筛选 patch_ 开头的补丁脚本并加载
- 按 fix_type 执行对应修复逻辑
- 输出日志,补丁生效,业务正常运行
7 日志验证与问题排查
7.1 正常加载日志示例
[bmc_time]load all patches start
[bmc_time]load package time_manager successfully
[bmc_time]load public fun time_manager.initialize successfully
[bmc_time]load private fun set successfully
[bmc_time]load all patches end
7.2 常见问题排查
- 补丁未加载
- 检查文件是否放在 src/service/
- 检查文件名是否以 patch_ 开头
- 模块劫持失败
- 核对 target 模块名与业务中 require 名称完全一致
- 私有函数不生效
- 核对外层入口函数、私有函数名拼写
- SDK 版本升级后闭包变量名可能变更,需重新适配
- 补丁报错
- 查看 main.lua 捕获的 error 日志,定位代码异常
8 选型建议与使用约束
8.1 补丁类型优先级(推荐)
public_fun(公开函数修复) > package(模块替换) > private_func(私有闭包修复)
8.2 使用约束
- 禁止修改闭源侧 main.lua 入口文件
- 所有补丁必须增加日志,方便线上问题定位
- 私有函数补丁强依赖闭包变量名,SDK 升级后务必回归测试
- 模块替换补丁需完整实现原模块所有接口,避免功能缺失
- 所有补丁仅部署在开源社区目录,不触碰二进制文件
