Libevent 是纯 C 库,接口充满了 void*
和手动内存管理。在现代 C++ 项目中,我们希望拥有 RAII
的安全性和 Lambda 的便捷性。本篇将介绍如何优雅地封装
Libevent。
1. RAII 资源管理
使用 std::unique_ptr 自动管理
event_base, event,
bufferevent 的生命周期,彻底告别内存泄露。
1.1. 自定义删除器 (Custom Deleter)
#include <memory>
#include <event2/event.h>
#include <event2/bufferevent.h>
// 定义删除器类型
struct EventBaseDeleter {
void operator()(struct event_base* p) const { event_base_free(p); }
};
struct EventDeleter {
void operator()(struct event* p) const { event_free(p); }
};
struct BuffereventDeleter {
void operator()(struct bufferevent* p) const { bufferevent_free(p); }
};
// 定义智能指针别名
using EventBasePtr = std::unique_ptr<struct event_base, EventBaseDeleter>;
using EventPtr = std::unique_ptr<struct event, EventDeleter>;
using BuffereventPtr = std::unique_ptr<struct bufferevent, BuffereventDeleter>;
// 使用示例
void example() {
EventBasePtr base(event_base_new());
if (!base) throw std::runtime_error("Failed to create base");
// base 离开作用域时自动释放
}2. Lambda 回调封装
Libevent 的回调函数签名是固定的 C 函数指针:
void (*)(evutil_socket_t, short, void *)
我们无法直接传入 C++ 的 Lambda(除非是无捕获的 Lambda)。解决方案是使用 Trampoline (蹦床) 模式。
2.1. 实现原理
- 将 Lambda 对象(
std::function)的指针作为void* arg传入 Libevent。 - 编写一个静态的 C 函数作为“蹦床”。
- 在蹦床函数中,将
void* arg强转回std::function*并调用。
2.2. 封装示例
#include <functional>
#include <iostream>
class Timer {
public:
using Callback = std::function<void()>;
Timer(struct event_base* base, Callback cb)
: cb_(std::move(cb)) {
// 传入 this 指针作为参数
ev_.reset(evtimer_new(base, &Timer::trampoline, this));
}
void start(int seconds) {
struct timeval tv = {seconds, 0};
evtimer_add(ev_.get(), &tv);
}
private:
// 蹦床函数
static void trampoline(evutil_socket_t, short, void* arg) {
Timer* self = static_cast<Timer*>(arg);
if (self->cb_) {
self->cb_();
}
}
EventPtr ev_;
Callback cb_;
};
// 使用
void test(struct event_base* base) {
// 可以捕获上下文的 Lambda
int count = 0;
auto t = new Timer(base, [&count]() {
std::cout << "Timeout! Count: " << ++count << std::endl;
});
t->start(1);
}3. 进阶:避免悬垂指针
上面的例子有一个隐患:如果 Timer
对象被销毁了,但 Libevent 中还有挂起的事件,回调触发时访问
this 指针会导致 Crash。
3.1. 解决方案
- 在析构函数中取消事件:
evtimer_del(ev_.get())。这是最安全的做法。 - 使用
std::shared_ptr和std::weak_ptr: 在回调中检查对象是否还活着。
4. 总结
通过 RAII 和 Trampoline 模式,我们可以将 Libevent 包装成符合 C++ 习惯的现代库。这不仅提高了代码的可读性,更大大降低了内存管理错误的风险。
上一篇: 04-architecture/process-pitfalls.md - 进程模型陷阱 下一篇: 04-architecture/business-integration.md - 业务集成模式