使用 LuaBridge 封装 C++ 日志库 microLog 为 Lua 模块
深入浅出:将高性能 C++ 日志库与 Lua 脚本生态无缝融合
一、缘起
在游戏开发、嵌入式系统和快速原型开发中,Lua 因其轻量、高效、易于嵌入的特性而被广泛用作脚本语言。而 microLog 是一个轻量级、高性能的 C++ 日志库,支持 vlog 变参日志宏、多后端(文件、Redis 等)和异步刷盘。
如果能在 Lua 脚本中直接调用 microLog 记录日志,便能兼得 C++ 日志库的高性能与 Lua 脚本的灵活性------游戏逻辑由 Lua 动态编写,性能敏感的日志系统由 C++ 实现。本文将通过 LuaBridge (一个轻量级、仅头文件的 C++ ↔ Lua 绑定库)将 microLog 的 C++ 接口封装为 Lua 模块,重点处理 vlog 变参日志方法的跨语言传递。
二、microLog C++ 库概览
microLog 的核心设计包括:
- 日志门面(
logItfc) :提供统一的log、vlog、debug等接口。 - 后端抽象(
logBackend):支持文件、Redis、自定义后端。 - 工厂模式:动态创建后端实例。
vlog宏 :变参日志宏,支持格式化字符串,类似printf。
C++ 示例代码:
cpp
// 使用 vlog 宏
vlog(INFO, "用户 %s 登录成功,ID: %d", username.c_str(), userId);
我们的目标是在 Lua 中实现相同效果:
lua
logger:vlog('INFO', '用户 %s 登录成功,ID: %d', 'Alice', 1001)
三、LuaBridge 简介
LuaBridge 是一个强大而轻量级的 C++ 库,用于在 C++ 和 Lua 脚本语言之间搭建桥梁。它的主要优势包括:
- 纯头文件库:只需包含头文件,无需编译额外的库
- 语法简洁:API 设计直观,学习曲线平缓
- 类型安全:编译时进行类型检查
- 高性能:运行时开销极小
- 自动类型转换 :自动处理
std::string、容器等复杂类型 - 类绑定支持:完整支持构造函数、成员函数、成员变量、继承
- 支持 Lua 5.1+ 及 LuaJIT
本文将使用 LuaBridge 3(最新版本)构建 Lua 扩展模块。
四、整体架构设计
下图展示了封装后的模块层次结构:
#mermaid-svg-Mr8HEmtIZACtDoN8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Mr8HEmtIZACtDoN8 .error-icon{fill:#552222;}#mermaid-svg-Mr8HEmtIZACtDoN8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Mr8HEmtIZACtDoN8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .marker.cross{stroke:#333333;}#mermaid-svg-Mr8HEmtIZACtDoN8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Mr8HEmtIZACtDoN8 p{margin:0;}#mermaid-svg-Mr8HEmtIZACtDoN8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster-label text{fill:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster-label span{color:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Mr8HEmtIZACtDoN8 .label text,#mermaid-svg-Mr8HEmtIZACtDoN8 span{fill:#333;color:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .node rect,#mermaid-svg-Mr8HEmtIZACtDoN8 .node circle,#mermaid-svg-Mr8HEmtIZACtDoN8 .node ellipse,#mermaid-svg-Mr8HEmtIZACtDoN8 .node polygon,#mermaid-svg-Mr8HEmtIZACtDoN8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .rough-node .label text,#mermaid-svg-Mr8HEmtIZACtDoN8 .node .label text,#mermaid-svg-Mr8HEmtIZACtDoN8 .image-shape .label,#mermaid-svg-Mr8HEmtIZACtDoN8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Mr8HEmtIZACtDoN8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .rough-node .label,#mermaid-svg-Mr8HEmtIZACtDoN8 .node .label,#mermaid-svg-Mr8HEmtIZACtDoN8 .image-shape .label,#mermaid-svg-Mr8HEmtIZACtDoN8 .icon-shape .label{text-align:center;}#mermaid-svg-Mr8HEmtIZACtDoN8 .node.clickable{cursor:pointer;}#mermaid-svg-Mr8HEmtIZACtDoN8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .arrowheadPath{fill:#333333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Mr8HEmtIZACtDoN8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Mr8HEmtIZACtDoN8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Mr8HEmtIZACtDoN8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster text{fill:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 .cluster span{color:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Mr8HEmtIZACtDoN8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Mr8HEmtIZACtDoN8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Mr8HEmtIZACtDoN8 .icon-shape,#mermaid-svg-Mr8HEmtIZACtDoN8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Mr8HEmtIZACtDoN8 .icon-shape p,#mermaid-svg-Mr8HEmtIZACtDoN8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Mr8HEmtIZACtDoN8 .icon-shape .label rect,#mermaid-svg-Mr8HEmtIZACtDoN8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Mr8HEmtIZACtDoN8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Mr8HEmtIZACtDoN8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Mr8HEmtIZACtDoN8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 后端存储
C++层
Lua层
Lua 脚本
Logger 对象
LuaBridge 绑定的 C++ 模块
microLog 门面 logItfc
后端工厂 logFactoryHelper
文件后端
Redis后端
自定义后端
本地日志文件
Redis List
外部系统
- Lua 层 :业务脚本通过
Logger对象调用vlog等方法。 - LuaBridge 绑定层:作为胶水层,将 Lua 调用转换为 C++ 调用。
- microLog 核心:原生 C++ 日志引擎,负责格式化、过滤、写入。
五、实现步骤
5.1 环境准备
确保系统已安装:
- Lua(5.1+ 或 LuaJIT)
- C++ 编译器(支持 C++11 及以上)
- LuaBridge 3(从 GitHub 获取)
- microLog 库源码(或已编译的动态库)
项目结构:
my-microlog-lua/
├── CMakeLists.txt # 构建配置
├── src/
│ ├── logger_bind.cpp # LuaBridge 绑定入口
│ └── logger_wrap.h # 封装类定义
├── lua/
│ └── test.lua # Lua 测试脚本
└── README.md
5.2 集成 LuaBridge
LuaBridge 3 是纯头文件库,只需将 Source/LuaBridge/ 目录加入头文件路径:
cpp
#include "LuaBridge/LuaBridge.h"
同时需要链接 Lua 库(如 liblua.a 或 liblua.so)。
5.3 定义 C++ 封装类 LoggerWrap
我们创建一个 C++ 类,用于包装 microLog 的 logItfc 实例,并通过 LuaBridge 暴露给 Lua。
logger_wrap.h
cpp
#pragma once
#include <string>
#include <memory>
#include <vector>
#include "microlog/logItfc.h" // microLog 核心头文件
class LoggerWrap {
public:
// 构造函数:创建 microLog 实例
LoggerWrap(const std::string& backend_type = "console");
~LoggerWrap();
// 暴露给 Lua 的方法
void vlog(const std::string& level, const std::string& format,
const std::vector<std::string>& args);
void log(const std::string& level, const std::string& message);
void setLevel(const std::string& level);
void close();
private:
// 内部 microLog 门面指针
std::unique_ptr<logItfc> m_logger;
// 辅助方法:将日志级别字符串转为枚举
int parseLevel(const std::string& level);
// 辅助方法:格式化消息(支持 %s, %d, %f 等占位符)
std::string formatMessage(const std::string& format,
const std::vector<std::string>& args);
};
logger_wrap.cpp
cpp
#include "logger_wrap.h"
#include <sstream>
#include <cstdarg>
// 假设 microLog 有 consoleLog 后端类
LoggerWrap::LoggerWrap(const std::string& backend_type) {
// 根据后端类型创建不同的后端实例
// 此处简化:默认使用控制台后端
// 实际可从参数解析配置
m_logger = std::make_unique<logItfc>(new consoleLog());
}
LoggerWrap::~LoggerWrap() {
close();
}
int LoggerWrap::parseLevel(const std::string& level) {
if (level == "DEBUG") return 0;
if (level == "INFO") return 1;
if (level == "WARN") return 2;
if (level == "ERROR") return 3;
return 1; // 默认 INFO
}
std::string LoggerWrap::formatMessage(const std::string& format,
const std::vector<std::string>& args) {
std::string result = format;
size_t argIdx = 0;
size_t pos = 0;
while (pos < result.length() && argIdx < args.size()) {
// 查找 % 占位符
size_t pct = result.find('%', pos);
if (pct == std::string::npos) break;
// 检查是否是 %%(转义)
if (pct + 1 < result.length() && result[pct + 1] == '%') {
result.erase(pct, 1); // 删除一个 %
pos = pct + 1;
continue;
}
// 提取占位符类型,直接用参数替换
// 由于所有参数已转为字符串,统一按 %s 处理
result.replace(pct, 2, args[argIdx++]);
pos = pct + args[argIdx - 1].length();
}
return result;
}
void LoggerWrap::vlog(const std::string& level, const std::string& format,
const std::vector<std::string>& args) {
// 将格式化字符串和参数列表合并为最终消息
std::string message = formatMessage(format, args);
// 调用 microLog 的 log 方法
int levelEnum = parseLevel(level);
m_logger->log(levelEnum, message);
}
void LoggerWrap::log(const std::string& level, const std::string& message) {
int levelEnum = parseLevel(level);
m_logger->log(levelEnum, message);
}
void LoggerWrap::setLevel(const std::string& level) {
int levelEnum = parseLevel(level);
// 假设 logItfc 有 setLevel 方法
// m_logger->setLevel(levelEnum);
}
void LoggerWrap::close() {
// 释放资源,刷新缓冲区
// m_logger->flush();
}
5.4 编写 LuaBridge 绑定入口
LuaBridge 的语法非常简洁,只需几行代码即可完成绑定。
logger_bind.cpp
cpp
#include <lua.hpp>
#include "LuaBridge/LuaBridge.h"
#include "logger_wrap.h"
extern "C" {
// Lua 模块入口函数
LUALIB_API int luaopen_microlog(lua_State* L) {
// 使用 LuaBridge 注册类
luabridge::getGlobalNamespace(L)
.beginClass<LoggerWrap>("Logger")
// 构造函数
.addConstructor<void (*)(const std::string&)>()
// vlog 方法:变参日志记录
.addFunction("vlog", &LoggerWrap::vlog)
// log 方法:简单日志记录
.addFunction("log", &LoggerWrap::log)
// 设置日志级别
.addFunction("setLevel", &LoggerWrap::setLevel)
// 关闭并释放资源
.addFunction("close", &LoggerWrap::close)
.endClass();
return 0;
}
} // extern "C"
关键点:
LUALIB_API int luaopen_microlog(lua_State* L)是 Lua 模块的标准入口函数,模块名为microlog。luabridge::getGlobalNamespace(L)获取全局命名空间。beginClass<LoggerWrap>("Logger")开始注册名为Logger的类。addConstructor<void (*)(const std::string&)>()注册构造函数,支持传递后端类型参数。addFunction注册成员函数。
5.5 重点:处理 vlog 变参方法
vlog 方法在 C++ 中通常接受格式化字符串和可变参数(...)。在 Lua 中,我们无法直接传递变参,但可以利用 Lua 的可变参数 特性(...)来模拟。
Lua 调用方式:
lua
logger:vlog('INFO', '用户 %s 登录成功,ID: %d', 'Alice', 1001)
在 C++ 端,我们需要处理从 Lua 传递过来的可变参数。但 LuaBridge 的 addFunction 不支持直接绑定 C++ 变参函数。解决方案有两种:
方案一:在 Lua 层封装(推荐)
在 Lua 中编写一个包装函数,将可变参数收集为 table,再调用 C++ 的 vlog 方法:
lua
-- lua/microlog.lua
local core = require("microlog_core")
local Logger = {}
Logger.__index = Logger
function Logger.new(backend_type)
local self = setmetatable({}, Logger)
self._impl = core.Logger(backend_type or "console")
return self
end
function Logger:vlog(level, format, ...)
local args = {...}
-- 将所有参数转为字符串
local str_args = {}
for i, v in ipairs(args) do
str_args[i] = tostring(v)
end
self._impl:vlog(level, format, str_args)
end
function Logger:log(level, message)
self._impl:log(level, message)
end
function Logger:setLevel(level)
self._impl:setLevel(level)
end
function Logger:close()
self._impl:close()
end
return Logger
方案二:在 C++ 端处理
利用 Lua C API 在 C++ 中直接操作 Lua 栈,获取可变参数。但这样就失去了 LuaBridge 的简洁性。
本文采用方案一,因为它保持了 LuaBridge 绑定的简洁性,同时将变参处理放在 Lua 层,更符合 Lua 的编程习惯。
5.6 编译为 Lua 共享库
使用 CMake 构建:
cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.12)
project(microlog_lua)
find_package(Lua REQUIRED)
find_package(PkgConfig REQUIRED)
# microLog 库
add_library(microlog STATIC IMPORTED)
set_target_properties(microlog PROPERTIES
IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/deps/microlog/lib/libmicrolog.a
)
# Lua 模块
add_library(microlog SHARED
src/logger_bind.cpp
src/logger_wrap.cpp
)
target_include_directories(microlog PRIVATE
${LUA_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/deps/microlog/include
${CMAKE_SOURCE_DIR}/deps/LuaBridge/Source
)
target_link_libraries(microlog
${LUA_LIBRARIES}
microlog
)
set_target_properties(microlog PROPERTIES
PREFIX ""
SUFFIX ".so" # Linux/macOS
# SUFFIX ".dll" # Windows
)
编译后将生成 microlog.so(Linux)或 microlog.dll(Windows),放入 Lua 的 package.cpath 路径即可。
六、在 Lua 中使用
6.1 基础使用
lua
-- test.lua
local Logger = require("microlog")
-- 创建 Logger 实例(默认控制台后端)
local logger = Logger.new()
-- 使用 vlog 变参方法
logger:vlog('INFO', '用户 %s 登录成功,ID: %d', 'Alice', 1001)
logger:vlog('DEBUG', '调试信息: 变量 x = %s, y = %s', 42, true)
logger:vlog('WARN', '磁盘使用率 %d%%', 85)
logger:vlog('ERROR', '捕获异常: %s', '连接超时')
-- 使用简单 log 方法
logger:log('INFO', '服务启动完成')
-- 设置日志级别
logger:setLevel('WARN')
-- 关闭并释放资源
logger:close()
6.2 指定后端类型
lua
-- 使用 Redis 后端
local logger = Logger.new('redis')
logger:vlog('INFO', '日志已写入 Redis')
logger:close()
-- 使用文件后端
local logger = Logger.new('file')
logger:vlog('INFO', '日志已写入文件')
logger:close()
6.3 在游戏循环中使用
lua
-- game.lua
local Logger = require("microlog")
local logger = Logger.new("file")
function on_player_login(player_name, player_id)
logger:vlog('INFO', '玩家 %s (ID: %d) 登录游戏', player_name, player_id)
-- 游戏逻辑...
end
function on_player_action(player_id, action)
logger:vlog('DEBUG', '玩家 %d 执行操作: %s', player_id, action)
-- 操作处理...
end
function on_error(error_msg)
logger:vlog('ERROR', '游戏错误: %s', error_msg)
end
-- 程序退出前关闭
-- 可注册为退出回调
logger:close()
七、高级特性
7.1 传递后端配置
若需要从 Lua 配置后端(如 Redis 连接参数),可以扩展构造函数:
cpp
// C++ 端:接受配置表
LoggerWrap::LoggerWrap(const std::string& backend_type,
const std::map<std::string, std::string>& config) {
if (backend_type == "redis") {
std::string host = config.at("host");
int port = std::stoi(config.at("port"));
m_logger = std::make_unique<logItfc>(new redisLog(host, port));
} else {
m_logger = std::make_unique<logItfc>(new consoleLog());
}
}
Lua 调用:
lua
local logger = Logger.new('redis', {host = '127.0.0.1', port = 6379})
7.2 支持上下文管理器(__gc)
在 Lua 中,可以为 userdata 设置 __gc 元方法,实现自动资源释放:
cpp
// C++ 端:在绑定中注册 gc 元方法
luabridge::getGlobalNamespace(L)
.beginClass<LoggerWrap>("Logger")
.addConstructor<void (*)(const std::string&)>()
.addFunction("vlog", &LoggerWrap::vlog)
.addFunction("log", &LoggerWrap::log)
.addFunction("setLevel", &LoggerWrap::setLevel)
.addFunction("close", &LoggerWrap::close)
// 添加 __gc 元方法
.addStaticFunction("__gc", [](LoggerWrap* obj) {
delete obj;
})
.endClass();
这样当 Lua 垃圾回收时,会自动调用析构函数释放资源。
7.3 与现有 Lua 日志框架集成
可以将 microLog 作为现有 Lua 日志框架(如 lualog)的后端:
lua
-- 将 microLog 集成到自定义日志系统
local Logger = require("microlog")
local log = Logger.new()
function log_info(msg, ...)
log:vlog('INFO', msg, ...)
end
function log_error(msg, ...)
log:vlog('ERROR', msg, ...)
end
-- 在其他模块中使用
log_info('系统初始化完成,版本: %s', '1.0.0')
八、性能优化建议
8.1 避免频繁的字符串转换
每次 vlog 调用都会将 Lua 字符串转为 C++ std::string,如果日志量极大,这可能成为瓶颈。可考虑:
- 在 microLog 内部使用异步批处理
- 减少不必要的日志级别检查
8.2 批量写入
在 Lua 端实现批量写入接口:
cpp
// C++ 端
void LoggerWrap::vlog_batch(const std::vector<LogEntry>& entries) {
for (const auto& entry : entries) {
m_logger->log(parseLevel(entry.level), entry.message);
}
}
Lua 调用:
lua
logger:log_batch({
{level = 'INFO', message = '批量日志 1'},
{level = 'INFO', message = '批量日志 2'},
})
8.3 使用 LuaJIT
LuaJIT 的 FFI 机制可以提供更高的调用性能。如果追求极致性能,可以考虑使用 LuaJIT FFI 直接调用 C 接口,而非通过 LuaBridge。
九、常见问题
Q1: 如何传递 table 作为参数?
A: 可以将 table 序列化为 JSON 字符串后再传递,或在 C++ 端解析 Lua table(使用 luabridge::LuaRef)。
Q2: 如何处理阻塞操作?
A: vlog 方法应尽快返回,将实际 I/O 操作放在后台线程。microLog 本身若支持异步,可直接启用。
Q3: 多个后端如何同时使用?
A: microLog 本身支持多后端,可在 C++ 端组合多个后端实例。
Q4: 如何在 Lua 中捕获 C++ 异常?
A: LuaBridge 会自动将 C++ 异常转换为 Lua 错误,Lua 端可用 pcall 捕获:
lua
local ok, err = pcall(function()
logger:vlog('INFO', '可能出错的日志')
end)
if not ok then
print('日志记录失败:', err)
end
Q5: 模块加载失败怎么办?
A: 检查 package.cpath 是否包含模块路径,以及共享库的依赖是否完整(如 liblua.so 是否可找到)。
十、总结
本文展示了如何利用 LuaBridge 将 C++ 日志库 microLog 封装为 Lua 模块。通过 LuaBridge 的简洁语法,我们能够:
- 轻松暴露 C++ 类和方法:只需几行代码即可完成绑定
- 优雅地处理变参函数:在 Lua 层封装可变参数,保持接口自然
- 保持高性能:LuaBridge 运行时开销极小
- 类型安全:编译时进行类型检查
将 C++ 的高效与 Lua 的灵活结合,为游戏开发、嵌入式系统等场景提供了强大的日志解决方案。希望本文能为您在 Lua 中引入原生 C++ 能力提供参考。