diff --git a/CMakeLists.txt b/CMakeLists.txt index d64326ae6..c4c52e16e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,19 @@ if(WITH_LUA) else() set(LIBS ${LIBS} lua) endif() + # lua bindings that wrap higher-level modules are conditionally compiled on + # HVLUA_WITH_* (kept in sync with Makefile.in): enabled only when both WITH_LUA + # and the underlying module are on. Without these, the http/redis/ws/mqtt + # binding bodies are #ifdef'd out and hv.http/redis/ws/mqtt silently become nil. + if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT) + add_definitions(-DHVLUA_WITH_HTTP) + endif() + if(WITH_EVPP AND WITH_REDIS) + add_definitions(-DHVLUA_WITH_REDIS) + endif() + if(WITH_EVPP AND WITH_MQTT) + add_definitions(-DHVLUA_WITH_MQTT) + endif() endif() if(WIN32 OR MINGW) @@ -236,6 +249,15 @@ if(WITH_PROTOCOL) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} protocol) endif() +if(WITH_LUA) + set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hvlua.h lua/hvlua_json.h lua/hvlua_util.h) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} lua) + if(NOT WITH_EVPP) + set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS}) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil) + endif() +endif() + if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) diff --git a/Makefile b/Makefile index 6eec0d1d2..60733f9ea 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,15 @@ LIBHV_HEADERS += $(PROTOCOL_HEADERS) LIBHV_SRCDIRS += protocol endif +ifeq ($(WITH_LUA), yes) +LIBHV_HEADERS += lua/hvlua.h lua/hvlua_json.h lua/hvlua_util.h +LIBHV_SRCDIRS += lua +ifneq ($(WITH_EVPP), yes) +LIBHV_HEADERS += $(CPPUTIL_HEADERS) +LIBHV_SRCDIRS += cpputil +endif +endif + ifeq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp @@ -101,6 +110,12 @@ ifeq ($(WITH_MQTT), yes) EXAMPLES += mqtt_sub mqtt_pub mqtt_client_test endif +ifeq ($(WITH_LUA), yes) +ifeq ($(WITH_EVPP), yes) +EXAMPLES += hvlua +endif +endif + examples: $(EXAMPLES) @echo "make examples done." @@ -179,6 +194,9 @@ socks5_proxy_server: prepare host: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/host.c" +hvlua: prepare libhv + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/hvlua examples/hvlua.cpp -Llib -lhv -pthread $(LUA_LIBS) + multi-acceptor-processes: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS)" SRCS="examples/multi-thread/multi-acceptor-processes.c" @@ -200,7 +218,6 @@ tinyproxyd: prepare nmap: prepare libhv $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) cpputil examples/nmap" DEFINES="PRINT_DEBUG" -ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_REDIS), yes) redis_client_example: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) cpputil evpp redis" SRCS="examples/redis_client_test.cpp" @@ -208,14 +225,13 @@ redis_client_example: prepare redis_subscriber_example: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) cpputil evpp redis" SRCS="examples/redis_subscriber_test.cpp" endif -endif wrk: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http" SRCS="examples/wrk.cpp" httpd: prepare $(RM) examples/httpd/*.o - $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/client http/server examples/httpd" + $(MAKEF) TARGET=$@ SRCDIRS="$(LIBHV_SRCDIRS) util cpputil evpp http http/client http/server examples/httpd" consul: prepare libhv $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/client examples/consul" DEFINES="PRINT_DEBUG" @@ -228,13 +244,13 @@ wget: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/client" SRCS="examples/wget.cpp" http_server_test: prepare - $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/server" SRCS="examples/http_server_test.cpp" + $(MAKEF) TARGET=$@ SRCDIRS="$(LIBHV_SRCDIRS)" SRCS="examples/http_server_test.cpp" http_client_test: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/client" SRCS="examples/http_client_test.cpp" websocket_server_test: prepare - $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/server" SRCS="examples/websocket_server_test.cpp" + $(MAKEF) TARGET=$@ SRCDIRS="$(LIBHV_SRCDIRS) util cpputil evpp http http/server" SRCS="examples/websocket_server_test.cpp" websocket_client_test: prepare $(MAKEF) TARGET=$@ SRCDIRS="$(CORE_SRCDIRS) util cpputil evpp http http/client" SRCS="examples/websocket_client_test.cpp" @@ -281,7 +297,7 @@ protorpc_server: prepare protorpc_protoc SRCS="examples/protorpc/protorpc_server.cpp examples/protorpc/protorpc.c" \ LIBS="protobuf" -unittest: prepare +unittest: prepare libhv $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/rbtree_test unittest/rbtree_test.c base/rbtree.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/hbase_test unittest/hbase_test.c base/hbase.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -o bin/mkdir_p unittest/mkdir_test.c base/hbase.c @@ -312,31 +328,45 @@ unittest: prepare $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ping unittest/ping_test.c protocol/icmp.c base/hsocket.c base/htime.c -DPRINT_DEBUG $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ftp unittest/ftp_test.c protocol/ftp.c base/hsocket.c base/htime.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -Iutil -o bin/sendmail unittest/sendmail_test.c protocol/smtp.c base/hsocket.c base/htime.c util/base64.c - $(MAKE) libhv $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Issl -Ievent -o bin/hdns_test unittest/hdns_test.c -Llib -lhv -pthread $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Issl -Ievent -o bin/hdns_benchmark unittest/hdns_benchmark.c -Llib -lhv -pthread ifeq ($(WITH_EVPP), yes) - $(MAKE) libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -o bin/tcpclient_dns_test unittest/tcpclient_dns_test.cpp -Llib -lhv -pthread -ifeq ($(WITH_LUA), yes) - $(MAKE) libhv - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_test.cpp -Llib -lhv -pthread $(LUA_LIBS) -else - $(RM) bin/http_lua_handler_test endif +ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_REDIS), yes) - $(MAKE) libhv $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Icpputil -Iredis -o bin/redis_protocol_test unittest/redis_protocol_test.cpp redis/RedisMessage.cpp $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -o bin/redis_async_client_test unittest/redis_async_client_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -o bin/redis_client_test unittest/redis_client_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -o bin/redis_batch_test unittest/redis_batch_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -o bin/redis_subscriber_test unittest/redis_subscriber_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread -else - $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test endif -else - $(RM) bin/tcpclient_dns_test - $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test +endif +ifeq ($(WITH_LUA), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/lua_binding_test unittest/lua_binding_test.cpp -Llib -lhv -pthread $(LUA_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -o bin/lua_io_test unittest/lua_io_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_SERVER), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -o bin/http_script_handler_test unittest/http_script_handler_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +ifeq ($(WITH_HTTP_CLIENT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_handler_test unittest/http_lua_handler_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +endif +endif +ifeq ($(WITH_HTTP_SERVER), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA -DHVLUA_WITH_HTTP $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -Ihttp/client -o bin/lua_http_test unittest/lua_http_test.cpp -Llib -lhv -pthread $(LUA_LIBS) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA -DHVLUA_WITH_HTTP $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -Ihttp/client -o bin/lua_ws_test unittest/lua_ws_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +endif +endif +endif +ifeq ($(WITH_MQTT), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA -DHVLUA_WITH_MQTT $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Imqtt -o bin/lua_mqtt_test unittest/lua_mqtt_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +endif +ifeq ($(WITH_REDIS), yes) + $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA -DHVLUA_WITH_REDIS $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Iredis -Ievpp -Ilua -o bin/lua_redis_test unittest/lua_redis_test.cpp unittest/redis_test_server.cpp -Llib -lhv -pthread $(LUA_LIBS) +endif +endif endif run-unittest: unittest diff --git a/Makefile.in b/Makefile.in index 6c9b4392b..27c7dbd95 100644 --- a/Makefile.in +++ b/Makefile.in @@ -164,6 +164,25 @@ endif ifeq ($(WITH_LUA), yes) CPPFLAGS += -DWITH_LUA $(LUA_CFLAGS) LDFLAGS += $(LUA_LIBS) +# lua bindings that wrap higher-level modules are enabled only when both the lua +# binding and the underlying module are built (HVLUA_WITH_* preprocessor macros). +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) + CPPFLAGS += -DHVLUA_WITH_HTTP +endif +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_REDIS), yes) + CPPFLAGS += -DHVLUA_WITH_REDIS +endif +endif +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_MQTT), yes) + CPPFLAGS += -DHVLUA_WITH_MQTT +endif +endif endif CPPFLAGS += $(addprefix -D, $(DEFINES)) diff --git a/configure b/configure index 9cf1b15f3..b04c08bda 100755 --- a/configure +++ b/configure @@ -31,6 +31,7 @@ modules: --with-http-server compile http server module? (DEFAULT: $WITH_HTTP_SERVER) --with-mqtt compile mqtt module? (DEFAULT: $WITH_MQTT) --with-redis compile redis module? (DEFAULT: $WITH_REDIS) + --with-lua compile lua module? (DEFAULT: $WITH_LUA) features: --enable-uds enable Unix Domain Socket? (DEFAULT: $ENABLE_UDS) diff --git a/docs/PLAN.md b/docs/PLAN.md index badb081fb..cfd1a4b9a 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -6,15 +6,15 @@ - rudp: KCP - evpp: c++ EventLoop interface similar to muduo and evpp - http client/server: include https http1/x http2 +- http server sync/async/ctx/state/script handlers - websocket client/server - mqtt client - redis client - async DNS -- http lua handler +- lua binding ## Plan -- lua binding - js binding - hrpc = libhv + protobuf - rudp: FEC, ARQ, UDT, QUIC diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index c7293c81f..d77a32518 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -110,14 +110,61 @@ end ## hv API -首版只提供少量宿主能力: +脚本运行在所属 IO 线程的 **协程** 里,因此可以用 **同步写法** 调用异步能力:调用会挂起当前请求的协程、把控制权交还事件循环,结果就绪后在同一线程恢复,全程不阻塞 loop。 + +所有脚本可用能力都挂在统一的全局 `hv` 表下: + +```lua +hv.version() -- libhv 版本串, 如 "1.3.4" +hv.log(...) -- 日志 (INFO), 等价 hv.logi +hv.logd(...) / hv.logi(...) / hv.logw(...) / hv.loge(...) -- debug/info/warn/error +hv.json.encode(tbl) -- table -> json string +hv.json.decode(str) -- json string -> table + +hv.setTimeout(ms, fn) -- 定时器 (返回句柄) +hv.setInterval(ms, fn) +hv.clearTimer(handle) +hv.sleep(ms) -- 协程同步 sleep: 挂起当前协程 ms 毫秒, 不阻塞 loop +hv.resolveDns(host) -- 协程同步 DNS 解析: 返回 { ip, ... } 或 nil, err +hv.run() / hv.stop() -- 运行/停止当前线程的 event loop (独立脚本用; HTTP handler 内不需要) + +-- TCP/UDP (协程同步, event 层, 仅当前 loop) +local conn, err = hv.connect(host, port [, timeout_ms]) -- TCP 客户端 +hv.tcpServer(host, port, function(conn) ... end) -- 每连接一个协程 +local sock = hv.udpClient(host, port) +hv.udpServer(host, port, function(sock, data, peer) ... end) +-- conn: conn:read() / conn:readline() / conn:readuntil(d) / conn:readbytes(n) +-- conn:setUnpack(opts) / conn:write(s) / conn:close() / conn:fd() / conn:peeraddr() +-- sock: sock:sendto(s) / sock:recvfrom() -> data,peer / sock:close() +``` + +`conn:setUnpack(opts)` 让后续 `conn:read()` 每次返回一个完整的包(参考 libhv `hio_set_unpack`): + +```lua +conn:setUnpack({ + mode = "length_field", -- none|fixed|delimiter|length_field + body_offset = 5, length_field_offset = 1, + length_field_bytes = 4, length_field_coding = "be", -- be|le|varint|asn1 + -- delimiter 模式: delimiter = "\r\n" ; fixed 模式: fixed_length = 16 +}) +``` + +示例(handler 内部“同步”写法,实际异步,loop 不阻塞): ```lua -hv.log(...) -hv.now() +function handle(ctx) + local addrs, err = hv.resolveDns("example.com") + if err then + ctx:status(502) + return ctx:json({ ok = false, error = err }) + end + return ctx:json({ ok = true, addrs = addrs }) +end ``` -暂不暴露 `hv.event_loop`、TCP/HTTP client、Redis 等能力,避免脚本直接操作底层事件循环。后续可以按业务需要增加受控的 `hv.redis`、`hv.http` 等模块。 +多个请求会在同一 IO 线程上并发交错执行:某个请求在 `hv.resolveDns` / `hv.sleep` 处挂起时,同线程的其它请求会继续推进。注意协作式调度的语义——跨越挂起点不要对全局状态做原子性假设。 + +> `hv.setTimeout` / `hv.resolveDns` 对应 `event/` 层能力,`hv.log` / `hv.version` 对应 `base/` 层,`hv.json` 对应 `cpputil/`;实现分别在 `lua/hvlua_event.c`、`lua/hvlua_base.c`、`lua/hvlua_json.cpp`。TCP/HTTP client、Redis 等高层 client 绑定后续按 `WITH_LUA` / `WITH_REDIS` 等开关编入。 ## 热更新 diff --git a/docs/cn/README.md b/docs/cn/README.md index db22adb57..eaab34632 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -5,6 +5,10 @@ - [hbase: 基础函数](hbase.md) - [hlog: 日志](hlog.md) +## lua接口 + +- [Lua Binding: hv.* Lua 绑定](lua.md) + ## c++接口 - [class EventLoop: 事件循环类](EventLoop.md) diff --git a/docs/cn/lua.md b/docs/cn/lua.md new file mode 100644 index 000000000..ec7c4ea4d --- /dev/null +++ b/docs/cn/lua.md @@ -0,0 +1,230 @@ +# Lua Binding + +libhv 提供一套 Lua 绑定,让 Lua 脚本直接驱动 libhv 的事件循环与异步网络能力。核心特点是**用同步写法表达异步 IO**:脚本里 `local data = conn:read()`、`local resp = hv.http.get(url)`、`local v = r:get(k)` 读起来都是阻塞式直觉,但底层通过协程 `yield → 异步回调 → resume`,事件循环全程不阻塞(与 OpenResty 同模型)。 + +该功能是可选模块,默认不编译。 + +## 编译 + +需要 Lua 5.3 或更新版本的开发库。 + +Makefile: + +```bash +./configure --with-lua --with-http --with-redis --with-mqtt +make libhv +make hvlua # 独立 Lua 运行时 +make unittest # 编译 lua 相关单测 +``` + +CMake: + +```bash +cmake -S . -B build -DWITH_LUA=ON -DWITH_HTTP=ON -DWITH_REDIS=ON -DWITH_MQTT=ON +cmake --build build +``` + +分层说明:`hloop.*` 定时器、`hv.tcpClient/tcpServer/udpClient/udpServer`、`hv.resolveDns`、`hv.json`、`hv.log` 只依赖纯 C 的 event 层,`WITH_LUA` 即可用;`hv.http` / `hv.ws` 需要 `WITH_HTTP`,`hv.redis` 需要 `WITH_REDIS`,`hv.mqtt` 需要 `WITH_MQTT`。未开启对应模块时,Lua 里 `hv.http` / `hv.redis` / `hv.mqtt` 为 `nil`。 + +## 运行脚本 + +独立运行时 `hvlua`: + +```bash +bin/hvlua examples/lua/timer.lua +bin/hvlua examples/lua/tcp_client.lua 127.0.0.1 10514 +``` + +`examples/lua/` 下有 timer / sleep / dns / tcp / udp / http / ws / redis / mqtt 各场景的示例脚本。 + +## 核心模型 + +- **每个 loop 线程一个 lua_State**,存放在 `hloop_t` 上,loop 销毁时关闭。 +- **协程同步写法**:每个任务(一段脚本 / 每个 HTTP 请求 / 每个接入连接)跑在独立协程里;可挂起的绑定(`conn:read`、`hv.sleep`、`hv.http.get`、`r:get` 等)内部 `yield`,异步结果回来后在**同一 loop 线程**上 `resume`,脚本从挂起点继续。 +- **无锁**:单 loop 线程 + 协作式协程,任意时刻只有一个协程在执行,其余停在各自 yield 点。注意跨 yield 点的全局状态可能被其它任务穿插修改(与 OpenResty 同模型)。 +- **协作式调度**:纯 CPU 死循环会阻塞该 loop 线程。 + +## 返回值约定(统一) + +- 成功:返回 Lua 原生值(string / integer / boolean / table)。 +- 无结果(Redis nil 回复、DNS 无记录):返回 `nil`。 +- 失败:返回 `nil, "错误消息"`(第二个返回值为错误串),脚本用 `local v, err = ...; if err then ... end` 处理。 + +--- + +## hv 通用工具 + +```lua +hv.version() -- libhv 版本串,如 "1.3.4" +hv.log(...) -- 以 tab 连接参数,info 级日志(logi 的别名) +hv.logd(...) / hv.logi(...) / hv.logw(...) / hv.loge(...) -- debug/info/warn/error + +hv.json.encode(tbl) -- table -> json 字符串 +hv.json.decode(str) -- json 字符串 -> table +``` + +## 定时器与协程 sleep + +```lua +local id = hv.setTimeout(1000, function() print("once") end) -- 返回句柄 +local id2 = hv.setInterval(500, function() print("tick") end) +hv.clearTimer(id) + +hv.sleep(1000) -- 协程同步:挂起当前协程 1000ms,loop 不阻塞 + +hv.run() -- 兼容保留;loop 由宿主自动驱动,无需调用 +hv.stop() -- 停止当前 loop +``` + +## hv.resolveDns(协程同步) + +```lua +local addrs, err = hv.resolveDns("example.com") +-- addrs: { "93.184.216.34", ... } ; 失败: nil, err +``` + +## TCP / UDP(event 层,协程同步) + +命名对齐 C++ 类 `hv::TcpClient` / `TcpServer` / `UdpClient` / `UdpServer`;`hv.connect` 是 `hv.tcpClient` 的别名,`hv.listen` 是 `hv.tcpServer` 的别名。 + +### TCP 客户端 + +```lua +local conn, err = hv.tcpClient(host, port [, timeout_ms]) -- 别名 hv.connect;协程同步,连上或失败 +conn:write("hello") -- 非阻塞,进写队列,即发即走 +local data, err = conn:read() -- 协程同步:挂起直到有数据;对端关闭返回 nil,"closed" +conn:close() +conn:fd() -- fd 或 -1 +conn:peeraddr() -- "ip:port" +``` + +拆包读(文本协议 / 二进制协议): + +```lua +local line = conn:readline() -- 读到 '\n'(含) +local data = conn:readuntil("\n") -- 读到单字节分隔符(含) +local buf = conn:readbytes(16) -- 读满 16 字节 + +-- 设置一次后,conn:read() 每次返回一个完整的包(二进制协议最常用) +conn:setUnpack({ + mode = "length_field", -- none | fixed | delimiter | length_field + package_max_length = 1 << 21, + -- length_field 模式: + body_offset = 5, length_field_offset = 1, + length_field_bytes = 4, length_field_coding = "be", -- be | le | varint | asn1 + length_adjustment = 0, + -- delimiter 模式: delimiter = "\r\n" + -- fixed 模式: fixed_length = 16 +}) +``` + +### TCP 服务端 + +`on_conn(conn)` 在每个新连接的独立协程里被调用,因此可在里面用同步写法。 + +```lua +hv.tcpServer("0.0.0.0", 8080, function(conn) -- 别名 hv.listen + while true do + local data, err = conn:read() + if err then break end -- 连接关闭 + conn:write(data) -- echo + end +end) +``` + +### UDP + +UDP 无连接,`sock` 复用 conn 对象。 + +```lua +local sock = hv.udpClient("127.0.0.1", 8080) +sock:sendto("ping") +local data, peer = sock:recvfrom() -- 协程同步,返回数据 + 对端地址 + +hv.udpServer("0.0.0.0", 8080, function(sock, data, peer) + sock:sendto("pong") +end) +``` + +## hv.http(协程同步,需 WITH_HTTP) + +```lua +local resp, err = hv.http.get("http://127.0.0.1:8080/ping") +-- resp: { status = 200, body = "...", headers = { ... } } +local resp2 = hv.http.post("http://.../echo", "body", { ["Content-Type"] = "text/plain" }) +local resp3 = hv.http.put("http://.../item", "body") +local resp4 = hv.http.delete("http://.../item") +local resp5 = hv.http.request("GET", url [, body [, headers]]) +``` + +## hv.ws(WebSocket,协程同步,需 WITH_HTTP) + +WebSocket 是消息驱动的,收到的消息缓存在收件箱,`ws:recv()` 挂起直到有一条消息。连接断开时 `recv()`/`send()` 返回 `(nil, err)`:开启了自动重连时 `err="reconnecting"`(临时断开,底层正在重连),否则 `err="closed"`(终止)。 + +```lua +-- 第二参数为可选 opts 表 +local ws, err = hv.ws.connect("ws://127.0.0.1:8888/path", { + headers = { ["X-Token"] = "..." }, -- 可选:握手请求头 + ping_interval = 10000, -- 可选:心跳 ping 间隔 ms + reconnect = { -- 可选:给了就开自动重连 + min_delay = 1000, -- ms + max_delay = 10000, -- ms + delay_policy = 2, -- 0 固定 / 1 线性 / 2 指数退避 + max_retry = 0, -- 最大重试次数,0 = 无限 + }, +}) +ws:send("hello") -- 文本帧 +ws:send(payload, "binary") -- 二进制帧 +local msg, err = ws:recv() -- 协程同步:挂起直到收到一条消息 +if err == "reconnecting" then ... end -- 临时断开,正在重连 +ws:close() -- 显式关闭(终止,禁用重连) +``` + +> 开启重连后:重连成功会自动恢复,后续 `recv()`/`send()` 继续可用;断开期间 `send()` 会返回 `(nil,"reconnecting")` 而非静默丢弃,`recv()` 同样返回 `(nil,"reconnecting")` 让脚本自行决定继续等待还是退出。`ws:close()` 是显式终止,会禁用重连。 + +## hv.redis(协程同步,需 WITH_REDIS) + +```lua +local r = hv.redis.new({ host = "127.0.0.1", port = 6379, auth = "", db = 0, timeout = 3000 }) + +r:set("k", "v") -- 语法糖 +local v = r:get("k") -- bulk -> string / nil +local n = r:incr("c") -- integer +r:del("k") / r:decr("c") / r:expire("k", 60) / r:exists("k") + +-- 任意命令:变参或数组表两种形态 +local pong = r:command("PING") +local ret = r:command({ "HSET", "u:1", "name", "tom" }) +``` + +回复到 Lua 值的映射:string -> string,integer -> integer,nil 回复 -> nil,array -> 表(1 起始,其中嵌套的 nil 元素用 `false` 占位),error 回复 -> `nil, "错误消息"`。 + +> `hv.redis` 用 `new` 而非 `connect`:它是纯构造,连接是懒发起 + 断线重连,命令可在未连接时排队;这与 `hv.ws.connect` / `hv.mqtt.connect` 挂起到握手完成才返回的语义不同。 + +## hv.mqtt(协程同步,需 WITH_MQTT) + +MQTT 是消息驱动的,`m:recv()` 挂起直到 broker 推来一条 PUBLISH。 + +```lua +local m, err = hv.mqtt.connect({ + host = "127.0.0.1", port = 1883, + id = "client-1", username = "", password = "", + keepalive = 60, clean_session = true, ssl = false, + reconnect = { -- 可选:给了就开自动重连 + min_delay = 1000, max_delay = 10000, delay_policy = 2, max_retry = 0, + }, +}) -- 协程同步:挂起到 CONNACK 或失败 + +m:subscribe("topic", 1) -- 返回 mid +m:publish("topic", "payload", 1, 0) -- topic, payload, qos, retain -> mid +m:unsubscribe("topic") +local msg, err = m:recv() -- { topic =, payload =, qos = } +if err == "reconnecting" then ... end -- 临时断开,正在重连;err="closed" 为终止 +m:disconnect() -- 显式断开(终止,禁用重连) +``` + +> 与 hv.ws 一致:开启重连后断线是临时的,`recv()` 在断线期间返回 `(nil,"reconnecting")`,重连成功后自动恢复;`m:disconnect()` 是显式终止,禁用重连。 + +## HTTP Lua Handler + +在 HTTP 服务端里用 Lua 脚本处理请求(`handle(ctx)`),请求在 IO 线程的协程里执行,脚本内可用上述同步写法调用异步 client。详见 [HttpLuaHandler.md](HttpLuaHandler.md)。 diff --git a/etc/httpd.conf b/etc/httpd.conf index 74e826050..d55c32556 100644 --- a/etc/httpd.conf +++ b/etc/httpd.conf @@ -52,3 +52,9 @@ trust_proxies = *httpbin.org;*postman-echo.com;*apifox.com /httpbin/ => http://httpbin.org/ /postman/ => http://postman-echo.com/ /apifox/ => https://echo.apifox.com/ + +# script +# Map a URL prefix to a directory of Lua scripts. A request to /script/foo runs +# examples/scripts/foo.lua's get/post/... or handle(ctx). Requires WITH_LUA. +[script] +/script/ => examples/scripts/ \ No newline at end of file diff --git a/event/hevent.h b/event/hevent.h index 8f72a0317..84a8fa66f 100644 --- a/event/hevent.h +++ b/event/hevent.h @@ -68,6 +68,10 @@ struct hloop_s { hmutex_t custom_events_mutex; // async dns resolver (event/hdns.c), created lazily, freed in hloop_cleanup void* dns_resolver; + // per-loop lua_State (lua/), stored as opaque void* so the C core stays + // lua-free. Set via hloop_set_lua_state with a destructor; freed in hloop_cleanup. + void* lua_state; + void (*lua_state_dtor)(void* lua_state); }; uint64_t hloop_next_event_id(); diff --git a/event/hloop.c b/event/hloop.c index 27ec8acef..603b5ef5a 100644 --- a/event/hloop.c +++ b/event/hloop.c @@ -364,6 +364,14 @@ static void hloop_cleanup(hloop_t* loop) { printd("cleanup dns_resolver...\n"); hdns_resolver_free(loop); + // per-loop lua_State (opaque; destructor supplied by lua/ layer) + if (loop->lua_state && loop->lua_state_dtor) { + printd("cleanup lua_state...\n"); + loop->lua_state_dtor(loop->lua_state); + } + loop->lua_state = NULL; + loop->lua_state_dtor = NULL; + // ios printd("cleanup ios...\n"); for (int i = 0; i < loop->ios.maxsize; ++i) { @@ -602,6 +610,15 @@ void* hloop_userdata(hloop_t* loop) { return loop->userdata; } +void hloop_set_lua_state(hloop_t* loop, void* lua_state, void (*dtor)(void* lua_state)) { + loop->lua_state = lua_state; + loop->lua_state_dtor = dtor; +} + +void* hloop_lua_state(hloop_t* loop) { + return loop->lua_state; +} + static hloop_t* s_signal_loop = NULL; static void signal_handler(int signo) { if (!s_signal_loop) return; diff --git a/event/hloop.h b/event/hloop.h index 6547cda3b..a94e1edab 100644 --- a/event/hloop.h +++ b/event/hloop.h @@ -175,6 +175,14 @@ HV_EXPORT uint32_t hloop_nactives(hloop_t* loop); HV_EXPORT void hloop_set_userdata(hloop_t* loop, void* userdata); HV_EXPORT void* hloop_userdata(hloop_t* loop); +// per-loop lua_State (used by the lua/ binding layer). +// The C core treats it as an opaque pointer and never depends on lua. +// @dtor: optional destructor invoked on this pointer in hloop_cleanup +// (e.g. a wrapper around lua_close). Replacing an existing lua_state +// does NOT call the previous dtor; the caller manages that. +HV_EXPORT void hloop_set_lua_state(hloop_t* loop, void* lua_state, void (*dtor)(void* lua_state)); +HV_EXPORT void* hloop_lua_state(hloop_t* loop); + // custom_event /* * hevent_t ev; diff --git a/evpp/EventLoop.h b/evpp/EventLoop.h index fa8128efe..483fd34c7 100644 --- a/evpp/EventLoop.h +++ b/evpp/EventLoop.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "hloop.h" @@ -17,7 +18,14 @@ namespace hv { // EventLoop is a loop-bound wrapper around hloop_t. // When constructed with an external hloop_t, the caller remains responsible for that loop's lifetime. -class EventLoop : public Status { +// +// Inherits enable_shared_from_this so code holding a raw EventLoop* (e.g. from +// currentThreadEventLoop / TLS) can recover a shared EventLoopPtr that shares +// ownership with the original shared_ptr (see currentThreadEventLoopPtr). This +// requires the EventLoop to be managed by a shared_ptr (make_shared); +// calling shared_from_this() on a stack/`new`-constructed EventLoop throws +// std::bad_weak_ptr. +class EventLoop : public Status, public std::enable_shared_from_this { public: typedef std::function Functor; @@ -56,6 +64,19 @@ class EventLoop : public Status { setStatus(kRunning); hloop_run(loop_); setStatus(kStopped); + if (ThreadLocalStorage::get(ThreadLocalStorage::EVENT_LOOP) == this) { + ThreadLocalStorage::set(ThreadLocalStorage::EVENT_LOOP, NULL); + } + // An owned loop is created with HLOOP_FLAG_AUTO_FREE, so hloop_run() has + // already freed the hloop_t by the time it returns. Drop the now-dangling + // pointer so a later stop()/~EventLoop doesn't touch freed memory. This + // matters when the loop was stopped via the raw C hloop_stop() (e.g. the + // Lua binding's hv.stop()) rather than EventLoop::stop(), which would + // otherwise have nulled loop_ itself. A non-owned (external) loop is left + // untouched — the caller owns its lifetime. + if (is_loop_owner) { + loop_ = NULL; + } } // stop thread-safe @@ -299,6 +320,24 @@ static inline EventLoop* tlsEventLoop() { } #define currentThreadEventLoop ::hv::tlsEventLoop() +// Get the current thread's EventLoop as a shared_ptr, sharing ownership with +// whoever created it. Returns NULL if there is no current loop, or if it is not +// managed by a shared_ptr (e.g. a stack-constructed EventLoop) — in which case +// shared_from_this() would throw std::bad_weak_ptr, caught here. Callers should +// treat NULL as "no shared loop available" rather than crash. +// Used e.g. to hand the current loop to AsyncHttpClient/AsyncRedisClient so they +// run on this same loop thread instead of spawning their own. +static inline EventLoopPtr tlsEventLoopPtr() { + EventLoop* loop = tlsEventLoop(); + if (loop == NULL) return EventLoopPtr(); + try { + return loop->shared_from_this(); + } catch (const std::bad_weak_ptr&) { + return EventLoopPtr(); + } +} +#define currentThreadEventLoopPtr ::hv::tlsEventLoopPtr() + static inline TimerID setTimer(int timeout_ms, TimerCallback cb, uint32_t repeat = INFINITE) { EventLoop* loop = tlsEventLoop(); assert(loop != NULL); diff --git a/evpp/EventLoopThread.h b/evpp/EventLoopThread.h index c876accaa..f0567e745 100644 --- a/evpp/EventLoopThread.h +++ b/evpp/EventLoopThread.h @@ -17,6 +17,26 @@ class EventLoopThread : public Status { EventLoopThread(EventLoopPtr loop = NULL) { setStatus(kInitializing); + // is_loop_owner_ records whether this object created its own loop. + // When an external loop is passed in, the caller owns that loop's + // lifetime (and its thread), so subclasses must NOT stop it on their + // own teardown. Exposed to subclasses via isLoopOwner() so the + // "own loop -> stop it / external loop -> leave it" decision lives in + // one place instead of a duplicated flag in every client/server class. + // + // CONTRACT (external loop): when an external loop is supplied, the + // caller must have it already running (loop->run() on its own thread, or + // published as this thread's running loop) BEFORE calling start(). This + // is the normal usage (HttpServer IO loop, EventLoopThreadPool loop, the + // Lua runtime loop obtained via currentThreadEventLoopPtr — which is only + // non-NULL on an already-running loop thread). If instead an external, + // not-yet-running loop is passed and start() is called, start() falls + // back to spinning its OWN worker thread to drive that loop (see start() + // below); stop() then joins that thread (thread_ != NULL) and does stop + // the loop. That fallback is intentional and safe (used by + // redis_async_client_test), NOT a bug — but it is not the intended path + // for a loop the caller means to keep using elsewhere. + is_loop_owner_ = (loop == NULL); loop_ = loop ? loop : std::make_shared(); setStatus(kInitialized); } @@ -30,6 +50,13 @@ class EventLoopThread : public Status { return loop_; } + // Whether this object created (and therefore owns) its EventLoop. False when + // an external loop was supplied at construction. Subclasses use this to + // decide whether their stop() should also stop the loop/thread. + bool isLoopOwner() const { + return is_loop_owner_; + } + hloop_t* hloop() { return loop_->loop(); } @@ -41,6 +68,10 @@ class EventLoopThread : public Status { // @param wait_thread_started: if ture this method will block until loop_thread started. // @param pre: This functor will be executed when loop_thread started. // @param post:This Functor will be executed when loop_thread stopped. + // NOTE: if isRunning() is already true (the common case for an external loop + // per the constructor's CONTRACT), start() is a no-op below and the loop is + // driven by whoever already runs it. Only a not-yet-running loop reaches the + // thread_ spin-up here (own loop, or the intentional external-loop fallback). void start(bool wait_thread_started = true, Functor pre = Functor(), Functor post = Functor()) { @@ -59,7 +90,17 @@ class EventLoopThread : public Status { // @param wait_thread_started: if ture this method will block until loop_thread stopped. // stop thread-safe + // + // Ownership rule: stop() only stops the loop it is entitled to. A shared, + // externally-supplied loop (is_loop_owner_ == false) must NOT be stopped + // here — its creator owns that decision; a mere user has no right to stop + // it. The one exception is when this object had to spin up its OWN thread + // for an external loop that was not yet running (start() falls back to + // EventLoopThread::start() then): that thread IS ours, so thread_ != NULL + // and we must stop/join it to avoid a hang in the destructor's join(). + // So the guard is "own the loop, OR own a thread". void stop(bool wait_thread_stopped = false) { + if (!is_loop_owner_ && !thread_) return; if (status() < kStarting || status() >= kStopping) return; setStatus(kStopping); @@ -108,6 +149,7 @@ class EventLoopThread : public Status { private: EventLoopPtr loop_; std::shared_ptr thread_; + bool is_loop_owner_; }; typedef std::shared_ptr EventLoopThreadPtr; diff --git a/evpp/TcpClient.h b/evpp/TcpClient.h index 1e2d61317..bc61e4d04 100644 --- a/evpp/TcpClient.h +++ b/evpp/TcpClient.h @@ -411,7 +411,6 @@ class TcpClientTmpl : private EventLoopThread, public TcpClientEventLoopTmpl(EventLoopThread::loop()) - , is_loop_owner(loop == NULL) {} virtual ~TcpClientTmpl() { stop(true); @@ -434,16 +433,12 @@ class TcpClientTmpl : private EventLoopThread, public TcpClientEventLoopTmpl::closesocket(); - if (is_loop_owner) { - EventLoopThread::stop(wait_threads_stopped); - } + EventLoopThread::stop(wait_threads_stopped); } - -private: - bool is_loop_owner; }; typedef TcpClientTmpl TcpClient; diff --git a/evpp/TcpServer.h b/evpp/TcpServer.h index 251660341..5d8c14403 100644 --- a/evpp/TcpServer.h +++ b/evpp/TcpServer.h @@ -294,7 +294,6 @@ class TcpServerTmpl : private EventLoopThread, public TcpServerEventLoopTmpl(EventLoopThread::loop()) - , is_loop_owner(loop == NULL) {} virtual ~TcpServerTmpl() { stop(true); @@ -313,15 +312,12 @@ class TcpServerTmpl : private EventLoopThread, public TcpServerEventLoopTmpl::stop(wait_threads_stopped); } - -private: - bool is_loop_owner; }; typedef TcpServerTmpl TcpServer; diff --git a/evpp/UdpClient.h b/evpp/UdpClient.h index c85c879cf..5651f7f09 100644 --- a/evpp/UdpClient.h +++ b/evpp/UdpClient.h @@ -162,7 +162,6 @@ class UdpClientTmpl : private EventLoopThread, public UdpClientEventLoopTmpl(EventLoopThread::loop()) - , is_loop_owner(loop == NULL) {} virtual ~UdpClientTmpl() { stop(true); @@ -182,16 +181,12 @@ class UdpClientTmpl : private EventLoopThread, public UdpClientEventLoopTmpl::closesocket(); - if (is_loop_owner) { - EventLoopThread::stop(wait_threads_stopped); - } + EventLoopThread::stop(wait_threads_stopped); } - -private: - bool is_loop_owner; }; typedef UdpClientTmpl UdpClient; diff --git a/evpp/UdpServer.h b/evpp/UdpServer.h index f23d4a07f..71c1186a1 100644 --- a/evpp/UdpServer.h +++ b/evpp/UdpServer.h @@ -136,7 +136,6 @@ class UdpServerTmpl : private EventLoopThread, public UdpServerEventLoopTmpl(EventLoopThread::loop()) - , is_loop_owner(loop == NULL) {} virtual ~UdpServerTmpl() { stop(true); @@ -156,16 +155,12 @@ class UdpServerTmpl : private EventLoopThread, public UdpServerEventLoopTmpl::closesocket(); - if (is_loop_owner) { - EventLoopThread::stop(wait_threads_stopped); - } + EventLoopThread::stop(wait_threads_stopped); } - -private: - bool is_loop_owner; }; typedef UdpServerTmpl UdpServer; diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 324030d3f..3d3c74c3c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -123,6 +123,16 @@ if(WITH_EVPP) list(APPEND EXAMPLES redis_client_example redis_subscriber_example) endif() + + if(WITH_LUA) + include_directories(../lua) + + # hvlua: standalone Lua runtime on top of libhv's event loop + add_executable(hvlua hvlua.cpp) + target_link_libraries(hvlua ${HV_LIBRARIES}) + + list(APPEND EXAMPLES hvlua) + endif() if(WITH_HTTP) include_directories(../http) diff --git a/examples/http_server_test.cpp b/examples/http_server_test.cpp index 4782b33a3..b6854008d 100644 --- a/examples/http_server_test.cpp +++ b/examples/http_server_test.cpp @@ -96,6 +96,7 @@ int main(int argc, char** argv) { // curl -v "http://ip:port/lua/hello?id=42" router.GET("/lua/hello", HttpScriptHandler("examples/scripts/hello.lua")); // curl -v "http://ip:port/script/hello?id=42" + // curl -v "http://ip:port/script/async?host=example.com" (coroutine sync-style async) router.Script("/script/", "examples/scripts"); #endif diff --git a/examples/httpd/httpd.cpp b/examples/httpd/httpd.cpp index 32fd54096..fb8c42d34 100644 --- a/examples/httpd/httpd.cpp +++ b/examples/httpd/httpd.cpp @@ -252,6 +252,28 @@ int parse_confile(const char* confile) { } } + // script (Lua) + // [script] maps a URL prefix to a directory of scripts, e.g. + // /script/ => examples/lua/ + // A request to /script/foo runs examples/lua/foo.lua's handle(ctx). + // HttpService::Script is only available when built with WITH_LUA. +#ifdef WITH_LUA + auto script_keys = ini.GetKeys("script"); + for (const auto& script_key : script_keys) { + if (script_key.empty() || script_key[0] != '/') continue; + str = ini.GetValue(script_key, "script"); + if (str.empty()) continue; + const std::string& path = script_key; + std::string script_dir = hv::ltrim(str, "> "); + hlogi("script %s => %s", path.c_str(), script_dir.c_str()); + g_http_service.Script(path.c_str(), script_dir.c_str()); + } +#else + if (!ini.GetKeys("script").empty()) { + hlogw("[script] section ignored: httpd was built without WITH_LUA"); + } +#endif + hlogi("parse_confile('%s') OK", confile); return 0; } diff --git a/examples/hvlua.cpp b/examples/hvlua.cpp new file mode 100644 index 000000000..aef8d9c48 --- /dev/null +++ b/examples/hvlua.cpp @@ -0,0 +1,74 @@ +// hvlua: standalone Lua runtime on top of libhv's event loop. +// +// Usage: hvlua script.lua [args...] +// +// Holds the loop as a shared_ptr (make_shared) and publishes it as +// this thread's loop via EventLoop::run(). Using a shared_ptr (not a stack +// object) is required so the hv.* client bindings can obtain an EventLoopPtr +// for the current thread via currentThreadEventLoopPtr (EventLoop:: +// shared_from_this()) and share this one loop/thread with AsyncHttpClient / +// AsyncRedisClient etc. The script runs inside a coroutine so it may use +// synchronous-style async APIs. +#include + +extern "C" { +#include +#include +#include +} + +#include "hlog.h" +#include "EventLoop.h" +#include "hvlua.h" + +static void usage(const char* prog) { + fprintf(stderr, "Usage: %s script.lua [args...]\n", prog); +} + +int main(int argc, char** argv) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + const char* script = argv[1]; + + // Route hv.log() to stdout for a CLI runtime (default logger writes a file). + // Line-buffer stdout so logs from long-running scripts (e.g. servers) appear + // promptly even when stdout is redirected to a file/pipe (not a TTY). + setvbuf(stdout, NULL, _IOLBF, 0); + hlog_set_handler(stdout_logger); + + // A default-constructed EventLoop owns its own hloop_t (auto-freed on run + // exit). Held via shared_ptr so currentThreadEventLoopPtr / shared_from_this + // work — that is how the hv.* client bindings share this one loop/thread. + // EventLoop::run() publishes it as this thread's loop (TLS) and returns when + // the script calls hv.stop(). We do NOT quit on "no active events": async + // work posted from a coroutine (e.g. hv.http.get) may not have registered + // its socket/timer yet when that check runs; scripts terminate via hv.stop(). + hv::EventLoopPtr loop = std::make_shared(); + + // Create the per-loop lua_State and expose script args as global `arg`. + lua_State* L = hv::hvlua_state(loop->loop()); + if (L == NULL) { + fprintf(stderr, "hvlua: failed to create lua state\n"); + return 1; + } + lua_createtable(L, argc - 1, 0); + for (int i = 1; i < argc; ++i) { + lua_pushstring(L, argv[i]); + lua_seti(L, -2, i - 1); // arg[0] = script, arg[1..] = extra args + } + lua_setglobal(L, "arg"); + + // Publish this thread's loop (TLS) before running the script, so bindings + // that resolve currentThreadEventLoopPtr during script load also work. + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); + + // Run the script (may yield on async ops), then drive the loop until the + // script calls hv.stop() or no work remains. The lua_State is closed when + // the owned hloop is torn down (dtor registered via hloop_set_lua_state). + if (hv::hvlua_dofile(loop->loop(), script) == 0) { + loop->run(); // re-sets TLS + hloop_run; frees the owned hloop on exit + } + return 0; +} diff --git a/examples/lua/dns.lua b/examples/lua/dns.lua new file mode 100644 index 000000000..77648127b --- /dev/null +++ b/examples/lua/dns.lua @@ -0,0 +1,28 @@ +-- hvlua async DNS example: synchronous-style resolve, non-blocking loop. +-- Run: bin/hvlua examples/lua/dns.lua [host ...] + +local hosts = { "localhost", "example.com", "github.com" } +if arg and #arg >= 1 then + hosts = {} + for i = 1, #arg do hosts[i] = arg[i] end +end + +-- Run each resolve in its own coroutine (via a 1ms timer) so they fire +-- concurrently on the loop; total time ~= the slowest single lookup. +-- (setTimeout requires timeout_ms >= 1; 0 is not a valid timer in libhv) +local pending = #hosts + +for _, host in ipairs(hosts) do + hv.setTimeout(1, function() + local addrs, err = hv.resolveDns(host) -- synchronous-style, async underneath + if err then + hv.log("resolve", host, "failed:", err) + else + hv.log("resolve", host, "->", table.concat(addrs, ", ")) + end + pending = pending - 1 + if pending == 0 then + hv.stop() + end + end) +end diff --git a/examples/lua/http_client.lua b/examples/lua/http_client.lua new file mode 100644 index 000000000..c1551bbbf --- /dev/null +++ b/examples/lua/http_client.lua @@ -0,0 +1,18 @@ +-- hv.http coroutine-synchronous client demo. +-- Usage: hvlua examples/lua/http_client.lua [url] +local url = arg[1] or "http://127.0.0.1:18090/ping" + +hv.setTimeout(1, function() + local resp, err = hv.http.get(url) + if err then + hv.loge("GET failed:", err) + else + hv.log("GET", url, "->", resp.status, "body:", resp.body) + end + + -- a second request on the same client/loop + local r2, e2 = hv.http.get(url) + if not e2 then hv.log("2nd GET ->", r2.status) end + + hv.stop() +end) diff --git a/examples/lua/mqtt_client.lua b/examples/lua/mqtt_client.lua new file mode 100644 index 000000000..df076e3e1 --- /dev/null +++ b/examples/lua/mqtt_client.lua @@ -0,0 +1,33 @@ +-- hv.mqtt coroutine-synchronous MQTT client demo. +-- Usage: hvlua examples/lua/mqtt_client.lua [host] [port] [topic] +local host = arg[1] or "127.0.0.1" +local port = tonumber(arg[2]) or 1883 +local topic = arg[3] or "hv/lua/test" + +hv.setTimeout(1, function() + -- connect() suspends until CONNACK (or fails with nil, err) + -- reconnect is optional; given it enables auto-reconnect. + local m, err = hv.mqtt.connect({ + host = host, port = port, id = "hvlua-demo", keepalive = 60, + reconnect = { min_delay = 1000, max_delay = 10000, delay_policy = 2 }, + }) + if err then + hv.loge("connect failed:", err) + hv.stop() + return + end + hv.log("connected to mqtt", host, port) + + m:subscribe(topic, 1) + m:publish(topic, "hello from lua", 1) + + -- recv() suspends until a PUBLISH arrives; returns { topic, payload, qos }. + for i = 1, 3 do + local msg, rerr = m:recv() + if rerr then hv.log("recv:", rerr); break end + hv.log("recv ->", msg.topic, msg.payload, "qos", msg.qos) + end + + m:disconnect() + hv.stop() +end) diff --git a/examples/lua/redis_client.lua b/examples/lua/redis_client.lua new file mode 100644 index 000000000..0f82745ed --- /dev/null +++ b/examples/lua/redis_client.lua @@ -0,0 +1,26 @@ +-- hv.redis coroutine-synchronous client demo. +-- Usage: hvlua examples/lua/redis_client.lua [host] [port] +local host = arg[1] or "127.0.0.1" +local port = tonumber(arg[2]) or 6379 + +hv.setTimeout(1, function() + local r = hv.redis.new({ host = host, port = port, timeout = 3000 }) + + -- SET / GET (coroutine-synchronous: these yield until the reply arrives) + local ok, err = r:set("hv:lua:key", "hello") + if err then hv.loge("SET failed:", err); hv.stop(); return end + hv.log("SET ->", ok) + + local v, gerr = r:get("hv:lua:key") + if gerr then hv.loge("GET failed:", gerr) else hv.log("GET ->", v) end + + -- INCR returns an integer + local n = r:incr("hv:lua:counter") + hv.log("INCR ->", n) + + -- arbitrary command via array form; error replies come back as nil,err + local res, cerr = r:command({ "HSET", "hv:lua:hash", "field", "val" }) + if cerr then hv.log("HSET err:", cerr) else hv.log("HSET ->", res) end + + hv.stop() +end) diff --git a/examples/lua/sleep.lua b/examples/lua/sleep.lua new file mode 100644 index 000000000..8ddc245a6 --- /dev/null +++ b/examples/lua/sleep.lua @@ -0,0 +1,26 @@ +-- hvlua coroutine sleep example: synchronous-style, non-blocking. +-- Run: bin/hvlua examples/lua/sleep.lua + +hv.log("sleep example start") + +-- This runs inside a coroutine, so hv.sleep() suspends WITHOUT blocking the +-- event loop: the two "workers" below interleave rather than run serially. + +workers_done = 0 + +local function worker(name, ms) + for i = 1, 3 do + hv.log(name, "step", i) + hv.sleep(ms) -- looks blocking, actually yields to the loop + end + hv.log(name, "done") + workers_done = workers_done + 1 + if workers_done == 2 then + hv.stop() + end +end + +-- start two workers via timers so both run on the loop +-- (setTimeout requires timeout_ms >= 1; 0 is not a valid timer in libhv) +hv.setTimeout(1, function() worker("A", 300) end) +hv.setTimeout(1, function() worker("B", 500) end) diff --git a/examples/lua/tcp_client.lua b/examples/lua/tcp_client.lua new file mode 100644 index 000000000..756b5fce7 --- /dev/null +++ b/examples/lua/tcp_client.lua @@ -0,0 +1,28 @@ +-- TCP client coroutine echo test +-- Usage: hvlua examples/lua/tcp_client.lua [host] [port] +local host = arg[1] or "127.0.0.1" +local port = tonumber(arg[2] or "10514") + +hv.setTimeout(1, function() + local conn, err = hv.connect(host, port, 3000) + if err then + hv.loge("connect failed:", err) + hv.stop() + return + end + hv.log("connected to", conn:peeraddr(), "fd", conn:fd()) + + for i = 1, 3 do + local msg = "hello " .. i + conn:write(msg) + local data, rerr = conn:read() + if rerr then + hv.loge("read err:", rerr) + break + end + hv.log("sent:", msg, "| echo:", data) + end + + conn:close() + hv.stop() +end) diff --git a/examples/lua/tcp_server.lua b/examples/lua/tcp_server.lua new file mode 100644 index 000000000..deab12e9e --- /dev/null +++ b/examples/lua/tcp_server.lua @@ -0,0 +1,23 @@ +-- TCP echo server in Lua (coroutine per connection). +-- Usage: hvlua examples/lua/tcp_server.lua [host] [port] +local host = arg[1] or "0.0.0.0" +local port = tonumber(arg[2] or "10520") + +local ok, err = hv.listen(host, port, function(conn) + hv.log("accepted", conn:peeraddr()) + while true do + local data, rerr = conn:read() + if rerr then + hv.log("conn closed:", conn:peeraddr()) + break + end + conn:write(data) -- echo + end +end) + +if not ok then + hv.loge("listen failed:", err) + return +end +hv.log("echo server listening on", host .. ":" .. port) +-- no hv.stop(): run until killed (server keeps the loop alive) diff --git a/examples/lua/tcp_unpack.lua b/examples/lua/tcp_unpack.lua new file mode 100644 index 000000000..425ea4c7c --- /dev/null +++ b/examples/lua/tcp_unpack.lua @@ -0,0 +1,39 @@ +-- TCP unpack test: length-field framing round-trip against a raw echo server. +-- Usage: hvlua examples/lua/tcp_unpack.lua [host] [port] +-- Frame: [flags:1][length:4 BE][body] (length = body length) +local host = arg[1] or "127.0.0.1" +local port = tonumber(arg[2] or "10514") + +local function frame(body) + local n = #body + local len = string.char( + (n >> 24) & 0xff, (n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff) + return string.char(0) .. len .. body -- flags=0, 4-byte BE length, body +end + +hv.setTimeout(1, function() + local conn, err = hv.connect(host, port, 3000) + if err then hv.loge("connect:", err); hv.stop(); return end + + conn:setUnpack({ + mode = "length_field", + body_offset = 5, -- head = flags(1) + length(4) + length_field_offset = 1, + length_field_bytes = 4, + length_field_coding = "be", + }) + + -- send two frames back-to-back; unpack must split them into 2 reads + conn:write(frame("hello")) + conn:write(frame("world!!")) + + for i = 1, 2 do + local pkt, rerr = conn:read() -- returns exactly one full frame + if rerr then hv.loge("read:", rerr); break end + local body = pkt:sub(6) -- skip 5-byte head + hv.log("packet", i, "len", #pkt, "body", body) + end + + conn:close() + hv.stop() +end) diff --git a/examples/lua/timer.lua b/examples/lua/timer.lua new file mode 100644 index 000000000..29203ff62 --- /dev/null +++ b/examples/lua/timer.lua @@ -0,0 +1,19 @@ +-- hvlua timer example +-- Run: bin/hvlua examples/lua/timer.lua + +hv.log("timer example start, libhv", hv.version()) + +local n = 0 +local id +id = hv.setInterval(500, function() + n = n + 1 + hv.log("tick", n) + if n >= 3 then + hv.clearTimer(id) + hv.log("cleared interval after 3 ticks") + hv.setTimeout(200, function() + hv.log("bye") + hv.stop() + end) + end +end) diff --git a/examples/lua/udp_client.lua b/examples/lua/udp_client.lua new file mode 100644 index 000000000..d7c3ea317 --- /dev/null +++ b/examples/lua/udp_client.lua @@ -0,0 +1,20 @@ +-- UDP client in Lua: sendto + recvfrom (coroutine-synchronous). +-- Usage: hvlua examples/lua/udp_client.lua [host] [port] +local host = arg[1] or "127.0.0.1" +local port = tonumber(arg[2] or "10530") + +hv.setTimeout(1, function() + local sock = hv.udpClient(host, port) + for i = 1, 3 do + local msg = "ping " .. i + sock:sendto(msg) + local data, peer = sock:recvfrom() + if not data then + hv.loge("recvfrom err:", peer) + break + end + hv.log("sent:", msg, "| recv from", peer, ":", data) + end + sock:close() + hv.stop() +end) diff --git a/examples/lua/udp_server.lua b/examples/lua/udp_server.lua new file mode 100644 index 000000000..04ffd359a --- /dev/null +++ b/examples/lua/udp_server.lua @@ -0,0 +1,15 @@ +-- UDP echo server in Lua. +-- Usage: hvlua examples/lua/udp_server.lua [host] [port] +local host = arg[1] or "0.0.0.0" +local port = tonumber(arg[2] or "10530") + +local ok, err = hv.udpServer(host, port, function(sock, data, peer) + hv.log("recv from", peer, ":", data) + sock:sendto(data) -- echo back to the last peer +end) + +if not ok then + hv.loge("udpServer failed:", err) + return +end +hv.log("udp echo server listening on", host .. ":" .. port) diff --git a/examples/lua/ws_client.lua b/examples/lua/ws_client.lua new file mode 100644 index 000000000..aa2a27a73 --- /dev/null +++ b/examples/lua/ws_client.lua @@ -0,0 +1,30 @@ +-- hv.ws coroutine-synchronous WebSocket client demo. +-- Usage: hvlua examples/lua/ws_client.lua [url] +local url = arg[1] or "ws://127.0.0.1:8888/" + +hv.setTimeout(1, function() + -- opts (all optional): headers, ping_interval, reconnect + local ws, err = hv.ws.connect(url, { + reconnect = { min_delay = 1000, max_delay = 10000, delay_policy = 2 }, + }) + if err then + hv.loge("connect failed:", err) + hv.stop() + return + end + hv.log("connected to", url) + + ws:send("hello from lua") + + -- recv() suspends the coroutine until a message arrives (or the peer closes, + -- in which case it returns nil, "closed"). Loop until closed. + for i = 1, 3 do + local msg, rerr = ws:recv() + if rerr then hv.log("recv:", rerr); break end + hv.log("recv ->", msg) + ws:send("echo " .. i) + end + + ws:close() + hv.stop() +end) diff --git a/examples/scripts/async.lua b/examples/scripts/async.lua new file mode 100644 index 000000000..4d06dcfb4 --- /dev/null +++ b/examples/scripts/async.lua @@ -0,0 +1,30 @@ +-- Async HTTP handler demo: synchronous-style code, non-blocking loop. +-- +-- The handler resolves a hostname via hv.resolveDns (which yields the request +-- coroutine to the event loop and resumes when the answer arrives) and can also +-- hv.sleep without blocking other requests on the same IO thread. +-- +-- Register in C++: router.GET("/async", HttpScriptHandler("examples/scripts/async.lua")) +-- Try: curl "http://127.0.0.1:8080/async?host=example.com" + +function handle(ctx) + local host = ctx:query("host", "example.com") + + -- optional artificial delay to demonstrate non-blocking concurrency + local delay = tonumber(ctx:query("delay", "0")) + if delay > 0 then + hv.sleep(delay) + end + + local addrs, err = hv.resolveDns(host) + if err then + ctx:status(502) + return ctx:json({ ok = false, host = host, error = err }) + end + + return ctx:json({ + ok = true, + host = host, + addrs = addrs, + }) +end diff --git a/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 31fd262fb..64eb0dc9f 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -104,16 +104,27 @@ struct HttpClientContext { class HV_EXPORT AsyncHttpClient : private EventLoopThread { public: - AsyncHttpClient(EventLoopPtr loop = NULL) : EventLoopThread(loop) { + AsyncHttpClient(EventLoopPtr loop = NULL) + : EventLoopThread(loop) + { if (loop == NULL) { EventLoopThread::start(true); } } ~AsyncHttpClient() { - // Stop (and, for an owned loop, free) the event loop. Any in-flight - // async DNS queries are owned by EventLoop::resolveDns and the C - // resolver, which are torn down with the loop; nothing to clean here. EventLoopThread::stop(true); + // Detach per-connection close callbacks before members are destroyed. + // Member dtors run in reverse declaration order (conn_pools before + // channels); tearing down `channels` fires ~Channel -> close -> the + // onclose lambda, which references conn_pools/channels and would touch + // an already-destroyed map (UAF). With onclose cleared, Channel:: + // on_close is a no-op. This matters for external (non-owned) loops, + // where in-flight keep-alive connections outlive send() and are only + // closed here. Safe now: no loop thread is running concurrently + // (owned: joined above; external: same-thread teardown). + for (auto& pair : channels) { + if (pair.second) pair.second->onclose = NULL; + } } // thread-safe diff --git a/http/client/WebSocketClient.cpp b/http/client/WebSocketClient.cpp index aa4c7d5bf..375bbbfcf 100644 --- a/http/client/WebSocketClient.cpp +++ b/http/client/WebSocketClient.cpp @@ -16,6 +16,15 @@ WebSocketClient::WebSocketClient(EventLoopPtr loop) } WebSocketClient::~WebSocketClient() { + // Detach the channel's close callback before teardown. ~TcpClientTmpl (base) + // and the channel dtor run ~Channel -> close() -> the onclose lambda, which + // was set to [this]{ notifyDisconnectThenReconnect(); } and captures this + // TcpClient. During destruction (e.g. a Lua __gc dropping a still-open ws) + // that would touch a partially-destroyed object (UAF). Clearing onclose makes + // Channel::on_close a no-op (mirrors AsyncHttpClient::~AsyncHttpClient). + if (channel) { + channel->onclose = NULL; + } stop(); } diff --git a/http/http_content.cpp b/http/http_content.cpp index 1d358a00d..a13151e71 100644 --- a/http/http_content.cpp +++ b/http/http_content.cpp @@ -236,7 +236,17 @@ int parse_multipart(const std::string& str, MultiPart& mp, const char* boundary) std::string dump_json(const hv::Json& json, int indent) { if (json.empty()) return ""; - return json.dump(indent); + // json.dump throws (e.g. type_error.316 on non-UTF-8 bytes) for otherwise + // valid values built from arbitrary input. Catch it so a bad value can't + // abort the process — e.g. an HTTP handler putting untrusted bytes into the + // response JSON would otherwise be a remote DoS. On error return an empty + // body rather than throwing (mirrors parse_json's try/catch below). + try { + return json.dump(indent); + } + catch (const std::exception&) { + return ""; + } } int parse_json(const char* str, hv::Json& json, std::string& errmsg) { diff --git a/http/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp index 85a6268d2..fcef37e2c 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -17,6 +17,10 @@ extern "C" { #include "hstring.h" #include "htime.h" +#include "EventLoop.h" +#include "hvlua.h" +#include "hvlua_json.h" // shared lua<->json conversion (single implementation) + namespace hv { namespace { @@ -106,85 +110,12 @@ static int lua_ctx_text(lua_State* L) { return 1; } -static Json lua_to_json(lua_State* L, int index); - -static Json lua_table_to_json(lua_State* L, int index) { - index = lua_absindex(L, index); - bool is_array = true; - lua_Integer max_index = 0; - size_t count = 0; - - lua_pushnil(L); - while (lua_next(L, index) != 0) { - ++count; - if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) { - lua_Integer k = lua_tointeger(L, -2); - if (k <= 0) { - is_array = false; - } else if (k > max_index) { - max_index = k; - } - } else { - is_array = false; - } - lua_pop(L, 1); - } - - if (is_array && (lua_Integer)count == max_index) { - Json j = Json::array(); - for (lua_Integer i = 1; i <= max_index; ++i) { - lua_geti(L, index, i); - j.push_back(lua_to_json(L, -1)); - lua_pop(L, 1); - } - return j; - } - - Json j = Json::object(); - lua_pushnil(L); - while (lua_next(L, index) != 0) { - std::string key; - if (lua_type(L, -2) == LUA_TSTRING) { - size_t len = 0; - const char* s = lua_tolstring(L, -2, &len); - key.assign(s, len); - } else if (lua_type(L, -2) == LUA_TNUMBER) { - key = hv::to_string((int64_t)lua_tointeger(L, -2)); - } - if (!key.empty()) { - j[key] = lua_to_json(L, -1); - } - lua_pop(L, 1); - } - return j; -} - -static Json lua_to_json(lua_State* L, int index) { - switch (lua_type(L, index)) { - case LUA_TNIL: - return nullptr; - case LUA_TBOOLEAN: - return lua_toboolean(L, index) != 0; - case LUA_TNUMBER: - if (lua_isinteger(L, index)) { - return (int64_t)lua_tointeger(L, index); - } - return lua_tonumber(L, index); - case LUA_TSTRING: { - size_t len = 0; - const char* s = lua_tolstring(L, index, &len); - return std::string(s, len); - } - case LUA_TTABLE: - return lua_table_to_json(L, index); - default: - return nullptr; - } -} +// lua <-> json conversion (incl. the cyclic-table depth guard) is shared via +// hvlua_json.h: hv::hvlua_lua_to_json. Do not duplicate it here. static int lua_ctx_json(lua_State* L) { LuaHttpContext* holder = lua_check_ctx(L); - Json j = lua_to_json(L, 2); + Json j = hvlua_lua_to_json(L, 2); holder->ctx->response->Json(j); lua_pushinteger(L, holder->ctx->response->status_code); return 1; @@ -204,25 +135,6 @@ static void lua_push_ctx(lua_State* L, const HttpContextPtr& ctx) { lua_setmetatable(L, -2); } -static int lua_hv_log(lua_State* L) { - int n = lua_gettop(L); - std::string line; - for (int i = 1; i <= n; ++i) { - size_t len = 0; - const char* s = luaL_tolstring(L, i, &len); - if (i > 1) line += "\t"; - line.append(s, len); - lua_pop(L, 1); - } - hlogi("[lua] %s", line.c_str()); - return 0; -} - -static int lua_hv_now(lua_State* L) { - lua_pushinteger(L, (lua_Integer)time(NULL)); - return 1; -} - static void register_ctx(lua_State* L) { if (luaL_newmetatable(L, LUA_CTX_META)) { lua_pushcfunction(L, lua_ctx_gc); @@ -254,15 +166,6 @@ static void register_ctx(lua_State* L) { lua_pop(L, 1); } -static void register_hv(lua_State* L) { - lua_newtable(L); - lua_pushcfunction(L, lua_hv_log); - lua_setfield(L, -2, "log"); - lua_pushcfunction(L, lua_hv_now); - lua_setfield(L, -2, "now"); - lua_setglobal(L, "hv"); -} - static time_t file_mtime(const std::string& filepath) { struct stat st; if (stat(filepath.c_str(), &st) != 0) { @@ -271,155 +174,221 @@ static time_t file_mtime(const std::string& filepath) { return st.st_mtime; } -} // namespace - -HttpLuaHandler::HttpLuaHandler(const char* filepath, const HttpLuaHandlerOptions& options) - : filepath_(filepath ? filepath : "") - , options_(options) - , L_(NULL) - , mtime_(0) { -} - -HttpLuaHandler::HttpLuaHandler(const HttpLuaHandler& rhs) - : filepath_(rhs.filepath_) - , options_(rhs.options_) - , L_(NULL) - , mtime_(0) { -} - -HttpLuaHandler& HttpLuaHandler::operator=(const HttpLuaHandler& rhs) { - if (this == &rhs) return *this; - std::lock_guard lock(mutex_); - closeLocked(); - filepath_ = rhs.filepath_; - options_ = rhs.options_; - mtime_ = 0; - last_error_.clear(); - return *this; -} +// Per-loop script cache: a registry table "hv.lua_http_scripts" mapping +// filepath -> { env = , mtime = }. Each script is loaded into its +// own environment table (its globals), so multiple scripts on one lua_State do +// not clobber each other's handle/get/post functions. +static const char* SCRIPTS_REG = "hv.lua_http_scripts"; -HttpLuaHandler::~HttpLuaHandler() { - std::lock_guard lock(mutex_); - closeLocked(); -} - -std::string HttpLuaHandler::lastError() const { - std::lock_guard lock(mutex_); - return last_error_; -} - -void HttpLuaHandler::setErrorLocked(const std::string& error) { - last_error_ = error; -} - -void HttpLuaHandler::closeLocked() { - if (L_) { - lua_close(L_); - L_ = NULL; - } -} - -bool HttpLuaHandler::loadLocked(time_t mtime) { - lua_State* L = luaL_newstate(); - if (L == NULL) { - setErrorLocked("luaL_newstate failed"); +// Push (loading/reloading as needed) the script's env table onto L. +// Returns true with the env table on the stack top, or false with an error +// message on top. +static bool push_script_env(lua_State* L, const std::string& filepath, + const HttpLuaHandlerOptions& options) { + time_t mtime = file_mtime(filepath); + if (mtime == 0) { + lua_pushstring(L, strerror(errno)); return false; } - luaL_openlibs(L); - register_ctx(L); - register_hv(L); + // registry[SCRIPTS_REG] (create if missing) + lua_getfield(L, LUA_REGISTRYINDEX, SCRIPTS_REG); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, SCRIPTS_REG); + } + // scripts[filepath] + lua_getfield(L, -1, filepath.c_str()); + if (lua_istable(L, -1)) { + lua_getfield(L, -1, "mtime"); + time_t cached = (time_t)lua_tointeger(L, -1); + lua_pop(L, 1); + if (!options.reload_on_change || cached == mtime) { + lua_getfield(L, -1, "env"); // -> scripts, entry, env + lua_remove(L, -2); // -> scripts, env + lua_remove(L, -2); // -> env + return true; + } + } + lua_pop(L, 1); // pop entry (nil or stale); stack: scripts - if (luaL_loadfile(L, filepath_.c_str()) != LUA_OK || lua_pcall(L, 0, 0, 0) != LUA_OK) { - std::string error = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load script failed"; - lua_close(L); - setErrorLocked(error); - hloge("load lua script %s failed: %s", filepath_.c_str(), error.c_str()); + // Load the chunk. + if (luaL_loadfile(L, filepath.c_str()) != LUA_OK) { + std::string err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load failed"; + lua_pop(L, 2); // chunk err + scripts + lua_pushstring(L, err.c_str()); return false; } - - lua_getglobal(L, "handle"); - if (!lua_isfunction(L, -1)) { - lua_close(L); - setErrorLocked("global handle(ctx) is not a function"); - hloge("load lua script %s failed: handle(ctx) not found", filepath_.c_str()); + // New environment table with an __index to _G so scripts can use globals + // (hloop, hv, print, ...) while their own defs stay isolated. + lua_newtable(L); // env + lua_newtable(L); // metatable + lua_pushglobaltable(L); + lua_setfield(L, -2, "__index"); + lua_setmetatable(L, -2); // setmetatable(env, {__index=_G}) + // set the chunk's _ENV upvalue to env (Lua 5.2+: first upvalue) + lua_pushvalue(L, -1); // env copy +#if LUA_VERSION_NUM >= 502 + const char* upname = lua_setupvalue(L, -3, 1); // chunk's _ENV = env + if (upname == NULL) lua_pop(L, 1); +#else + lua_setfenv(L, -3); +#endif + // stack: scripts, chunk, env + lua_pushvalue(L, -2); // chunk + if (lua_pcall(L, 0, 0, 0) != LUA_OK) { // run chunk to populate env + std::string err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "run failed"; + lua_pop(L, 4); // err, env, chunk, scripts + lua_pushstring(L, err.c_str()); return false; } - lua_pop(L, 1); - - closeLocked(); - L_ = L; - mtime_ = mtime; - last_error_.clear(); + // stack: scripts, chunk, env + lua_remove(L, -2); // scripts, env + + // Cache: scripts[filepath] = { env = env, mtime = mtime } + lua_newtable(L); // entry + lua_pushvalue(L, -2); // env + lua_setfield(L, -2, "env"); + lua_pushinteger(L, (lua_Integer)mtime); + lua_setfield(L, -2, "mtime"); + lua_setfield(L, -3, filepath.c_str()); // scripts[filepath] = entry + // stack: scripts, env + lua_remove(L, -2); // env return true; } -bool HttpLuaHandler::reloadIfNeeded() { - std::lock_guard lock(mutex_); - time_t mtime = file_mtime(filepath_); - if (mtime == 0) { - setErrorLocked(strerror(errno)); - return L_ != NULL; - } - if (L_ != NULL && (!options_.reload_on_change || mtime == mtime_)) { - return true; - } - return loadLocked(mtime) || L_ != NULL; +// Resolve the handler function for this request from the script env: prefer a +// per-method function (get/post/...), else handle. Pushes the function on L, or +// pushes nil if none found. Consumes nothing (env stays where it was). +static bool push_handler_fn(lua_State* L, int env_index, http_method method) { + std::string name = http_method_str(method); + tolower(name); + lua_getfield(L, env_index, name.c_str()); + if (lua_isfunction(L, -1)) return true; + lua_pop(L, 1); + lua_getfield(L, env_index, "handle"); + if (lua_isfunction(L, -1)) return true; + lua_pop(L, 1); + return false; } -int HttpLuaHandler::callLocked(const HttpContextPtr& ctx) { - std::string handler_name = http_method_str(ctx->request->method); - tolower(handler_name); - lua_getglobal(L_, handler_name.c_str()); - if (!lua_isfunction(L_, -1)) { - lua_pop(L_, 1); - lua_getglobal(L_, "handle"); - } - lua_push_ctx(L_, ctx); - if (lua_pcall(L_, 1, 1, 0) != LUA_OK) { - std::string error = lua_tostring(L_, -1) ? lua_tostring(L_, -1) : "call handle failed"; - lua_pop(L_, 1); - setErrorLocked(error); - hloge("call lua script %s failed: %s", filepath_.c_str(), error.c_str()); - ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(error); - return HTTP_STATUS_INTERNAL_SERVER_ERROR; - } - - int status = ctx->response->status_code; - if (lua_isinteger(L_, -1)) { - status = (int)lua_tointeger(L_, -1); +// Build the HTTP response from the coroutine's return value (top of `co`). +static void apply_result(lua_State* co, const HttpContextPtr& ctx) { + if (lua_isinteger(co, -1)) { + int status = (int)lua_tointeger(co, -1); if (ctx->response->status_code == HTTP_STATUS_OK) { ctx->response->status_code = (http_status)status; } - } else if (lua_isstring(L_, -1)) { + } else if (lua_isstring(co, -1)) { size_t len = 0; - const char* s = lua_tolstring(L_, -1, &len); + const char* s = lua_tolstring(co, -1, &len); ctx->response->String(std::string(s, len)); - status = ctx->response->status_code; - } else if (lua_istable(L_, -1)) { - Json j = lua_to_json(L_, -1); + } else if (lua_istable(co, -1)) { + Json j = hvlua_lua_to_json(co, -1); ctx->response->Json(j); - status = ctx->response->status_code; } - lua_pop(L_, 1); - return status; +} + +// Task completion state shared between operator() and on_task_done. +struct LuaHttpTask { + HttpContextPtr ctx; + bool async; // set true once operator() knows the coroutine yielded +}; + +static void on_task_done(void* ud, bool ok, lua_State* co) { + LuaHttpTask* task = (LuaHttpTask*)ud; + HttpContextPtr ctx = task->ctx; + bool async = task->async; + delete task; + + if (co == NULL) { + return; + } + if (!ok) { + const char* msg = lua_tostring(co, -1); + std::string err = msg ? msg : "lua handler error"; + hloge("[lua] http handler error: %s", err.c_str()); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + if (ctx->response->body.empty()) ctx->response->String(err); + } else if (co) { + apply_result(co, ctx); + } + + // For the async (yielded) path, the normal HttpHandler flow already + // returned NEXT, so we must flush the response ourselves now. + if (async) { + ctx->send(); + } +} + +} // namespace + +HttpLuaHandler::HttpLuaHandler(const char* filepath, const HttpLuaHandlerOptions& options) + : filepath_(filepath ? filepath : "") + , options_(options) { } int HttpLuaHandler::operator()(const HttpContextPtr& ctx) { if (!ctx || !ctx->response || !ctx->request) { return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - if (!reloadIfNeeded()) { - std::lock_guard lock(mutex_); + + EventLoop* loop = currentThreadEventLoop; + if (loop == NULL) { ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; - ctx->response->String(last_error_); + ctx->response->String("lua handler: no event loop on this thread"); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - std::lock_guard lock(mutex_); - return callLocked(ctx); + lua_State* L = hvlua_state(loop->loop()); + if (L == NULL) { + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String("lua handler: failed to create lua state"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + + // Ensure the HttpContext metatable is registered on this per-loop state. + register_ctx(L); + + // Load/reload the script; on failure return 500 with the error. + if (!push_script_env(L, filepath_, options_)) { + std::string err = lua_tostring(L, -1) ? lua_tostring(L, -1) : "load error"; + lua_pop(L, 1); + hloge("load lua script %s failed: %s", filepath_.c_str(), err.c_str()); + ctx->response->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR; + ctx->response->String(err); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + // stack: env + if (!push_handler_fn(L, -1, ctx->request->method)) { + lua_pop(L, 1); // env + hloge("lua script %s: no handler (get/post/.../handle)", filepath_.c_str()); + ctx->response->status_code = HTTP_STATUS_NOT_IMPLEMENTED; + ctx->response->String("no lua handler function"); + return HTTP_STATUS_NOT_IMPLEMENTED; + } + // stack: env, fn + lua_remove(L, -2); // stack: fn + lua_push_ctx(L, ctx); // stack: fn, ctx (the handler's single argument) + + LuaHttpTask* task = new LuaHttpTask(); + task->ctx = ctx; + task->async = false; + + // Run fn(ctx) in a coroutine. If it finishes synchronously, on_task_done + // runs now (async=false) and builds the response; we return the status so + // the normal HttpHandler flow sends it. If it yields, we return NEXT and + // the response is flushed later in on_task_done (async=true). + int finished = hvlua_start_task(L, 1, on_task_done, task); + if (finished) { + return ctx->response->status_code; + } + task->async = true; + return HTTP_STATUS_NEXT; // 0: response completed asynchronously } } // namespace hv #endif // WITH_LUA + diff --git a/http/server/HttpLuaHandler.h b/http/server/HttpLuaHandler.h index 305c96c9c..c390a6be5 100644 --- a/http/server/HttpLuaHandler.h +++ b/http/server/HttpLuaHandler.h @@ -2,15 +2,11 @@ #define HV_HTTP_LUA_HANDLER_H_ #include -#include #include -#include #include "hexport.h" #include "HttpService.h" -struct lua_State; - namespace hv { struct HV_EXPORT HttpLuaHandlerOptions { @@ -21,12 +17,24 @@ struct HV_EXPORT HttpLuaHandlerOptions { } }; +// HttpLuaHandler runs a Lua script to handle an HTTP request. +// +// Execution model (Stage B): the handler runs on the server IO thread and uses +// that thread's per-loop lua_State (EventLoop::luaState()). Each request runs +// the script's handler function inside a fresh coroutine, so the script may use +// synchronous-style async APIs (hloop.sleep, hv.dns.resolve, ...) that yield to +// the loop and resume on the same thread. If the script yields, the HTTP +// response is completed asynchronously via the HttpResponseWriter. +// +// The script defines either a per-method function (get/post/put/delete/...) or +// a generic handle(ctx); the per-method function takes precedence. +// +// The handler object itself is cheap and copyable; the compiled script lives in +// the per-loop lua_State and is (re)loaded on demand, tracking file mtime for +// hot reload. class HV_EXPORT HttpLuaHandler { public: HttpLuaHandler(const char* filepath, const HttpLuaHandlerOptions& options = HttpLuaHandlerOptions()); - HttpLuaHandler(const HttpLuaHandler& rhs); - HttpLuaHandler& operator=(const HttpLuaHandler& rhs); - ~HttpLuaHandler(); int operator()(const HttpContextPtr& ctx); @@ -34,22 +42,9 @@ class HV_EXPORT HttpLuaHandler { return filepath_; } - std::string lastError() const; - -private: - bool reloadIfNeeded(); - bool loadLocked(time_t mtime); - void closeLocked(); - int callLocked(const HttpContextPtr& ctx); - void setErrorLocked(const std::string& error); - private: std::string filepath_; HttpLuaHandlerOptions options_; - lua_State* L_; - time_t mtime_; - std::string last_error_; - mutable std::mutex mutex_; }; typedef std::shared_ptr HttpLuaHandlerPtr; diff --git a/lua/hvlua.c b/lua/hvlua.c new file mode 100644 index 000000000..b29fc758e --- /dev/null +++ b/lua/hvlua.c @@ -0,0 +1,429 @@ +#include +#include +#include + +#include "hvlua.h" + +#include "hbase.h" // HV_ALLOC / HV_FREE +#include "hlog.h" + +// --------------------------------------------------------------------------- +// coroutine scheduler +// --------------------------------------------------------------------------- +// +// A suspended coroutine is kept alive by an int ref into the registry (the +// lua_State* thread object). The token stores that ref plus a valid flag so a +// stale resume (loop teardown, double resume) is a safe no-op. +typedef struct HvLuaStateCtx HvLuaStateCtx; + +struct HvLuaCoroutine { + lua_State* L; // the coroutine thread + int ref; // luaL_ref of the thread in the registry, LUA_NOREF if freed + HvLuaStateCtx* owner; + struct HvLuaCoroutine* prev; + struct HvLuaCoroutine* next; +}; + +// A "task" is a top-level coroutine with a C completion callback. The task +// struct is stored in the registry keyed by the coroutine pointer (lua_rawsetp) +// so whichever resume finishes it (initial run or a later async resume) can +// fire on_done once, without needing a C++ container. +typedef struct HvLuaTask { + hvlua_done_cb on_done; + void* ud; + int thread_ref; // keeps the coroutine alive between resumes + HvLuaStateCtx* owner; + struct HvLuaTask* prev; + struct HvLuaTask* next; +} HvLuaTask; + +struct HvLuaCleanup { + hvlua_cleanup_cb cb; + void* userdata; + HvLuaStateCtx* owner; + struct HvLuaCleanup* prev; + struct HvLuaCleanup* next; +}; + +struct HvLuaStateCtx { + HvLuaCoroutine* coroutines; + HvLuaTask* tasks; + HvLuaCleanup* cleanups; + int closing; +}; + +static char s_state_ctx_key; + +static HvLuaStateCtx* state_ctx(lua_State* L) { + HvLuaStateCtx* ctx; + lua_rawgetp(L, LUA_REGISTRYINDEX, &s_state_ctx_key); + ctx = (HvLuaStateCtx*)lua_touserdata(L, -1); + lua_pop(L, 1); + return ctx; +} + +static void coroutine_link(HvLuaStateCtx* ctx, HvLuaCoroutine* co) { + co->owner = ctx; + co->prev = NULL; + co->next = ctx->coroutines; + if (co->next) co->next->prev = co; + ctx->coroutines = co; +} + +static void coroutine_unlink(HvLuaCoroutine* co) { + HvLuaStateCtx* ctx = co->owner; + if (ctx == NULL) return; + if (co->prev) co->prev->next = co->next; + else ctx->coroutines = co->next; + if (co->next) co->next->prev = co->prev; + co->owner = NULL; +} + +static void task_link(HvLuaStateCtx* ctx, HvLuaTask* task) { + task->owner = ctx; + task->prev = NULL; + task->next = ctx->tasks; + if (task->next) task->next->prev = task; + ctx->tasks = task; +} + +static void task_unlink(HvLuaTask* task) { + HvLuaStateCtx* ctx = task->owner; + if (ctx == NULL) return; + if (task->prev) task->prev->next = task->next; + else ctx->tasks = task->next; + if (task->next) task->next->prev = task->prev; + task->owner = NULL; +} + +// registry[co] = lightuserdata(task) (NULL entry == not a task / finished) +static HvLuaTask* task_get(lua_State* co) { + HvLuaTask* task; + lua_rawgetp(co, LUA_REGISTRYINDEX, co); + task = (HvLuaTask*)lua_touserdata(co, -1); + lua_pop(co, 1); + return task; +} + +static void task_set(lua_State* co, HvLuaTask* task) { + if (task) { + lua_pushlightuserdata(co, task); + } else { + lua_pushnil(co); + } + lua_rawsetp(co, LUA_REGISTRYINDEX, co); +} + +// Drive one resume step of a task coroutine. Fires on_done when it finishes. +// `co` is the coroutine; nresults is the number of values already pushed on it. +// @return 1 if the task finished (on_done fired, thread ref released), 0 if it +// yielded again. The caller must NOT touch `co` after a return of 1: finishing +// released the thread ref, so `co` may be collected — do not re-read it. +static int hvlua_task_step(lua_State* co, int nresults) { + HvLuaTask* task; + int nres = 0; + int status; + (void)nres; + status = lua_resume(co, NULL, nresults +#if LUA_VERSION_NUM >= 504 + , &nres +#endif + ); + if (status == LUA_YIELD) { + return 0; // suspended again; a resume token owns the next wakeup + } + // Finished (LUA_OK) or errored: fire the completion callback, then release. + task = task_get(co); + if (task == NULL) return 1; + task_set(co, NULL); // erase before callback so a re-entrant step no-ops + if (task->on_done) task->on_done(task->ud, status == LUA_OK, co); + task_unlink(task); + luaL_unref(co, LUA_REGISTRYINDEX, task->thread_ref); + HV_FREE(task); + return 1; +} + +int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud) { + lua_State* co; + int thread_ref; + HvLuaTask* task; + int finished; + // stack (top): fn, arg1, ..., argN + co = lua_newthread(L); // push thread + // move fn+args (below the thread) into the coroutine + lua_insert(L, -(nargs + 2)); // move thread below fn+args + lua_xmove(L, co, nargs + 1); // move fn+args into co; thread stays on L + thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // pop+ref the thread + + HV_ALLOC_SIZEOF(task); + task->on_done = on_done; + task->ud = ud; + task->thread_ref = thread_ref; + task_link(state_ctx(L), task); + task_set(co, task); + + // Use the return value instead of re-reading `co`: if it finished + // synchronously, hvlua_task_step already released the thread ref, so + // touching `co` again (e.g. task_get(co)) would read a coroutine whose last + // reference is gone. + finished = hvlua_task_step(co, nargs); + return finished; +} + +HvLuaCoroutine* hvlua_suspend(lua_State* L) { + // L is the running coroutine. Keep it alive by ref'ing the thread object + // in the registry so it survives GC while suspended. + HvLuaCoroutine* co; + HV_ALLOC_SIZEOF(co); + co->L = L; + lua_pushthread(L); // push the running thread onto its own stack + co->ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops it, stores ref in registry + coroutine_link(state_ctx(L), co); + return co; +} + +lua_State* hvlua_coroutine_state(HvLuaCoroutine* co) { + if (co == NULL || co->ref == LUA_NOREF) return NULL; + if (co->owner && co->owner->closing) return NULL; + return co->L; +} + +void hvlua_cancel(HvLuaCoroutine* co) { + if (co == NULL) return; + if (co->ref != LUA_NOREF && (co->owner == NULL || !co->owner->closing)) { + luaL_unref(co->L, LUA_REGISTRYINDEX, co->ref); + co->ref = LUA_NOREF; + } + coroutine_unlink(co); + HV_FREE(co); +} + +void hvlua_resume(HvLuaCoroutine* co, int nresults) { + lua_State* L; + int ref; + if (co == NULL) return; + if (co->owner && co->owner->closing) { + hvlua_cancel(co); + return; + } + if (co->ref == LUA_NOREF) { // stale: already resumed/freed + HV_FREE(co); + return; + } + L = co->L; + ref = co->ref; + // Release the suspend token's registry ref BEFORE resuming so a re-entrant + // resume can't double-free. Task tracking keeps its own ref, so the + // coroutine stays alive across this transition. + co->ref = LUA_NOREF; + coroutine_unlink(co); + HV_FREE(co); + + // Drive the coroutine; hvlua_task_step fires on_done if it finishes and is + // a tracked task. For non-task coroutines it just resumes/logs errors. + if (task_get(L) != NULL) { + hvlua_task_step(L, nresults); + } else { + int nres = 0; + int status; + (void)nres; + status = lua_resume(L, NULL, nresults +#if LUA_VERSION_NUM >= 504 + , &nres +#endif + ); + if (status != LUA_OK && status != LUA_YIELD) { + const char* msg = lua_tostring(L, -1); + hloge("[lua] coroutine error: %s", msg ? msg : "unknown"); + } + } + luaL_unref(L, LUA_REGISTRYINDEX, ref); +} + +HvLuaCleanup* hvlua_cleanup_add(lua_State* L, hvlua_cleanup_cb cb, void* userdata) { + HvLuaStateCtx* ctx = state_ctx(L); + HvLuaCleanup* cleanup; + if (ctx == NULL || cb == NULL) return NULL; + HV_ALLOC_SIZEOF(cleanup); + cleanup->cb = cb; + cleanup->userdata = userdata; + cleanup->owner = ctx; + cleanup->prev = NULL; + cleanup->next = ctx->cleanups; + if (cleanup->next) cleanup->next->prev = cleanup; + ctx->cleanups = cleanup; + return cleanup; +} + +void hvlua_cleanup_del(HvLuaCleanup* cleanup) { + HvLuaStateCtx* ctx; + if (cleanup == NULL) return; + ctx = cleanup->owner; + if (ctx) { + if (cleanup->prev) cleanup->prev->next = cleanup->next; + else ctx->cleanups = cleanup->next; + if (cleanup->next) cleanup->next->prev = cleanup->prev; + } + HV_FREE(cleanup); +} + +// --------------------------------------------------------------------------- +// per-loop lua_State +// --------------------------------------------------------------------------- + +static void hvlua_state_dtor(void* L) { + lua_State* state = (lua_State*)L; + HvLuaStateCtx* ctx; + if (state == NULL) return; + ctx = state_ctx(state); + if (ctx == NULL) { + lua_close(state); + return; + } + ctx->closing = 1; + while (ctx->cleanups) { + HvLuaCleanup* cleanup = ctx->cleanups; + hvlua_cleanup_cb cb = cleanup->cb; + void* userdata = cleanup->userdata; + ctx->cleanups = cleanup->next; + if (ctx->cleanups) ctx->cleanups->prev = NULL; + cleanup->owner = NULL; + HV_FREE(cleanup); + cb(userdata); + } + while (ctx->tasks) { + HvLuaTask* task = ctx->tasks; + ctx->tasks = task->next; + if (task->on_done) task->on_done(task->ud, false, NULL); + task->owner = NULL; + HV_FREE(task); + } + lua_close(state); + while (ctx->coroutines) { + HvLuaCoroutine* co = ctx->coroutines; + ctx->coroutines = co->next; + HV_FREE(co); + } + HV_FREE(ctx); +} + +static lua_State* hvlua_new_state(hloop_t* loop) { + lua_State* L = luaL_newstate(); + HvLuaStateCtx* ctx; + if (L == NULL) return NULL; + HV_ALLOC_SIZEOF(ctx); + if (ctx == NULL) { + lua_close(L); + return NULL; + } + lua_pushlightuserdata(L, ctx); + lua_rawsetp(L, LUA_REGISTRYINDEX, &s_state_ctx_key); + luaL_openlibs(L); + + // Stash the owning hloop_t* in the registry so bindings can reach the loop. + lua_pushlightuserdata(L, (void*)loop); + lua_setfield(L, LUA_REGISTRYINDEX, "hv.loop"); + + // Register modules from most basic to higher-level (mirrors libhv layering): + // base -> event -> json -> http/ws -> redis -> mqtt + hvlua_open_base(L); + hvlua_open_event(L); + hvlua_open_json(L); +#ifdef HVLUA_WITH_HTTP + hvlua_open_http(L); + hvlua_open_ws(L); +#endif +#ifdef HVLUA_WITH_REDIS + hvlua_open_redis(L); +#endif +#ifdef HVLUA_WITH_MQTT + hvlua_open_mqtt(L); +#endif + + hloop_set_lua_state(loop, L, hvlua_state_dtor); + return L; +} + +lua_State* hvlua_state(hloop_t* loop) { + lua_State* L; + if (loop == NULL) return NULL; + L = (lua_State*)hloop_lua_state(loop); + if (L == NULL) { + L = hvlua_new_state(loop); + } + return L; +} + +// Recover the owning loop for a state (used by bindings). +hloop_t* hvlua_loop(lua_State* L) { + hloop_t* loop; + lua_getfield(L, LUA_REGISTRYINDEX, "hv.loop"); + loop = (hloop_t*)lua_touserdata(L, -1); + lua_pop(L, 1); + return loop; +} + +// --------------------------------------------------------------------------- +// run helpers +// --------------------------------------------------------------------------- + +// Run a loaded chunk (on stack top of main L) inside a fresh coroutine. +static int hvlua_run_chunk(lua_State* L) { + lua_State* co; + int ref; + int nres = 0; + int status; + (void)nres; + // stack: [chunk] + co = lua_newthread(L); // stack: [chunk][thread] + lua_pushvalue(L, -2); // stack: [chunk][thread][chunk] + lua_xmove(L, co, 1); // move chunk into co; stack: [chunk][thread] + + // Keep the thread referenced while it may yield. + ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread; stack: [chunk] + lua_pop(L, 1); // pop chunk; stack: [] + + status = lua_resume(co, NULL, 0 +#if LUA_VERSION_NUM >= 504 + , &nres +#endif + ); + if (status == LUA_YIELD) { + // Suspended on an async op; a resume token (from hvlua_suspend) now + // owns its own ref. Release ours; the coroutine stays alive via that. + luaL_unref(L, LUA_REGISTRYINDEX, ref); + return 0; + } + if (status != LUA_OK) { + const char* msg = lua_tostring(co, -1); + hloge("[lua] script error: %s", msg ? msg : "unknown"); + luaL_unref(L, LUA_REGISTRYINDEX, ref); + return -1; + } + luaL_unref(L, LUA_REGISTRYINDEX, ref); + return 0; +} + +int hvlua_dofile(hloop_t* loop, const char* filepath) { + lua_State* L = hvlua_state(loop); + if (L == NULL) return -1; + if (luaL_loadfile(L, filepath) != LUA_OK) { + const char* msg = lua_tostring(L, -1); + hloge("[lua] load %s failed: %s", filepath, msg ? msg : "unknown"); + lua_pop(L, 1); + return -1; + } + return hvlua_run_chunk(L); +} + +int hvlua_dostring(hloop_t* loop, const char* code) { + lua_State* L = hvlua_state(loop); + if (L == NULL) return -1; + if (luaL_loadstring(L, code) != LUA_OK) { + const char* msg = lua_tostring(L, -1); + hloge("[lua] load string failed: %s", msg ? msg : "unknown"); + lua_pop(L, 1); + return -1; + } + return hvlua_run_chunk(L); +} diff --git a/lua/hvlua.h b/lua/hvlua.h new file mode 100644 index 000000000..066bb8f07 --- /dev/null +++ b/lua/hvlua.h @@ -0,0 +1,147 @@ +#ifndef HV_LUA_H_ +#define HV_LUA_H_ + +// libhv Lua binding: entry points. +// +// Design (see docs/superpowers/specs/2026-07-28-lua-binding-design.md): +// - one lua_State per loop-thread, stored on hloop_t via hloop_set_lua_state. +// - coroutine-based synchronous-style async IO: suspendable bindings call +// hvlua_suspend()/lua_yieldk() and are resumed on the same loop thread when +// the libhv async callback fires (hvlua_resume()). +// - all script-facing functions live under a single global "hv" table +// (hv.setTimeout, hv.sleep, hv.resolveDns, hv.log, hv.json, ...). +// - binding modules mirror libhv's C-layer dirs: lua_hv_event.c (event/) and +// lua_hv_base.c (base/) are plain C; lua_hv_json.cpp (cpputil/json) is C++. +// +// This header is usable from both C and C++. The core scheduler and the event/ +// base bindings are implemented in C; hv.json (nlohmann) is C++. C++ callers may +// use the hv:: aliases at the bottom. + +#include "hloop.h" // hloop_t + hexport.h (BEGIN_EXTERN_C) + hplatform (bool) + +#ifndef lua_h +typedef struct lua_State lua_State; +#endif + +// Opaque suspend token (see hvlua_suspend). +typedef struct HvLuaCoroutine HvLuaCoroutine; +typedef struct HvLuaCleanup HvLuaCleanup; +typedef void (*hvlua_cleanup_cb)(void* userdata); + +// Task completion callback: ok = true on normal finish; on error ok = false and +// the error message is on top of `co`'s stack. +typedef void (*hvlua_done_cb)(void* ud, bool ok, lua_State* co); + +BEGIN_EXTERN_C + +// Get (creating on first use) the lua_State bound to `loop`. The state is +// stored on the hloop_t and closed when the loop is cleaned up. Registers all +// hv.* modules on creation. Returns NULL if `loop` is NULL. +lua_State* hvlua_state(hloop_t* loop); + +// Run a script file on `loop`'s lua_State inside a fresh coroutine, so the +// script may use synchronous-style async APIs (which yield internally). +// @return 0 on success (or when the top coroutine yields), <0 on load/runtime error. +int hvlua_dofile(hloop_t* loop, const char* filepath); + +// Run a script string on `loop`'s lua_State inside a fresh coroutine. +int hvlua_dostring(hloop_t* loop, const char* code); + +// ---- coroutine scheduler (used by suspendable bindings) ---- +// +// A suspendable binding does: +// 1. start the libhv async op, capturing a resume token for the running +// coroutine via hvlua_suspend(L); +// 2. return lua_yieldk(L, ...) to suspend; +// 3. in the libhv async callback (same loop thread), push the results onto +// the coroutine and call hvlua_resume(token, nresults). +// +// The token keeps the coroutine alive (luaL_ref in the registry) while it is +// suspended, and detects staleness so a late/duplicate resume is a safe no-op. +// +// IMPORTANT (C++ bindings): lua_yieldk() does NOT return to the C/C++ frame that +// calls it — Lua unwinds via longjmp (liblua is built as C), which does NOT run +// C++ destructors. Therefore, when the frame that calls lua_yieldk() is C++, it +// MUST NOT hold any live non-trivial C++ object (std::string, std::shared_ptr, +// std::map, ...) at the point of the yield, or that object's destructor is +// skipped and it leaks on every suspend. Confine such objects to a scope that +// ends before lua_yieldk (see hvlua_http.cpp / hvlua_ws.cpp / hvlua_redis.cpp / +// hvlua_mqtt.cpp for the pattern). The pure-C bindings (hvlua_event.c) are +// unaffected (POD + HV_ALLOC only). + +// Register the running coroutine `L` as suspendable; returns an opaque token. +// Must be called from a coroutine (a lua_State created by lua_newthread). +HvLuaCoroutine* hvlua_suspend(lua_State* L); + +// Resume a previously suspended coroutine. `nresults` values must already be +// pushed on the coroutine. Safe no-op if the token is stale. Frees the token. +void hvlua_resume(HvLuaCoroutine* co, int nresults); + +// Discard a suspend token WITHOUT resuming (e.g. an async op failed to start +// before the coroutine yielded). Releases the registry ref and frees the token. +void hvlua_cancel(HvLuaCoroutine* co); + +// The coroutine's lua_State (to push results before hvlua_resume). NULL if stale. +lua_State* hvlua_coroutine_state(HvLuaCoroutine* co); + +// Track heap state owned by an async operation. The callback runs if the +// lua_State closes before normal completion; normal completion removes it. +HvLuaCleanup* hvlua_cleanup_add(lua_State* L, hvlua_cleanup_cb cb, void* userdata); +void hvlua_cleanup_del(HvLuaCleanup* cleanup); + +// Recover the owning hloop_t* for a lua_State (stashed at state creation). +hloop_t* hvlua_loop(lua_State* L); + +// ---- coroutine "task" runner (used by the HTTP handler and hvlua) ---- +// +// Runs fn(args...) inside a fresh coroutine. If the coroutine yields on an +// async op, it is resumed later (on the same loop thread) by hvlua_resume. +// When the coroutine finally finishes (success or error), `on_done` is invoked +// exactly once on the loop thread. +// +// The function and `nargs` arguments must already be pushed on `L` (a main or +// coroutine state); they are moved into the new coroutine. Returns 1 if the +// task finished synchronously (on_done already called), 0 if it yielded. +int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud); + +// Module registration entry points (called by hvlua_state on state creation, +// in this order — most basic first, mirroring libhv's layering). +// Declared extern "C" so the C core can call the C++-implemented hv.json module. +// Each hvlua_ module mirrors a libhv C-layer directory and registers into +// the single global "hv" table. +void hvlua_open_base(lua_State* L); // hvlua_base.c -> hv.version / hv.log(=logi) / hv.logd/logi/logw/loge (base/) +void hvlua_open_event(lua_State* L); // hvlua_event.c -> hv.setTimeout/sleep/resolveDns/run/stop (event/) +void hvlua_open_json(lua_State* L); // hvlua_json.cpp -> hv.json (cpputil/, nlohmann) +#ifdef HVLUA_WITH_HTTP +void hvlua_open_http(lua_State* L); // hvlua_http.cpp -> hv.http (http/client AsyncHttpClient) +void hvlua_open_ws(lua_State* L); // hvlua_ws.cpp -> hv.ws (http/client WebSocketClient) +#endif +#ifdef HVLUA_WITH_REDIS +void hvlua_open_redis(lua_State* L); // hvlua_redis.cpp -> hv.redis (redis AsyncRedisClient) +#endif +#ifdef HVLUA_WITH_MQTT +void hvlua_open_mqtt(lua_State* L); // hvlua_mqtt.cpp -> hv.mqtt (mqtt MqttClient) +#endif + +END_EXTERN_C + +#ifdef __cplusplus +// Convenience aliases so existing C++ call sites can use hv::hvlua_*. +namespace hv { + using ::HvLuaCoroutine; + using ::hvlua_done_cb; + using ::hvlua_state; + using ::hvlua_dofile; + using ::hvlua_dostring; + using ::hvlua_suspend; + using ::hvlua_resume; + using ::hvlua_cancel; + using ::hvlua_coroutine_state; + using ::hvlua_cleanup_add; + using ::hvlua_cleanup_del; + using ::hvlua_loop; + using ::hvlua_start_task; +} +#endif + +#endif // HV_LUA_H_ diff --git a/lua/hvlua_base.c b/lua/hvlua_base.c new file mode 100644 index 000000000..75112b00d --- /dev/null +++ b/lua/hvlua_base.c @@ -0,0 +1,70 @@ +#include +#include +#include + +#include "hvlua.h" + +#include + +#include "hlog.h" +#include "hversion.h" + +// hv.version() -> libhv version string, e.g. "1.3.4" +static int l_hv_version(lua_State* L) { + lua_pushstring(L, HV_VERSION_STRING); + return 1; +} + +// hv.log*(...) : join args with tabs and log at the given level. +// Uses a bounded stack buffer (log lines are short); overflow is truncated. +static int hvlua_log_at(lua_State* L, int level) { + char line[1024]; + size_t off = 0; + int n = lua_gettop(L); + int i; + line[0] = '\0'; + for (i = 1; i <= n; ++i) { + size_t len = 0; + const char* s = luaL_tolstring(L, i, &len); // pushes a string repr + if (i > 1 && off < sizeof(line) - 1) { + line[off++] = '\t'; + } + if (off < sizeof(line) - 1) { + size_t space = sizeof(line) - 1 - off; + size_t cpy = len < space ? len : space; + memcpy(line + off, s, cpy); + off += cpy; + } + lua_pop(L, 1); // pop the string pushed by luaL_tolstring + } + line[off] = '\0'; + logger_print(hlog, level, "[lua] %s", line); + return 0; +} + +// hv.logd/logi/logw/loge : debug/info/warn/error. hv.log is an alias of logi. +static int l_hv_logd(lua_State* L) { return hvlua_log_at(L, LOG_LEVEL_DEBUG); } +static int l_hv_logi(lua_State* L) { return hvlua_log_at(L, LOG_LEVEL_INFO); } +static int l_hv_logw(lua_State* L) { return hvlua_log_at(L, LOG_LEVEL_WARN); } +static int l_hv_loge(lua_State* L) { return hvlua_log_at(L, LOG_LEVEL_ERROR); } + +static const luaL_Reg hv_base_funcs[] = { + { "version", l_hv_version }, + { "log", l_hv_logi }, // alias of logi + { "logd", l_hv_logd }, + { "logi", l_hv_logi }, + { "logw", l_hv_logw }, + { "loge", l_hv_loge }, + { NULL, NULL } +}; + +// Create/extend the global "hv" table with the base functions. +void hvlua_open_base(lua_State* L) { + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_setfuncs(L, hv_base_funcs, 0); + lua_setglobal(L, "hv"); +} diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c new file mode 100644 index 000000000..ac17036cd --- /dev/null +++ b/lua/hvlua_event.c @@ -0,0 +1,1055 @@ +#include +#include +#include + +#include "hvlua.h" + +#include // memset / memcpy / strcmp + +#include "hbase.h" // HV_ALLOC / HV_FREE +#include "hevent.h" // MAX_READ_BUFSIZE +#include "hloop.h" +#include "hlog.h" +#include "hdns.h" // async DNS is part of the event/ layer (hv.resolveDns) +#include "hsocket.h" // sockaddr_ip + +// A Lua timer: holds a ref to the callback function and the htimer_t. +// For setInterval the ref persists across fires; for setTimeout it is released +// after the single fire. The callback runs inside a fresh coroutine so it may +// itself use synchronous-style async APIs (sleep, dns, ...). +// +// Re-entrancy: the callback may call hv.clearTimer(self). To avoid a +// use-after-free, clearTimer during the callback only marks `dead` (and deletes +// the htimer_t); on_lua_timer performs the deferred free after the callback. +typedef struct LuaTimer { + lua_State* L; // per-loop main state + htimer_t* timer; + HvLuaCleanup* cleanup; + int fn_ref; // LUA_NOREF when released + int once; + int in_callback; + int dead; // clearTimer requested during the callback +} LuaTimer; + +static void lua_timer_release_ref(LuaTimer* lt) { + if (lt->fn_ref != LUA_NOREF) { + luaL_unref(lt->L, LUA_REGISTRYINDEX, lt->fn_ref); + lt->fn_ref = LUA_NOREF; + } +} + +// Live-timer registry: registry["hv.timers"][lightuserdata(timer)] = true. +// hv.clearTimer receives a raw htimer_t* as an opaque handle, but a fired +// once-timer's htimer_t is freed by the loop after its callback, leaving the +// script holding a stale pointer. Validating the handle against this registry +// (instead of blindly dereferencing hevent_userdata) makes clearTimer on a +// stale/already-freed handle a safe no-op. +#define HVLUA_TIMERS_REG "hv.timers" + +static void lua_timers_reg_get(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, HVLUA_TIMERS_REG); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_setfield(L, LUA_REGISTRYINDEX, HVLUA_TIMERS_REG); + } +} + +static void lua_timer_reg_add(lua_State* L, htimer_t* timer) { + lua_timers_reg_get(L); // [timers] + lua_pushlightuserdata(L, timer); // [timers][key] + lua_pushboolean(L, 1); // [timers][key][true] + lua_settable(L, -3); // timers[key]=true + lua_pop(L, 1); +} + +static void lua_timer_reg_remove(lua_State* L, htimer_t* timer) { + if (timer == NULL) return; + lua_timers_reg_get(L); // [timers] + lua_pushlightuserdata(L, timer); // [timers][key] + lua_pushnil(L); // [timers][key][nil] + lua_settable(L, -3); // timers[key]=nil + lua_pop(L, 1); +} + +// Is `timer` a currently-live lua timer handle? +static int lua_timer_reg_has(lua_State* L, htimer_t* timer) { + int has; + lua_timers_reg_get(L); // [timers] + lua_pushlightuserdata(L, timer); // [timers][key] + lua_gettable(L, -2); // [timers][val] + has = lua_toboolean(L, -1); + lua_pop(L, 2); + return has; +} + +static void lua_timer_free(LuaTimer* lt) { + hvlua_cleanup_del(lt->cleanup); + lt->cleanup = NULL; + // Unregister the handle so a later clearTimer on this (soon-freed) timer is + // a safe no-op, and detach the timer's back-pointer to lt. + if (lt->timer) { + lua_timer_reg_remove(lt->L, lt->timer); + hevent_set_userdata(lt->timer, NULL); + } + lua_timer_release_ref(lt); + HV_FREE(lt); +} + +static void lua_timer_cleanup(void* userdata) { + LuaTimer* lt = (LuaTimer*)userdata; + lt->cleanup = NULL; + lua_timer_free(lt); +} + +static void on_lua_timer(htimer_t* timer) { + LuaTimer* lt = (LuaTimer*)hevent_userdata(timer); + lua_State* L; + lua_State* co; + int thread_ref; + int nres = 0; + int status; + if (lt == NULL || lt->fn_ref == LUA_NOREF) return; + L = lt->L; + (void)nres; + + // Run the callback in a fresh coroutine so it may yield on async ops. + lt->in_callback = 1; + co = lua_newthread(L); + thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // keep coroutine alive + lua_rawgeti(co, LUA_REGISTRYINDEX, lt->fn_ref); // push callback fn onto co + + status = lua_resume(co, NULL, 0 +#if LUA_VERSION_NUM >= 504 + , &nres +#endif + ); + if (status != LUA_OK && status != LUA_YIELD) { + const char* msg = lua_tostring(co, -1); + hloge("[lua] timer callback error: %s", msg ? msg : "unknown"); + } + luaL_unref(L, LUA_REGISTRYINDEX, thread_ref); + lt->in_callback = 0; + + // The callback may have cleared this timer (dead) — free it now. Otherwise + // a once-timer (repeat==1, auto-deleted by hloop after this fire) frees here. + if (lt->dead || lt->once) { + lua_timer_free(lt); + } +} + +static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repeat, int once) { + hloop_t* loop = hvlua_loop(L); + LuaTimer* lt; + htimer_t* timer; + luaL_checktype(L, 2, LUA_TFUNCTION); + + HV_ALLOC_SIZEOF(lt); + // Store the per-loop MAIN lua_State, not the calling coroutine L: on_lua_timer + // later does lua_newthread(lt->L), and the calling coroutine may be collected + // before the timer fires (e.g. a timer registered inside another timer's + // callback — that callback coroutine is unref'd right after it returns), + // which would make lt->L a dangling pointer (UAF). The main state lives as + // long as the loop. (LUA_REGISTRYINDEX is shared across all threads of the + // state, so the fn ref below is valid regardless of which thread refs it.) + // Mirrors on_server_accept / on_udp_server_read which use hloop_lua_state(). + lt->L = (lua_State*)hloop_lua_state(loop); + lt->timer = NULL; + lt->cleanup = NULL; + lt->once = once; + lt->in_callback = 0; + lt->dead = 0; + // ref the callback function (arg 2) + lua_pushvalue(L, 2); + lt->fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + timer = htimer_add(loop, on_lua_timer, timeout_ms, repeat); + if (timer == NULL) { + lua_timer_free(lt); + return NULL; + } + hevent_set_userdata(timer, lt); + lt->timer = timer; + lt->cleanup = hvlua_cleanup_add(L, lua_timer_cleanup, lt); + lua_timer_reg_add(L, timer); // mark handle live for clearTimer validation + return timer; +} + +// hv.setTimeout(ms, fn) -> lightuserdata handle +static int l_hloop_setTimeout(lua_State* L) { + uint32_t ms = (uint32_t)luaL_checkinteger(L, 1); + htimer_t* timer = add_lua_timer(L, ms, 1, 1); + if (timer == NULL) { lua_pushnil(L); return 1; } + lua_pushlightuserdata(L, timer); + return 1; +} + +// hv.setInterval(ms, fn) -> lightuserdata handle +static int l_hloop_setInterval(lua_State* L) { + uint32_t ms = (uint32_t)luaL_checkinteger(L, 1); + htimer_t* timer = add_lua_timer(L, ms, INFINITE, 0); + if (timer == NULL) { lua_pushnil(L); return 1; } + lua_pushlightuserdata(L, timer); + return 1; +} + +// hv.clearTimer(handle) +static int l_hloop_clearTimer(lua_State* L) { + htimer_t* timer; + LuaTimer* lt; + if (!lua_islightuserdata(L, 1)) return 0; + timer = (htimer_t*)lua_touserdata(L, 1); + if (timer == NULL) return 0; + // Validate the handle: a fired once-timer's htimer_t is freed by the loop, + // so the script may hold a stale pointer. Only proceed if the handle is a + // currently-live timer we registered; otherwise it's a safe no-op (avoids + // dereferencing freed memory via hevent_userdata). + if (!lua_timer_reg_has(L, timer)) return 0; + lt = (LuaTimer*)hevent_userdata(timer); + htimer_del(timer); + if (lt) { + if (lt->in_callback) { + // Called from within this timer's own callback: defer the free to + // on_lua_timer so we don't free `lt` while it's still in use. Drop it + // from the live registry now so no further clearTimer can match. + lt->dead = 1; + lua_timer_reg_remove(L, timer); + lua_timer_release_ref(lt); + } else { + lua_timer_free(lt); + } + } + return 0; +} + +// ---- hv.sleep(ms): suspend the running coroutine for ms milliseconds ---- + +typedef struct SleepCtx { + HvLuaCoroutine* co; + htimer_t* timer; + HvLuaCleanup* cleanup; +} SleepCtx; + +static void sleep_cleanup(void* userdata) { + SleepCtx* s = (SleepCtx*)userdata; + s->cleanup = NULL; + if (s->timer) hevent_set_userdata(s->timer, NULL); + hvlua_cancel(s->co); + HV_FREE(s); +} + +static void on_sleep_timer(htimer_t* timer) { + SleepCtx* s = (SleepCtx*)hevent_userdata(timer); + HvLuaCoroutine* co = s->co; + hvlua_cleanup_del(s->cleanup); + s->cleanup = NULL; + HV_FREE(s); + // resume the sleeping coroutine with no results + hvlua_resume(co, 0); +} + +// continuation: nothing to return after wakeup +static int sleep_k(lua_State* L, int status, lua_KContext ctx) { + (void)L; (void)status; (void)ctx; + return 0; +} + +// hv.sleep(ms) +static int l_hloop_sleep(lua_State* L) { + uint32_t ms = (uint32_t)luaL_checkinteger(L, 1); + hloop_t* loop = hvlua_loop(L); + SleepCtx* s; + htimer_t* timer; + + // Create the timer BEFORE suspending: if it fails (NULL loop / out of + // resources) we must not deref NULL nor leave the coroutine suspended + // forever with nothing to wake it. Fail fast with (nil, err) instead. + timer = htimer_add(loop, on_sleep_timer, ms, 1); + if (timer == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.sleep: create timer failed"); + return 2; + } + + HV_ALLOC_SIZEOF(s); + s->co = hvlua_suspend(L); + s->timer = timer; + s->cleanup = hvlua_cleanup_add(L, sleep_cleanup, s); + hevent_set_userdata(timer, s); + + return lua_yieldk(L, 0, (lua_KContext)0, sleep_k); +} + +// ---- hv.resolveDns(host): coroutine-synchronous async DNS ---- +// +// DNS resolution is part of the event/ layer (event/hdns.c), same as timers, +// so it lives here alongside setTimeout/sleep rather than in a hv.* module. +// Suspends the running coroutine, issues an async hdns query on the loop, and +// resumes with the resolved addresses (or nil,err) on the same loop thread. + +typedef struct DnsCtx { + HvLuaCoroutine* co; + HvLuaCleanup* cleanup; +} DnsCtx; + +static void dns_cleanup(void* userdata) { + DnsCtx* d = (DnsCtx*)userdata; + d->cleanup = NULL; + hvlua_cancel(d->co); + HV_FREE(d); +} + +static void on_dns_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + DnsCtx* d = (DnsCtx*)userdata; + HvLuaCoroutine* co = d->co; + lua_State* L; + (void)query; + hvlua_cleanup_del(d->cleanup); + d->cleanup = NULL; + HV_FREE(d); + + L = hvlua_coroutine_state(co); + if (L == NULL) { // coroutine gone (loop teardown); release the token + hvlua_cancel(co); + return; + } + + if (result->status == HDNS_STATUS_OK && result->naddrs > 0) { + int i; + lua_createtable(L, result->naddrs, 0); + for (i = 0; i < result->naddrs; ++i) { + char ip[64]; + ip[0] = '\0'; + sockaddr_ip((sockaddr_u*)&result->addrs[i], ip, sizeof(ip)); + lua_pushstring(L, ip); + lua_seti(L, -2, i + 1); + } + hvlua_resume(co, 1); // one result: the address table + } else { + lua_pushnil(L); + lua_pushfstring(L, "dns resolve failed: status=%d", result->status); + hvlua_resume(co, 2); // nil, err + } +} + +static int resolve_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + // Results were pushed by on_dns_resolved before resume; return them. + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +// hv.resolveDns(host) -> { ip, ip, ... } | nil, err +static int l_hloop_resolveDns(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + hloop_t* loop = hvlua_loop(L); + DnsCtx* d; + hdns_t* q; + + HV_ALLOC_SIZEOF(d); + d->co = hvlua_suspend(L); + d->cleanup = hvlua_cleanup_add(L, dns_cleanup, d); + + q = hdns_resolve(loop, host, on_dns_resolved, d); + if (q == NULL) { + // Immediate failure before yielding: release the token and return + // nil,err inline (the coroutine keeps running, no yield happened). + hvlua_cancel(d->co); + hvlua_cleanup_del(d->cleanup); + HV_FREE(d); + lua_pushnil(L); + lua_pushstring(L, "dns resolve: failed to start query"); + return 2; + } + + return lua_yieldk(L, 0, (lua_KContext)0, resolve_k); +} + +// hv.run() / hv.stop() +static int l_hloop_run(lua_State* L) { + (void)L; + return 0; +} + +static void on_hloop_stop_timer(htimer_t* timer) { + hloop_stop(hevent_loop(timer)); +} + +static int l_hloop_stop(lua_State* L) { + hloop_t* loop = hvlua_loop(L); + hloop_status_e status = hloop_status(loop); + if (status == HLOOP_STATUS_RUNNING || status == HLOOP_STATUS_PAUSE) { + hloop_stop(loop); + } else { + htimer_add(loop, on_hloop_stop_timer, 1, 1); + } + return 0; +} + +// ============================================================================ +// TCP client: hv.tcpClient(host, port [, timeout_ms]) -> conn | nil, err +// (alias: hv.connect) +// +// conn is a userdata wrapping an hio_t that lives on the current loop (no evpp, +// no extra thread). All callbacks fire on this loop thread, so we reuse the same +// same-thread suspend/resume machinery as sleep/dns. A conn has at most one +// pending coroutine at a time (connect OR read); close resumes it with nil,err. +// ============================================================================ + +static const char* CONN_META = "hv.conn"; + +typedef struct LuaConn { + hio_t* io; + HvLuaCoroutine* co; // the coroutine waiting on connect/read (or NULL) + int closed; // set in the close callback + int connecting; // pending op is connect (vs read), for resume shape + unpack_setting_t* unpack; // owned; hio_t only stores the pointer + // Sync-read detection: hio_read_until_length/delim may invoke the read + // callback INLINE (buffered data already satisfies the request) before the + // coroutine yields. In that window co is not yet set, so on_conn_read must + // not resume; instead it stashes the data here and l_conn_read* returns it + // directly without suspending. + lua_State* reading_L; // non-NULL while arming a read (the running coroutine) + int read_done; // set true if the read completed synchronously + int read_nres; // number of results pushed on reading_L (sync path) +} LuaConn; + +// forward decl: conn_push_new is defined lower (after the read helpers) but +// used by l_hv_connect above it. +static LuaConn* conn_push_new(lua_State* L, hio_t* io); + +static LuaConn* lua_check_conn(lua_State* L) { + return (LuaConn*)luaL_checkudata(L, 1, CONN_META); +} + +// Resume the conn's pending coroutine (if any) with values already pushed on it. +static void conn_resume(LuaConn* c, int nresults) { + HvLuaCoroutine* co = c->co; + c->co = NULL; + hvlua_resume(co, nresults); +} + +static void on_conn_close(hio_t* io) { + LuaConn* c = (LuaConn*)hevent_userdata(io); + if (c == NULL) return; + c->closed = 1; + c->io = NULL; // hio_t is freed by the loop after this returns; drop it + if (c->co) { + lua_State* co = hvlua_coroutine_state(c->co); + if (co) { + lua_pushnil(co); + lua_pushstring(co, "closed"); + conn_resume(c, 2); + } else { + hvlua_cancel(c->co); + c->co = NULL; + } + } +} + +static void on_conn_connect(hio_t* io) { + LuaConn* c = (LuaConn*)hevent_userdata(io); + if (c == NULL || c->co == NULL) return; + lua_State* co = hvlua_coroutine_state(c->co); + if (co == NULL) { hvlua_cancel(c->co); c->co = NULL; return; } + // resume with the conn userdata itself (kept in the registry via co ref). + // The conn is already on the coroutine stack as the yielded self; we just + // signal success by pushing true, and the continuation returns the conn. + lua_pushboolean(co, 1); + conn_resume(c, 1); +} + +static void on_conn_read(hio_t* io, void* buf, int len) { + LuaConn* c = (LuaConn*)hevent_userdata(io); + if (c == NULL) return; + // Synchronous completion: hio_read_until_* found buffered data and called + // us inline, before the coroutine yielded. co is not set yet; push the data + // onto the running coroutine and let l_conn_read* return it directly (do NOT + // resume — the coroutine is still running). + if (c->reading_L) { + lua_pushlstring(c->reading_L, (const char*)buf, len); + c->read_done = 1; + c->read_nres = 1; + return; + } + if (c->co == NULL) return; + lua_State* co = hvlua_coroutine_state(c->co); + if (co == NULL) { hvlua_cancel(c->co); c->co = NULL; return; } + lua_pushlstring(co, (const char*)buf, len); + conn_resume(c, 1); +} + +// continuation for hv.connect: success resumes with a boolean `true` marker on +// top; failure (close before connect) resumes with (nil, err) already on the +// stack. Distinguish by the type at the top of the stack. +static int connect_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) { + // success: replace the `true` marker with the conn userdata (self, + // captured at stack index 1 before the yield). + lua_pop(L, 1); + lua_pushvalue(L, 1); + return 1; + } + // failure: (nil, err) were pushed by on_conn_close; hand them back. + return 2; +} + +static int l_hv_connect(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + int port = (int)luaL_checkinteger(L, 2); + int timeout_ms = (int)luaL_optinteger(L, 3, 0); + hloop_t* loop = hvlua_loop(L); + + hio_t* io = hio_create_socket(loop, host, port, HIO_TYPE_TCP, HIO_CLIENT_SIDE); + if (io == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.tcpClient: create socket failed"); + return 2; + } + + // conn userdata (becomes stack slot 1's sibling; we return it on success) + LuaConn* c = conn_push_new(L, io); + c->connecting = 1; + // move the conn userdata to stack index 1 so connect_k can return it as self + lua_replace(L, 1); + + hio_setcb_connect(io, on_conn_connect); + if (timeout_ms > 0) hio_set_connect_timeout(io, timeout_ms); + + c->co = hvlua_suspend(L); + hio_connect(io); + return lua_yieldk(L, 0, (lua_KContext)0, connect_k); +} + +// Push a new conn userdata wrapping an existing hio_t (used by connect + accept). +// Sets the close callback; leaves read/connect callbacks to the caller. +static LuaConn* conn_push_new(lua_State* L, hio_t* io) { + LuaConn* c = (LuaConn*)lua_newuserdata(L, sizeof(LuaConn)); + c->io = io; + c->co = NULL; + c->closed = 0; + c->connecting = 0; + c->unpack = NULL; + c->reading_L = NULL; + c->read_done = 0; + c->read_nres = 0; + luaL_getmetatable(L, CONN_META); + lua_setmetatable(L, -2); + hevent_set_userdata(io, c); + hio_setcb_close(io, on_conn_close); + return c; +} + +// conn:read* -> data | nil, err +static int read_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +// Read protocol (fixes sync-callback data loss): the hio_read_until_* calls may +// invoke on_conn_read INLINE when buffered data already satisfies the request. +// So we (1) arm the read callback and mark reading_L BEFORE issuing hio_read*, +// (2) issue hio_read*, then (3) if the callback fired synchronously (read_done), +// return the data directly; otherwise suspend the coroutine. +// +// conn_begin_read: arm callback + enter the "reading" window. Returns 0 on ok. +static void conn_begin_read(lua_State* L, LuaConn* c) { + hio_setcb_read(c->io, on_conn_read); + c->reading_L = L; + c->read_done = 0; + c->read_nres = 0; +} + +// conn_end_read: leave the reading window; return results if the read completed +// synchronously, else suspend the coroutine and yield. +static int conn_end_read(lua_State* L, LuaConn* c) { + c->reading_L = NULL; + if (c->read_done) { + // Data already pushed on L by on_conn_read; return it directly. + return c->read_nres; + } + // Also handle the case where the read synchronously closed the connection + // (on_conn_close ran inline and set closed): report it without suspending. + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + c->co = hvlua_suspend(L); + return lua_yieldk(L, 0, (lua_KContext)0, read_k); +} + +// conn:read() -> data (read once: whatever bytes are available) +static int l_conn_read(lua_State* L) { + LuaConn* c = lua_check_conn(L); + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + conn_begin_read(L, c); + hio_read(c->io); + return conn_end_read(L, c); +} + +// conn:readbytes(n) -> exactly n bytes (hio_read_until_length) +static int l_conn_readbytes(lua_State* L) { + LuaConn* c = lua_check_conn(L); + lua_Integer value = luaL_checkinteger(L, 2); + unsigned int n; + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + if (value <= 0 || (lua_Unsigned)value > MAX_READ_BUFSIZE) { + return luaL_error(L, "conn:readbytes length must be between 1 and %u", MAX_READ_BUFSIZE); + } + n = (unsigned int)value; + conn_begin_read(L, c); + hio_read_until_length(c->io, n); + return conn_end_read(L, c); +} + +// conn:readuntil(delim) -> data up to and including the 1-byte delimiter +static int l_conn_readuntil(lua_State* L) { + LuaConn* c = lua_check_conn(L); + size_t dlen = 0; + const char* delim = luaL_checklstring(L, 2, &dlen); + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + if (dlen != 1) { + return luaL_error(L, "conn:readuntil expects a single-byte delimiter"); + } + conn_begin_read(L, c); + hio_read_until_delim(c->io, (unsigned char)delim[0]); + return conn_end_read(L, c); +} + +// conn:readline() -> data up to and including '\n' +static int l_conn_readline(lua_State* L) { + LuaConn* c = lua_check_conn(L); + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + conn_begin_read(L, c); + hio_read_until_delim(c->io, '\n'); + return conn_end_read(L, c); +} + +// conn:setUnpack(opts): configure automatic message unpacking so subsequent +// conn:read() returns one complete packet. opts is a table: +// mode = "none"|"fixed"|"delimiter"|"length_field" +// package_max_length = +// fixed_length = (fixed) +// delimiter = "" (delimiter) +// body_offset, length_field_offset, +// length_field_bytes, length_adjustment, +// length_field_coding = "be"|"le"|"varint"|"asn1" (length_field) +static int l_conn_setUnpack(lua_State* L) { + LuaConn* c = lua_check_conn(L); + const char* mode; + lua_Integer value; + if (c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + luaL_checktype(L, 2, LUA_TTABLE); + + if (c->unpack == NULL) { + HV_ALLOC_SIZEOF(c->unpack); + } + memset(c->unpack, 0, sizeof(*c->unpack)); + c->unpack->package_max_length = DEFAULT_PACKAGE_MAX_LENGTH; + + lua_getfield(L, 2, "package_max_length"); + if (lua_isinteger(L, -1)) { + value = lua_tointeger(L, -1); + if (value <= 0 || (lua_Unsigned)value > MAX_READ_BUFSIZE) { + return luaL_error(L, "conn:setUnpack: invalid package_max_length"); + } + c->unpack->package_max_length = (unsigned int)value; + } + lua_pop(L, 1); + + lua_getfield(L, 2, "mode"); + mode = luaL_optstring(L, -1, "length_field"); + lua_pop(L, 1); + + if (strcmp(mode, "none") == 0) { + c->unpack->mode = UNPACK_MODE_NONE; + } else if (strcmp(mode, "fixed") == 0) { + c->unpack->mode = UNPACK_BY_FIXED_LENGTH; + lua_getfield(L, 2, "fixed_length"); + value = luaL_optinteger(L, -1, 0); + if (value <= 0 || (lua_Unsigned)value > c->unpack->package_max_length) { + return luaL_error(L, "conn:setUnpack: invalid fixed_length"); + } + c->unpack->fixed_length = (unsigned int)value; + lua_pop(L, 1); + } else if (strcmp(mode, "delimiter") == 0) { + size_t dlen = 0; + const char* delim; + c->unpack->mode = UNPACK_BY_DELIMITER; + lua_getfield(L, 2, "delimiter"); + delim = luaL_optlstring(L, -1, "", &dlen); + if (dlen == 0 || dlen > PACKAGE_MAX_DELIMITER_BYTES) { + return luaL_error(L, "conn:setUnpack: delimiter must be 1..%u bytes", PACKAGE_MAX_DELIMITER_BYTES); + } + memcpy(c->unpack->delimiter, delim, dlen); + c->unpack->delimiter_bytes = (unsigned short)dlen; + lua_pop(L, 1); + } else if (strcmp(mode, "length_field") == 0) { + const char* coding; + lua_Integer body_offset; + lua_Integer field_offset; + lua_Integer field_bytes; + c->unpack->mode = UNPACK_BY_LENGTH_FIELD; + lua_getfield(L, 2, "body_offset"); + body_offset = luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + lua_getfield(L, 2, "length_field_offset"); + field_offset = luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + lua_getfield(L, 2, "length_field_bytes"); + field_bytes = luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + if (body_offset <= 0 || body_offset > UINT16_MAX || + field_offset < 0 || field_offset > UINT16_MAX || + field_bytes <= 0 || field_bytes > 8 || + body_offset < field_offset + field_bytes || + (lua_Unsigned)body_offset > c->unpack->package_max_length) { + return luaL_error(L, "conn:setUnpack: invalid length_field layout"); + } + c->unpack->body_offset = (unsigned short)body_offset; + c->unpack->length_field_offset = (unsigned short)field_offset; + c->unpack->length_field_bytes = (unsigned short)field_bytes; + lua_getfield(L, 2, "length_adjustment"); + value = luaL_optinteger(L, -1, 0); + if (value < INT16_MIN || value > INT16_MAX) { + return luaL_error(L, "conn:setUnpack: invalid length_adjustment"); + } + c->unpack->length_adjustment = (short)value; + lua_pop(L, 1); + lua_getfield(L, 2, "length_field_coding"); + coding = luaL_optstring(L, -1, "be"); + if (strcmp(coding, "le") == 0) c->unpack->length_field_coding = ENCODE_BY_LITTLE_ENDIAN; + else if (strcmp(coding, "varint") == 0) c->unpack->length_field_coding = ENCODE_BY_VARINT; + else if (strcmp(coding, "asn1") == 0) c->unpack->length_field_coding = ENCODE_BY_ASN1; + else c->unpack->length_field_coding = ENCODE_BY_BIG_ENDIAN; + lua_pop(L, 1); + } else { + return luaL_error(L, "conn:setUnpack: unknown mode '%s'", mode); + } + + // hio_t stores only the pointer; c->unpack (owned by the conn) outlives it. + hio_set_unpack(c->io, c->unpack); + return 0; +} + +// conn:write(data) -> nbytes | nil, err (non-blocking, no suspend) +static int l_conn_write(lua_State* L) { + LuaConn* c = lua_check_conn(L); + size_t len = 0; + const char* data; + if (c->closed || c->io == NULL) { + lua_pushnil(L); + lua_pushstring(L, "closed"); + return 2; + } + data = luaL_checklstring(L, 2, &len); + lua_pushinteger(L, hio_write(c->io, data, len)); + return 1; +} + +// conn:close() +static int l_conn_close(lua_State* L) { + LuaConn* c = lua_check_conn(L); + if (c->io && !c->closed) { + hio_close(c->io); // triggers on_conn_close (which clears c->io) + } + return 0; +} + +// conn:fd() +static int l_conn_fd(lua_State* L) { + LuaConn* c = lua_check_conn(L); + lua_pushinteger(L, (c->io && !c->closed) ? hio_fd(c->io) : -1); + return 1; +} + +// conn:peeraddr() -> "ip:port" +static int l_conn_peeraddr(lua_State* L) { + LuaConn* c = lua_check_conn(L); + if (c->io && !c->closed) { + char addr[SOCKADDR_STRLEN] = {0}; + SOCKADDR_STR(hio_peeraddr(c->io), addr); + lua_pushstring(L, addr); + } else { + lua_pushnil(L); + } + return 1; +} + +static int l_conn_gc(lua_State* L) { + LuaConn* c = (LuaConn*)luaL_checkudata(L, 1, CONN_META); + if (c->io && !c->closed) { + hevent_set_userdata(c->io, NULL); // detach: no resume into a dead conn + hio_close(c->io); + } + if (c->unpack) { HV_FREE(c->unpack); c->unpack = NULL; } + return 0; +} + +// ============================================================================ +// TCP server: hv.tcpServer(host, port, on_conn) -> true | nil, err +// (alias: hv.listen) +// +// on_conn(conn) runs in a fresh coroutine per accepted connection, so it can +// use conn:read()/write() synchronously. All on the current loop thread. +// +// The on_conn handler is stored in the registry keyed by the LISTEN io pointer. +// libhv copies the listen io's userdata to each accepted io (nio.c: connio-> +// userdata = io->userdata), so the accept callback recovers the listen io ptr +// from the accepted io's userdata and uses it as the registry key. +// ============================================================================ +static void on_server_accept(hio_t* io) { + hloop_t* loop = hevent_loop(io); + lua_State* L = (lua_State*)hloop_lua_state(loop); + void* listen_key = hevent_userdata(io); // inherited listen io ptr + if (L == NULL || listen_key == NULL) return; + + lua_rawgetp(L, LUA_REGISTRYINDEX, listen_key); // on_conn fn + if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return; } + + // wrap accepted io in a conn (this overwrites io userdata -> conn ptr) + conn_push_new(L, io); // stack: fn, conn + // run on_conn(conn) in a fresh coroutine (moves fn+conn into it) + hvlua_start_task(L, 1, NULL, NULL); +} + +static int l_hv_tcpServer(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + int port = (int)luaL_checkinteger(L, 2); + hloop_t* loop = hvlua_loop(L); + hio_t* listenio; + + luaL_checktype(L, 3, LUA_TFUNCTION); + + listenio = hloop_create_tcp_server(loop, host, port, on_server_accept); + if (listenio == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.tcpServer: create server failed"); + return 2; + } + + // registry[listenio] = on_conn; accepted ios inherit userdata=listenio, + // which the accept callback uses as this key. + lua_pushvalue(L, 3); + lua_rawsetp(L, LUA_REGISTRYINDEX, listenio); + hevent_set_userdata(listenio, listenio); + + lua_pushboolean(L, 1); + return 1; +} + +// ============================================================================ +// UDP: hv.udpClient(host, port) -> sock ; hv.udpServer(host, port, on_recv) +// +// UDP has no connection/accept. A udp "sock" reuses the conn userdata (it wraps +// an hio_t). sock:recvfrom() coroutine-suspends for one datagram and returns +// (data, peeraddr). sock:sendto(data) sends to the bound peer (client) or the +// last peer (server reply). All on the current loop thread. +// ============================================================================ + +// UDP read callback: push data + peeraddr string, resume with 2 results. +static void on_udp_read(hio_t* io, void* buf, int len) { + LuaConn* c = (LuaConn*)hevent_userdata(io); + lua_State* co; + char addr[SOCKADDR_STRLEN]; + if (c == NULL) return; + // Synchronous completion: hio_read found buffered data and called us inline, + // before the coroutine yielded (same hazard as TCP on_conn_read). co is not + // set yet; push onto the running coroutine and let l_udp_recvfrom return the + // results directly instead of resuming. + if (c->reading_L) { + lua_pushlstring(c->reading_L, (const char*)buf, len); + addr[0] = '\0'; + SOCKADDR_STR(hio_peeraddr(io), addr); + lua_pushstring(c->reading_L, addr); + c->read_done = 1; + c->read_nres = 2; + return; + } + if (c->co == NULL) return; + co = hvlua_coroutine_state(c->co); + if (co == NULL) { hvlua_cancel(c->co); c->co = NULL; return; } + lua_pushlstring(co, (const char*)buf, len); + addr[0] = '\0'; + SOCKADDR_STR(hio_peeraddr(io), addr); + lua_pushstring(co, addr); + conn_resume(c, 2); +} + +static int recvfrom_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return 2; // data, peeraddr (or nil,err on close) +} + +// sock:recvfrom() -> data, peeraddr | nil, err +static int l_udp_recvfrom(lua_State* L) { + LuaConn* c = lua_check_conn(L); + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + // Arm the read callback and enter the "reading" window BEFORE hio_read: + // hio_read may invoke on_udp_read inline (buffered datagram / read_remain), + // and resuming the not-yet-yielded coroutine would be illegal. If it fires + // synchronously, on_udp_read pushes (data, peer) and sets read_done, and + // conn_end_read returns them directly instead of suspending. (Same fix as + // the TCP read path.) + hio_setcb_read(c->io, on_udp_read); + c->reading_L = L; + c->read_done = 0; + c->read_nres = 0; + hio_read(c->io); + c->reading_L = NULL; + if (c->read_done) { + return c->read_nres; // data, peer already pushed on L + } + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + c->co = hvlua_suspend(L); + return lua_yieldk(L, 0, (lua_KContext)0, recvfrom_k); +} + +// sock:sendto(data) -> nbytes (to bound peer / last recvfrom peer) +static int l_udp_sendto(lua_State* L) { + LuaConn* c = lua_check_conn(L); + size_t len = 0; + const char* data; + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + data = luaL_checklstring(L, 2, &len); + lua_pushinteger(L, hio_write(c->io, data, len)); + return 1; +} + +static int l_hv_udpClient(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + int port = (int)luaL_checkinteger(L, 2); + hloop_t* loop = hvlua_loop(L); + hio_t* io = hloop_create_udp_client(loop, host, port); + if (io == NULL) { + lua_pushnil(L); lua_pushstring(L, "hv.udpClient: create failed"); return 2; + } + conn_push_new(L, io); // returns conn userdata on stack + return 1; +} + +// hv.udpServer(host, port, on_recv): on_recv(sock, data, peeraddr) per datagram. +static void on_udp_server_read(hio_t* io, void* buf, int len) { + hloop_t* loop = hevent_loop(io); + lua_State* L = (lua_State*)hloop_lua_state(loop); + void* key = hevent_userdata(io); + char addr[SOCKADDR_STRLEN]; + if (L == NULL || key == NULL) return; + lua_rawgetp(L, LUA_REGISTRYINDEX, key); // on_recv fn + if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return; } + // args: sock (the server conn userdata, stashed in registry too), data, peer + lua_rawgetp(L, LUA_REGISTRYINDEX, (char*)key + 1); // sock userdata + lua_pushlstring(L, (const char*)buf, len); + addr[0] = '\0'; + SOCKADDR_STR(hio_peeraddr(io), addr); + lua_pushstring(L, addr); + // run on_recv(sock, data, peer) in a fresh coroutine + hvlua_start_task(L, 3, NULL, NULL); +} + +static int l_hv_udpServer(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + int port = (int)luaL_checkinteger(L, 2); + hloop_t* loop = hvlua_loop(L); + hio_t* io; + LuaConn* c; + + luaL_checktype(L, 3, LUA_TFUNCTION); + io = hloop_create_udp_server(loop, host, port); + if (io == NULL) { + lua_pushnil(L); lua_pushstring(L, "hv.udpServer: create failed"); return 2; + } + + // registry[io] = on_recv ; registry[io+1] = sock userdata + lua_pushvalue(L, 3); + lua_rawsetp(L, LUA_REGISTRYINDEX, io); + c = conn_push_new(L, io); // pushes sock userdata; sets userdata=c + (void)c; + lua_rawsetp(L, LUA_REGISTRYINDEX, (char*)io + 1); // pops the sock userdata + // the read callback keys off hevent_userdata(io); set it back to io (not c) + hevent_set_userdata(io, io); + + hio_setcb_read(io, on_udp_server_read); + hio_read(io); + + lua_pushboolean(L, 1); + return 1; +} + +static const luaL_Reg conn_methods[] = { + { "read", l_conn_read }, + { "readbytes", l_conn_readbytes }, + { "readuntil", l_conn_readuntil }, + { "readline", l_conn_readline }, + { "setUnpack", l_conn_setUnpack }, + { "write", l_conn_write }, + { "close", l_conn_close }, + { "fd", l_conn_fd }, + { "peeraddr", l_conn_peeraddr }, + { "sendto", l_udp_sendto }, + { "recvfrom", l_udp_recvfrom }, + { NULL, NULL } +}; + +static void register_conn(lua_State* L) { + if (luaL_newmetatable(L, CONN_META)) { + lua_pushcfunction(L, l_conn_gc); + lua_setfield(L, -2, "__gc"); + lua_newtable(L); + luaL_setfuncs(L, conn_methods, 0); + lua_setfield(L, -2, "__index"); + } + lua_pop(L, 1); +} + +static const luaL_Reg hloop_funcs[] = { + { "setTimeout", l_hloop_setTimeout }, + { "setInterval", l_hloop_setInterval }, + { "clearTimer", l_hloop_clearTimer }, + { "sleep", l_hloop_sleep }, + { "resolveDns", l_hloop_resolveDns }, + // Primary names mirror the C++ classes hv::TcpClient / TcpServer / + // UdpClient / UdpServer for a consistent 2x2 (client/server x tcp/udp). + { "tcpClient", l_hv_connect }, + { "tcpServer", l_hv_tcpServer }, + { "udpClient", l_hv_udpClient }, + { "udpServer", l_hv_udpServer }, + // Aliases: connect/listen are idiomatic verbs kept for convenience. + { "connect", l_hv_connect }, + { "listen", l_hv_tcpServer }, + { "run", l_hloop_run }, + { "stop", l_hloop_stop }, + { NULL, NULL } +}; + +// Register the event-loop primitives into the global "hv" table: +// hv.setTimeout / hv.setInterval / hv.clearTimer / hv.sleep / hv.resolveDns / +// hv.tcpClient (alias hv.connect) / hv.tcpServer (alias hv.listen) / +// hv.udpClient / hv.udpServer / hv.run / hv.stop +// These operate on the current thread's event loop. +void hvlua_open_event(lua_State* L) { + register_conn(L); + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_setfuncs(L, hloop_funcs, 0); + lua_setglobal(L, "hv"); +} diff --git a/lua/hvlua_http.cpp b/lua/hvlua_http.cpp new file mode 100644 index 000000000..6c0886e28 --- /dev/null +++ b/lua/hvlua_http.cpp @@ -0,0 +1,170 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" + +#ifdef HVLUA_WITH_HTTP + +#include + +#include "hlog.h" +#include "EventLoop.h" +#include "AsyncHttpClient.h" + +using namespace hv; + +// hv.http.get/post/request(...) -> { status=, body=, headers={} } | nil, err +// +// Coroutine-synchronous HTTP client. Single-loop model: the AsyncHttpClient is +// bound to the CURRENT loop (currentThreadEventLoopPtr), so its completion +// callback fires on this same loop thread — we resume the coroutine directly, +// no cross-thread hop, no data copy. One client per lua_State (lazily created, +// owned by a registry userdata with __gc). + +static const char* HTTP_CLIENT_REG = "hv.http.client"; + +struct LuaHttpClientBox { + AsyncHttpClient* client; +}; + +static int http_client_gc(lua_State* L) { + LuaHttpClientBox* box = (LuaHttpClientBox*)lua_touserdata(L, 1); + if (box && box->client) { + delete box->client; // external loop (not owner): does NOT stop the shared loop + box->client = NULL; + } + return 0; +} + +// Get (lazily create) the per-lua_State AsyncHttpClient bound to the current loop. +// Returns NULL if there is no shared current loop. +static AsyncHttpClient* get_http_client(lua_State* L) { + lua_getfield(L, LUA_REGISTRYINDEX, HTTP_CLIENT_REG); + if (lua_isuserdata(L, -1)) { + LuaHttpClientBox* box = (LuaHttpClientBox*)lua_touserdata(L, -1); + lua_pop(L, 1); + return box->client; + } + lua_pop(L, 1); + + EventLoopPtr loop = currentThreadEventLoopPtr; + if (!loop) return NULL; + + LuaHttpClientBox* box = (LuaHttpClientBox*)lua_newuserdata(L, sizeof(LuaHttpClientBox)); + box->client = new AsyncHttpClient(loop); // bound to current loop, not owner + if (luaL_newmetatable(L, "hv.http.client.mt")) { + lua_pushcfunction(L, http_client_gc); + lua_setfield(L, -2, "__gc"); + } + lua_setmetatable(L, -2); + lua_setfield(L, LUA_REGISTRYINDEX, HTTP_CLIENT_REG); + return box->client; +} + +// Push { status, body, headers } for a response onto L. +static void push_response(lua_State* L, const HttpResponsePtr& resp) { + lua_createtable(L, 0, 3); + lua_pushinteger(L, resp->status_code); + lua_setfield(L, -2, "status"); + lua_pushlstring(L, resp->body.data(), resp->body.size()); + lua_setfield(L, -2, "body"); + lua_createtable(L, 0, (int)resp->headers.size()); + for (auto& kv : resp->headers) { + lua_pushlstring(L, kv.second.data(), kv.second.size()); + lua_setfield(L, -2, kv.first.c_str()); + } + lua_setfield(L, -2, "headers"); +} + +static int http_result_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +static int do_request(lua_State* L, http_method method, int url_index) { + const char* url = luaL_checkstring(L, url_index); + AsyncHttpClient* client = get_http_client(L); + if (client == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.http: no shared event loop on this thread"); + return 2; + } + + // NOTE: lua_yieldk never returns to this C++ frame (it longjmps in a C-built + // Lua), so C++ destructors of locals in this frame are SKIPPED. Any + // non-trivial local (here the shared_ptr) must therefore be + // scoped to end BEFORE the yield, or its destructor is leaked. client->send + // holds its own ref to req, so dropping our local ref here is fine. + HvLuaCoroutine* co = hvlua_suspend(L); + { + auto req = std::make_shared(); + req->method = method; + req->url = url; + // optional body + if (!lua_isnoneornil(L, url_index + 1)) { + size_t len = 0; + const char* body = lua_tolstring(L, url_index + 1, &len); + if (body) req->body.assign(body, len); + } + // optional headers table + if (lua_istable(L, url_index + 2)) { + lua_pushnil(L); + while (lua_next(L, url_index + 2) != 0) { + if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TSTRING) { + req->headers[lua_tostring(L, -2)] = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + } + client->send(req, [co](const HttpResponsePtr& resp) { + // Same loop thread (client bound to current loop): resume directly. + lua_State* cur = hvlua_coroutine_state(co); + if (cur == NULL) { hvlua_cancel(co); return; } // coroutine gone + if (resp) { + push_response(cur, resp); + hvlua_resume(co, 1); + } else { + lua_pushnil(cur); + lua_pushstring(cur, "hv.http: request failed"); + hvlua_resume(co, 2); + } + }); + } // ~req runs here, before the yield below + return lua_yieldk(L, 0, (lua_KContext)0, http_result_k); +} + +static int l_http_get(lua_State* L) { return do_request(L, HTTP_GET, 1); } +static int l_http_post(lua_State* L) { return do_request(L, HTTP_POST, 1); } +static int l_http_put(lua_State* L) { return do_request(L, HTTP_PUT, 1); } +static int l_http_delete(lua_State* L) { return do_request(L, HTTP_DELETE, 1); } + +// hv.http.request("GET", url, [body], [headers]) +static int l_http_request(lua_State* L) { + const char* m = luaL_checkstring(L, 1); + return do_request(L, http_method_enum(m), 2); +} + +static const luaL_Reg http_funcs[] = { + { "request", l_http_request }, + { "get", l_http_get }, + { "post", l_http_post }, + { "put", l_http_put }, + { "delete", l_http_delete }, + { NULL, NULL } +}; + +extern "C" void hvlua_open_http(lua_State* L) { + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, http_funcs); + lua_setfield(L, -2, "http"); + lua_setglobal(L, "hv"); +} + +#endif // HVLUA_WITH_HTTP diff --git a/lua/hvlua_json.cpp b/lua/hvlua_json.cpp new file mode 100644 index 000000000..73de87ae4 --- /dev/null +++ b/lua/hvlua_json.cpp @@ -0,0 +1,205 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" +#include "hvlua_json.h" // shared lua<->json conversion (also used by HttpLuaHandler) + +#include + +#include "hstring.h" + +using nlohmann::json; + +// The lua <-> json conversion is defined here (non-static, in namespace hv) as +// the single shared implementation; http/server/HttpLuaHandler.cpp reuses it via +// hvlua_json.h. Only hvlua_open_json is exported with C linkage for hvlua.c. + +namespace hv { + +json hvlua_lua_to_json(lua_State* L, int index, int depth); + +static json lua_table_to_json(lua_State* L, int index, int depth) { + index = lua_absindex(L, index); + bool is_array = true; + lua_Integer max_index = 0; + size_t count = 0; + + lua_pushnil(L); + while (lua_next(L, index) != 0) { + ++count; + if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) { + lua_Integer k = lua_tointeger(L, -2); + if (k <= 0) is_array = false; + else if (k > max_index) max_index = k; + } else { + is_array = false; + } + lua_pop(L, 1); + } + + if (is_array && (lua_Integer)count == max_index) { + json j = json::array(); + for (lua_Integer i = 1; i <= max_index; ++i) { + lua_geti(L, index, i); + j.push_back(hvlua_lua_to_json(L, -1, depth + 1)); + lua_pop(L, 1); + } + return j; + } + + json j = json::object(); + lua_pushnil(L); + while (lua_next(L, index) != 0) { + std::string key; + if (lua_type(L, -2) == LUA_TSTRING) { + size_t len = 0; + const char* s = lua_tolstring(L, -2, &len); + key.assign(s, len); + } else if (lua_type(L, -2) == LUA_TNUMBER) { + key = hv::to_string((int64_t)lua_tointeger(L, -2)); + } + if (!key.empty()) j[key] = hvlua_lua_to_json(L, -1, depth + 1); + lua_pop(L, 1); + } + return j; +} + +json hvlua_lua_to_json(lua_State* L, int index, int depth) { + switch (lua_type(L, index)) { + case LUA_TNIL: return nullptr; + case LUA_TBOOLEAN: return lua_toboolean(L, index) != 0; + case LUA_TNUMBER: + if (lua_isinteger(L, index)) return (int64_t)lua_tointeger(L, index); + return lua_tonumber(L, index); + case LUA_TSTRING: { + size_t len = 0; + const char* s = lua_tolstring(L, index, &len); + return std::string(s, len); + } + case LUA_TTABLE: + // Guard against cyclic / pathologically deep tables. Two limits: + // (1) a depth cap so a self-referential table (t.self=t) can't recurse + // forever; (2) lua_checkstack, because each level uses Lua stack + // slots (lua_next / lua_geti) and Lua only guarantees LUA_MINSTACK — + // deep nesting without reserving would overflow the value stack and + // corrupt Lua's table internals (crash in luaH_*/getgeneric). + if (depth >= HVLUA_JSON_MAX_DEPTH) return nullptr; + if (!lua_checkstack(L, 4)) return nullptr; + return lua_table_to_json(L, index, depth); + default: return nullptr; + } +} + +static bool json_to_lua(lua_State* L, const json& j, int depth) { + switch (j.type()) { + case json::value_t::null: + lua_pushnil(L); + return true; + case json::value_t::boolean: + lua_pushboolean(L, j.get()); + return true; + case json::value_t::number_integer: + lua_pushinteger(L, (lua_Integer)j.get()); + return true; + case json::value_t::number_unsigned: + lua_pushinteger(L, (lua_Integer)j.get()); + return true; + case json::value_t::number_float: + lua_pushnumber(L, j.get()); + return true; + case json::value_t::string: { + const std::string& s = j.get_ref(); + lua_pushlstring(L, s.data(), s.size()); + return true; + } + case json::value_t::array: { + if (depth >= HVLUA_JSON_MAX_DEPTH || !lua_checkstack(L, 4)) return false; + lua_createtable(L, (int)j.size(), 0); + int i = 1; + for (const auto& item : j) { + if (!json_to_lua(L, item, depth + 1)) return false; + lua_seti(L, -2, i++); + } + return true; + } + case json::value_t::object: { + if (depth >= HVLUA_JSON_MAX_DEPTH || !lua_checkstack(L, 4)) return false; + lua_createtable(L, 0, (int)j.size()); + for (auto it = j.begin(); it != j.end(); ++it) { + lua_pushlstring(L, it.key().data(), it.key().size()); + if (!json_to_lua(L, it.value(), depth + 1)) return false; + lua_settable(L, -3); + } + return true; + } + default: + lua_pushnil(L); + return true; + } +} + +bool hvlua_json_to_lua(lua_State* L, const json& j, int depth) { + int top = lua_gettop(L); + if (json_to_lua(L, j, depth)) return true; + lua_settop(L, top); + return false; +} + +} // namespace hv + +// hv.json.encode(value) -> string | nil, err +static int l_hv_json_encode(lua_State* L) { + json j = hv::hvlua_lua_to_json(L, 1, 0); + // Lua strings are arbitrary byte strings; nlohmann throws type_error.316 on + // invalid UTF-8. Catch it (and any other dump error) and return (nil, err) + // instead of letting the exception abort the process — this path is + // reachable from untrusted input (e.g. an HTTP handler doing ctx:json). + try { + std::string s = j.dump(); + lua_pushlstring(L, s.data(), s.size()); + return 1; + } catch (const std::exception& e) { + lua_pushnil(L); + lua_pushstring(L, e.what()); + return 2; + } +} + +// hv.json.decode(string) -> value | nil,err +static int l_hv_json_decode(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + json j = json::parse(s, s + len, nullptr, false); + if (j.is_discarded()) { + lua_pushnil(L); + lua_pushstring(L, "json parse error"); + return 2; + } + if (!hv::hvlua_json_to_lua(L, j)) { + lua_pushnil(L); + lua_pushstring(L, "json too deep"); + return 2; + } + return 1; +} + +static const luaL_Reg hv_json_funcs[] = { + { "encode", l_hv_json_encode }, + { "decode", l_hv_json_decode }, + { NULL, NULL } +}; + +// Add the hv.json subtable to the (already created) global "hv" table. +extern "C" void hvlua_open_json(lua_State* L) { + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, hv_json_funcs); + lua_setfield(L, -2, "json"); + lua_setglobal(L, "hv"); +} diff --git a/lua/hvlua_json.h b/lua/hvlua_json.h new file mode 100644 index 000000000..3a3d491d4 --- /dev/null +++ b/lua/hvlua_json.h @@ -0,0 +1,34 @@ +#ifndef HV_LUA_JSON_H_ +#define HV_LUA_JSON_H_ + +// Shared lua <-> nlohmann::json conversion, used by both the hv.json binding +// (lua/hvlua_json.cpp) and the HTTP Lua handler (http/server/HttpLuaHandler.cpp) +// so the conversion (and its safety guards: recursion-depth cap for cyclic +// tables) lives in ONE place. +// +// C++ only (pulls in json.hpp). The pure-C lua files (hvlua.c / hvlua_event.c / +// hvlua_base.c) must not include this. + +#include "json.hpp" // nlohmann::json + +struct lua_State; + +namespace hv { + +// Max lua -> json nesting depth. A self-referential table (t.self=t) or +// pathologically deep nesting would otherwise recurse until the C stack +// overflows (SIGSEGV); conversion stops descending past this depth. +#define HVLUA_JSON_MAX_DEPTH 64 + +// Convert the Lua value at stack `index` to json. `depth` is the current +// nesting level (callers pass 0); tables deeper than HVLUA_JSON_MAX_DEPTH are +// converted to null instead of recursing. +nlohmann::json hvlua_lua_to_json(lua_State* L, int index, int depth = 0); + +// Push `j` onto the Lua stack as the corresponding Lua value. Returns false +// without changing the stack if nesting exceeds the limit or stack growth fails. +bool hvlua_json_to_lua(lua_State* L, const nlohmann::json& j, int depth = 0); + +} // namespace hv + +#endif // HV_LUA_JSON_H_ diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp new file mode 100644 index 000000000..33078ae18 --- /dev/null +++ b/lua/hvlua_mqtt.cpp @@ -0,0 +1,380 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" +#include "hvlua_util.h" // hvlua_parse_reconnect + +#ifdef HVLUA_WITH_MQTT + +#include +#include +#include + +#include "EventLoop.h" +#include "mqtt_client.h" + +using namespace hv; + +// hv.mqtt — coroutine-synchronous MQTT client. Single-loop model (mirrors +// hvlua_ws.cpp): the mqtt_client_t is created on the CURRENT loop's hloop_t, so +// its callbacks fire on this same loop thread and resume the coroutine directly. +// +// NOTE: this binds the raw C API (mqtt_client_t) rather than the C++ MqttClient +// wrapper on purpose. MqttClient::run() is what installs the dispatch callback, +// but run() also calls hloop_run() (blocks) — we must NOT run the shared loop +// here. So we install our own mqtt_client_cb via mqtt_client_set_callback and +// drive connect/publish/subscribe on the already-running shared loop. +// +// MQTT is message-DRIVEN (broker pushes PUBLISH anytime), so like hv.ws we +// buffer inbound messages and expose a coroutine-synchronous recv(): +// local m, err = hv.mqtt.connect({ host=, port=, id=, username=, password=, +// keepalive=, clean_session=, ssl= }) +// m:subscribe("topic", 1) +// m:publish("topic", "payload", 1) +// local msg = m:recv() -- { topic=, payload=, qos= } ; suspends until a msg +// m:disconnect() + +static const char* MQTT_CLIENT_MT = "hv.mqtt.client.mt"; + +struct MqttInboxItem { + std::string topic; + std::string payload; + int qos; +}; +typedef std::deque MqttInbox; + +struct LuaMqttClient { + mqtt_client_t* client; + MqttInbox inbox; + HvLuaCoroutine* wait_co; // coroutine waiting in connect() or recv(), or NULL + bool closed; + bool connecting; // wait_co holds a connect() waiter (vs recv()) + bool reconnect; // auto-reconnect enabled +}; + +// Push an inbox item as a Lua table { topic=, payload=, qos= }. +static void mqtt_push_msg(lua_State* L, const MqttInboxItem& item) { + lua_createtable(L, 0, 3); + lua_pushlstring(L, item.topic.data(), item.topic.size()); + lua_setfield(L, -2, "topic"); + lua_pushlstring(L, item.payload.data(), item.payload.size()); + lua_setfield(L, -2, "payload"); + lua_pushinteger(L, item.qos); + lua_setfield(L, -2, "qos"); +} + +// Wake a pending recv() with the front queued message, or an error when the +// connection is gone: (nil,"reconnecting") if auto-reconnect is on (transient) +// else (nil,"closed") (terminal). No-op for a connect() waiter. +static void mqtt_try_deliver(LuaMqttClient* box) { + if (box->wait_co == NULL || box->connecting) return; + lua_State* co = hvlua_coroutine_state(box->wait_co); + if (co == NULL) { hvlua_cancel(box->wait_co); box->wait_co = NULL; return; } + if (!box->inbox.empty()) { + MqttInboxItem item = std::move(box->inbox.front()); + box->inbox.pop_front(); + HvLuaCoroutine* tok = box->wait_co; + box->wait_co = NULL; + mqtt_push_msg(co, item); + hvlua_resume(tok, 1); + } else if (box->closed) { + HvLuaCoroutine* tok = box->wait_co; + box->wait_co = NULL; + lua_pushnil(co); + lua_pushstring(co, box->reconnect ? "reconnecting" : "closed"); + hvlua_resume(tok, 2); + } +} + +// The single mqtt_client_cb: dispatched by type. Installed via +// mqtt_client_set_callback; the LuaMqttClient box is the client userdata. +static void on_mqtt(mqtt_client_t* cli, int type) { + LuaMqttClient* box = (LuaMqttClient*)mqtt_client_get_userdata(cli); + if (box == NULL) return; + switch (type) { + case MQTT_TYPE_CONNACK: + box->closed = false; // reset for a (re)established session + if (box->wait_co && box->connecting) { + lua_State* co = hvlua_coroutine_state(box->wait_co); + if (co == NULL) { hvlua_cancel(box->wait_co); box->wait_co = NULL; return; } + HvLuaCoroutine* tok = box->wait_co; + box->wait_co = NULL; + box->connecting = false; + lua_pushboolean(co, 1); // success marker for mqtt_connect_k + hvlua_resume(tok, 1); + } + break; + case MQTT_TYPE_PUBLISH: { + MqttInboxItem item; + item.topic.assign(cli->message.topic, cli->message.topic_len); + item.payload.assign(cli->message.payload, cli->message.payload_len); + item.qos = cli->message.qos; + box->inbox.push_back(std::move(item)); + mqtt_try_deliver(box); + break; + } + case MQTT_TYPE_DISCONNECT: + box->closed = true; + if (box->wait_co) { + lua_State* co = hvlua_coroutine_state(box->wait_co); + if (co == NULL) { hvlua_cancel(box->wait_co); box->wait_co = NULL; return; } + HvLuaCoroutine* tok = box->wait_co; + bool was_connecting = box->connecting; + box->wait_co = NULL; + box->connecting = false; + lua_pushnil(co); + // connect() waiter -> connect failed; recv() waiter -> transient + // "reconnecting" if auto-reconnect is on (the C client retries under + // the hood and a future CONNACK/PUBLISH wakes fresh recvs), else + // terminal "closed". + lua_pushstring(co, was_connecting ? "connect failed" + : (box->reconnect ? "reconnecting" : "closed")); + hvlua_resume(tok, 2); + } + break; + default: + break; + } +} + +static int mqtt_client_gc(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + if (box) { + if (box->wait_co) { + hvlua_cancel(box->wait_co); + box->wait_co = NULL; + } + if (box->client) { + // detach userdata so a late callback can't touch the freed box, then + // free the client. mqtt_client_free does NOT stop the shared loop. + mqtt_client_set_userdata(box->client, NULL); + mqtt_client_free(box->client); + box->client = NULL; + } + box->inbox.~MqttInbox(); + } + return 0; +} + +// Continuation for connect: (m) on success, or (nil,err) already on stack. +static int mqtt_connect_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) { + lua_pop(L, 1); + lua_pushvalue(L, 1); // return the mqtt userdata (self) + return 1; + } + return 2; // (nil, err) +} + +// hv.mqtt.connect(cfg) -> client | nil, err +static int l_mqtt_connect(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + // NOTE: lua_yieldk at the end never returns to this C++ frame (longjmp in a + // C-built Lua), so destructors of non-trivial locals here are SKIPPED and + // leak. All non-trivial locals (the EventLoopPtr and the std::strings from + // the config) are therefore confined to the scope below, which ends BEFORE + // hvlua_suspend/lua_yieldk. Only POD state crosses the yield. + LuaMqttClient* box = NULL; + char host[256] = "127.0.0.1"; + int port = DEFAULT_MQTT_PORT; + int ssl = 0; + { + EventLoopPtr loop = currentThreadEventLoopPtr; + if (!loop) { + lua_pushnil(L); + lua_pushstring(L, "hv.mqtt: no shared event loop on this thread"); + return 2; + } + + std::string id, username, password; + int keepalive = 0, clean_session = -1; + lua_getfield(L, 1, "host"); if (lua_isstring(L, -1)) { strncpy(host, lua_tostring(L, -1), sizeof(host) - 1); host[sizeof(host) - 1] = '\0'; } lua_pop(L, 1); + lua_getfield(L, 1, "port"); if (lua_isinteger(L, -1)) port = (int)lua_tointeger(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "id"); if (lua_isstring(L, -1)) id = lua_tostring(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "username"); if (lua_isstring(L, -1)) username = lua_tostring(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "password"); if (lua_isstring(L, -1)) password = lua_tostring(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "keepalive");if (lua_isinteger(L, -1)) keepalive = (int)lua_tointeger(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "ssl"); if (lua_isboolean(L, -1)) ssl = lua_toboolean(L, -1); lua_pop(L, 1); + lua_getfield(L, 1, "clean_session"); if (lua_isboolean(L, -1)) clean_session = lua_toboolean(L, -1); lua_pop(L, 1); + + box = (LuaMqttClient*)lua_newuserdata(L, sizeof(LuaMqttClient)); + new (&box->inbox) MqttInbox(); + box->wait_co = NULL; + box->closed = false; + box->connecting = true; + box->reconnect = false; + box->client = mqtt_client_new(loop->loop()); // bound to current loop's hloop + if (box->client == NULL) { + box->inbox.~MqttInbox(); + lua_pushnil(L); + lua_pushstring(L, "hv.mqtt: create client failed"); + return 2; + } + luaL_setmetatable(L, MQTT_CLIENT_MT); + lua_replace(L, 1); // move userdata to slot 1 for mqtt_connect_k + + mqtt_client_set_userdata(box->client, box); + mqtt_client_set_callback(box->client, on_mqtt); + if (!id.empty()) mqtt_client_set_id(box->client, id.c_str()); + if (!username.empty() || !password.empty()) { + mqtt_client_set_auth(box->client, username.c_str(), password.c_str()); + } + if (keepalive > 0) box->client->keepalive = (unsigned short)keepalive; + if (clean_session >= 0) box->client->clean_session = clean_session ? 1 : 0; + // optional reconnect = { min_delay, max_delay, delay_policy, max_retry } + reconn_setting_t reconn; + if (hvlua_parse_reconnect(L, 1, &reconn)) { + mqtt_client_set_reconnect(box->client, &reconn); + box->reconnect = true; + } + } // ~loop / ~id / ~username / ~password run here, before the yield + + box->wait_co = hvlua_suspend(L); + int ret = mqtt_client_connect(box->client, host, port, ssl); + if (ret != 0) { + hvlua_cancel(box->wait_co); + box->wait_co = NULL; + box->connecting = false; + lua_pushnil(L); + lua_pushfstring(L, "hv.mqtt: connect failed (%d)", ret); + return 2; + } + return lua_yieldk(L, 0, (lua_KContext)0, mqtt_connect_k); +} + +static int mqtt_recv_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +static int mqtt_push_closed(lua_State* L, const LuaMqttClient* box) { + lua_pushnil(L); + lua_pushstring(L, box && box->reconnect ? "reconnecting" : "closed"); + return 2; +} + +// m:recv() -> { topic=, payload=, qos= } | nil, err (coroutine-synchronous) +static int l_mqtt_recv(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + if (box == NULL || box->client == NULL) { + return mqtt_push_closed(L, box); + } + if (box->wait_co != NULL) { + lua_pushnil(L); lua_pushstring(L, "hv.mqtt: recv already pending"); return 2; + } + if (!box->inbox.empty()) { + MqttInboxItem item = std::move(box->inbox.front()); + box->inbox.pop_front(); + mqtt_push_msg(L, item); + return 1; + } + if (box->closed) { + return mqtt_push_closed(L, box); + } + box->connecting = false; + box->wait_co = hvlua_suspend(L); + return lua_yieldk(L, 0, (lua_KContext)0, mqtt_recv_k); +} + +// m:publish(topic, payload [, qos [, retain]]) -> mid | nil, err +static int l_mqtt_publish(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + size_t tlen = 0, plen = 0; + const char* topic = luaL_checklstring(L, 2, &tlen); + const char* payload = luaL_checklstring(L, 3, &plen); + int qos = (int)luaL_optinteger(L, 4, 0); + int retain = (int)luaL_optinteger(L, 5, 0); + if (box == NULL || box->client == NULL || box->closed) { + return mqtt_push_closed(L, box); + } + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.topic = topic; msg.topic_len = (unsigned int)tlen; + msg.payload = payload; msg.payload_len = (unsigned int)plen; + msg.qos = (unsigned char)qos; + msg.retain = (unsigned char)retain; + int mid = mqtt_client_publish(box->client, &msg); + if (mid < 0) { + lua_pushnil(L); lua_pushstring(L, "hv.mqtt: publish failed"); return 2; + } + lua_pushinteger(L, mid); + return 1; +} + +// m:subscribe(topic [, qos]) -> mid | nil, err +static int l_mqtt_subscribe(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + const char* topic = luaL_checkstring(L, 2); + int qos = (int)luaL_optinteger(L, 3, 0); + if (box == NULL || box->client == NULL || box->closed) { + return mqtt_push_closed(L, box); + } + int mid = mqtt_client_subscribe(box->client, topic, qos); + if (mid < 0) { + lua_pushnil(L); lua_pushstring(L, "hv.mqtt: subscribe failed"); return 2; + } + lua_pushinteger(L, mid); + return 1; +} + +// m:unsubscribe(topic) -> mid | nil, err +static int l_mqtt_unsubscribe(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + const char* topic = luaL_checkstring(L, 2); + if (box == NULL || box->client == NULL || box->closed) { + return mqtt_push_closed(L, box); + } + int mid = mqtt_client_unsubscribe(box->client, topic); + if (mid < 0) { + lua_pushnil(L); lua_pushstring(L, "hv.mqtt: unsubscribe failed"); return 2; + } + lua_pushinteger(L, mid); + return 1; +} + +// m:disconnect() +static int l_mqtt_disconnect(lua_State* L) { + LuaMqttClient* box = (LuaMqttClient*)luaL_checkudata(L, 1, MQTT_CLIENT_MT); + if (box && box->client && !box->closed) { + // Explicit disconnect is terminal: mqtt_client_disconnect also cancels + // the underlying reconnect; clear our flag so recv() reports "closed" + // (not "reconnecting"). + box->reconnect = false; + mqtt_client_disconnect(box->client); + } + return 0; +} + +static const luaL_Reg mqtt_methods[] = { + { "recv", l_mqtt_recv }, + { "publish", l_mqtt_publish }, + { "subscribe", l_mqtt_subscribe }, + { "unsubscribe", l_mqtt_unsubscribe }, + { "disconnect", l_mqtt_disconnect }, + { NULL, NULL } +}; + +static const luaL_Reg mqtt_funcs[] = { + { "connect", l_mqtt_connect }, + { NULL, NULL } +}; + +extern "C" void hvlua_open_mqtt(lua_State* L) { + hvlua_new_class(L, MQTT_CLIENT_MT, mqtt_client_gc, mqtt_methods); + lua_pop(L, 1); + + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, mqtt_funcs); + lua_setfield(L, -2, "mqtt"); + lua_setglobal(L, "hv"); +} + +#endif // HVLUA_WITH_MQTT diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp new file mode 100644 index 000000000..e8e9d41a7 --- /dev/null +++ b/lua/hvlua_redis.cpp @@ -0,0 +1,339 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" +#include "hvlua_util.h" // hvlua_new_class + +#ifdef HVLUA_WITH_REDIS + +#include +#include +#include +#include + +#include "EventLoop.h" +#include "AsyncRedisClient.h" + +using namespace hv; + +// hv.redis — coroutine-synchronous Redis client. Single-loop model (mirrors +// hvlua_http.cpp): the AsyncRedisClient is bound to the CURRENT loop +// (currentThreadEventLoopPtr), so its command completion callback fires on this +// same loop thread — we resume the coroutine directly, no cross-thread hop. +// +// API (see docs/superpowers/specs/2026-07-28-lua-binding-design.md §4.6): +// local r = hv.redis.new({ host=, port=, auth=, db=, timeout= }) +// local v, err = r:command("GET", "k") -- variadic args +// local v, err = r:command({"SET","k","v"}) -- or a single array table +// r:get(k) / r:set(k,v) / r:del(k) ... -- thin command() sugar +// Reply -> Lua value mapping (see reply_push): string/int/nil/array table; +// redis error reply -> (nil, "err message"). Transport failure -> (nil, err). + +static const char* REDIS_CLIENT_MT = "hv.redis.client.mt"; + +struct LuaRedisClient { + AsyncRedisClient* client; + // Set true by __gc before deleting the client. An in-flight command's + // completion callback checks this: destroying the client runs + // ~AsyncRedisClient -> stop(true) -> failPending on THIS (loop) thread, which + // fires the pending callbacks synchronously from inside __gc. Resuming a + // coroutine (lua_resume) from within a __gc metamethod is illegal, so when + // destroyed we only release the suspend token (hvlua_cancel is __gc-safe: it + // does luaL_unref + free, no lua_resume) and skip the resume. + bool destroyed; +}; + +static int redis_client_gc(lua_State* L) { + LuaRedisClient* box = (LuaRedisClient*)luaL_checkudata(L, 1, REDIS_CLIENT_MT); + if (box && box->client) { + box->destroyed = true; // neutralize in-flight callbacks before teardown + delete box->client; // external loop (not owner): does NOT stop the shared loop + box->client = NULL; + } + return 0; +} + +// Push a RedisReply onto L as a native Lua value. +// STRING -> string ; INTEGER -> integer ; NIL -> nil ; +// ERROR -> (handled by caller as nil,err) ; ARRAY -> table (1-based), +// with nested nil elements represented as `false` (Lua arrays cannot hold nil). +static void reply_push(lua_State* L, const RedisReply& reply) { + switch (reply.type) { + case REDIS_REPLY_STRING: + lua_pushlstring(L, reply.str.data(), reply.str.size()); + break; + case REDIS_REPLY_INTEGER: + lua_pushinteger(L, (lua_Integer)reply.integer); + break; + case REDIS_REPLY_ARRAY: { + if (reply.null_array) { + lua_pushnil(L); + break; + } + lua_createtable(L, (int)reply.elements.size(), 0); + for (size_t i = 0; i < reply.elements.size(); ++i) { + const RedisReply& e = reply.elements[i]; + if (e.isNil()) { + lua_pushboolean(L, 0); // nil placeholder (keeps array contiguous) + } else { + reply_push(L, e); + } + lua_rawseti(L, -2, (int)(i + 1)); + } + break; + } + case REDIS_REPLY_NIL: + default: + lua_pushnil(L); + break; + } +} + +// hv.redis.new([cfg]) -> client userdata | nil, err +static int l_redis_new(lua_State* L) { + EventLoopPtr loop = currentThreadEventLoopPtr; + if (!loop) { + lua_pushnil(L); + lua_pushstring(L, "hv.redis: no shared event loop on this thread"); + return 2; + } + + std::string host = "127.0.0.1"; + int port = 6379; + std::string auth; + int db = 0; + int timeout = 0; + bool has_timeout = false; + if (lua_istable(L, 1)) { + lua_getfield(L, 1, "host"); + if (lua_isstring(L, -1)) host = lua_tostring(L, -1); + lua_pop(L, 1); + lua_getfield(L, 1, "port"); + if (lua_isinteger(L, -1)) port = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, 1, "auth"); + if (lua_isstring(L, -1)) auth = lua_tostring(L, -1); + lua_pop(L, 1); + lua_getfield(L, 1, "db"); + if (lua_isinteger(L, -1)) db = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, 1, "timeout"); + if (lua_isinteger(L, -1)) { timeout = (int)lua_tointeger(L, -1); has_timeout = true; } + lua_pop(L, 1); + } + + LuaRedisClient* box = (LuaRedisClient*)lua_newuserdata(L, sizeof(LuaRedisClient)); + box->destroyed = false; + box->client = new AsyncRedisClient(loop); // bound to current loop, not owner + box->client->setHost(host); + box->client->setPort(port); + if (!auth.empty()) box->client->setAuth(auth); + if (db > 0) box->client->setDb(db); + if (has_timeout) box->client->setTimeout(timeout); + // The shared metatable (with __gc + methods) is created once in + // hvlua_open_redis; just attach it here. + luaL_setmetatable(L, REDIS_CLIENT_MT); + // start the client now so the connection is established up front (the first + // command would otherwise trigger startConnect lazily). + box->client->start(false); + return 1; +} + +// Continuation for a command: (value) on success, or (nil, err) already on stack. +static int redis_cmd_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +// Build a RedisCommand from Lua args starting at `first`. Either a single array +// table {"GET","k"} or a variadic list "GET","k". Numbers are stringified. +static bool build_command(lua_State* L, int first, RedisCommand* cmd) { + if (lua_istable(L, first) && lua_gettop(L) == first) { + int n = (int)lua_rawlen(L, first); + for (int i = 1; i <= n; ++i) { + lua_rawgeti(L, first, i); + size_t len = 0; + const char* s = lua_tolstring(L, -1, &len); + if (s == NULL) { lua_pop(L, 1); return false; } + cmd->emplace_back(s, len); + lua_pop(L, 1); + } + } else { + int top = lua_gettop(L); + for (int i = first; i <= top; ++i) { + size_t len = 0; + const char* s = lua_tolstring(L, i, &len); + if (s == NULL) return false; + cmd->emplace_back(s, len); + } + } + return !cmd->empty(); +} + +// Tracks whether a command's completion callback fired synchronously (inline, +// before the coroutine yields) vs asynchronously (later, on the loop). +struct RedisCmdState { + HvLuaCoroutine* co; // suspend token (async path) + lua_State* L; // the running coroutine (sync path pushes here) + bool yielded; // set true once we know the coroutine yielded + bool done; // callback fired + int nresults; // results pushed (sync path) +}; + +// Push the (value) / (nil,err) results for a reply onto `st`'s coroutine stack. +// Returns the number of values pushed. +static int redis_push_result(lua_State* co, const RedisResult& result) { + if (result.code != 0) { + lua_pushnil(co); + lua_pushfstring(co, "hv.redis: request failed (%d)", result.code); + return 2; + } + if (result.reply.isError()) { + lua_pushnil(co); + lua_pushlstring(co, result.reply.str.data(), result.reply.str.size()); + return 2; + } + reply_push(co, result.reply); + return 1; +} + +// Start a command and return its synchronous result count, or 0 when the +// coroutine must yield. The caller owns `cmd` and destroys it before yielding. +static int redis_start_command(lua_State* L, const RedisCommand& cmd) { + LuaRedisClient* box = (LuaRedisClient*)luaL_checkudata(L, 1, REDIS_CLIENT_MT); + if (box == NULL || box->client == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.redis: client closed"); + return 2; + } + + // AsyncRedisClient::command() may invoke the callback SYNCHRONOUSLY (e.g. + // enqueue rejected because the loop is not running). If that happens after + // hvlua_suspend() but before lua_yieldk(), resuming the still-running + // coroutine is illegal. So track sync vs async completion and, on sync + // completion, return the results directly instead of yielding. + auto st = std::make_shared(); + st->co = hvlua_suspend(L); + st->L = L; + st->yielded = false; + st->done = false; + st->nresults = 0; + + box->client->command(cmd, [st, box](const RedisResult& result) { + // Client being destroyed (callback fires from ~AsyncRedisClient -> + // stop -> failPending inside the Lua __gc metamethod): never resume. + if (box->destroyed) { hvlua_cancel(st->co); st->co = NULL; return; } + if (!st->yielded) { + // Synchronous completion: coroutine hasn't yielded yet. Push + // results onto its stack; the caller returns them without yielding. + st->done = true; + st->nresults = redis_push_result(st->L, result); + hvlua_cancel(st->co); + st->co = NULL; + return; + } + // Async completion on the loop: resume the suspended coroutine. + lua_State* cur = hvlua_coroutine_state(st->co); + if (cur == NULL) { hvlua_cancel(st->co); st->co = NULL; return; } + int n = redis_push_result(cur, result); + HvLuaCoroutine* tok = st->co; + st->co = NULL; + hvlua_resume(tok, n); + }); + + int nresults = st->nresults; + if (!st->done) st->yielded = true; + return st->done ? nresults : 0; +} + +// r:command("GET","k") | r:command({"GET","k"}) +static int l_redis_command(lua_State* L) { + int nresults; + { + RedisCommand cmd; + if (!build_command(L, 2, &cmd)) { + lua_pushnil(L); + lua_pushstring(L, "hv.redis: empty or invalid command"); + return 2; + } + nresults = redis_start_command(L, cmd); + } + if (nresults != 0) return nresults; + return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); +} + +// Sugar: r:(args...) == r:command("", args...). The verb string is +// carried as an upvalue set when the method is registered (see redis_methods). +static int l_redis_verb(lua_State* L) { + const char* verb = lua_tostring(L, lua_upvalueindex(1)); + int nresults; + { + RedisCommand cmd; + cmd.emplace_back(verb); + int top = lua_gettop(L); + for (int i = 2; i <= top; ++i) { + size_t len = 0; + const char* s = lua_tolstring(L, i, &len); + if (s == NULL) { + lua_pushnil(L); + lua_pushstring(L, "hv.redis: invalid argument"); + return 2; + } + cmd.emplace_back(s, len); + } + nresults = redis_start_command(L, cmd); + } + if (nresults != 0) return nresults; + return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); +} + +// verb sugar methods, registered as closures carrying the uppercase verb. +static const char* const redis_verbs[] = { + "GET", "SET", "DEL", "INCR", "DECR", "EXPIRE", "EXISTS", NULL +}; + +static const luaL_Reg redis_methods[] = { + { "command", l_redis_command }, + { NULL, NULL } +}; + +static const luaL_Reg redis_funcs[] = { + { "new", l_redis_new }, + { NULL, NULL } +}; + +// Register redis verb sugar (get/set/... lowercase methods whose closure upvalue +// is the UPPER verb) into the metatable on top of L. The base redis_methods and +// __gc/__index are installed by hvlua_new_class. +static void register_redis_verbs(lua_State* L) { + for (int i = 0; redis_verbs[i] != NULL; ++i) { + // method name is the lowercase verb; closure upvalue is the UPPER verb. + std::string name(redis_verbs[i]); + for (char& c : name) c = (char)tolower((unsigned char)c); + lua_pushstring(L, redis_verbs[i]); + lua_pushcclosure(L, l_redis_verb, 1); + lua_setfield(L, -2, name.c_str()); + } +} + +extern "C" void hvlua_open_redis(lua_State* L) { + // Create the shared client metatable once: __gc + __index=self + methods. + if (hvlua_new_class(L, REDIS_CLIENT_MT, redis_client_gc, redis_methods)) { + register_redis_verbs(L); // command + verb sugar into the mt + } + lua_pop(L, 1); + + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, redis_funcs); + lua_setfield(L, -2, "redis"); + lua_setglobal(L, "hv"); +} + +#endif // HVLUA_WITH_REDIS diff --git a/lua/hvlua_util.c b/lua/hvlua_util.c new file mode 100644 index 000000000..808df3612 --- /dev/null +++ b/lua/hvlua_util.c @@ -0,0 +1,40 @@ +#include "hvlua_util.h" + +int hvlua_parse_reconnect(lua_State* L, int table_index, reconn_setting_t* out) { + if (!lua_istable(L, table_index)) return 0; + lua_getfield(L, table_index, "reconnect"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return 0; + } + reconn_setting_init(out); + lua_getfield(L, -1, "min_delay"); + if (lua_isinteger(L, -1)) out->min_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_delay"); + if (lua_isinteger(L, -1)) out->max_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "delay_policy"); + if (lua_isinteger(L, -1)) out->delay_policy = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_retry"); + if (lua_isinteger(L, -1)) out->max_retry_cnt = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + if (out->min_delay == 0) out->min_delay = 1; + if (out->max_delay < out->min_delay) out->max_delay = out->min_delay; + if (out->delay_policy > 1 && out->delay_policy > UINT32_MAX / out->min_delay) { + out->delay_policy = DEFAULT_RECONNECT_DELAY_POLICY; + } + lua_pop(L, 1); // pop the reconnect sub-table + return 1; +} + +int hvlua_new_class(lua_State* L, const char* mt_name, lua_CFunction gc, const luaL_Reg* methods) { + if (!luaL_newmetatable(L, mt_name)) return 0; // already registered; leaves mt on top + lua_pushcfunction(L, gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); // methods live on the metatable itself + if (methods) luaL_setfuncs(L, methods, 0); + return 1; +} diff --git a/lua/hvlua_util.h b/lua/hvlua_util.h new file mode 100644 index 000000000..eadc41a93 --- /dev/null +++ b/lua/hvlua_util.h @@ -0,0 +1,38 @@ +#ifndef HV_LUA_UTIL_H_ +#define HV_LUA_UTIL_H_ + +// Small shared helpers for the lua bindings. Pure C (C-includable) so both the +// C bindings (hvlua_event.c) and the C++ bindings (hvlua_ws.cpp / hvlua_mqtt.cpp) +// can use them. Keep C++-only helpers (json <-> lua) out of here — those live in +// hvlua_json.h. + +#include "hloop.h" // reconn_setting_t + +#include // lua_State, lua_CFunction +#include // luaL_Reg + +#ifdef __cplusplus +extern "C" { +#endif + +// Parse an optional reconnect config sub-table from the Lua table at +// `table_index`: +// reconnect = { min_delay=, max_delay=, delay_policy=, max_retry= } +// On success fills *out (starting from reconn_setting_init defaults, only the +// provided fields overridden) and returns 1. Returns 0 if there is no +// `reconnect` sub-table (out is left untouched). Used by hv.ws / hv.mqtt so the +// reconnect parsing lives in one place. +int hvlua_parse_reconnect(lua_State* L, int table_index, reconn_setting_t* out); + +// Register (once) a client metatable named `mt_name` with: +// __gc = gc, __index = the metatable itself, plus `methods` on it. +// Leaves the metatable on top of L (matching luaL_newmetatable) so callers may +// append extra entries. Returns 1 if it was newly created (caller should add +// its own extras), 0 if it already existed. Pair with lua_pop(L, 1) after. +int hvlua_new_class(lua_State* L, const char* mt_name, lua_CFunction gc, const luaL_Reg* methods); + +#ifdef __cplusplus +} +#endif + +#endif // HV_LUA_UTIL_H_ diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp new file mode 100644 index 000000000..dbeae395f --- /dev/null +++ b/lua/hvlua_ws.cpp @@ -0,0 +1,303 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" +#include "hvlua_util.h" // hvlua_parse_reconnect + +#ifdef HVLUA_WITH_HTTP + +#include +#include +#include + +#include "EventLoop.h" +#include "WebSocketClient.h" + +using namespace hv; + +// hv.ws — coroutine-synchronous WebSocket client. Single-loop model (mirrors +// hvlua_http.cpp / hvlua_redis.cpp): the WebSocketClient is bound to the CURRENT +// loop, so onopen/onmessage/onclose fire on this same loop thread and resume the +// coroutine directly, no cross-thread hop. +// +// WebSocket is message-DRIVEN (the peer may push at any time), unlike the +// request/response clients. So instead of a per-call callback we buffer inbound +// messages in a queue and expose a coroutine-synchronous ws:recv(): +// local ws, err = hv.ws.connect("ws://127.0.0.1:8888/path") +// ws:send("hello") +// local msg, err = ws:recv() -- suspends until a message arrives / closed +// ws:close() +// +// recv() returns a buffered message immediately if one is queued; otherwise it +// suspends the coroutine until onmessage (resume with msg) or onclose (resume +// with nil,"closed"). Only one recv() may be pending at a time. + +static const char* WS_CLIENT_MT = "hv.ws.client.mt"; + +typedef std::deque WsInbox; + +struct LuaWsClient { + WebSocketClient* client; + WsInbox inbox; // buffered inbound messages + HvLuaCoroutine* recv_co; // coroutine waiting in recv(), or NULL + bool connected; // handshake completed (reset on close) + bool reconnect; // auto-reconnect enabled + bool opened_once; // onopen has fired at least once +}; + +// Resume a pending recv() coroutine (if any) with the front queued message, or +// with an error when the socket is gone: (nil,"reconnecting") if auto-reconnect +// is enabled (transient), else (nil,"closed") (terminal). +static void ws_try_deliver(LuaWsClient* box) { + if (box->recv_co == NULL) return; + lua_State* co = hvlua_coroutine_state(box->recv_co); + if (co == NULL) { // coroutine was GC'd / stale + hvlua_cancel(box->recv_co); + box->recv_co = NULL; + return; + } + if (!box->inbox.empty()) { + std::string msg = std::move(box->inbox.front()); + box->inbox.pop_front(); + HvLuaCoroutine* co_tok = box->recv_co; + box->recv_co = NULL; + lua_pushlstring(co, msg.data(), msg.size()); + hvlua_resume(co_tok, 1); + } else if (!box->connected) { + HvLuaCoroutine* co_tok = box->recv_co; + box->recv_co = NULL; + lua_pushnil(co); + // Distinguish a transient disconnect (reconnecting) from a terminal + // close so the script can choose to keep waiting or stop. + lua_pushstring(co, box->reconnect ? "reconnecting" : "closed"); + hvlua_resume(co_tok, 2); + } +} + +static int ws_client_gc(lua_State* L) { + LuaWsClient* box = (LuaWsClient*)luaL_checkudata(L, 1, WS_CLIENT_MT); + if (box) { + if (box->recv_co) { // release a still-suspended recv token + hvlua_cancel(box->recv_co); + box->recv_co = NULL; + } + if (box->client) { + delete box->client; // external loop (not owner): does NOT stop it + box->client = NULL; + } + box->inbox.~WsInbox(); // placement-constructed; destroy explicitly + } + return 0; +} + +// Continuation for connect: (ws) on success, or (nil,err) already on stack. +static int ws_connect_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) { + lua_pop(L, 1); // drop the `true` success marker + lua_pushvalue(L, 1); // return the ws userdata (self, stack slot 1) + return 1; + } + return 2; // (nil, err) +} + +// hv.ws.connect(url [, opts]) -> ws | nil, err +// opts = { headers = {..}, ping_interval = ms, reconnect = {min_delay, max_delay, +// delay_policy, max_retry} } +static int l_ws_connect(lua_State* L) { + const char* url = luaL_checkstring(L, 1); + LuaWsClient* box = NULL; + { + EventLoopPtr loop = currentThreadEventLoopPtr; + if (!loop) { + lua_pushnil(L); + lua_pushstring(L, "hv.ws: no shared event loop on this thread"); + return 2; + } + + // userdata carries the client + inbox; placement-new the non-POD members. + box = (LuaWsClient*)lua_newuserdata(L, sizeof(LuaWsClient)); + new (&box->inbox) WsInbox(); + box->recv_co = NULL; + box->connected = false; + box->reconnect = false; + box->opened_once = false; + box->client = new WebSocketClient(loop); // bound to current loop, not owner + luaL_setmetatable(L, WS_CLIENT_MT); + lua_replace(L, 1); // move ws userdata to slot 1 for ws_connect_k + } // ~loop runs here, before the yield + + // Parse headers later in the scope that ends before lua_yieldk. + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "ping_interval"); + if (lua_isinteger(L, -1)) box->client->setPingInterval((int)lua_tointeger(L, -1)); + lua_pop(L, 1); + reconn_setting_t reconn; + if (hvlua_parse_reconnect(L, 2, &reconn)) { + box->reconnect = true; + box->client->setReconnect(&reconn); + } + } + + box->client->onopen = [box]() { + // Connection established (initial or after a reconnect): mark connected + // and reset for the (possibly reused) session. + box->connected = true; + if (!box->opened_once) { + // Initial connect: resume the connect() coroutine held in recv_co. + box->opened_once = true; + lua_State* co = hvlua_coroutine_state(box->recv_co); + if (co == NULL) { return; } + HvLuaCoroutine* tok = box->recv_co; + box->recv_co = NULL; + lua_pushboolean(co, 1); // success marker for ws_connect_k + hvlua_resume(tok, 1); + } + // A reconnect's onopen does not resume anything; new recv() calls wait + // for the next message. + }; + box->client->onmessage = [box](const std::string& msg) { + box->inbox.push_back(msg); + ws_try_deliver(box); + }; + box->client->onclose = [box]() { + box->connected = false; + // Wake whoever is waiting. On the initial connect this is the connect() + // coroutine (handshake failed -> never opened_once); ws_try_deliver + // resumes it with (nil, "closed"/"reconnecting"). After a successful + // open it's a pending recv(). With auto-reconnect on, the socket will + // retry underneath and a future onopen/onmessage resumes fresh recvs. + ws_try_deliver(box); + }; + + box->recv_co = hvlua_suspend(L); // reuse recv_co to hold the connect wait + // NOTE: lua_yieldk below never returns to this C++ frame (longjmp in a + // C-built Lua), so destructors of locals here are SKIPPED. Scope the + // non-trivial `headers` (std::map) so it is destroyed BEFORE the yield; + // open() copies what it needs. Keep only the trivial `ret` for the check. + int ret; + { + http_headers headers = DefaultHeaders; + if (lua_istable(L, 2)) { + lua_getfield(L, 2, "headers"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TSTRING) { + headers[lua_tostring(L, -2)] = lua_tostring(L, -1); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop headers (or nil) + } + ret = box->client->open(url, headers); + } // ~headers runs here, before the yield + if (ret != 0) { + hvlua_cancel(box->recv_co); + box->recv_co = NULL; + lua_pushnil(L); + lua_pushfstring(L, "hv.ws: open failed (%d)", ret); + return 2; + } + return lua_yieldk(L, 0, (lua_KContext)0, ws_connect_k); +} + +// Continuation for recv: (msg) or (nil,err) already on stack. +static int ws_recv_k(lua_State* L, int status, lua_KContext ctx) { + (void)status; (void)ctx; + return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; +} + +// ws:recv() -> msg | nil, err (coroutine-synchronous) +static int l_ws_recv(lua_State* L) { + LuaWsClient* box = (LuaWsClient*)luaL_checkudata(L, 1, WS_CLIENT_MT); + if (box == NULL || box->client == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + if (box->recv_co != NULL) { + lua_pushnil(L); lua_pushstring(L, "hv.ws: recv already pending"); return 2; + } + // fast path: a message is already queued -> return it without suspending. + if (!box->inbox.empty()) { + std::string msg = std::move(box->inbox.front()); + box->inbox.pop_front(); + lua_pushlstring(L, msg.data(), msg.size()); + return 1; + } + if (!box->connected) { + lua_pushnil(L); + lua_pushstring(L, box->reconnect ? "reconnecting" : "closed"); + return 2; + } + box->recv_co = hvlua_suspend(L); + return lua_yieldk(L, 0, (lua_KContext)0, ws_recv_k); +} + +// ws:send(msg [, "binary"]) -> nbytes | nil, err (non-blocking, no suspend) +static int l_ws_send(lua_State* L) { + LuaWsClient* box = (LuaWsClient*)luaL_checkudata(L, 1, WS_CLIENT_MT); + size_t len = 0; + const char* data = luaL_checklstring(L, 2, &len); + if (box == NULL || box->client == NULL || !box->connected) { + // Not connected (never opened, or between reconnect attempts): sending + // now would silently drop, so report it instead. + lua_pushnil(L); + lua_pushstring(L, (box && box->reconnect) ? "reconnecting" : "closed"); + return 2; + } + enum ws_opcode opcode = WS_OPCODE_TEXT; + if (lua_isstring(L, 3) && std::string(lua_tostring(L, 3)) == "binary") { + opcode = WS_OPCODE_BINARY; + } + int n = box->client->send(data, (int)len, opcode); + if (n < 0) { + lua_pushnil(L); lua_pushstring(L, "hv.ws: send failed"); return 2; + } + lua_pushinteger(L, n); + return 1; +} + +// ws:close() +static int l_ws_close(lua_State* L) { + LuaWsClient* box = (LuaWsClient*)luaL_checkudata(L, 1, WS_CLIENT_MT); + if (box && box->client) { + // Explicit close is a terminal user action: disable auto-reconnect so + // recv() reports "closed" (not "reconnecting") and the socket stays down. + box->reconnect = false; + box->connected = false; + box->client->close(); + } + return 0; +} + +static const luaL_Reg ws_methods[] = { + { "recv", l_ws_recv }, + { "send", l_ws_send }, + { "close", l_ws_close }, + { NULL, NULL } +}; + +static const luaL_Reg ws_funcs[] = { + { "connect", l_ws_connect }, + { NULL, NULL } +}; + +extern "C" void hvlua_open_ws(lua_State* L) { + hvlua_new_class(L, WS_CLIENT_MT, ws_client_gc, ws_methods); + lua_pop(L, 1); + + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, ws_funcs); + lua_setfield(L, -2, "ws"); + lua_setglobal(L, "hv"); +} + +#endif // HVLUA_WITH_HTTP diff --git a/mqtt/mqtt_client.c b/mqtt/mqtt_client.c index 9e4566d34..e7814c30c 100644 --- a/mqtt/mqtt_client.c +++ b/mqtt/mqtt_client.c @@ -71,10 +71,10 @@ static int mqtt_v5_skip_properties(unsigned char** pp, unsigned char* end) { unsigned char* p = *pp; int bytes = end - p; if (bytes <= 0) return 0; - int prop_len = (int)varint_decode(p, &bytes); + long long prop_len = varint_decode(p, &bytes); if (bytes <= 0) return 0; p += bytes; // skip varint bytes - if (p + prop_len > end) return 0; + if (prop_len < 0 || prop_len > end - p) return 0; p += prop_len; // skip properties data *pp = p; return 1; @@ -441,6 +441,10 @@ static void on_connect(hio_t* io) { } mqtt_client_t* mqtt_client_new(hloop_t* loop) { + // Own the loop only if we create it here (loop==NULL). When the caller + // supplies a loop, the caller owns its run/stop/free lifetime, so + // mqtt_client_run/stop must not drive or stop it (see those functions). + int is_loop_owner = (loop == NULL); if (loop == NULL) { loop = hloop_new(HLOOP_FLAG_AUTO_FREE); if (loop == NULL) return NULL; @@ -449,6 +453,7 @@ mqtt_client_t* mqtt_client_new(hloop_t* loop) { HV_ALLOC_SIZEOF(cli); if (cli == NULL) return NULL; cli->loop = loop; + cli->is_loop_owner = is_loop_owner; cli->protocol_version = MQTT_PROTOCOL_V311; cli->keepalive = DEFAULT_MQTT_KEEPALIVE; hmutex_init(&cli->mutex_); @@ -457,6 +462,21 @@ mqtt_client_t* mqtt_client_new(hloop_t* loop) { void mqtt_client_free(mqtt_client_t* cli) { if (!cli) return; + // Tear down the io and timer BEFORE freeing cli. They were registered on the + // loop with hevent_set_userdata(..., cli); if left armed, a later on_close / + // connect_timeout_cb / reconnect_timer_cb would dereference the freed cli + // (use-after-free). Detach their back-pointers and close/delete them first. + if (cli->timer) { + hevent_set_userdata(cli->timer, NULL); + htimer_del(cli->timer); + cli->timer = NULL; + } + if (cli->io) { + hevent_set_userdata(cli->io, NULL); + hio_setcb_close(cli->io, NULL); // no on_close callback into freed cli + hio_close(cli->io); + cli->io = NULL; + } hmutex_destroy(&cli->mutex_); if (cli->ssl_ctx && cli->alloced_ssl_ctx) { hssl_ctx_free(cli->ssl_ctx); @@ -469,11 +489,23 @@ void mqtt_client_free(mqtt_client_t* cli) { void mqtt_client_run (mqtt_client_t* cli) { if (!cli || !cli->loop) return; + // Only drive the loop we own. When the caller supplied the loop, they are + // responsible for running it (e.g. an existing IO thread / runtime loop); + // running it here would block or double-drive someone else's loop. + if (!cli->is_loop_owner) return; hloop_run(cli->loop); + // The owned loop uses HLOOP_FLAG_AUTO_FREE, so its IOs/timers and the loop + // itself are gone when hloop_run returns. Drop the stale non-owning handles + // before mqtt_client_free inspects them. + cli->loop = NULL; + cli->io = NULL; + cli->timer = NULL; } void mqtt_client_stop(mqtt_client_t* cli) { if (!cli || !cli->loop) return; + // Only stop the loop we own; never stop a caller-supplied loop. + if (!cli->is_loop_owner) return; hloop_stop(cli->loop); } diff --git a/mqtt/mqtt_client.h b/mqtt/mqtt_client.h index ec9110f41..8429fe71e 100644 --- a/mqtt/mqtt_client.h +++ b/mqtt/mqtt_client.h @@ -28,6 +28,7 @@ struct mqtt_client_s { unsigned char ssl: 1; // Read Only unsigned char alloced_ssl_ctx: 1; // intern unsigned char connected : 1; + unsigned char is_loop_owner: 1; // intern: 1 if this client created its own loop unsigned short keepalive; int ping_cnt; char client_id[64]; diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index 8626c10b6..f6e9f8e3e 100644 --- a/redis/AsyncRedisClient.cpp +++ b/redis/AsyncRedisClient.cpp @@ -8,10 +8,13 @@ #include #include -#include "TcpClient.h" - namespace hv { +// The AsyncRedisClient IS a TcpClient (see header). Impl keeps a back-pointer to +// that TcpClient (`self`) and drives the RESP protocol through the inherited +// base API: self->channel / self->send / self->isConnected / self->startConnect +// / self->setReconnect. Connect/reconnect/DNS/loop-ownership all live in the +// base, so there is no duplicated tcp_client member and no isLoopOwner logic here. struct AsyncRedisClient::Impl { struct PendingRequest { size_t expected_replies; @@ -48,22 +51,27 @@ struct AsyncRedisClient::Impl { }; struct CleanupState { + enum Owner { + kPending, + kLoop, + kCancelled, + }; + std::mutex mutex; std::condition_variable cv; + std::atomic owner; bool done; CleanupState() - : done(false) {} + : owner(kPending) + , done(false) {} }; AsyncRedisClient* self; - TcpClientEventLoopTmpl tcp_client; std::deque > pending; RedisParser parser; - bool is_loop_owner; std::string host; int port; - int connect_timeout_ms; int timeout_ms; std::string password; int db; @@ -75,12 +83,9 @@ struct AsyncRedisClient::Impl { size_t handshake_index; std::vector handshake_commands; - Impl(AsyncRedisClient* client, const EventLoopPtr& loop, bool loop_owner) + explicit Impl(AsyncRedisClient* client) : self(client) - , tcp_client(loop) - , is_loop_owner(loop_owner) , port(6379) - , connect_timeout_ms(5000) , timeout_ms(5000) , db(0) , handshake_pending(false) @@ -96,17 +101,20 @@ struct AsyncRedisClient::Impl { } bool acceptsRequests() { - if (!started || destroyed || stop_in_progress || !accept_requests || self->loop() == NULL || self->loop()->loop() == NULL) { + if (!started || destroyed || stop_in_progress || !accept_requests) { return false; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (self->loop() == NULL || self->loop()->loop() == NULL) { return false; } - return true; + // The request is dispatched onto the loop; it can only be served if that + // loop is actually running (whether the client owns it or the caller + // supplied and started it). + return self->loop()->isRunning(); } void initCallbacks() { - tcp_client.onConnection = [this](const SocketChannelPtr& channel) { + self->onConnection = [this](const SocketChannelPtr& channel) { if (destroyed) { return; } @@ -126,7 +134,7 @@ struct AsyncRedisClient::Impl { } }; - tcp_client.onMessage = [this](const SocketChannelPtr&, Buffer* buf) { + self->onMessage = [this](const SocketChannelPtr&, Buffer* buf) { if (destroyed) { return; } @@ -141,56 +149,41 @@ struct AsyncRedisClient::Impl { }; } + // Copy the redis target into the inherited TcpClient fields. For a numeric IP + // (or UDS) resolve the sockaddr up front; for a hostname leave remote_addr + // zeroed so the base startConnect() runs the non-blocking async DNS path. int applySettings() { - tcp_client.remote_host = host.empty() ? "127.0.0.1" : host; - tcp_client.remote_port = port; - tcp_client.connect_timeout = connect_timeout_ms; - memset(&tcp_client.remote_addr, 0, sizeof(tcp_client.remote_addr)); - int ret = sockaddr_set_ipport(&tcp_client.remote_addr, tcp_client.remote_host.c_str(), tcp_client.remote_port); - if (ret != 0) { - return NABS(ret); + self->remote_host = host.empty() ? "127.0.0.1" : host; + self->remote_port = port; + memset(&self->remote_addr, 0, sizeof(self->remote_addr)); + if (self->remote_port < 0 || is_ipaddr(self->remote_host.c_str())) { + int ret = sockaddr_set_ipport(&self->remote_addr, self->remote_host.c_str(), self->remote_port); + if (ret != 0) { + return NABS(ret); + } } return 0; } - int startConnectInLoop() { - if (!accept_requests || destroyed || self->loop() == NULL || self->loop()->loop() == NULL) { - return ERR_CONNECT; - } - if (!is_loop_owner && !self->loop()->isRunning()) { - return ERR_CONNECT; - } - int ret = applySettings(); - if (ret != 0) { - notifyError(ret); - return ret; - } - ret = tcp_client.startConnect(); - if (ret != 0) { - notifyError(ret); - } - return ret; - } - void clearCallbacks() { - tcp_client.onConnection = NULL; - tcp_client.onMessage = NULL; - tcp_client.onWriteComplete = NULL; - if (tcp_client.channel) { - tcp_client.channel->onconnect = NULL; - tcp_client.channel->onread = NULL; - tcp_client.channel->onwrite = NULL; - tcp_client.channel->onclose = NULL; + self->onConnection = NULL; + self->onMessage = NULL; + self->onWriteComplete = NULL; + if (self->channel) { + self->channel->onconnect = NULL; + self->channel->onread = NULL; + self->channel->onwrite = NULL; + self->channel->onclose = NULL; } } void cleanupInPlace() { - tcp_client.setReconnect(NULL); + self->setReconnect(NULL); failPending(ERR_CONNECT); clearProtocolState(); clearCallbacks(); - if (tcp_client.channel && !tcp_client.channel->isClosed()) { - tcp_client.channel->close(); + if (self->channel && !self->channel->isClosed()) { + self->channel->close(); } } @@ -201,6 +194,10 @@ struct AsyncRedisClient::Impl { } std::shared_ptr state = std::make_shared(); self->loop()->queueInLoop([this, state]() { + int expected = CleanupState::kPending; + if (!state->owner.compare_exchange_strong(expected, CleanupState::kLoop)) { + return; + } cleanupInPlace(); { std::lock_guard lock(state->mutex); @@ -219,7 +216,13 @@ struct AsyncRedisClient::Impl { } lock.unlock(); if (!state->done) { - cleanupInPlace(); + int expected = CleanupState::kPending; + if (state->owner.compare_exchange_strong(expected, CleanupState::kCancelled)) { + cleanupInPlace(); + } else { + std::unique_lock wait_lock(state->mutex); + state->cv.wait(wait_lock, [state]() { return state->done; }); + } } } @@ -276,11 +279,11 @@ struct AsyncRedisClient::Impl { } pending.push_back(request); armTimeout(request); - if (tcp_client.isConnected() && !handshake_pending) { + if (self->isConnected() && !handshake_pending) { flushPending(); } else if (started && !handshake_pending) { - tcp_client.start(); + self->startConnect(); } return 0; } @@ -306,7 +309,7 @@ struct AsyncRedisClient::Impl { finishHandshake(); return; } - int ret = tcp_client.send(RedisEncodeCommand(handshake_commands[index])); + int ret = self->send(RedisEncodeCommand(handshake_commands[index])); if (ret < 0) { handleClientError(ret); return; @@ -327,9 +330,9 @@ struct AsyncRedisClient::Impl { } void flushPending() { - if (!tcp_client.isConnected()) { + if (!self->isConnected()) { if (started) { - tcp_client.start(); + self->startConnect(); } return; } @@ -338,7 +341,7 @@ struct AsyncRedisClient::Impl { if (request->sent) { continue; } - int ret = tcp_client.send(request->payload); + int ret = self->send(request->payload); if (ret < 0) { handleClientError(ret); return; @@ -387,7 +390,7 @@ struct AsyncRedisClient::Impl { // endless reconnect + re-auth storm (TcpClient resets its retry // counter on every successful TCP connect). Disable reconnect before // closing so the failure is reported once and the client stays down. - tcp_client.setReconnect(NULL); + self->setReconnect(NULL); handleClientError(ERR_RESPONSE); return; } @@ -422,8 +425,8 @@ struct AsyncRedisClient::Impl { notifyError(code); failPending(code); clearProtocolState(); - if (tcp_client.channel && !tcp_client.channel->isClosed()) { - tcp_client.channel->close(); + if (self->channel && !self->channel->isClosed()) { + self->channel->close(); } } @@ -443,8 +446,10 @@ struct AsyncRedisClient::Impl { }; AsyncRedisClient::AsyncRedisClient(EventLoopPtr loop) - : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop(), loop == NULL)) { + : TcpClientTmpl(loop) + , impl_(std::make_shared(this)) { + // preserve the historical redis default connect timeout (base default differs) + setConnectTimeout(5000); impl_->initCallbacks(); } @@ -468,44 +473,25 @@ void AsyncRedisClient::setDb(int db) { impl_->db = db; } -void AsyncRedisClient::setConnectTimeout(int ms) { - impl_->connect_timeout_ms = ms; -} - void AsyncRedisClient::setTimeout(int ms) { impl_->timeout_ms = ms; } -void AsyncRedisClient::setReconnect(reconn_setting_t* setting) { - impl_->tcp_client.setReconnect(setting); -} - void AsyncRedisClient::start(bool wait_threads_started) { impl_->destroyed = false; impl_->stop_in_progress = false; impl_->started = true; impl_->accept_requests = true; - if (!impl_->is_loop_owner) { - if (!loop() || !loop()->loop() || !loop()->isRunning()) { - impl_->started = false; - impl_->accept_requests = false; - impl_->notifyError(ERR_CONNECT); - return; - } - loop()->runInLoop([this]() { - impl_->startConnectInLoop(); - }); - return; - } - if (isRunning()) { - loop()->runInLoop([this]() { - impl_->startConnectInLoop(); - }); + int ret = impl_->applySettings(); + if (ret != 0) { + impl_->started = false; + impl_->accept_requests = false; + impl_->notifyError(ret); return; } - EventLoopThread::start(wait_threads_started, [this]() { - return impl_->startConnectInLoop(); - }); + // Delegate connect/thread/reconnect to the base. startConnect() runs on the + // loop and wires channel callbacks into self->onConnection / onMessage. + TcpClientTmpl::start(wait_threads_started); } void AsyncRedisClient::stop(bool wait_threads_stopped) { @@ -513,33 +499,25 @@ void AsyncRedisClient::stop(bool wait_threads_stopped) { impl_->started = false; impl_->destroyed = true; impl_->stop_in_progress = true; - if (!loop()) { - impl_->cleanupInPlace(); - impl_->stop_in_progress = false; - return; - } - if (!impl_->is_loop_owner) { + // Fail any in-flight requests and detach protocol callbacks on the loop + // thread before the base tears down the socket / loop. + if (loop() && loop()->loop()) { if (loop()->isRunning()) { impl_->runCleanupOnLoopAndWait(); } else { impl_->cleanupInPlace(); } - impl_->stop_in_progress = false; - return; - } - if (loop()->isRunning()) { - impl_->runCleanupOnLoopAndWait(); } else { impl_->cleanupInPlace(); } - EventLoopThread::stop(wait_threads_stopped); + TcpClientTmpl::stop(wait_threads_stopped); impl_->stop_in_progress = false; } bool AsyncRedisClient::isConnected() const { - return impl_->tcp_client.channel && impl_->tcp_client.channel->isConnected(); + return channel && channel->isConnected(); } bool AsyncRedisClient::isStarted() const { diff --git a/redis/AsyncRedisClient.h b/redis/AsyncRedisClient.h index dfd793a56..0030e5438 100644 --- a/redis/AsyncRedisClient.h +++ b/redis/AsyncRedisClient.h @@ -6,9 +6,7 @@ #include #include -#include "herr.h" - -#include "EventLoopThread.h" +#include "TcpClient.h" #include "RedisMessage.h" namespace hv { @@ -16,7 +14,11 @@ namespace hv { using RedisCallback = std::function; using RedisRepliesCallback = std::function&)>; -class HV_EXPORT AsyncRedisClient : private EventLoopThread { +// AsyncRedisClient is a coroutine-free async Redis client built directly on +// TcpClient: it reuses the base connect/reconnect/DNS/loop-ownership machinery +// (start/stop/setReconnect/setConnectTimeout are inherited) and only adds the +// RESP protocol layer (handshake, request pipelining, reply dispatch). +class HV_EXPORT AsyncRedisClient : public TcpClientTmpl { public: AsyncRedisClient(EventLoopPtr loop = NULL); ~AsyncRedisClient(); @@ -25,10 +27,11 @@ class HV_EXPORT AsyncRedisClient : private EventLoopThread { void setPort(int port); void setAuth(const std::string& password); void setDb(int db); - void setConnectTimeout(int ms); + // per-request reply timeout (ms); connect timeout / reconnect are inherited + // from TcpClient (setConnectTimeout / setReconnect). void setTimeout(int ms); - void setReconnect(reconn_setting_t* setting); + // start/stop thread-safe; delegate to TcpClient after (re)setting redis state. void start(bool wait_threads_started = true); void stop(bool wait_threads_stopped = true); diff --git a/redis/RedisSubscriber.cpp b/redis/RedisSubscriber.cpp index df52c4234..fc5465758 100644 --- a/redis/RedisSubscriber.cpp +++ b/redis/RedisSubscriber.cpp @@ -10,10 +10,14 @@ #include #include "RedisMessage.h" -#include "TcpClient.h" namespace hv { +// The RedisSubscriber IS a TcpClient (see header). Impl keeps a back-pointer to +// that TcpClient (`self`) and drives the RESP subscribe protocol through the +// inherited base API: self->channel / self->send / self->isConnected / +// self->startConnect / self->setReconnect. Connect/reconnect/DNS/loop-ownership +// all live in the base, so there is no duplicated tcp_client member here. struct RedisSubscriber::Impl { struct EnqueueState { enum Owner { @@ -35,12 +39,20 @@ struct RedisSubscriber::Impl { }; struct CleanupState { + enum Owner { + kPending, + kLoop, + kCancelled, + }; + std::mutex mutex; std::condition_variable cv; + std::atomic owner; bool done; CleanupState() - : done(false) {} + : owner(kPending) + , done(false) {} }; enum OperationType { @@ -51,9 +63,7 @@ struct RedisSubscriber::Impl { }; RedisSubscriber* self; - TcpClientEventLoopTmpl tcp_client; RedisParser parser; - bool is_loop_owner; std::string host; int port; std::string password; @@ -68,10 +78,8 @@ struct RedisSubscriber::Impl { std::set channels; std::set patterns; - Impl(RedisSubscriber* subscriber, const EventLoopPtr& loop, bool loop_owner) + explicit Impl(RedisSubscriber* subscriber) : self(subscriber) - , tcp_client(loop) - , is_loop_owner(loop_owner) , port(6379) , db(0) , handshake_pending(false) @@ -87,17 +95,23 @@ struct RedisSubscriber::Impl { } bool acceptsRequests() { - if (!started || destroyed || stop_in_progress || !accept_requests || self->loop() == NULL || self->loop()->loop() == NULL) { + if (!started || destroyed || stop_in_progress || !accept_requests) { return false; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (self->loop() == NULL || self->loop()->loop() == NULL) { return false; } - return true; + return self->loop()->isRunning(); } void initCallbacks() { - tcp_client.onConnection = [this](const SocketChannelPtr& channel) { + // Wire the base (TcpClient) transport callbacks. RedisSubscriber also + // declares a public onMessage(channel, message) for pub/sub delivery, + // which HIDES the base onMessage(channelPtr, Buffer*); route through a + // base-typed pointer so we set the transport callback, not the + // user-facing one. + TcpClientEventLoopTmpl* tcp = self; + tcp->onConnection = [this](const SocketChannelPtr& channel) { if (destroyed) { return; } @@ -112,7 +126,7 @@ struct RedisSubscriber::Impl { } }; - tcp_client.onMessage = [this](const SocketChannelPtr&, Buffer* buf) { + tcp->onMessage = [this](const SocketChannelPtr&, Buffer* buf) { if (destroyed) { return; } @@ -127,54 +141,44 @@ struct RedisSubscriber::Impl { }; } + // Copy the redis target into the inherited TcpClient fields. For a numeric IP + // (or UDS) resolve the sockaddr up front; for a hostname leave remote_addr + // zeroed so the base startConnect() runs the non-blocking async DNS path. int applySettings() { - tcp_client.remote_host = host.empty() ? "127.0.0.1" : host; - tcp_client.remote_port = port; - memset(&tcp_client.remote_addr, 0, sizeof(tcp_client.remote_addr)); - int ret = sockaddr_set_ipport(&tcp_client.remote_addr, tcp_client.remote_host.c_str(), tcp_client.remote_port); - if (ret != 0) { - return NABS(ret); + self->remote_host = host.empty() ? "127.0.0.1" : host; + self->remote_port = port; + memset(&self->remote_addr, 0, sizeof(self->remote_addr)); + if (self->remote_port < 0 || is_ipaddr(self->remote_host.c_str())) { + int ret = sockaddr_set_ipport(&self->remote_addr, self->remote_host.c_str(), self->remote_port); + if (ret != 0) { + return NABS(ret); + } } return 0; } - int startConnectInLoop() { - if (!accept_requests || destroyed || self->loop() == NULL || self->loop()->loop() == NULL) { - return ERR_CONNECT; - } - if (!is_loop_owner && !self->loop()->isRunning()) { - return ERR_CONNECT; - } - int ret = applySettings(); - if (ret != 0) { - notifyError(ret); - return ret; - } - ret = tcp_client.startConnect(); - if (ret != 0) { - notifyError(ret); - } - return ret; - } - void clearCallbacks() { - tcp_client.onConnection = NULL; - tcp_client.onMessage = NULL; - tcp_client.onWriteComplete = NULL; - if (tcp_client.channel) { - tcp_client.channel->onconnect = NULL; - tcp_client.channel->onread = NULL; - tcp_client.channel->onwrite = NULL; - tcp_client.channel->onclose = NULL; + // Clear the base transport callbacks via a base-typed pointer (see + // initCallbacks): self->onMessage would resolve to the hiding pub/sub + // callback, not the base one. + TcpClientEventLoopTmpl* tcp = self; + tcp->onConnection = NULL; + tcp->onMessage = NULL; + tcp->onWriteComplete = NULL; + if (self->channel) { + self->channel->onconnect = NULL; + self->channel->onread = NULL; + self->channel->onwrite = NULL; + self->channel->onclose = NULL; } } void cleanupInPlace() { - tcp_client.setReconnect(NULL); + self->setReconnect(NULL); clearProtocolState(); clearCallbacks(); - if (tcp_client.channel && !tcp_client.channel->isClosed()) { - tcp_client.channel->close(); + if (self->channel && !self->channel->isClosed()) { + self->channel->close(); } } @@ -185,6 +189,10 @@ struct RedisSubscriber::Impl { } std::shared_ptr state = std::make_shared(); self->loop()->queueInLoop([this, state]() { + int expected = CleanupState::kPending; + if (!state->owner.compare_exchange_strong(expected, CleanupState::kLoop)) { + return; + } cleanupInPlace(); { std::lock_guard lock(state->mutex); @@ -203,7 +211,13 @@ struct RedisSubscriber::Impl { } lock.unlock(); if (!state->done) { - cleanupInPlace(); + int expected = CleanupState::kPending; + if (state->owner.compare_exchange_strong(expected, CleanupState::kCancelled)) { + cleanupInPlace(); + } else { + std::unique_lock wait_lock(state->mutex); + state->cv.wait(wait_lock, [state]() { return state->done; }); + } } } @@ -280,7 +294,7 @@ struct RedisSubscriber::Impl { if (channels.erase(name) == 0) { return 0; } - if (tcp_client.isConnected() && !handshake_pending) { + if (self->isConnected() && !handshake_pending) { command = RedisCommand{"UNSUBSCRIBE", name}; } break; @@ -288,17 +302,17 @@ struct RedisSubscriber::Impl { if (patterns.erase(name) == 0) { return 0; } - if (tcp_client.isConnected() && !handshake_pending) { + if (self->isConnected() && !handshake_pending) { command = RedisCommand{"PUNSUBSCRIBE", name}; } break; } - if (!command.empty() && tcp_client.isConnected() && !handshake_pending) { + if (!command.empty() && self->isConnected() && !handshake_pending) { return sendCommand(command); } - if (!tcp_client.isConnected() && started && !handshake_pending) { - tcp_client.start(); + if (!self->isConnected() && started && !handshake_pending) { + self->startConnect(); } return 0; } @@ -333,7 +347,7 @@ struct RedisSubscriber::Impl { } int sendCommand(const RedisCommand& command) { - int ret = tcp_client.send(RedisEncodeCommand(command)); + int ret = self->send(RedisEncodeCommand(command)); if (ret < 0) { handleClientError(ret); return ret; @@ -361,7 +375,7 @@ struct RedisSubscriber::Impl { void handleHandshakeReply(const RedisReply& reply) { if (reply.isError()) { - tcp_client.setReconnect(NULL); + self->setReconnect(NULL); handleClientError(ERR_RESPONSE); return; } @@ -463,8 +477,8 @@ struct RedisSubscriber::Impl { void handleClientError(int code) { notifyError(code); clearProtocolState(); - if (tcp_client.channel && !tcp_client.channel->isClosed()) { - tcp_client.channel->close(); + if (self->channel && !self->channel->isClosed()) { + self->channel->close(); } } @@ -483,8 +497,8 @@ struct RedisSubscriber::Impl { }; RedisSubscriber::RedisSubscriber(EventLoopPtr loop) - : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop(), loop == NULL)) { + : TcpClientTmpl(loop) + , impl_(std::make_shared(this)) { impl_->initCallbacks(); } @@ -508,36 +522,19 @@ void RedisSubscriber::setDb(int db) { impl_->db = db; } -void RedisSubscriber::setReconnect(reconn_setting_t* setting) { - impl_->tcp_client.setReconnect(setting); -} - void RedisSubscriber::start(bool wait_threads_started) { impl_->destroyed = false; impl_->stop_in_progress = false; impl_->started = true; impl_->accept_requests = true; - if (!impl_->is_loop_owner) { - if (!loop() || !loop()->loop() || !loop()->isRunning()) { - impl_->started = false; - impl_->accept_requests = false; - impl_->notifyError(ERR_CONNECT); - return; - } - loop()->runInLoop([this]() { - impl_->startConnectInLoop(); - }); + int ret = impl_->applySettings(); + if (ret != 0) { + impl_->started = false; + impl_->accept_requests = false; + impl_->notifyError(ret); return; } - if (isRunning()) { - loop()->runInLoop([this]() { - impl_->startConnectInLoop(); - }); - return; - } - EventLoopThread::start(wait_threads_started, [this]() { - return impl_->startConnectInLoop(); - }); + TcpClientTmpl::start(wait_threads_started); } void RedisSubscriber::stop(bool wait_threads_stopped) { @@ -545,28 +542,18 @@ void RedisSubscriber::stop(bool wait_threads_stopped) { impl_->started = false; impl_->destroyed = true; impl_->stop_in_progress = true; - if (!loop()) { - impl_->cleanupInPlace(); - impl_->stop_in_progress = false; - return; - } - if (!impl_->is_loop_owner) { + if (loop() && loop()->loop()) { if (loop()->isRunning()) { impl_->runCleanupOnLoopAndWait(); } else { impl_->cleanupInPlace(); } - impl_->stop_in_progress = false; - return; - } - if (loop()->isRunning()) { - impl_->runCleanupOnLoopAndWait(); } else { impl_->cleanupInPlace(); } - EventLoopThread::stop(wait_threads_stopped); + TcpClientTmpl::stop(wait_threads_stopped); impl_->stop_in_progress = false; } diff --git a/redis/RedisSubscriber.h b/redis/RedisSubscriber.h index d9b7fb79a..99fd5b262 100644 --- a/redis/RedisSubscriber.h +++ b/redis/RedisSubscriber.h @@ -5,13 +5,14 @@ #include #include -#include "herr.h" - -#include "EventLoopThread.h" +#include "TcpClient.h" namespace hv { -class HV_EXPORT RedisSubscriber : private EventLoopThread { +// RedisSubscriber implements the Redis pub/sub client on top of TcpClient: it +// reuses the base connect/reconnect/DNS/loop-ownership machinery (start/stop/ +// setReconnect are inherited) and only adds the RESP subscribe protocol layer. +class HV_EXPORT RedisSubscriber : public TcpClientTmpl { public: RedisSubscriber(EventLoopPtr loop = NULL); ~RedisSubscriber(); @@ -20,7 +21,7 @@ class HV_EXPORT RedisSubscriber : private EventLoopThread { void setPort(int port); void setAuth(const std::string& password); void setDb(int db); - void setReconnect(reconn_setting_t* setting); + // setReconnect is inherited from TcpClient. void start(bool wait_threads_started = true); void stop(bool wait_threads_stopped = true); diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 2a795efa1..0be55a0c0 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -32,16 +32,34 @@ bin/socketpair_test # bin/objectpool_test bin/sizeof_test bin/http_router_test +if [ -x bin/lua_binding_test ]; then + bin/lua_binding_test +fi +if [ -x bin/lua_io_test ]; then + bin/lua_io_test +fi +if [ -x bin/http_script_handler_test ]; then + bin/http_script_handler_test +fi if [ -x bin/http_lua_handler_test ]; then bin/http_lua_handler_test fi +if [ -x bin/lua_http_test ]; then + bin/lua_http_test +fi +if [ -x bin/lua_ws_test ]; then + bin/lua_ws_test +fi +if [ -x bin/lua_mqtt_test ]; then + bin/lua_mqtt_test +fi if [ -x bin/hdns_test ]; then bin/hdns_test fi if [ -x bin/tcpclient_dns_test ]; then bin/tcpclient_dns_test fi -for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test; do +for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test lua_redis_test; do if [ -x bin/${redis_test} ]; then bin/${redis_test} fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 9109d8420..2b62d081d 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -91,10 +91,46 @@ add_executable(http_router_test http_router_test.cpp) target_include_directories(http_router_test PRIVATE ../http/server) if(WITH_LUA) +add_executable(lua_binding_test lua_binding_test.cpp) +target_include_directories(lua_binding_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua) +target_link_libraries(lua_binding_test ${HV_LIBRARIES}) +add_executable(lua_io_test lua_io_test.cpp) +target_include_directories(lua_io_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua) +target_link_libraries(lua_io_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test) +if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_SERVER) +add_executable(http_script_handler_test http_script_handler_test.cpp) +target_include_directories(http_script_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server) +target_link_libraries(http_script_handler_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} http_script_handler_test) +if(WITH_HTTP_CLIENT) add_executable(http_lua_handler_test http_lua_handler_test.cpp) -target_include_directories(http_lua_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../http ../http/server) +target_include_directories(http_lua_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) target_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS http_lua_handler_test) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} http_lua_handler_test) +endif() +endif() +if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT AND WITH_HTTP_SERVER) +add_executable(lua_http_test lua_http_test.cpp) +target_include_directories(lua_http_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) +target_link_libraries(lua_http_test ${HV_LIBRARIES}) +add_executable(lua_ws_test lua_ws_test.cpp) +target_include_directories(lua_ws_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) +target_link_libraries(lua_ws_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_http_test lua_ws_test) +endif() +if(WITH_EVPP AND WITH_MQTT) +add_executable(lua_mqtt_test lua_mqtt_test.cpp) +target_include_directories(lua_mqtt_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../mqtt) +target_link_libraries(lua_mqtt_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_mqtt_test) +endif() +if(WITH_EVPP AND WITH_REDIS) +add_executable(lua_redis_test lua_redis_test.cpp redis_test_server.cpp) +target_include_directories(lua_redis_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../redis) +target_link_libraries(lua_redis_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_redis_test) +endif() endif() # ------event: async dns------ diff --git a/unittest/http_lua_handler_test.cpp b/unittest/http_lua_handler_test.cpp index 2f9919aee..976df7223 100644 --- a/unittest/http_lua_handler_test.cpp +++ b/unittest/http_lua_handler_test.cpp @@ -1,22 +1,36 @@ -#include "HttpScriptHandler.h" +/* + * http_lua_handler_test — Stage B integration test for the coroutine-based + * HttpLuaHandler: proves .lua HTTP handlers run as coroutines concurrently on + * a single IO thread. + * + * Starts a real HttpServer whose Lua route calls hv.sleep(ms) — a + * synchronous-style API that yields the coroutine to the event loop. Fires + * several concurrent requests and asserts: + * 1. every response is correct (200 + expected body), proving the async + * writer path completes the deferred response; + * 2. total wall time is far less than N * sleep, proving the requests are + * handled concurrently on one IO thread (coroutines interleave, the loop + * is never blocked). + */ #include #include #include +#include +#include +#include #include "hbase.h" #include "hfile.h" #include "hpath.h" +#include "htime.h" +#include "HttpServer.h" #include "HttpService.h" -#include "HttpContext.h" +#include "HttpScriptHandler.h" +#include "requests.h" static std::string write_script(const char* name, const char* content) { - std::string dir = "tmp/http_lua_handler_test"; - const char* slash = strrchr(name, '/'); - if (slash) { - dir = HPath::join(dir, std::string(name, slash - name)); - } - hv_mkdir_p(dir.c_str()); + hv_mkdir_p("tmp/http_lua_handler_test"); std::string path = HPath::join("tmp/http_lua_handler_test", name); HFile file; int ret = file.open(path.c_str(), "wb"); @@ -26,143 +40,55 @@ static std::string write_script(const char* name, const char* content) { return path; } -static HttpContextPtr make_ctx(const char* method, const char* path) { - HttpContextPtr ctx = std::make_shared(); - ctx->request = std::make_shared(); - ctx->response = std::make_shared(); - ctx->request->method = http_method_enum(method); - ctx->request->path = path; - ctx->request->query_params["id"] = "42"; - ctx->request->headers["X-Test"] = "header-value"; - ctx->request->body = "request-body"; - return ctx; -} - -static void test_text_response() { - std::string script = write_script("text.lua", - "function handle(ctx)\n" - " ctx:status(201)\n" - " ctx:set_header('X-Lua', ctx:query('id'))\n" - " return ctx:text(ctx:method() .. ' ' .. ctx:path() .. ' ' .. ctx:header('X-Test'))\n" - "end\n"); - - hv::HttpScriptHandler handler(script.c_str()); - HttpContextPtr ctx = make_ctx("GET", "/hello"); - int status = handler(ctx); - assert(status == 201); - assert(ctx->response->status_code == 201); - assert(ctx->response->body == "GET /hello header-value"); - assert(ctx->response->GetHeader("X-Lua") == "42"); - assert(ctx->response->ContentType() == TEXT_PLAIN); -} - -static void test_json_response() { - std::string script = write_script("json.lua", - "function handle(ctx)\n" - " return ctx:json({ok=true, id=ctx:query('id')})\n" - "end\n"); - - hv::HttpScriptHandler handler(script.c_str()); - HttpContextPtr ctx = make_ctx("POST", "/json"); - int status = handler(ctx); - assert(status == 200); - assert(ctx->response->ContentType() == APPLICATION_JSON); - assert(ctx->response->body.find("\"ok\": true") != std::string::npos); - assert(ctx->response->body.find("\"id\": \"42\"") != std::string::npos); -} - -static void test_method_function_preferred() { - std::string script = write_script("method.lua", - "function get(ctx)\n" - " return ctx:text('get:' .. ctx:query('id'))\n" - "end\n" - "function handle(ctx)\n" - " return ctx:text('handle')\n" - "end\n"); - - hv::HttpScriptHandler handler(script.c_str()); - HttpContextPtr ctx = make_ctx("GET", "/method"); - int status = handler(ctx); - assert(status == 200); - assert(ctx->response->body == "get:42"); -} - -static void test_method_function_fallback_to_handle() { - std::string script = write_script("method_fallback.lua", - "function get(ctx)\n" - " return ctx:text('get')\n" - "end\n" - "function handle(ctx)\n" - " return ctx:text('fallback:' .. ctx:method())\n" - "end\n"); - - hv::HttpScriptHandler handler(script.c_str()); - HttpContextPtr ctx = make_ctx("POST", "/method"); - int status = handler(ctx); - assert(status == 200); - assert(ctx->response->body == "fallback:POST"); -} - -static void test_script_dir_mapping() { - write_script("api/user.lua", +int main() { + // The handler sleeps 300ms inside the coroutine, then echoes the id. + std::string script = write_script("sleep.lua", "function handle(ctx)\n" - " return ctx:text('script:' .. ctx:query('id'))\n" + " hv.sleep(300)\n" + " return ctx:json({ ok = true, id = ctx:query('id') })\n" "end\n"); - hv::HttpService service; - service.Script("/api/", "tmp/http_lua_handler_test/api"); - - http_handler* handler = NULL; - std::map params; - int ret = service.GetRoute("/api/user?id=42", HTTP_GET, &handler, params); - assert(ret == 0); - assert(handler != NULL); - assert(handler->ctx_handler != NULL); - - HttpContextPtr ctx = make_ctx("GET", "/api/user"); - ctx->service = &service; - ctx->request->query_params = params; - ctx->request->query_params["id"] = "42"; - int status = handler->ctx_handler(ctx); - assert(status == 200); - assert(ctx->response->body == "script:42"); -} - -static void test_script_dir_parent_path_forbidden() { - hv::HttpService service; - service.Script("/api/", "tmp/http_lua_handler_test/api"); - - http_handler* handler = NULL; - std::map params; - int ret = service.GetRoute("/api/foo/..bar", HTTP_GET, &handler, params); - assert(ret == 0); - assert(handler != NULL); - assert(handler->ctx_handler != NULL); - - HttpContextPtr ctx = make_ctx("GET", "/api/foo/..bar"); - ctx->service = &service; - int status = handler->ctx_handler(ctx); - assert(status == HTTP_STATUS_FORBIDDEN); -} - -static void test_unknown_script_suffix() { - std::string script = write_script("unknown.py", "def handle(ctx): pass\n"); - - hv::HttpScriptHandler handler(script.c_str()); - HttpContextPtr ctx = make_ctx("GET", "/unknown"); - int status = handler(ctx); - assert(status == HTTP_STATUS_NOT_IMPLEMENTED); - assert(ctx->response->status_code == HTTP_STATUS_NOT_IMPLEMENTED); -} - -int main() { - test_text_response(); - test_json_response(); - test_method_function_preferred(); - test_method_function_fallback_to_handle(); - test_script_dir_mapping(); - test_script_dir_parent_path_forbidden(); - test_unknown_script_suffix(); + HttpService service; + service.GET("/sleep", hv::HttpScriptHandler(script.c_str())); + + hv::HttpServer server(&service); + server.setThreadNum(1); // single IO thread: proves coroutine concurrency + server.setPort(18080); + server.start(); + hv_msleep(200); // let the server come up + + const int N = 5; + std::vector threads; + std::atomic ok_count{0}; + + uint64_t start = gettimeofday_ms(); + for (int i = 0; i < N; ++i) { + threads.emplace_back([i, &ok_count]() { + char url[128]; + snprintf(url, sizeof(url), "http://127.0.0.1:18080/sleep?id=%d", i); + auto resp = requests::get(url); + if (resp == NULL) return; + if (resp->status_code != 200) return; + char needle[32]; + snprintf(needle, sizeof(needle), "\"id\": \"%d\"", i); + if (resp->body.find("\"ok\": true") != std::string::npos && + resp->body.find(needle) != std::string::npos) { + ok_count++; + } + }); + } + for (auto& t : threads) t.join(); + uint64_t elapsed = gettimeofday_ms() - start; + + server.stop(); + hv_msleep(100); + + printf("ok_count=%d/%d elapsed=%llums (each handler sleeps 300ms)\n", + ok_count.load(), N, (unsigned long long)elapsed); + assert(ok_count.load() == N); + // Concurrent: total should be well under N*300ms. Allow generous slack for + // CI, but it must be clearly less than serial execution (5*300=1500ms). + assert(elapsed < 1200); printf("ALL http_lua_handler_test PASSED\n"); return 0; } diff --git a/unittest/http_script_handler_test.cpp b/unittest/http_script_handler_test.cpp new file mode 100644 index 000000000..04b46c6a6 --- /dev/null +++ b/unittest/http_script_handler_test.cpp @@ -0,0 +1,180 @@ +#include "HttpScriptHandler.h" + +#include +#include +#include + +#include "hbase.h" +#include "hfile.h" +#include "hpath.h" +#include "HttpService.h" +#include "HttpContext.h" +#include "EventLoop.h" + +static std::string write_script(const char* name, const char* content) { + std::string dir = "tmp/http_script_handler_test"; + const char* slash = strrchr(name, '/'); + if (slash) { + dir = HPath::join(dir, std::string(name, slash - name)); + } + hv_mkdir_p(dir.c_str()); + std::string path = HPath::join("tmp/http_script_handler_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + assert(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +static HttpContextPtr make_ctx(const char* method, const char* path) { + HttpContextPtr ctx = std::make_shared(); + ctx->request = std::make_shared(); + ctx->response = std::make_shared(); + ctx->request->method = http_method_enum(method); + ctx->request->path = path; + ctx->request->query_params["id"] = "42"; + ctx->request->headers["X-Test"] = "header-value"; + ctx->request->body = "request-body"; + return ctx; +} + +static void test_text_response() { + std::string script = write_script("text.lua", + "function handle(ctx)\n" + " ctx:status(201)\n" + " ctx:set_header('X-Lua', ctx:query('id'))\n" + " return ctx:text(ctx:method() .. ' ' .. ctx:path() .. ' ' .. ctx:header('X-Test'))\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/hello"); + int status = handler(ctx); + assert(status == 201); + assert(ctx->response->status_code == 201); + assert(ctx->response->body == "GET /hello header-value"); + assert(ctx->response->GetHeader("X-Lua") == "42"); + assert(ctx->response->ContentType() == TEXT_PLAIN); +} + +static void test_json_response() { + std::string script = write_script("json.lua", + "function handle(ctx)\n" + " return ctx:json({ok=true, id=ctx:query('id')})\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("POST", "/json"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->ContentType() == APPLICATION_JSON); + assert(ctx->response->body.find("\"ok\": true") != std::string::npos); + assert(ctx->response->body.find("\"id\": \"42\"") != std::string::npos); +} + +static void test_method_function_preferred() { + std::string script = write_script("method.lua", + "function get(ctx)\n" + " return ctx:text('get:' .. ctx:query('id'))\n" + "end\n" + "function handle(ctx)\n" + " return ctx:text('handle')\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/method"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->body == "get:42"); +} + +static void test_method_function_fallback_to_handle() { + std::string script = write_script("method_fallback.lua", + "function get(ctx)\n" + " return ctx:text('get')\n" + "end\n" + "function handle(ctx)\n" + " return ctx:text('fallback:' .. ctx:method())\n" + "end\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("POST", "/method"); + int status = handler(ctx); + assert(status == 200); + assert(ctx->response->body == "fallback:POST"); +} + +static void test_script_dir_mapping() { + write_script("api/user.lua", + "function handle(ctx)\n" + " return ctx:text('script:' .. ctx:query('id'))\n" + "end\n"); + + hv::HttpService service; + service.Script("/api/", "tmp/http_script_handler_test/api"); + + http_handler* handler = NULL; + std::map params; + int ret = service.GetRoute("/api/user?id=42", HTTP_GET, &handler, params); + assert(ret == 0); + assert(handler != NULL); + assert(handler->ctx_handler != NULL); + + HttpContextPtr ctx = make_ctx("GET", "/api/user"); + ctx->service = &service; + ctx->request->query_params = params; + ctx->request->query_params["id"] = "42"; + int status = handler->ctx_handler(ctx); + assert(status == 200); + assert(ctx->response->body == "script:42"); +} + +static void test_script_dir_parent_path_forbidden() { + hv::HttpService service; + service.Script("/api/", "tmp/http_script_handler_test/api"); + + http_handler* handler = NULL; + std::map params; + int ret = service.GetRoute("/api/foo/..bar", HTTP_GET, &handler, params); + assert(ret == 0); + assert(handler != NULL); + assert(handler->ctx_handler != NULL); + + HttpContextPtr ctx = make_ctx("GET", "/api/foo/..bar"); + ctx->service = &service; + int status = handler->ctx_handler(ctx); + assert(status == HTTP_STATUS_FORBIDDEN); +} + +static void test_unknown_script_suffix() { + std::string script = write_script("unknown.py", "def handle(ctx): pass\n"); + + hv::HttpScriptHandler handler(script.c_str()); + HttpContextPtr ctx = make_ctx("GET", "/unknown"); + int status = handler(ctx); + assert(status == HTTP_STATUS_NOT_IMPLEMENTED); + assert(ctx->response->status_code == HTTP_STATUS_NOT_IMPLEMENTED); +} + +int main() { + // HttpLuaHandler runs on the IO thread's per-loop lua_State, obtained via + // currentThreadEventLoop. Bind an EventLoop to this thread's TLS so the + // handler can create/reuse its lua_State (these scripts finish synchronously). + // Use make_shared (not a stack object) so that if a script ever reaches a + // client binding (hv.http/redis/ws/mqtt -> currentThreadEventLoopPtr -> + // shared_from_this()), it resolves to a real EventLoopPtr instead of the + // bad_weak_ptr fallback — matching the other lua tests and avoiding a future + // "silently no shared loop" trap. + hv::EventLoopPtr loop = std::make_shared(); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, loop.get()); + + test_text_response(); + test_json_response(); + test_method_function_preferred(); + test_method_function_fallback_to_handle(); + test_script_dir_mapping(); + test_script_dir_parent_path_forbidden(); + test_unknown_script_suffix(); + printf("ALL http_script_handler_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp new file mode 100644 index 000000000..a9ebad045 --- /dev/null +++ b/unittest/lua_binding_test.cpp @@ -0,0 +1,269 @@ +/* + * lua_binding_test — unit test for the libhv Lua binding core (lua/). + * + * Runs deterministically without network access. Drives the lua layer directly + * (not the hvlua binary) and asserts observable side effects via a tiny + * "probe" C function exposed to the scripts. + * + * Covered: + * 1. hv.version / hv.json encode+decode roundtrip + * 2. hv.setTimeout fires the callback + * 3. hv.setInterval + clearTimer (including clearTimer from within the + * timer's own callback — the re-entrant free regression) + * 4. hv.sleep suspends/resumes the coroutine (synchronous-style async) + * 5. two coroutines interleave on one loop thread (concurrency, not parallel) + */ + +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "hbase.h" +#include "hvlua.h" + +// A probe table the scripts write to, so C can assert what happened. +static std::string g_probe; + +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +// Run a script string on a fresh AUTO_FREE loop, drive the loop to completion. +static void run_script(const char* code) { + g_probe.clear(); + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE | HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); + assert(loop != NULL); + + lua_State* L = hv::hvlua_state(loop); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + + int ret = hv::hvlua_dostring(loop, code); + assert(ret == 0); + + hloop_run(loop); // AUTO_FREE closes the lua_State on return +} + +static void test_core_json() { + run_script( + "local t = hv.json.decode('{\"a\":1,\"b\":[2,3],\"c\":\"x\"}')\n" + "assert(t.a == 1)\n" + "assert(t.b[1] == 2 and t.b[2] == 3)\n" + "assert(t.c == 'x')\n" + "assert(type(hv.version()) == 'string')\n" + "local s = hv.json.encode({ok=true, n=42})\n" + "local u = hv.json.decode(s)\n" + "assert(u.ok == true and u.n == 42)\n" + "probe('json-ok')\n" + ); + assert(g_probe == "json-ok"); + printf(" test_core_json OK\n"); +} + +static void test_set_timeout() { + run_script( + "hv.setTimeout(10, function()\n" + " probe('fired')\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "fired"); + printf(" test_set_timeout OK\n"); +} + +static void test_interval_clear() { + // clearTimer is called from within the timer's own callback (re-entrant). + run_script( + "local n = 0\n" + "local id\n" + "id = hv.setInterval(10, function()\n" + " n = n + 1\n" + " probe(tostring(n))\n" + " if n >= 3 then\n" + " hv.clearTimer(id)\n" + " hv.stop()\n" + " end\n" + "end)\n" + ); + assert(g_probe == "1,2,3"); + printf(" test_interval_clear OK\n"); +} + +static void test_sleep_coroutine() { + // sleep suspends the coroutine; probe order proves it resumes after wait. + run_script( + "hv.setTimeout(1, function()\n" + " probe('a')\n" + " hv.sleep(30)\n" + " probe('b')\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "a,b"); + printf(" test_sleep_coroutine OK\n"); +} + +static void test_two_coroutines_interleave() { + // A sleeps 20ms, B sleeps 50ms; both start together. Expected wake order: + // A1,B1 (start), A2 (@~20), B2 stays..., so A's second probe precedes B's. + run_script( + "local done = 0\n" + "local function worker(name, ms)\n" + " probe(name..'1')\n" + " hv.sleep(ms)\n" + " probe(name..'2')\n" + " done = done + 1\n" + " if done == 2 then hv.stop() end\n" + "end\n" + "hv.setTimeout(1, function() worker('A', 20) end)\n" + "hv.setTimeout(1, function() worker('B', 60) end)\n" + ); + // Both start (A1,B1 in some order), then A2 before B2 since A sleeps less. + // Assert A2 appears before B2 and both first-probes precede both second. + size_t a2 = g_probe.find("A2"); + size_t b2 = g_probe.find("B2"); + size_t a1 = g_probe.find("A1"); + size_t b1 = g_probe.find("B1"); + assert(a1 != std::string::npos && b1 != std::string::npos); + assert(a2 != std::string::npos && b2 != std::string::npos); + assert(a1 < a2 && b1 < b2); // each worker's order preserved + assert(a2 < b2); // A (shorter sleep) wakes first + printf(" test_two_coroutines_interleave OK\n"); +} + +// Regression: hv.json.encode on a cyclic table must not crash (depth cap + +// lua_checkstack); it returns a depth-truncated string, not a segfault. +static void test_json_cyclic() { + run_script( + "hv.setTimeout(1, function()\n" + " local t = {}; t.self = t\n" + " local s, e = hv.json.encode(t)\n" + " assert(type(s) == 'string' and #s > 0)\n" // truncated, but produced + " probe('cyclic-ok')\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "cyclic-ok"); + printf(" test_json_cyclic OK\n"); +} + +// Regression: hv.json.encode on non-UTF-8 bytes must return (nil, err) instead +// of throwing an uncaught nlohmann exception that aborts the process. +static void test_json_non_utf8() { + run_script( + "hv.setTimeout(1, function()\n" + " local s, e = hv.json.encode({ x = '\\255\\254' })\n" + " assert(s == nil and type(e) == 'string')\n" + " probe('nonutf8-ok')\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "nonutf8-ok"); + printf(" test_json_non_utf8 OK\n"); +} + +static void test_json_decode_too_deep() { + run_script( + "local s = string.rep('[', 80)..'0'..string.rep(']', 80)\n" + "local v, e = hv.json.decode(s)\n" + "assert(v == nil and e == 'json too deep')\n" + "probe('deep-json-ok')\n" + ); + assert(g_probe == "deep-json-ok"); + printf(" test_json_decode_too_deep OK\n"); +} + +static void test_stop_before_run() { + g_probe.clear(); + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE); + assert(loop != NULL); + lua_State* L = hv::hvlua_state(loop); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + + int ret = hv::hvlua_dostring(loop, "probe('before-stop'); hv.stop()"); + assert(ret == 0); + assert(hloop_nactives(loop) > 0); + hloop_run(loop); + + assert(g_probe == "before-stop"); + printf(" test_stop_before_run OK\n"); +} + +static void test_run_is_safe_noop() { + run_script( + "hv.run()\n" + "probe('run-ok')\n" + ); + assert(g_probe == "run-ok"); + printf(" test_run_is_safe_noop OK\n"); +} + +static long run_script_alloc_delta(const char* code) { + long alloc = hv_alloc_cnt(); + long freed = hv_free_cnt(); + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE); + assert(loop != NULL); + assert(hv::hvlua_dostring(loop, code) == 0); + hloop_run(loop); + return (hv_alloc_cnt() - alloc) - (hv_free_cnt() - freed); +} + +static void test_pending_async_cleanup() { + long baseline = run_script_alloc_delta("hv.stop()"); + long pending = run_script_alloc_delta( + "for i=1,100 do hv.setInterval(60000,function() end) end\n" + "for i=1,100 do hv.setTimeout(1,function() hv.sleep(60000) end) end\n" + "hv.setTimeout(30,function() hv.stop() end)\n" + ); + assert(pending == baseline); + printf(" test_pending_async_cleanup OK\n"); +} + +// Regression: a timer registered inside another timer's callback must survive +// after that callback's coroutine is GC'd (timer stores the per-loop main +// state, not the calling coroutine). Force GC between fires. +static void test_timer_registered_in_callback_gc() { + run_script( + "hv.setTimeout(20, function()\n" + " hv.setTimeout(60, function() probe('inner') end)\n" + "end)\n" + "hv.setInterval(10, function() collectgarbage('collect') end)\n" + "hv.setTimeout(150, function() probe('done'); hv.stop() end)\n" + ); + // Must reach both without crashing (pre-fix: UAF -> SIGSEGV, no 'inner'). + assert(g_probe.find("inner") != std::string::npos); + assert(g_probe.find("done") != std::string::npos); + printf(" test_timer_registered_in_callback_gc OK\n"); +} + +int main() { + test_core_json(); + test_set_timeout(); + test_interval_clear(); + test_sleep_coroutine(); + test_two_coroutines_interleave(); + test_json_cyclic(); + test_json_non_utf8(); + test_stop_before_run(); + test_run_is_safe_noop(); + test_json_decode_too_deep(); + test_pending_async_cleanup(); + test_timer_registered_in_callback_gc(); + printf("ALL lua_binding_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_http_test.cpp b/unittest/lua_http_test.cpp new file mode 100644 index 000000000..31b72b48a --- /dev/null +++ b/unittest/lua_http_test.cpp @@ -0,0 +1,77 @@ +/* + * lua_http_test — unit test for the hv.http Lua binding (hvlua_http.cpp). + * + * Starts an in-process HttpServer, then runs a Lua script on a shared-ptr + * EventLoop (the single-loop model: AsyncHttpClient is bound to this loop, so + * its completion callback fires on the same thread and resumes the coroutine + * directly). Asserts hv.http.get returns the expected status/body. + */ + +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "EventLoop.h" +#include "HttpServer.h" +#include "HttpService.h" +#include "hvlua.h" + +static std::string g_probe; +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +int main() { + // In-process HTTP server on its own thread. + HttpService service; + service.GET("/ping", [](HttpRequest* req, HttpResponse* resp) { + resp->body = "pong"; + return 200; + }); + hv::HttpServer server(&service); + server.setPort(20801); + server.setThreadNum(1); + server.start(); + hv_msleep(200); + + // hvlua-style runtime loop: a default-constructed EventLoop owns its own + // hloop (auto-freed) and, via shared_ptr, lets currentThreadEventLoopPtr / + // shared_from_this() hand this loop to the per-state AsyncHttpClient. + // EventLoop::run() publishes it as this thread's loop (TLS). + hv::EventLoopPtr loop = std::make_shared(); + lua_State* L = hvlua_state(loop->loop()); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + + int ret = hvlua_dostring(loop->loop(), + "hv.setTimeout(1, function()\n" + " local resp, err = hv.http.get('http://127.0.0.1:20801/ping')\n" + " if err then probe('err:'..err)\n" + " else probe(tostring(resp.status)); probe(resp.body) end\n" + " hv.stop()\n" + "end)\n" + ); + assert(ret == 0); + loop->run(); + + loop.reset(); + server.stop(); + hv_msleep(100); + + printf("hv.http result: %s\n", g_probe.c_str()); + assert(g_probe == "200,pong"); + printf("ALL lua_http_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_io_test.cpp b/unittest/lua_io_test.cpp new file mode 100644 index 000000000..c505a4986 --- /dev/null +++ b/unittest/lua_io_test.cpp @@ -0,0 +1,147 @@ +/* + * lua_io_test — unit test for the libhv Lua event-layer IO binding (hvlua_event.c). + * + * Runs deterministically on a single loop, no external processes: a Lua TCP + * echo server and a client run on the same loop; the client drives the checks + * and stops the loop. Covers: + * 1. hv.tcpServer + hv.connect + conn:read/write echo round-trip + * 2. conn:setUnpack length_field framing (one read == one packet) + * 3. hv.udpServer + hv.udpClient sendto/recvfrom + */ + +#include +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "hvlua.h" + +static std::string g_probe; + +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +static void run_script(const char* code) { + g_probe.clear(); + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE | HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); + assert(loop != NULL); + lua_State* L = hvlua_state(loop); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + int ret = hvlua_dostring(loop, code); + assert(ret == 0); + hloop_run(loop); // AUTO_FREE closes the lua_State on return +} + +static void test_tcp_echo() { + run_script( + "hv.tcpServer('127.0.0.1', 20701, function(conn)\n" + " while true do\n" + " local d, e = conn:read()\n" + " if e then break end\n" + " conn:write(d)\n" + " end\n" + "end)\n" + "hv.setTimeout(1, function()\n" + " local c, err = hv.connect('127.0.0.1', 20701, 2000)\n" + " if err then probe('connect-err') hv.stop() return end\n" + " c:write('ping')\n" + " local d = c:read()\n" + " probe(d)\n" + " c:close()\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "ping"); + printf(" test_tcp_echo OK\n"); +} + +static void test_tcp_unpack() { + // server echoes raw bytes; client frames length_field packets and expects + // one read == one packet. + run_script( + "hv.tcpServer('127.0.0.1', 20702, function(conn)\n" + " while true do\n" + " local d, e = conn:read()\n" + " if e then break end\n" + " conn:write(d)\n" + " end\n" + "end)\n" + "local function frame(body)\n" + " local n = #body\n" + " return string.char(0, (n>>24)&255,(n>>16)&255,(n>>8)&255,n&255) .. body\n" + "end\n" + "hv.setTimeout(1, function()\n" + " local c = hv.connect('127.0.0.1', 20702, 2000)\n" + " c:setUnpack({mode='length_field', body_offset=5, length_field_offset=1,\n" + " length_field_bytes=4, length_field_coding='be'})\n" + " c:write(frame('aa'))\n" + " c:write(frame('bbbb'))\n" + " for i=1,2 do\n" + " local pkt = c:read()\n" + " probe(pkt:sub(6))\n" + " end\n" + " c:close()\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "aa,bbbb"); + printf(" test_tcp_unpack OK\n"); +} + +static void test_udp_echo() { + run_script( + "hv.udpServer('127.0.0.1', 20703, function(sock, data, peer)\n" + " sock:sendto(data)\n" + "end)\n" + "hv.setTimeout(1, function()\n" + " local s = hv.udpClient('127.0.0.1', 20703)\n" + " s:sendto('hello-udp')\n" + " local d = s:recvfrom()\n" + " probe(d)\n" + " s:close()\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "hello-udp"); + printf(" test_udp_echo OK\n"); +} + +static void test_invalid_read_and_unpack_options() { + run_script( + "hv.setTimeout(1, function()\n" + " local sock = hv.udpClient('127.0.0.1', 1)\n" + " assert(not pcall(function() sock:readbytes(-1) end))\n" + " assert(not pcall(function() sock:setUnpack({mode='fixed', fixed_length=0}) end))\n" + " assert(not pcall(function() sock:setUnpack({mode='delimiter', delimiter=''}) end))\n" + " assert(not pcall(function() sock:setUnpack({mode='length_field', body_offset=1, length_field_offset=1, length_field_bytes=4}) end))\n" + " sock:close()\n" + " probe('invalid-ok')\n" + " hv.stop()\n" + "end)\n" + ); + assert(g_probe == "invalid-ok"); + printf(" test_invalid_read_and_unpack_options OK\n"); +} + +int main() { + test_tcp_echo(); + test_tcp_unpack(); + test_udp_echo(); + test_invalid_read_and_unpack_options(); + printf("ALL lua_io_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_mqtt_test.cpp b/unittest/lua_mqtt_test.cpp new file mode 100644 index 000000000..f976885b6 --- /dev/null +++ b/unittest/lua_mqtt_test.cpp @@ -0,0 +1,89 @@ +/* + * lua_mqtt_test — smoke test for the hv.mqtt Lua binding (hvlua_mqtt.cpp). + * + * No live MQTT broker is required: this verifies the module registers under the + * hv.* table and that its connect() failure path returns (nil, err) on the + * coroutine without crashing or hanging (connection refused to a closed port). + * The single-loop model means the failure callback fires on this thread and + * resumes the coroutine. + */ + +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "mqtt_client.h" +#include "EventLoop.h" +#include "hvlua.h" + +using namespace hv; + +static std::string g_probe; +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +static void test_owned_loop_run_then_free() { + mqtt_client_t* client = mqtt_client_new(NULL); + assert(client != NULL); + htimer_add(client->loop, [](htimer_t* timer) { + hloop_stop(hevent_loop(timer)); + }, 1, 1); + mqtt_client_run(client); + assert(client->loop == NULL); + assert(client->io == NULL); + assert(client->timer == NULL); + mqtt_client_free(client); +} + +int main() { + test_owned_loop_run_then_free(); + + hv::EventLoopPtr loop = std::make_shared(); + lua_State* L = hvlua_state(loop->loop()); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + + int ret = hvlua_dostring(loop->loop(), + "assert(type(hv.mqtt) == 'table', 'hv.mqtt missing')\n" + "assert(type(hv.mqtt.connect) == 'function', 'hv.mqtt.connect missing')\n" + "probe('registered')\n" + "hv.setTimeout(1, function()\n" + // connect to a port with nothing listening -> (nil, err), not a crash. + " local m, merr = hv.mqtt.connect({ host='127.0.0.1', port=1 })\n" + " if m == nil then probe('mqtt:'..(merr or '?')) else probe('mqtt:opened?') end\n" + " hv.stop()\n" + "end)\n" + ); + assert(ret == 0); + + // Guard against a hang: stop the loop after 3s no matter what. + htimer_add(loop->loop(), [](htimer_t* t){ + hloop_stop(hevent_loop(t)); + }, 3000, 1); + + loop->run(); + loop.reset(); + + printf("hv.mqtt result: %s\n", g_probe.c_str()); + // "registered" always; the connect attempt must come back as a non-crashing + // failure (exact err text is platform/timing dependent, so we only assert it + // reported back rather than "succeeding"). + assert(g_probe.find("registered") != std::string::npos); + assert(g_probe.find("mqtt:") != std::string::npos); + assert(g_probe.find("opened?") == std::string::npos); + printf("ALL lua_mqtt_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_redis_test.cpp b/unittest/lua_redis_test.cpp new file mode 100644 index 000000000..5885038b2 --- /dev/null +++ b/unittest/lua_redis_test.cpp @@ -0,0 +1,88 @@ +/* + * lua_redis_test — unit test for the hv.redis Lua binding (hvlua_redis.cpp). + * + * Starts an in-process FakeRedisServer, then runs a Lua script on a shared-ptr + * EventLoop (single-loop model: AsyncRedisClient is bound to this loop, so its + * command completion callback fires on the same thread and resumes the coroutine + * directly). Asserts hv.redis command + verb sugar map replies to Lua values, + * and that an error reply comes back as (nil, err). + */ + +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "EventLoop.h" +#include "hvlua.h" +#include "redis_test_server.h" + +using namespace hv; + +static std::string g_probe; +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +int main() { + // In-process fake redis server: PING->PONG, SET->OK, GET k -> "v", + // INCR -> 1, anything else -> error reply. + FakeRedisServer server; + server.setCommandHandler([](const RedisCommand& cmd) { + RedisReply reply; + if (cmd[0] == "PING") { + reply.type = REDIS_REPLY_STRING; reply.str = "PONG"; + } else if (cmd[0] == "SET") { + reply.type = REDIS_REPLY_STRING; reply.str = "OK"; + } else if (cmd[0] == "GET") { + reply.type = REDIS_REPLY_STRING; reply.str = "v"; reply.bulk = true; + } else if (cmd[0] == "INCR") { + reply.type = REDIS_REPLY_INTEGER; reply.integer = 1; + } else { + reply.type = REDIS_REPLY_ERROR; reply.str = "ERR unsupported"; + } + return reply; + }); + server.start(); + + hv::EventLoopPtr loop = std::make_shared(); + lua_State* L = hvlua_state(loop->loop()); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + lua_pushinteger(L, server.port()); + lua_setglobal(L, "REDIS_PORT"); + + int ret = hvlua_dostring(loop->loop(), + "hv.setTimeout(1, function()\n" + " local r = hv.redis.new({ host='127.0.0.1', port=REDIS_PORT })\n" + " local ok = r:set('k', 'v'); probe(ok)\n" // OK + " local v = r:get('k'); probe(v)\n" // v + " local n = r:incr('c'); probe(tostring(n))\n" // 1 + " local cmd = r:command({'PING'}); probe(cmd)\n" // PONG + " local bad, err = r:command('BADCMD')\n" // nil, "ERR unsupported" + " if bad == nil then probe('err:'..err) end\n" + " hv.stop()\n" + "end)\n" + ); + assert(ret == 0); + loop->run(); + + loop.reset(); + server.stop(); + + printf("hv.redis result: %s\n", g_probe.c_str()); + assert(g_probe == "OK,v,1,PONG,err:ERR unsupported"); + printf("ALL lua_redis_test PASSED\n"); + return 0; +} diff --git a/unittest/lua_ws_test.cpp b/unittest/lua_ws_test.cpp new file mode 100644 index 000000000..851509346 --- /dev/null +++ b/unittest/lua_ws_test.cpp @@ -0,0 +1,97 @@ +/* + * lua_ws_test — end-to-end test for the hv.ws Lua binding (hvlua_ws.cpp). + * + * Starts an in-process WebSocket echo server on its own thread, then runs a Lua + * script on a shared-ptr EventLoop (single-loop model: WebSocketClient bound to + * this loop; onopen/onmessage fire on the same thread and resume the coroutine). + * Asserts hv.ws.connect + ws:send + ws:recv round-trips the echoed message. + */ + +#include +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "EventLoop.h" +#include "WebSocketServer.h" +#include "hvlua.h" + +using namespace hv; + +static std::string g_probe; +static int l_probe(lua_State* L) { + size_t len = 0; + const char* s = luaL_checklstring(L, 1, &len); + if (!g_probe.empty()) g_probe += ","; + g_probe.append(s, len); + return 0; +} + +int main() { + // In-process WebSocket echo server on its own thread. + WebSocketService ws; + ws.onmessage = [](const WebSocketChannelPtr& channel, const std::string& msg) { + channel->send(msg); // echo + }; + hv::WebSocketServer server(&ws); + server.setPort(20802); + server.setThreadNum(1); + server.start(); + hv_msleep(200); + + hv::EventLoopPtr loop = std::make_shared(); + lua_State* L = hvlua_state(loop->loop()); + assert(L != NULL); + lua_pushcfunction(L, l_probe); + lua_setglobal(L, "probe"); + + int ret = hvlua_dostring(loop->loop(), + "hv.setTimeout(1, function()\n" + " local ws, err = hv.ws.connect('ws://127.0.0.1:20802/')\n" + " if err then probe('err:'..err); hv.stop(); return end\n" + " probe('opened')\n" + " ws:send('hello')\n" + " local msg, rerr = ws:recv()\n" + " if rerr then probe('recverr:'..rerr) else probe(msg) end\n" + " ws:send('world')\n" + " local msg2 = ws:recv()\n" + " probe(msg2)\n" + " ws:close()\n" + // Second connect exercises the opts table: headers + ping_interval + + // reconnect. This proves the opts/reconnect parsing + setReconnect path + // does not break a normal round-trip (full drop->reconnect behavior is + // covered by the C++ websocket_client_test example). + " local ws2, e2 = hv.ws.connect('ws://127.0.0.1:20802/', {\n" + " headers = { ['X-Test'] = 'v' },\n" + " ping_interval = 10000,\n" + " reconnect = { min_delay = 1000, max_delay = 10000, delay_policy = 2, max_retry = 3 },\n" + " })\n" + " if e2 then probe('err2:'..e2); hv.stop(); return end\n" + " probe('opened2')\n" + " ws2:send('again')\n" + " local m3 = ws2:recv()\n" + " probe(m3)\n" + " ws2:close()\n" + " hv.stop()\n" + "end)\n" + ); + assert(ret == 0); + + // hang guard. + htimer_add(loop->loop(), [](htimer_t* t){ hloop_stop(hevent_loop(t)); }, 5000, 1); + loop->run(); + loop.reset(); + server.stop(); + hv_msleep(100); + + printf("hv.ws result: %s\n", g_probe.c_str()); + assert(g_probe == "opened,hello,world,opened2,again"); + printf("ALL lua_ws_test PASSED\n"); + return 0; +} diff --git a/unittest/redis_async_client_test.cpp b/unittest/redis_async_client_test.cpp index ef8999dce..7809425f5 100644 --- a/unittest/redis_async_client_test.cpp +++ b/unittest/redis_async_client_test.cpp @@ -274,25 +274,56 @@ static void test_external_loop_stopped_request_completes_once() { assert(ret == ERR_CONNECT || ret == 0); } -static void test_external_loop_not_running_rejects_start_and_command() { +// Under the unified TcpClient model, a client constructed with an external loop +// that is not yet running drives that loop on its OWN worker thread when started +// (matching TcpClientTmpl / WebSocketClient), instead of the old redis-specific +// ERR_CONNECT rejection. start(true) blocks until that thread's loop is running, +// so the subsequent command() is served normally. +static void test_external_loop_not_running_is_driven_by_own_thread() { + FakeRedisServer server; + server.setCommandHandler([](const RedisCommand& cmd) { + RedisReply reply; + if (cmd[0] == "PING") { + reply.type = REDIS_REPLY_STRING; + reply.str = "PONG"; + } + else { + reply.type = REDIS_REPLY_ERROR; + reply.str = "ERR unsupported"; + } + return reply; + }); + server.start(); + EventLoopPtr loop = std::make_shared(); AsyncRedisClient client(loop); + client.setHost("127.0.0.1"); + client.setPort(server.port()); + client.start(true); // spins its own thread to run the external loop - int error_code = 0; - client.onError = [&](int code) { - error_code = code; - }; - client.start(false); - assert(error_code == ERR_CONNECT); + std::mutex mutex; + std::condition_variable cv; + bool done = false; + RedisResult result; - int callback_count = 0; - int ret = client.command(RedisCommand{"PING"}, [&](const RedisResult& result) { - ++callback_count; - assert(result.code == ERR_CONNECT); + int ret = client.command(RedisCommand{"PING"}, [&](const RedisResult& redis_result) { + std::lock_guard lock(mutex); + result = redis_result; + done = true; + cv.notify_one(); }); + assert(ret == 0); - assert(ret == ERR_CONNECT); - assert(callback_count == 1); + { + std::unique_lock lock(mutex); + bool completed = cv.wait_for(lock, std::chrono::seconds(3), [&done]() { return done; }); + assert(completed); + } + assert(result.code == 0); + assert(result.reply.asString() == "PONG"); + + client.stop(true); + server.stop(); } static void test_external_loop_running_without_start_rejects_command() { @@ -381,7 +412,7 @@ int main() { test_external_loop_mode_can_start_and_command(); test_external_loop_stopped_rejects_command(); test_external_loop_stopped_request_completes_once(); - test_external_loop_not_running_rejects_start_and_command(); + test_external_loop_not_running_is_driven_by_own_thread(); test_external_loop_running_without_start_rejects_command(); test_close_after_reply_fails_pending_once(); return 0;