src/handle/、src/req/、src/uvcpp/ 是这一层:libuv 的薄封装。handle 是长期挂在
循环上的对象(定时器、信号、流、进程),req 是一次性的操作(写、连、DNS、文件)。
net/、web/、webapp/ 都建在它上面。
- 打开方式:没有开关。这三个目录总是编进库里,不需要任何
-D - 包含方式:
#include <uvcpp.h>只给基类——uvcpp_loop、uvcpp_handle、uvcpp_req加几个宏头(src/uvcpp.h:22-24)。uvcpp_timer、uvcpp_write、uvcpp_fs这些具体类型全都要单独 include,别以为聚合头把它们带进来了
本指南里的签名、默认值、行为都对着当前源码核过。凡是"这一层没做"或者"注释与实现对 不上"的地方都明确标出来——那些地方比 API 更容易踩。
| 目录 | 里面是什么 | 一个例子 |
|---|---|---|
src/handle/ |
长期对象,挂在循环上产生事件 | uvcpp_timer、uvcpp_signal、uvcpp_tcp、uvcpp_process |
src/req/ |
一次性操作,提交后回调一次就结束 | uvcpp_write、uvcpp_connect、uvcpp_getaddrinfo、uvcpp_fs |
src/uvcpp/ |
值类型与工具 | uvcpp_buf、uvcpp_alloc、uvcpp_thread、uvcpp_metrics |
handle 有生命周期(create → start → stop → close),req 没有:uvcpp_req 上
根本没有 start/stop/close,每个派生类只有 init() 和完成回调。req 的"结束"就是
回调跑完。
<uvcpp.h> 只聚合这七个头(src/uvcpp.h:22-24):
uvcpp/uvcpp_config.h ← 构建期生成,不在源码树里
uvcpp/uvcpp_define.h
uvcpp/uvcpp_version.h
uvcpp/uvcpp_export.h
handle/uvcpp_handle.h
handle/uvcpp_loop.h
req/uvcpp_req.h
所以它给你循环、两个基类、版本宏,一个具体 watcher 和具体 req 都不给。
net/、web/、webapp/、ssl/、http2/、expand/ 里的东西一个都不在里面——
那些是各自独立的模块指南的事。
一个重复三次就自己收工的定时器:
#include <cstdio>
#include <uvcpp.h>
#include <handle/uvcpp_timer.h>
int main() {
uvcpp::uvcpp_loop loop; // 构造函数里已经 init() 过,不用再调
uvcpp::uvcpp_timer timer(&loop); // 必须给 loop —— 默认构造的定时器不挂在任何循环上
int ticks = 0;
timer.start([&ticks](uvcpp::uvcpp_timer* t) {
std::printf("tick %d\n", ++ticks);
if (ticks >= 3) {
t->stop(); // 只是解除定时,句柄还在
t->close(); // 真正释放;close 回调在 run() 里跑完
}
}, 200, 200); // 首次 200ms,之后每 200ms
loop.run(); // UV_RUN_DEFAULT:没有活跃句柄时自己返回
loop.loop_close(); // 句柄队列已空,这里会成功
return 0;
}三个从例子里就能看出来的约定:
uvcpp_loop的构造函数自己调init()(src/handle/uvcpp_loop.cpp:57-63)。 再手写一句loop.init()是多余的。- 句柄的 loop 是构造参数,不是默认挂在
default_loop()上。uvcpp_timer t;之后 必须t.init(&loop)才可用——无参的init()不接循环,它只把句柄状态清零 (src/handle/uvcpp_timer.cpp:17-21)。唯一的例外还是uvcpp_loop。 stop()和close()是两件事。定时器stop()之后句柄仍然占着循环,uv_run不会因为没有"活跃"定时器就退出——必须close()。
uvcpp_loop(src/handle/uvcpp_loop.h:20)包一个 uv_loop_t。它派生自
uvcpp_handle,但 uv_loop_t 在 libuv 里并不是 uv_handle_t——继承关系只在包装层。
| 方法 | 签名 | 要点 |
|---|---|---|
run |
int run(uv_run_mode md = UV_RUN_DEFAULT) |
阻塞跑 |
stop |
void stop() |
让 uv_run 返回 |
loop_close |
int loop_close() |
幂等;第二次直接返回 0 |
loop_alive |
int loop_alive() |
还有活跃句柄/请求吗 |
is_running |
bool is_running() const |
此刻正在 uv_run 里面吗 |
default_loop |
static uvcpp_loop* default_loop() |
进程级单例 |
uv_run_mode 是 libuv 的枚举,不是本库的。 直接用 UV_RUN_DEFAULT /
UV_RUN_ONCE / UV_RUN_NOWAIT,别写 uvcpp::uv_run_mode。
run() 不可重入。 头里写得很直白(src/handle/uvcpp_loop.h:40-50):库里有几条
析构路径要在收尾时拨几轮循环放掉挂起的关闭回调,那些路径如果是在本循环自己的回调里
被调到,就既不能再拨一次、也不能把循环关掉。判据就是 is_running():
#include <uvcpp.h>
#include <handle/uvcpp_idle.h>
int main() {
uvcpp::uvcpp_loop loop;
uvcpp::uvcpp_idle idle(&loop);
idle.start([&loop](uvcpp::uvcpp_idle* self) {
// 在回调里:loop.run() 绝对不能调 —— 会重入 uv_run
if (loop.is_running()) {
self->close(); // 换个做法:把自己关掉,让外层 uv_run 自然返回
}
});
return loop.run();
}loop_close() 之后那块内存不能碰。 成功之后 libuv 已经把 uv_loop_t 收掉,
debug 构建里还会把整块内存填成 -1(src/handle/uvcpp_loop.h:61-67)——连
uv_loop_alive() 都是读垃圾。包装层有几处保护(run() 返回 0、loop_alive() 返回 0、
loop_close() 返回 0),但别指望它兜住一切。
stop() 可能什么都不做。 它只在 loop_alive() 非零时才调 uv_stop
(src/handle/uvcpp_loop.cpp:159-164)。没有活跃句柄时循环本来就会自己退出,
所以这不影响正确性——但别把 stop() 当成"一定能打断阻塞的 uv_run"。
default_loop() 每次调用都重跑 init()(src/handle/uvcpp_loop.cpp:136-140),
也就是对同一个 uv_loop_t 反复 uv_loop_init。libuv 没有定义重复初始化,所以
不要把它当成可以随便反复调的东西——拿一次指针存下来用。
基类是 uvcpp_handle(src/handle/uvcpp_handle.h:90):
uvcpp_handle
├── uvcpp_loop ← 特殊:uv_loop_t 不是 uv_handle_t
├── uvcpp_timer uvcpp_idle uvcpp_prepare uvcpp_check
├── uvcpp_async uvcpp_poll uvcpp_signal
├── uvcpp_fs_event uvcpp_fs_poll
├── uvcpp_stream
│ ├── uvcpp_tcp
│ └── uvcpp_pipe
├── uvcpp_udp uvcpp_tty uvcpp_process
基类里 virtual 的只有 get_handle()、set_handle() 和析构;close() 不是虚函数。
uvcpp_loop::close() 是隐藏基类那个同名函数,不是覆盖——通过 uvcpp_handle* 调
close() 会走到基类版本,而两个 close() 返回类型还不一样(void vs int)。
关闭只有两个入口(src/handle/uvcpp_handle.h:123,127):
// doc-snippet: fragment — 重载形状的摘录,不是完整翻译单元;这里要展示的是"两个
// close 返回 void 但语义不同",凑成能编的 TU 反而要编出连调两次 close 的错代码。
void close(); // 不关心收尾
void close(::std::function<void(uvcpp_handle*)> closeCallback); // 收尾时回调两个都返回 void,两个都容忍"句柄已经被释放"(内部有 _handle == nullptr 判断)。
但在关闭回调跑之前连调两次 close() 会调两次 uv_close——头里没有防这个,
要防就自己看 is_closing()。
在句柄自己的回调里 delete 它是合法的。库先把 std::function 搬出来再调用
(src/handle/uvcpp_handle.cpp:149-154),所以回调返回之后它不会再碰 wrapper。
常见写法是挂在 close() 的回调上:
#include <uvcpp.h>
#include <handle/uvcpp_timer.h>
int main() {
uvcpp::uvcpp_loop loop;
uvcpp::uvcpp_timer* t = new uvcpp::uvcpp_timer(&loop);
t->start([](uvcpp::uvcpp_timer* self) {
self->stop();
self->close([](uvcpp::uvcpp_handle* h) {
delete static_cast<uvcpp::uvcpp_timer*>(h); // 关闭回调里释放,安全
});
}, 100, 0);
loop.run();
loop.loop_close();
return 0;
}stop() 的行为每个类都不一样,这是句柄层最大的坑:
| 类 | stop() 干什么 |
|---|---|
uvcpp_timer uvcpp_check uvcpp_prepare uvcpp_poll uvcpp_signal uvcpp_fs_event uvcpp_fs_poll |
只调 uv_*_stop(),句柄还能再 start() |
uvcpp_idle |
调 close() —— 句柄被消费掉,不能重启(src/handle/uvcpp_idle.cpp:38-43) |
而且那七个"只 stop"的实现都不判空指针(src/handle/uvcpp_timer.cpp:36、src/handle/uvcpp_check.cpp:37
等),展开就是 reinterpret_cast<...>(this->get_handle()) 直接交给 uv_*_stop。
libuv 那边会解引用。所以关闭回调跑完之后再 stop() 是空指针解引用,而同样情况下
close() 是安全的。这个不对称头文件里没写。
uvcpp_req(src/req/uvcpp_req.h:88)是所有 req 的基类。回调签名没有统一约定,
每个类自己的成员:
| 类 | 回调签名 |
|---|---|
uvcpp_write |
void(uvcpp_write*, int status) |
uvcpp_connect |
void(uvcpp_connect*, int status) |
uvcpp_udp_send |
void(uvcpp_udp_send*, int status) |
uvcpp_getaddrinfo |
void(uvcpp_getaddrinfo*, int status, struct addrinfo*) |
uvcpp_getnameinfo |
void(uvcpp_getnameinfo*, int status, const char* host, const char* service) |
uvcpp_random |
void(uvcpp_random*, int status, void* buf, size_t buflen) |
uvcpp_fs |
void(uvcpp_fs*) —— status 不是参数,事后 get_result() |
uvcpp_work |
工作线程 void(uvcpp_work*),循环线程 void(uvcpp_work*, int status) |
规律是"先自己,再 status",uvcpp_fs 是唯一的例外。
set_self_free(true)(src/req/uvcpp_req.h:137)让 req 在完成回调返回后自己
delete,默认关。开了之后调用方不能再删。
为什么能在完成回调里 delete 自己:invoke_completion()(src/req/uvcpp_req.h:174-193)
先把闭包从槽位里 move 出来、再清空源槽、然后才调用。顺序反过来的话,删掉的就是
"此刻正在执行的那个 std::function",连同它的捕获一起——是未定义行为。
src/req/uvcpp_req.h:178-185 还专门写了"为什么一定要显式清空源":libc++ 的小对象
move 不会把源置空(libstdc++/MSVC 会),所以这个 bug 只在 macOS 上显形。
uvcpp_random 在旧 libuv 上不存在——它整段套在 #if UV_VERSION_MINOR >= 33 里。
uvcpp_work 的两个回调跑在不同的线程上。 工作回调在工作线程池里,
完成回调(after_work)在循环线程里。所以:
- 工作回调里不能碰循环、不能碰其他句柄、不能
delete这个 work 对象 - 原因写在工作回调的注释里(
src/req/uvcpp_work.h:51-54):它故意不走invoke_completion,因为那个函数会按self_free_删对象,而对象必须活到 after_work
#include <cstdio>
#include <uvcpp.h>
#include <req/uvcpp_work.h>
int main() {
uvcpp::uvcpp_loop loop;
uvcpp::uvcpp_loop* lp = &loop;
uvcpp::uvcpp_work* w = new uvcpp::uvcpp_work();
w->queue_work(&loop,
[](uvcpp::uvcpp_work*) {
// 工作线程:这里干重活。不要碰 loop,不要 delete self。
},
[lp](uvcpp::uvcpp_work* self, int status) {
// 循环线程:这里可以安全地碰循环和句柄
std::printf("status=%d\n", status);
delete self; // 先删,再别再解引用 self
lp->stop();
});
loop.run();
loop.loop_close();
return 0;
}queue_work 是一次性的:第二次调会抛(src/req/uvcpp_work.cpp:24-27),因为
loop 已经设过了。
跨线程唤醒循环用 uvcpp_async。send() 就是 uv_async_send,头里明说它用于
"从另一个线程给循环发通知"(src/handle/uvcpp_async.h:34)。除此之外这一层没有
任何线程安全承诺——头文件里没有一句话说 init/start/close 可以从别的线程调。
uvcpp_buf(src/uvcpp/uvcpp_buf.h:20)是可增长、可共享的字节缓冲,能和 uv_buf_t
互转。和它相关的契约都是生命周期:
- libuv 要求写缓冲活到完成回调,它不会替你拷一份(
src/req/uvcpp_write.h:36-46)。uv_write会把uv_buf_t数组本身拷走,但不拷数据(:122-128)。 append_uv_buf_view()必须在set_uv_buf()之后调(src/req/uvcpp_write.h:56), 而且第 2 块只有一个:再调一次(两种入口混着调也算)会把上一个占用者换掉。append_uv_buf_owned()传的必须是uvcpp_buf::out_uv_buf()返回的原指针 (:67-70)——释放用的就是它。别先给base加偏移再传进来。uvcpp_stream::try_write()只是借用那次调用的入参,不拷贝不保留 (src/handle/uvcpp_stream.h:63)。
int返回值:0 成功,失败是 libuv 的负错误码(UV_EINVAL、UV_EALREADY…)。 最后一个是粘性的,get_last_error()拿得到。- 内存不足抛
std::bad_alloc:src/uvcpp/uvcpp_alloc.h:121-126、uvcpp_loop的构造 (src/handle/uvcpp_loop.cpp:59-60)、uvcpp_req的拷贝构造与赋值。(uvcpp_handle的拷贝构造/赋值曾经也在这一列,但它们已删 —— 见 §10。) queue_work抛的是const char*,不是std::exception(src/req/uvcpp_work.cpp:24)。 单元测试统一的catch (const std::exception&)接不住它。- 这一层除此之外基本不抛异常——错误都走返回值。
uvcpp_idle::stop() 会把句柄关掉。 它不是"暂停"而是"销毁":stop() 之后不能再
start()(src/handle/uvcpp_idle.cpp:38-43)。uvcpp_check / uvcpp_prepare 的
stop() 才是真的只停。
关闭之后再 stop() 是空指针解引用。 那七个 stop() 实现都不判空,而 close()
判。要"关了之后还能安全再调一次"的场合,用 close()。
无参的 start() 在 uvcpp_idle / uvcpp_check 上会返回 UV_EINVAL。
它把 nullptr 当回调传下去,libuv 1.51 的 uv_idle_start 对空回调返回 -22
(src/handle/uvcpp_idle.cpp:30)。要判返回值,不要以为会崩。
在句柄自己的回调里调 loop.run()。 不可重入,用 is_running() 判。
抓 std::exception 抓不住 queue_work 的异常,它是字符串字面量。
Unix 上构造函数会忽略 SIGPIPE。 uvcpp_loop 的构造函数在进程内装一次
SIG_IGN,但只在当前处置是 SIG_DFL 时才动(src/handle/uvcpp_loop.cpp:26)。
想自己处理 SIGPIPE 就要在造循环之前装好。
- 没有逐成员的 API 参考。 全仓没有 Doxygen 生成步骤,本页讲的是"典型流程 + 坑"。
- 不支持拷贝句柄,也不支持"克隆" —— 而且是编译期拒绝,不是文档警告。
基类
uvcpp_handle与 16 个派生类的拷贝构造 / 拷贝赋值全部是= delete(src/handle/uvcpp_handle.h:94),uvcpp_handle::clone()与uvcpp_req::clone()同样= delete,任一调用都是编译错误。理由是这几个操作不可能有正确实现:uv_handle_t不是一个值,它是挂在某个uv_loop_t的handle_queue上的一个 节点。memcpy一个活着的它,拷出来的那份会带走源的loop指针与队列邻居, 却从来没被插进任何队列 —— 于是"那块内存要不要还"怎么答都不对:不还就是纯泄漏 (原先的实况),还了则会走uv_close,顺着源句柄邻居的地址把还活着的源 句柄从它自己的队列上静默摘掉。判据是编译期断言,见tests/unit/handle_unit.cpp。uvcpp_req的拷贝是另一回事,不受这批影响:它没有所有权标志,拷出来的那块 在析构里会被free_req()还掉。 - 线程契约没有成文。 除了
uvcpp_async::send(),头文件里没有任何一句话说哪个 线程能调哪个函数。实现注释暗示是循环线程,但那是从代码推的,不是承诺。 default_loop()的重复init()行为未定义。 见 §3。
相关文档:net 网络层指南、TLS 与证书指南、 项目 README。
本页用到的头文件:<uvcpp.h>、<handle/uvcpp_loop.h>、<handle/uvcpp_handle.h>、
<handle/uvcpp_timer.h>、<handle/uvcpp_idle.h>、<handle/uvcpp_async.h>、
<handle/uvcpp_stream.h>、<req/uvcpp_req.h>、<req/uvcpp_work.h>、
<req/uvcpp_write.h>、<uvcpp/uvcpp_buf.h>。