From d75723540f8bb698c28a3c621c3877182f84a2bd Mon Sep 17 00:00:00 2001 From: ithewei Date: Tue, 28 Jul 2026 23:14:29 +0800 Subject: [PATCH 01/33] feat(lua): stage A - hloop lua_State slot, coroutine scheduler, hloop/hv.core/hv.dns bindings + hvlua runtime --- CMakeLists.txt | 4 + Makefile | 15 ++- event/hevent.h | 4 + event/hloop.c | 17 +++ event/hloop.h | 8 ++ examples/CMakeLists.txt | 10 ++ examples/hvlua.cpp | 69 ++++++++++++ examples/lua/dns.lua | 28 +++++ examples/lua/sleep.lua | 26 +++++ examples/lua/timer.lua | 19 ++++ lua/hv_lua.cpp | 190 +++++++++++++++++++++++++++++++ lua/hv_lua.h | 66 +++++++++++ lua/lua_hloop.cpp | 196 ++++++++++++++++++++++++++++++++ lua/lua_hv_core.cpp | 206 ++++++++++++++++++++++++++++++++++ lua/lua_hv_dns.cpp | 95 ++++++++++++++++ scripts/unittest.sh | 3 + unittest/CMakeLists.txt | 5 +- unittest/lua_binding_test.cpp | 154 +++++++++++++++++++++++++ 18 files changed, 1113 insertions(+), 2 deletions(-) create mode 100644 examples/hvlua.cpp create mode 100644 examples/lua/dns.lua create mode 100644 examples/lua/sleep.lua create mode 100644 examples/lua/timer.lua create mode 100644 lua/hv_lua.cpp create mode 100644 lua/hv_lua.h create mode 100644 lua/lua_hloop.cpp create mode 100644 lua/lua_hv_core.cpp create mode 100644 lua/lua_hv_dns.cpp create mode 100644 unittest/lua_binding_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d64326ae6..27be38b10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -239,6 +239,10 @@ endif() if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) + if(WITH_LUA) + set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hv_lua.h) + set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} lua) + endif() if(WITH_REDIS) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${REDIS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} redis) diff --git a/Makefile b/Makefile index 6eec0d1d2..855932662 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,11 @@ ifeq ($(WITH_EVPP), yes) LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp +ifeq ($(WITH_LUA), yes) +LIBHV_HEADERS += lua/hv_lua.h +LIBHV_SRCDIRS += lua +endif + ifeq ($(WITH_REDIS), yes) LIBHV_HEADERS += $(REDIS_HEADERS) LIBHV_SRCDIRS += redis @@ -101,6 +106,10 @@ ifeq ($(WITH_MQTT), yes) EXAMPLES += mqtt_sub mqtt_pub mqtt_client_test endif +ifeq ($(WITH_LUA), yes) +EXAMPLES += hvlua +endif + examples: $(EXAMPLES) @echo "make examples done." @@ -179,6 +188,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" @@ -320,9 +332,10 @@ ifeq ($(WITH_EVPP), yes) $(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 -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 -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 + $(RM) bin/lua_binding_test bin/http_lua_handler_test endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv 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/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/hvlua.cpp b/examples/hvlua.cpp new file mode 100644 index 000000000..36c0283e0 --- /dev/null +++ b/examples/hvlua.cpp @@ -0,0 +1,69 @@ +// hvlua: standalone Lua runtime on top of libhv's event loop. +// +// Usage: hvlua script.lua [args...] +// +// Initializes the current thread's hloop lua_State, loads and runs the script +// (inside a coroutine so it may use synchronous-style async APIs), then runs +// the event loop so timers / async IO can complete. +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "hlog.h" +#include "hv_lua.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). + hlog_set_handler(stdout_logger); + + // AUTO_FREE: the loop frees itself (and closes its lua_State via the dtor) + // when hloop_run returns; we must NOT call hloop_free afterwards. + // QUIT_WHEN_NO_ACTIVE_EVENTS: exit once the script and all timers/async + // work are done, so a script without an explicit hloop.stop() still ends. + hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE | HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); + if (loop == NULL) { + fprintf(stderr, "hvlua: failed to create event loop\n"); + return 1; + } + + // Create the per-loop lua_State and expose script args as global `arg`. + lua_State* L = hv::hvlua_state(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"); + + // Run the script (may yield on async ops). + if (hv::hvlua_dofile(loop, script) != 0) { + // hloop_run has not been entered; with AUTO_FREE we still must not call + // hloop_free. Let process exit reclaim resources. + return 1; + } + + // Drive the loop until the script calls hloop.stop() or no work remains. + // The loop auto-frees itself (and closes the lua_State) on return. + hloop_run(loop); + return 0; +} diff --git a/examples/lua/dns.lua b/examples/lua/dns.lua new file mode 100644 index 000000000..c9b06ca19 --- /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 + hloop.setTimeout(1, function() + local addrs, err = hv.dns.resolve(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 + hloop.stop() + end + end) +end diff --git a/examples/lua/sleep.lua b/examples/lua/sleep.lua new file mode 100644 index 000000000..574279758 --- /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 hloop.sleep() suspends WITHOUT blocking the +-- event loop: the two "workers" below interleave rather than run serially. + +hloop_workers_done = 0 + +local function worker(name, ms) + for i = 1, 3 do + hv.log(name, "step", i) + hloop.sleep(ms) -- looks blocking, actually yields to the loop + end + hv.log(name, "done") + hloop_workers_done = hloop_workers_done + 1 + if hloop_workers_done == 2 then + hloop.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) +hloop.setTimeout(1, function() worker("A", 300) end) +hloop.setTimeout(1, function() worker("B", 500) end) diff --git a/examples/lua/timer.lua b/examples/lua/timer.lua new file mode 100644 index 000000000..67e978c4d --- /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, now =", hv.now()) + +local n = 0 +local id +id = hloop.setInterval(500, function() + n = n + 1 + hv.log("tick", n) + if n >= 3 then + hloop.clearTimer(id) + hv.log("cleared interval after 3 ticks") + hloop.setTimeout(200, function() + hv.log("bye") + hloop.stop() + end) + end +end) diff --git a/lua/hv_lua.cpp b/lua/hv_lua.cpp new file mode 100644 index 000000000..e9880ec41 --- /dev/null +++ b/lua/hv_lua.cpp @@ -0,0 +1,190 @@ +#include "hv_lua.h" + +#include + +extern "C" { +#include +#include +#include +} + +#include "hlog.h" + +// Module registration entry points implemented in the per-module files. +namespace hv { +void hvlua_open_hloop(lua_State* L); // lua_hloop.cpp -> global "hloop" +void hvlua_open_core(lua_State* L); // lua_hv_core.cpp -> table "hv" +void hvlua_open_dns(lua_State* L); // lua_hv_dns.cpp -> hv.dns +} + +namespace hv { + +// --------------------------------------------------------------------------- +// 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 generation/valid +// flag so a stale resume (loop teardown, double resume) is a safe no-op. +struct HvLuaCoroutine { + lua_State* main; // the per-loop main state (owns the registry) + lua_State* L; // the coroutine thread + int ref; // luaL_ref of the thread in the registry, LUA_NOREF if freed +}; + +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 = new HvLuaCoroutine(); + co->main = L; + 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 + return co; +} + +lua_State* hvlua_coroutine_state(HvLuaCoroutine* co) { + if (co == NULL || co->ref == LUA_NOREF) return NULL; + return co->L; +} + +void hvlua_cancel(HvLuaCoroutine* co) { + if (co == NULL) return; + if (co->ref != LUA_NOREF) { + luaL_unref(co->L, LUA_REGISTRYINDEX, co->ref); + co->ref = LUA_NOREF; + } + delete co; +} + +void hvlua_resume(HvLuaCoroutine* co, int nresults) { + if (co == NULL) return; + if (co->ref == LUA_NOREF) { // stale: already resumed/freed + delete co; + return; + } + lua_State* L = co->L; + int ref = co->ref; + // Release the registry ref BEFORE resuming so a re-entrant resume can't + // double-free, and so the thread can be GC'd once it finishes. + co->ref = LUA_NOREF; + + int status = lua_resume(L, NULL, nresults +#if LUA_VERSION_NUM >= 504 + , &nresults +#endif + ); + if (status != LUA_OK && status != LUA_YIELD) { + const char* msg = lua_tostring(L, -1); + hloge("[lua] coroutine error: %s", msg ? msg : "unknown"); + } + // Unref the thread now that it has either finished or yielded again. + // (If it yielded again, a new token was created via hvlua_suspend.) + luaL_unref(L, LUA_REGISTRYINDEX, ref); + delete co; +} + +// --------------------------------------------------------------------------- +// per-loop lua_State +// --------------------------------------------------------------------------- + +static void hvlua_state_dtor(void* L) { + if (L) lua_close((lua_State*)L); +} + +static lua_State* hvlua_new_state(hloop_t* loop) { + lua_State* L = luaL_newstate(); + if (L == NULL) return NULL; + 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"); + + hvlua_open_hloop(L); + hvlua_open_core(L); + hvlua_open_dns(L); + + hloop_set_lua_state(loop, L, hvlua_state_dtor); + return L; +} + +lua_State* hvlua_state(hloop_t* loop) { + if (loop == NULL) return NULL; + lua_State* 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) { + lua_getfield(L, LUA_REGISTRYINDEX, "hv.loop"); + hloop_t* 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) { + // stack: [chunk] + lua_State* 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. + int ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread; stack: [chunk] + lua_pop(L, 1); // pop chunk; stack: [] + + int nres = 0; + int 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); +} + +} // namespace hv diff --git a/lua/hv_lua.h b/lua/hv_lua.h new file mode 100644 index 000000000..2508f026c --- /dev/null +++ b/lua/hv_lua.h @@ -0,0 +1,66 @@ +#ifndef HV_LUA_H_ +#define HV_LUA_H_ + +// libhv Lua binding: public C++ 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 C bindings call +// hvlua_yield()/lua_yieldk() and are resumed on the same loop thread when +// the libhv async callback fires (hvlua_resume()). +// - two layers of modules: hloop.* (pure C hloop, no evpp/http dependency) +// and hv.* (higher-level helpers/clients). + +#include "hloop.h" + +struct lua_State; + +namespace hv { + +// 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 +// hloop.* / 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 hvlua_yield(L, nresults_ignored, k) 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. +struct HvLuaCoroutine; + +// 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 `co->L`. 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); + +// Recover the owning hloop_t* for a lua_State (stashed at state creation). +hloop_t* hvlua_loop(lua_State* L); + +} // namespace hv + +#endif // HV_LUA_H_ diff --git a/lua/lua_hloop.cpp b/lua/lua_hloop.cpp new file mode 100644 index 000000000..e1a931603 --- /dev/null +++ b/lua/lua_hloop.cpp @@ -0,0 +1,196 @@ +#include "hv_lua.h" + +extern "C" { +#include +#include +#include +} + +#include "hloop.h" +#include "hlog.h" + +namespace hv { + +// 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 hloop.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. +struct LuaTimer { + lua_State* L; // per-loop main state + htimer_t* timer; + int fn_ref; // LUA_NOREF when released + bool once; + bool in_callback; + bool dead; // clearTimer requested during the callback +}; + +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; + } +} + +static void lua_timer_free(LuaTimer* lt) { + lua_timer_release_ref(lt); + delete lt; +} + +static void on_lua_timer(htimer_t* timer) { + LuaTimer* lt = (LuaTimer*)hevent_userdata(timer); + if (lt == NULL || lt->fn_ref == LUA_NOREF) return; + lua_State* L = lt->L; + + // Run the callback in a fresh coroutine so it may yield on async ops. + lt->in_callback = true; + lua_State* co = lua_newthread(L); + int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // keep coroutine alive + lua_rawgeti(co, LUA_REGISTRYINDEX, lt->fn_ref); // push callback fn onto co + + int nres = 0; + int 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 = false; + + // 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, bool once) { + hloop_t* loop = hvlua_loop(L); + luaL_checktype(L, 2, LUA_TFUNCTION); + + LuaTimer* lt = new LuaTimer(); + lt->L = L; + lt->timer = NULL; + lt->once = once; + lt->in_callback = false; + lt->dead = false; + // ref the callback function (arg 2) + lua_pushvalue(L, 2); + lt->fn_ref = luaL_ref(L, LUA_REGISTRYINDEX); + + htimer_t* 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; + return timer; +} + +// hloop.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, true); + if (timer == NULL) { lua_pushnil(L); return 1; } + lua_pushlightuserdata(L, timer); + return 1; +} + +// hloop.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, false); + if (timer == NULL) { lua_pushnil(L); return 1; } + lua_pushlightuserdata(L, timer); + return 1; +} + +// hloop.clearTimer(handle) +static int l_hloop_clearTimer(lua_State* L) { + if (!lua_islightuserdata(L, 1)) return 0; + htimer_t* timer = (htimer_t*)lua_touserdata(L, 1); + if (timer == NULL) return 0; + LuaTimer* 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. + lt->dead = true; + lua_timer_release_ref(lt); + } else { + lua_timer_free(lt); + } + } + return 0; +} + +// ---- hloop.sleep(ms): suspend the running coroutine for ms milliseconds ---- + +struct SleepCtx { + HvLuaCoroutine* co; + htimer_t* timer; +}; + +static void on_sleep_timer(htimer_t* timer) { + SleepCtx* s = (SleepCtx*)hevent_userdata(timer); + HvLuaCoroutine* co = s->co; + delete 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; +} + +// hloop.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 = new SleepCtx(); + s->co = hvlua_suspend(L); + s->timer = htimer_add(loop, on_sleep_timer, ms, 1); + hevent_set_userdata(s->timer, s); + + return lua_yieldk(L, 0, (lua_KContext)0, sleep_k); +} + +// hloop.run() / hloop.stop() +static int l_hloop_run(lua_State* L) { + hloop_run(hvlua_loop(L)); + return 0; +} + +static int l_hloop_stop(lua_State* L) { + hloop_stop(hvlua_loop(L)); + return 0; +} + +static const luaL_Reg hloop_funcs[] = { + { "setTimeout", l_hloop_setTimeout }, + { "setInterval", l_hloop_setInterval }, + { "clearTimer", l_hloop_clearTimer }, + { "sleep", l_hloop_sleep }, + { "run", l_hloop_run }, + { "stop", l_hloop_stop }, + { NULL, NULL } +}; + +void hvlua_open_hloop(lua_State* L) { + luaL_newlib(L, hloop_funcs); + lua_setglobal(L, "hloop"); +} + +} // namespace hv diff --git a/lua/lua_hv_core.cpp b/lua/lua_hv_core.cpp new file mode 100644 index 000000000..feb6cf4ea --- /dev/null +++ b/lua/lua_hv_core.cpp @@ -0,0 +1,206 @@ +#include "hv_lua.h" + +#include +#include + +extern "C" { +#include +#include +#include +} + +#include "hlog.h" +#include "hstring.h" +#include "json.hpp" + +using nlohmann::json; + +namespace hv { + +// ---- lua <-> json conversion (shared style with HttpLuaHandler) ---- + +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; + } +} + +static void json_to_lua(lua_State* L, const json& j) { + switch (j.type()) { + case json::value_t::null: + lua_pushnil(L); + break; + case json::value_t::boolean: + lua_pushboolean(L, j.get()); + break; + case json::value_t::number_integer: + lua_pushinteger(L, (lua_Integer)j.get()); + break; + case json::value_t::number_unsigned: + lua_pushinteger(L, (lua_Integer)j.get()); + break; + case json::value_t::number_float: + lua_pushnumber(L, j.get()); + break; + case json::value_t::string: { + const std::string& s = j.get_ref(); + lua_pushlstring(L, s.data(), s.size()); + break; + } + case json::value_t::array: { + lua_createtable(L, (int)j.size(), 0); + int i = 1; + for (const auto& item : j) { + json_to_lua(L, item); + lua_seti(L, -2, i++); + } + break; + } + case json::value_t::object: { + 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()); + json_to_lua(L, it.value()); + lua_settable(L, -3); + } + break; + } + default: + lua_pushnil(L); + break; + } +} + +// ---- hv.* functions ---- + +// hv.log(...) +static int l_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; +} + +// hv.now() -> unix seconds +static int l_hv_now(lua_State* L) { + lua_pushinteger(L, (lua_Integer)time(NULL)); + return 1; +} + +// hv.json.encode(value) -> string +static int l_hv_json_encode(lua_State* L) { + json j = lua_to_json(L, 1); + std::string s = j.dump(); + lua_pushlstring(L, s.data(), s.size()); + return 1; +} + +// 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; + } + json_to_lua(L, j); + return 1; +} + +static const luaL_Reg hv_funcs[] = { + { "log", l_hv_log }, + { "now", l_hv_now }, + { NULL, NULL } +}; + +static const luaL_Reg hv_json_funcs[] = { + { "encode", l_hv_json_encode }, + { "decode", l_hv_json_decode }, + { NULL, NULL } +}; + +void hvlua_open_core(lua_State* L) { + // create/get global "hv" + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_setfuncs(L, hv_funcs, 0); + + // hv.json subtable + luaL_newlib(L, hv_json_funcs); + lua_setfield(L, -2, "json"); + + lua_setglobal(L, "hv"); +} + +} // namespace hv diff --git a/lua/lua_hv_dns.cpp b/lua/lua_hv_dns.cpp new file mode 100644 index 000000000..5d373f2bb --- /dev/null +++ b/lua/lua_hv_dns.cpp @@ -0,0 +1,95 @@ +#include "hv_lua.h" + +extern "C" { +#include +#include +#include +} + +#include "hdns.h" +#include "hsocket.h" + +namespace hv { + +// hv.dns.resolve(host [, opt]) -> { ip, ip, ... } | nil, err +// +// Coroutine-synchronous: suspends the running coroutine, issues an async hdns +// query on the loop, and resumes with the resolved addresses (or nil,err) when +// the callback fires on the same loop thread. + +struct DnsCtx { + HvLuaCoroutine* co; +}; + +static void on_dns_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { + (void)query; + DnsCtx* d = (DnsCtx*)userdata; + HvLuaCoroutine* co = d->co; + delete d; + + lua_State* L = hvlua_coroutine_state(co); + if (L == NULL) { // coroutine gone (loop teardown); nothing to resume + return; + } + + if (result->status == HDNS_STATUS_OK && result->naddrs > 0) { + lua_createtable(L, result->naddrs, 0); + for (int i = 0; i < result->naddrs; ++i) { + char ip[64] = {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.dns.resolve(host) +static int l_dns_resolve(lua_State* L) { + const char* host = luaL_checkstring(L, 1); + hloop_t* loop = hvlua_loop(L); + + DnsCtx* d = new DnsCtx(); + d->co = hvlua_suspend(L); + + hdns_t* 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); + delete 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); +} + +static const luaL_Reg dns_funcs[] = { + { "resolve", l_dns_resolve }, + { NULL, NULL } +}; + +void hvlua_open_dns(lua_State* L) { + lua_getglobal(L, "hv"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_newtable(L); + } + luaL_newlib(L, dns_funcs); + lua_setfield(L, -2, "dns"); + lua_setglobal(L, "hv"); +} + +} // namespace hv diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 2a795efa1..5272b4ecd 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -32,6 +32,9 @@ 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/http_lua_handler_test ]; then bin/http_lua_handler_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 9109d8420..2dd851694 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -91,10 +91,13 @@ 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(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_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS http_lua_handler_test) +set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test http_lua_handler_test) endif() # ------event: async dns------ diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp new file mode 100644 index 000000000..c5f5bfe9b --- /dev/null +++ b/unittest/lua_binding_test.cpp @@ -0,0 +1,154 @@ +/* + * 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.now / hv.json encode+decode roundtrip + * 2. hloop.setTimeout fires the callback + * 3. hloop.setInterval + clearTimer (including clearTimer from within the + * timer's own callback — the re-entrant free regression) + * 4. hloop.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 "hv_lua.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.now()) == 'number')\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( + "hloop.setTimeout(10, function()\n" + " probe('fired')\n" + " hloop.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 = hloop.setInterval(10, function()\n" + " n = n + 1\n" + " probe(tostring(n))\n" + " if n >= 3 then\n" + " hloop.clearTimer(id)\n" + " hloop.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( + "hloop.setTimeout(1, function()\n" + " probe('a')\n" + " hloop.sleep(30)\n" + " probe('b')\n" + " hloop.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" + " hloop.sleep(ms)\n" + " probe(name..'2')\n" + " done = done + 1\n" + " if done == 2 then hloop.stop() end\n" + "end\n" + "hloop.setTimeout(1, function() worker('A', 20) end)\n" + "hloop.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"); +} + +int main() { + test_core_json(); + test_set_timeout(); + test_interval_clear(); + test_sleep_coroutine(); + test_two_coroutines_interleave(); + printf("ALL lua_binding_test PASSED\n"); + return 0; +} From 34e07917ea3efdedbe8202c930cb4fab2d428b27 Mon Sep 17 00:00:00 2001 From: ithewei Date: Tue, 28 Jul 2026 23:34:25 +0800 Subject: [PATCH 02/33] feat(lua): stage B - coroutine-based async HttpLuaHandler on IO thread Run HTTP Lua handlers in a per-request coroutine on the server IO thread's per-loop lua_State, so scripts use synchronous-style async APIs (hloop.sleep, hv.dns.resolve) that yield to the loop and complete the response via the async HttpResponseWriter. Adds hvlua_start_task scheduler entry, per-(loop,file) script env cache with hot reload, and http_lua_async_test proving concurrent non-blocking execution. --- Makefile | 15 +- docs/cn/HttpLuaHandler.md | 35 ++- examples/http_server_test.cpp | 1 + examples/scripts/async.lua | 30 +++ http/server/HttpLuaHandler.cpp | 328 ++++++++++++++++------------- http/server/HttpLuaHandler.h | 35 ++- lua/hv_lua.cpp | 76 ++++++- lua/hv_lua.h | 16 ++ scripts/unittest.sh | 3 + unittest/CMakeLists.txt | 7 +- unittest/http_lua_async_test.cpp | 93 ++++++++ unittest/http_lua_handler_test.cpp | 7 + 12 files changed, 460 insertions(+), 186 deletions(-) create mode 100644 examples/scripts/async.lua create mode 100644 unittest/http_lua_async_test.cpp diff --git a/Makefile b/Makefile index 855932662..d80be1823 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,14 @@ endif BUILD_SHARED ?= yes BUILD_STATIC ?= yes +# http/server examples compile sources directly (not linking libhv), so when +# WITH_LUA is on they must also include the lua/ binding sources that +# HttpLuaHandler depends on. +HTTP_SERVER_EXAMPLE_SRCDIRS = $(CORE_SRCDIRS) util cpputil evpp http http/server +ifeq ($(WITH_LUA), yes) +HTTP_SERVER_EXAMPLE_SRCDIRS += lua +endif + LIBHV_SRCDIRS = $(CORE_SRCDIRS) util LIBHV_HEADERS = hv.h hconfig.h hexport.h LIBHV_HEADERS += $(BASE_HEADERS) $(SSL_HEADERS) $(EVENT_HEADERS) $(UTIL_HEADERS) @@ -240,7 +248,7 @@ 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="$(HTTP_SERVER_EXAMPLE_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" @@ -333,9 +341,10 @@ ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_LUA), yes) $(MAKE) libhv $(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 -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_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 -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_async_test unittest/http_lua_async_test.cpp -Llib -lhv -pthread $(LUA_LIBS) else - $(RM) bin/lua_binding_test bin/http_lua_handler_test + $(RM) bin/lua_binding_test bin/http_lua_handler_test bin/http_lua_async_test endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index c7293c81f..b9f21c9ed 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -108,16 +108,41 @@ function handle(ctx) end ``` -## hv API +## hv / hloop API -首版只提供少量宿主能力: +脚本运行在所属 IO 线程的 **协程** 里,因此可以用 **同步写法** 调用异步能力:调用会挂起当前请求的协程、把控制权交还事件循环,结果就绪后在同一线程恢复,全程不阻塞 loop。 + +当前可用的宿主能力: + +```lua +hv.log(...) -- 日志 +hv.now() -- unix 秒 +hv.json.encode(tbl) -- table -> json string +hv.json.decode(str) -- json string -> table +hv.dns.resolve(host) -- 协程同步 DNS 解析: 返回 { ip, ... } 或 nil, err + +hloop.setTimeout(ms, fn) -- 定时器 (返回句柄) +hloop.setInterval(ms, fn) +hloop.clearTimer(handle) +hloop.sleep(ms) -- 协程同步 sleep: 挂起当前协程 ms 毫秒, 不阻塞 loop +``` + +示例(handler 内部“同步”写法,实际异步,loop 不阻塞): ```lua -hv.log(...) -hv.now() +function handle(ctx) + local addrs, err = hv.dns.resolve("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.dns.resolve` / `hloop.sleep` 处挂起时,同线程的其它请求会继续推进。注意协作式调度的语义——跨越挂起点不要对全局状态做原子性假设。 + +> TCP/HTTP client、Redis 等高层 client 的协程绑定见 `lua/` 模块,按 `WITH_LUA` / `WITH_REDIS` 等开关编入。 ## 热更新 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/scripts/async.lua b/examples/scripts/async.lua new file mode 100644 index 000000000..1e689253a --- /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.dns.resolve (which yields the request +-- coroutine to the event loop and resumes when the answer arrives) and can also +-- hloop.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 + hloop.sleep(delay) + end + + local addrs, err = hv.dns.resolve(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/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp index 85a6268d2..27964f805 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -17,6 +17,9 @@ extern "C" { #include "hstring.h" #include "htime.h" +#include "EventLoop.h" +#include "hv_lua.h" + namespace hv { namespace { @@ -204,25 +207,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 +238,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 +246,218 @@ 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; -} - -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; -} +// 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"; -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, 3); // err, 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 = 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 (!ok) { + const char* msg = co ? lua_tostring(co, -1) : NULL; + 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("lua handler: no event loop on this thread"); + return HTTP_STATUS_INTERNAL_SERVER_ERROR; + } + 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(last_error_); + ctx->response->String(err); return HTTP_STATUS_INTERNAL_SERVER_ERROR; } - std::lock_guard lock(mutex_); - return callLocked(ctx); + // 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/hv_lua.cpp b/lua/hv_lua.cpp index e9880ec41..6c2b1b705 100644 --- a/lua/hv_lua.cpp +++ b/lua/hv_lua.cpp @@ -1,5 +1,6 @@ #include "hv_lua.h" +#include #include extern "C" { @@ -32,6 +33,53 @@ struct HvLuaCoroutine { int ref; // luaL_ref of the thread in the registry, LUA_NOREF if freed }; +// A "task" is a top-level coroutine with a C completion callback. We track the +// pending callback keyed by the coroutine's lua_State* so that whichever resume +// finishes it (initial run or a later async resume) can fire on_done once. +struct HvLuaTask { + hvlua_done_cb on_done; + void* ud; + int thread_ref; // keeps the coroutine alive between resumes +}; +static std::map g_tasks; + +// 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. +static void hvlua_task_step(lua_State* co, int nresults) { + int nres = 0; + (void)nres; + int status = lua_resume(co, NULL, nresults +#if LUA_VERSION_NUM >= 504 + , &nres +#endif + ); + if (status == LUA_YIELD) { + return; // suspended again; a resume token owns the next wakeup + } + // Finished (LUA_OK) or errored: fire the completion callback, then release. + auto iter = g_tasks.find(co); + if (iter == g_tasks.end()) return; + HvLuaTask task = iter->second; + g_tasks.erase(iter); + bool ok = (status == LUA_OK); + if (task.on_done) task.on_done(task.ud, ok, co); + luaL_unref(co, LUA_REGISTRYINDEX, task.thread_ref); +} + +int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud) { + // stack (top): fn, arg1, ..., argN + lua_State* 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 + int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // pop+ref the thread + + g_tasks[co] = HvLuaTask{ on_done, ud, thread_ref }; + hvlua_task_step(co, nargs); + // If the task is no longer tracked, it finished synchronously. + return g_tasks.find(co) == g_tasks.end() ? 1 : 0; +} + 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. @@ -65,23 +113,29 @@ void hvlua_resume(HvLuaCoroutine* co, int nresults) { } lua_State* L = co->L; int ref = co->ref; - // Release the registry ref BEFORE resuming so a re-entrant resume can't - // double-free, and so the thread can be GC'd once it finishes. + // Release the suspend token's registry ref BEFORE resuming so a re-entrant + // resume can't double-free. Task tracking (g_tasks) keeps its own ref, so + // the coroutine stays alive across this transition. co->ref = LUA_NOREF; + delete co; - int status = lua_resume(L, NULL, nresults + // 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 (g_tasks.find(L) != g_tasks.end()) { + hvlua_task_step(L, nresults); + } else { + int nres = 0; (void)nres; + int status = lua_resume(L, NULL, nresults #if LUA_VERSION_NUM >= 504 - , &nresults + , &nres #endif - ); - if (status != LUA_OK && status != LUA_YIELD) { - const char* msg = lua_tostring(L, -1); - hloge("[lua] coroutine error: %s", msg ? msg : "unknown"); + ); + if (status != LUA_OK && status != LUA_YIELD) { + const char* msg = lua_tostring(L, -1); + hloge("[lua] coroutine error: %s", msg ? msg : "unknown"); + } } - // Unref the thread now that it has either finished or yielded again. - // (If it yielded again, a new token was created via hvlua_suspend.) luaL_unref(L, LUA_REGISTRYINDEX, ref); - delete co; } // --------------------------------------------------------------------------- diff --git a/lua/hv_lua.h b/lua/hv_lua.h index 2508f026c..754089076 100644 --- a/lua/hv_lua.h +++ b/lua/hv_lua.h @@ -61,6 +61,22 @@ lua_State* hvlua_coroutine_state(HvLuaCoroutine* co); // 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. +// +// @on_done(ud, ok, L): ok = true on normal finish; on error ok = false and the +// error message is on top of L's stack. +typedef void (*hvlua_done_cb)(void* ud, bool ok, lua_State* co); + +// 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); + } // namespace hv #endif // HV_LUA_H_ diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 5272b4ecd..7d18c5b81 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -38,6 +38,9 @@ fi if [ -x bin/http_lua_handler_test ]; then bin/http_lua_handler_test fi +if [ -x bin/http_lua_async_test ]; then + bin/http_lua_async_test +fi if [ -x bin/hdns_test ]; then bin/hdns_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 2dd851694..d7e844f2d 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -95,9 +95,12 @@ 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(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) target_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test http_lua_handler_test) +add_executable(http_lua_async_test http_lua_async_test.cpp) +target_include_directories(http_lua_async_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) +target_link_libraries(http_lua_async_test ${HV_LIBRARIES}) +set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test http_lua_handler_test http_lua_async_test) endif() # ------event: async dns------ diff --git a/unittest/http_lua_async_test.cpp b/unittest/http_lua_async_test.cpp new file mode 100644 index 000000000..50ca028af --- /dev/null +++ b/unittest/http_lua_async_test.cpp @@ -0,0 +1,93 @@ +/* + * http_lua_async_test — Stage B integration test for coroutine-based async + * Lua HTTP handlers. + * + * Starts a real HttpServer whose Lua route calls hloop.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 "HttpScriptHandler.h" +#include "requests.h" + +static std::string write_script(const char* name, const char* content) { + hv_mkdir_p("tmp/http_lua_async_test"); + std::string path = HPath::join("tmp/http_lua_async_test", name); + HFile file; + int ret = file.open(path.c_str(), "wb"); + assert(ret == 0); + file.write(content, strlen(content)); + file.close(); + return path; +} + +int main() { + // The handler sleeps 300ms inside the coroutine, then echoes the id. + std::string script = write_script("sleep.lua", + "function handle(ctx)\n" + " hloop.sleep(300)\n" + " return ctx:json({ ok = true, id = ctx:query('id') })\n" + "end\n"); + + 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_async_test PASSED\n"); + return 0; +} diff --git a/unittest/http_lua_handler_test.cpp b/unittest/http_lua_handler_test.cpp index 2f9919aee..6e3180187 100644 --- a/unittest/http_lua_handler_test.cpp +++ b/unittest/http_lua_handler_test.cpp @@ -9,6 +9,7 @@ #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_lua_handler_test"; @@ -156,6 +157,12 @@ static void test_unknown_script_suffix() { } 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). + hv::EventLoop loop; + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, &loop); + test_text_response(); test_json_response(); test_method_function_preferred(); From 2fc977efccc6b1afdcb8e9c970ed5601d4590df2 Mon Sep 17 00:00:00 2001 From: ithewei Date: Tue, 28 Jul 2026 23:45:06 +0800 Subject: [PATCH 03/33] refactor(lua): bind hvlua runtime to an EventLoop (TLS) for parity with server IO thread hvlua now wraps its hloop in an hv::EventLoop and publishes it as the thread's currentThreadEventLoop, matching the environment lua bindings see inside an HTTP server IO thread. This is the correct foundation for future hv.* client bindings that resume coroutines via EventLoop::queueInLoop. --- examples/hvlua.cpp | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/examples/hvlua.cpp b/examples/hvlua.cpp index 36c0283e0..922651b17 100644 --- a/examples/hvlua.cpp +++ b/examples/hvlua.cpp @@ -2,7 +2,9 @@ // // Usage: hvlua script.lua [args...] // -// Initializes the current thread's hloop lua_State, loads and runs the script +// Binds an hv::EventLoop to this thread (so currentThreadEventLoop and the +// hv.* client bindings work exactly as they do inside an HTTP server IO +// thread), initializes the per-loop lua_State, loads and runs the script // (inside a coroutine so it may use synchronous-style async APIs), then runs // the event loop so timers / async IO can complete. #include @@ -16,6 +18,7 @@ extern "C" { #include "hloop.h" #include "hlog.h" +#include "EventLoop.h" #include "hv_lua.h" static void usage(const char* prog) { @@ -32,20 +35,25 @@ int main(int argc, char** argv) { // Route hv.log() to stdout for a CLI runtime (default logger writes a file). hlog_set_handler(stdout_logger); - // AUTO_FREE: the loop frees itself (and closes its lua_State via the dtor) - // when hloop_run returns; we must NOT call hloop_free afterwards. // QUIT_WHEN_NO_ACTIVE_EVENTS: exit once the script and all timers/async - // work are done, so a script without an explicit hloop.stop() still ends. - hloop_t* loop = hloop_new(HLOOP_FLAG_AUTO_FREE | HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); - if (loop == NULL) { + // work are done, so a script without an explicit hloop.stop() still ends. + // (No AUTO_FREE: the EventLoop wrapper owns/frees this hloop.) + hloop_t* hloop = hloop_new(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); + if (hloop == NULL) { fprintf(stderr, "hvlua: failed to create event loop\n"); return 1; } + // Wrap in an EventLoop and publish it as this thread's loop, so + // currentThreadEventLoop (used by hv.http etc.) resolves during the script. + hv::EventLoop loop(hloop); + hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, &loop); + // Create the per-loop lua_State and expose script args as global `arg`. - lua_State* L = hv::hvlua_state(loop); + lua_State* L = hv::hvlua_state(hloop); if (L == NULL) { fprintf(stderr, "hvlua: failed to create lua state\n"); + hloop_free(&hloop); return 1; } lua_createtable(L, argc - 1, 0); @@ -55,15 +63,11 @@ int main(int argc, char** argv) { } lua_setglobal(L, "arg"); - // Run the script (may yield on async ops). - if (hv::hvlua_dofile(loop, script) != 0) { - // hloop_run has not been entered; with AUTO_FREE we still must not call - // hloop_free. Let process exit reclaim resources. - return 1; + // Run the script (may yield on async ops), then drive the loop until the + // script calls hloop.stop() or no work remains. + if (hv::hvlua_dofile(hloop, script) == 0) { + loop.run(); // sets TLS again + hloop_run } - - // Drive the loop until the script calls hloop.stop() or no work remains. - // The loop auto-frees itself (and closes the lua_State) on return. - hloop_run(loop); + hloop_free(&hloop); return 0; } From a7870d5033e9a1a149562bb989d67803803b1e48 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 11:30:30 +0800 Subject: [PATCH 04/33] refactor(lua): implement hloop.* + scheduler + hv.dns in pure C Per the two-layer design, the lower layer (coroutine scheduler, hloop.* timers/ sleep/run/stop, hv.dns) is now plain C with no C++ dependency: std::map task tracking replaced by a registry slot keyed on the coroutine pointer, new/delete replaced by HV_ALLOC/HV_FREE. Only hv.json (lua_hv_core.cpp) stays C++ for nlohmann::json. hv_lua.h is now dual-mode (C via BEGIN/END_EXTERN_C, C++ via hv:: using-aliases). Verified: pure-C99 strict compile with no C++ symbols, Makefile+CMake builds, all lua unittests and hvlua examples pass. --- lua/{hv_lua.cpp => hv_lua.c} | 135 ++++++++++++++++----------- lua/hv_lua.h | 63 +++++++++---- lua/{lua_hloop.cpp => lua_hloop.c} | 75 ++++++++------- lua/lua_hv_core.cpp | 8 +- lua/{lua_hv_dns.cpp => lua_hv_dns.c} | 38 ++++---- 5 files changed, 188 insertions(+), 131 deletions(-) rename lua/{hv_lua.cpp => hv_lua.c} (71%) rename lua/{lua_hloop.cpp => lua_hloop.c} (82%) rename lua/{lua_hv_dns.cpp => lua_hv_dns.c} (83%) diff --git a/lua/hv_lua.cpp b/lua/hv_lua.c similarity index 71% rename from lua/hv_lua.cpp rename to lua/hv_lua.c index 6c2b1b705..67fac85cb 100644 --- a/lua/hv_lua.cpp +++ b/lua/hv_lua.c @@ -1,54 +1,60 @@ -#include "hv_lua.h" - -#include -#include - -extern "C" { #include #include #include -} - -#include "hlog.h" -// Module registration entry points implemented in the per-module files. -namespace hv { -void hvlua_open_hloop(lua_State* L); // lua_hloop.cpp -> global "hloop" -void hvlua_open_core(lua_State* L); // lua_hv_core.cpp -> table "hv" -void hvlua_open_dns(lua_State* L); // lua_hv_dns.cpp -> hv.dns -} +#include "hv_lua.h" -namespace hv { +#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 generation/valid -// flag so a stale resume (loop teardown, double resume) is a safe no-op. +// 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. struct HvLuaCoroutine { - lua_State* main; // the per-loop main state (owns the registry) lua_State* L; // the coroutine thread int ref; // luaL_ref of the thread in the registry, LUA_NOREF if freed }; -// A "task" is a top-level coroutine with a C completion callback. We track the -// pending callback keyed by the coroutine's lua_State* so that whichever resume -// finishes it (initial run or a later async resume) can fire on_done once. -struct HvLuaTask { +// 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 -}; -static std::map g_tasks; +} HvLuaTask; + +// 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. static void hvlua_task_step(lua_State* co, int nresults) { + HvLuaTask* task; int nres = 0; + int status; (void)nres; - int status = lua_resume(co, NULL, nresults + status = lua_resume(co, NULL, nresults #if LUA_VERSION_NUM >= 504 , &nres #endif @@ -57,34 +63,41 @@ static void hvlua_task_step(lua_State* co, int nresults) { return; // suspended again; a resume token owns the next wakeup } // Finished (LUA_OK) or errored: fire the completion callback, then release. - auto iter = g_tasks.find(co); - if (iter == g_tasks.end()) return; - HvLuaTask task = iter->second; - g_tasks.erase(iter); - bool ok = (status == LUA_OK); - if (task.on_done) task.on_done(task.ud, ok, co); - luaL_unref(co, LUA_REGISTRYINDEX, task.thread_ref); + task = task_get(co); + if (task == NULL) return; + 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); + luaL_unref(co, LUA_REGISTRYINDEX, task->thread_ref); + HV_FREE(task); } int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud) { + lua_State* co; + int thread_ref; + HvLuaTask* task; // stack (top): fn, arg1, ..., argN - lua_State* co = lua_newthread(L); // push thread + 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 - int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // pop+ref the thread + 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_set(co, task); - g_tasks[co] = HvLuaTask{ on_done, ud, thread_ref }; hvlua_task_step(co, nargs); // If the task is no longer tracked, it finished synchronously. - return g_tasks.find(co) == g_tasks.end() ? 1 : 0; + return task_get(co) == NULL ? 1 : 0; } 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 = new HvLuaCoroutine(); - co->main = L; + 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 @@ -102,30 +115,34 @@ void hvlua_cancel(HvLuaCoroutine* co) { luaL_unref(co->L, LUA_REGISTRYINDEX, co->ref); co->ref = LUA_NOREF; } - delete co; + HV_FREE(co); } void hvlua_resume(HvLuaCoroutine* co, int nresults) { + lua_State* L; + int ref; if (co == NULL) return; if (co->ref == LUA_NOREF) { // stale: already resumed/freed - delete co; + HV_FREE(co); return; } - lua_State* L = co->L; - int ref = co->ref; + 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 (g_tasks) keeps its own ref, so - // the coroutine stays alive across this transition. + // resume can't double-free. Task tracking keeps its own ref, so the + // coroutine stays alive across this transition. co->ref = LUA_NOREF; - delete 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 (g_tasks.find(L) != g_tasks.end()) { + if (task_get(L) != NULL) { hvlua_task_step(L, nresults); } else { - int nres = 0; (void)nres; - int status = lua_resume(L, NULL, nresults + int nres = 0; + int status; + (void)nres; + status = lua_resume(L, NULL, nresults #if LUA_VERSION_NUM >= 504 , &nres #endif @@ -164,8 +181,9 @@ static lua_State* hvlua_new_state(hloop_t* loop) { } lua_State* hvlua_state(hloop_t* loop) { + lua_State* L; if (loop == NULL) return NULL; - lua_State* L = (lua_State*)hloop_lua_state(loop); + L = (lua_State*)hloop_lua_state(loop); if (L == NULL) { L = hvlua_new_state(loop); } @@ -174,8 +192,9 @@ lua_State* hvlua_state(hloop_t* loop) { // 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"); - hloop_t* loop = (hloop_t*)lua_touserdata(L, -1); + loop = (hloop_t*)lua_touserdata(L, -1); lua_pop(L, 1); return loop; } @@ -186,17 +205,21 @@ hloop_t* hvlua_loop(lua_State* L) { // 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] - lua_State* co = lua_newthread(L); // stack: [chunk][thread] + 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. - int ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread; stack: [chunk] + ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread; stack: [chunk] lua_pop(L, 1); // pop chunk; stack: [] - int nres = 0; - int status = lua_resume(co, NULL, 0 + status = lua_resume(co, NULL, 0 #if LUA_VERSION_NUM >= 504 , &nres #endif @@ -240,5 +263,3 @@ int hvlua_dostring(hloop_t* loop, const char* code) { } return hvlua_run_chunk(L); } - -} // namespace hv diff --git a/lua/hv_lua.h b/lua/hv_lua.h index 754089076..bd68d6aed 100644 --- a/lua/hv_lua.h +++ b/lua/hv_lua.h @@ -1,21 +1,34 @@ #ifndef HV_LUA_H_ #define HV_LUA_H_ -// libhv Lua binding: public C++ entry points. +// 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 C bindings call -// hvlua_yield()/lua_yieldk() and are resumed on the same loop thread when +// - 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()). -// - two layers of modules: hloop.* (pure C hloop, no evpp/http dependency) -// and hv.* (higher-level helpers/clients). +// - two layers of modules: hloop.* + core scheduler are plain C (no evpp/http +// dependency); higher-level helpers like hv.json are C++. +// +// This header is usable from both C and C++. The core scheduler / hloop.* / +// hv.dns bindings are implemented in C; hv.json (nlohmann) is implemented in +// C++. C++ callers may use the hv:: aliases at the bottom. -#include "hloop.h" +#include "hloop.h" // hloop_t + hexport.h (BEGIN_EXTERN_C) + hplatform (bool) -struct lua_State; +#ifndef lua_h +typedef struct lua_State lua_State; +#endif -namespace hv { +// Opaque suspend token (see hvlua_suspend). +typedef struct HvLuaCoroutine HvLuaCoroutine; + +// 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 @@ -35,20 +48,19 @@ int hvlua_dostring(hloop_t* loop, const char* code); // A suspendable binding does: // 1. start the libhv async op, capturing a resume token for the running // coroutine via hvlua_suspend(L); -// 2. return hvlua_yield(L, nresults_ignored, k) to suspend; +// 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. -struct HvLuaCoroutine; // 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 `co->L`. Safe no-op if the token is stale. Frees the token. +// 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 @@ -68,15 +80,34 @@ hloop_t* hvlua_loop(lua_State* L); // When the coroutine finally finishes (success or error), `on_done` is invoked // exactly once on the loop thread. // -// @on_done(ud, ok, L): ok = true on normal finish; on error ok = false and the -// error message is on top of L's stack. -typedef void (*hvlua_done_cb)(void* ud, bool ok, lua_State* co); - // 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); -} // namespace hv +// Module registration entry points (called by hvlua_state on state creation). +// Declared extern "C" so the C core can call the C++-implemented hv.json module. +void hvlua_open_hloop(lua_State* L); // lua_hloop.c -> global "hloop" +void hvlua_open_core(lua_State* L); // lua_hv_core.cpp -> table "hv" (+ hv.json) +void hvlua_open_dns(lua_State* L); // lua_hv_dns.c -> hv.dns + +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_loop; + using ::hvlua_start_task; +} +#endif #endif // HV_LUA_H_ diff --git a/lua/lua_hloop.cpp b/lua/lua_hloop.c similarity index 82% rename from lua/lua_hloop.cpp rename to lua/lua_hloop.c index e1a931603..ef483da61 100644 --- a/lua/lua_hloop.cpp +++ b/lua/lua_hloop.c @@ -1,16 +1,13 @@ -#include "hv_lua.h" - -extern "C" { #include #include #include -} +#include "hv_lua.h" + +#include "hbase.h" // HV_ALLOC / HV_FREE #include "hloop.h" #include "hlog.h" -namespace hv { - // 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 @@ -19,14 +16,14 @@ namespace hv { // Re-entrancy: the callback may call hloop.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. -struct LuaTimer { +typedef struct LuaTimer { lua_State* L; // per-loop main state htimer_t* timer; int fn_ref; // LUA_NOREF when released - bool once; - bool in_callback; - bool dead; // clearTimer requested during the callback -}; + 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) { @@ -37,22 +34,27 @@ static void lua_timer_release_ref(LuaTimer* lt) { static void lua_timer_free(LuaTimer* lt) { lua_timer_release_ref(lt); - delete lt; + HV_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; - lua_State* L = lt->L; + L = lt->L; + (void)nres; // Run the callback in a fresh coroutine so it may yield on async ops. - lt->in_callback = true; - lua_State* co = lua_newthread(L); - int thread_ref = luaL_ref(L, LUA_REGISTRYINDEX); // keep coroutine alive + 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 - int nres = 0; - int status = lua_resume(co, NULL, 0 + status = lua_resume(co, NULL, 0 #if LUA_VERSION_NUM >= 504 , &nres #endif @@ -62,7 +64,7 @@ static void on_lua_timer(htimer_t* timer) { hloge("[lua] timer callback error: %s", msg ? msg : "unknown"); } luaL_unref(L, LUA_REGISTRYINDEX, thread_ref); - lt->in_callback = false; + 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. @@ -71,21 +73,23 @@ static void on_lua_timer(htimer_t* timer) { } } -static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repeat, bool once) { +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); - LuaTimer* lt = new LuaTimer(); + HV_ALLOC_SIZEOF(lt); lt->L = L; lt->timer = NULL; lt->once = once; - lt->in_callback = false; - lt->dead = false; + 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); - htimer_t* timer = htimer_add(loop, on_lua_timer, timeout_ms, repeat); + timer = htimer_add(loop, on_lua_timer, timeout_ms, repeat); if (timer == NULL) { lua_timer_free(lt); return NULL; @@ -98,7 +102,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea // hloop.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, true); + htimer_t* timer = add_lua_timer(L, ms, 1, 1); if (timer == NULL) { lua_pushnil(L); return 1; } lua_pushlightuserdata(L, timer); return 1; @@ -107,7 +111,7 @@ static int l_hloop_setTimeout(lua_State* L) { // hloop.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, false); + htimer_t* timer = add_lua_timer(L, ms, INFINITE, 0); if (timer == NULL) { lua_pushnil(L); return 1; } lua_pushlightuserdata(L, timer); return 1; @@ -115,16 +119,18 @@ static int l_hloop_setInterval(lua_State* L) { // hloop.clearTimer(handle) static int l_hloop_clearTimer(lua_State* L) { + htimer_t* timer; + LuaTimer* lt; if (!lua_islightuserdata(L, 1)) return 0; - htimer_t* timer = (htimer_t*)lua_touserdata(L, 1); + timer = (htimer_t*)lua_touserdata(L, 1); if (timer == NULL) return 0; - LuaTimer* lt = (LuaTimer*)hevent_userdata(timer); + 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. - lt->dead = true; + lt->dead = 1; lua_timer_release_ref(lt); } else { lua_timer_free(lt); @@ -135,15 +141,15 @@ static int l_hloop_clearTimer(lua_State* L) { // ---- hloop.sleep(ms): suspend the running coroutine for ms milliseconds ---- -struct SleepCtx { +typedef struct SleepCtx { HvLuaCoroutine* co; htimer_t* timer; -}; +} SleepCtx; static void on_sleep_timer(htimer_t* timer) { SleepCtx* s = (SleepCtx*)hevent_userdata(timer); HvLuaCoroutine* co = s->co; - delete s; + HV_FREE(s); // resume the sleeping coroutine with no results hvlua_resume(co, 0); } @@ -158,8 +164,9 @@ static int sleep_k(lua_State* L, int status, lua_KContext ctx) { 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; - SleepCtx* s = new SleepCtx(); + HV_ALLOC_SIZEOF(s); s->co = hvlua_suspend(L); s->timer = htimer_add(loop, on_sleep_timer, ms, 1); hevent_set_userdata(s->timer, s); @@ -192,5 +199,3 @@ void hvlua_open_hloop(lua_State* L) { luaL_newlib(L, hloop_funcs); lua_setglobal(L, "hloop"); } - -} // namespace hv diff --git a/lua/lua_hv_core.cpp b/lua/lua_hv_core.cpp index feb6cf4ea..839968a9f 100644 --- a/lua/lua_hv_core.cpp +++ b/lua/lua_hv_core.cpp @@ -15,7 +15,9 @@ extern "C" { using nlohmann::json; -namespace hv { +// This module stays C++ (nlohmann::json). All helpers are file-static; only +// hvlua_open_core is exported, with C linkage so the C core (hv_lua.c) can call +// it. No `namespace hv` wrapper is needed here. // ---- lua <-> json conversion (shared style with HttpLuaHandler) ---- @@ -187,7 +189,7 @@ static const luaL_Reg hv_json_funcs[] = { { NULL, NULL } }; -void hvlua_open_core(lua_State* L) { +extern "C" void hvlua_open_core(lua_State* L) { // create/get global "hv" lua_getglobal(L, "hv"); if (!lua_istable(L, -1)) { @@ -202,5 +204,3 @@ void hvlua_open_core(lua_State* L) { lua_setglobal(L, "hv"); } - -} // namespace hv diff --git a/lua/lua_hv_dns.cpp b/lua/lua_hv_dns.c similarity index 83% rename from lua/lua_hv_dns.cpp rename to lua/lua_hv_dns.c index 5d373f2bb..9e0dea9d6 100644 --- a/lua/lua_hv_dns.cpp +++ b/lua/lua_hv_dns.c @@ -1,41 +1,41 @@ -#include "hv_lua.h" - -extern "C" { #include #include #include -} +#include "hv_lua.h" + +#include "hbase.h" // HV_ALLOC / HV_FREE #include "hdns.h" #include "hsocket.h" -namespace hv { - -// hv.dns.resolve(host [, opt]) -> { ip, ip, ... } | nil, err +// hv.dns.resolve(host) -> { ip, ip, ... } | nil, err // // Coroutine-synchronous: suspends the running coroutine, issues an async hdns // query on the loop, and resumes with the resolved addresses (or nil,err) when // the callback fires on the same loop thread. -struct DnsCtx { +typedef struct DnsCtx { HvLuaCoroutine* co; -}; +} DnsCtx; static void on_dns_resolved(hdns_t* query, const hdns_result_t* result, void* userdata) { - (void)query; DnsCtx* d = (DnsCtx*)userdata; HvLuaCoroutine* co = d->co; - delete d; + lua_State* L; + (void)query; + HV_FREE(d); - lua_State* L = hvlua_coroutine_state(co); + L = hvlua_coroutine_state(co); if (L == NULL) { // coroutine gone (loop teardown); nothing to resume return; } if (result->status == HDNS_STATUS_OK && result->naddrs > 0) { + int i; lua_createtable(L, result->naddrs, 0); - for (int i = 0; i < result->naddrs; ++i) { - char ip[64] = {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); @@ -58,16 +58,18 @@ static int resolve_k(lua_State* L, int status, lua_KContext ctx) { static int l_dns_resolve(lua_State* L) { const char* host = luaL_checkstring(L, 1); hloop_t* loop = hvlua_loop(L); + DnsCtx* d; + hdns_t* q; - DnsCtx* d = new DnsCtx(); + HV_ALLOC_SIZEOF(d); d->co = hvlua_suspend(L); - hdns_t* q = hdns_resolve(loop, host, on_dns_resolved, 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); - delete d; + HV_FREE(d); lua_pushnil(L); lua_pushstring(L, "dns resolve: failed to start query"); return 2; @@ -91,5 +93,3 @@ void hvlua_open_dns(lua_State* L) { lua_setfield(L, -2, "dns"); lua_setglobal(L, "hv"); } - -} // namespace hv From f3d439b6502e4206d26f3c52dd7ebd231661fe14 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 11:38:32 +0800 Subject: [PATCH 05/33] refactor(lua): split lua_hv_core into pure-C lua_hv_base + C++ lua_hv_json hv.log/hv.now move to lua_hv_base.c (pure C, bounded stack buffer instead of std::string); hv.json stays in lua_hv_json.cpp (nlohmann). hvlua_state now calls hvlua_open_base + hvlua_open_json. lua/ now has a single C++ file (json), the rest is pure C. Verified: Makefile+CMake compile base.c as C / json.cpp as C++, all lua unittests pass, hv.log + hv.json roundtrip work in hvlua. --- lua/hv_lua.c | 3 +- lua/hv_lua.h | 3 +- lua/lua_hv_base.c | 60 ++++++++++++++++++++++++ lua/{lua_hv_core.cpp => lua_hv_json.cpp} | 51 ++++---------------- 4 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 lua/lua_hv_base.c rename lua/{lua_hv_core.cpp => lua_hv_json.cpp} (83%) diff --git a/lua/hv_lua.c b/lua/hv_lua.c index 67fac85cb..6a30d6a8c 100644 --- a/lua/hv_lua.c +++ b/lua/hv_lua.c @@ -173,7 +173,8 @@ static lua_State* hvlua_new_state(hloop_t* loop) { lua_setfield(L, LUA_REGISTRYINDEX, "hv.loop"); hvlua_open_hloop(L); - hvlua_open_core(L); + hvlua_open_base(L); + hvlua_open_json(L); hvlua_open_dns(L); hloop_set_lua_state(loop, L, hvlua_state_dtor); diff --git a/lua/hv_lua.h b/lua/hv_lua.h index bd68d6aed..7fa10fac8 100644 --- a/lua/hv_lua.h +++ b/lua/hv_lua.h @@ -88,7 +88,8 @@ 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). // Declared extern "C" so the C core can call the C++-implemented hv.json module. void hvlua_open_hloop(lua_State* L); // lua_hloop.c -> global "hloop" -void hvlua_open_core(lua_State* L); // lua_hv_core.cpp -> table "hv" (+ hv.json) +void hvlua_open_base(lua_State* L); // lua_hv_base.c -> table "hv" (log/now) +void hvlua_open_json(lua_State* L); // lua_hv_json.cpp -> hv.json (nlohmann) void hvlua_open_dns(lua_State* L); // lua_hv_dns.c -> hv.dns END_EXTERN_C diff --git a/lua/lua_hv_base.c b/lua/lua_hv_base.c new file mode 100644 index 000000000..ad8b99c25 --- /dev/null +++ b/lua/lua_hv_base.c @@ -0,0 +1,60 @@ +#include +#include +#include + +#include "hv_lua.h" + +#include +#include + +#include "hlog.h" + +// hv.log(...) : join args with tabs and log at INFO level. +// Uses a bounded stack buffer (log lines are short); overflow is truncated. +static int l_hv_log(lua_State* L) { + 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'; + hlogi("[lua] %s", line); + return 0; +} + +// hv.now() -> unix seconds +static int l_hv_now(lua_State* L) { + lua_pushinteger(L, (lua_Integer)time(NULL)); + return 1; +} + +static const luaL_Reg hv_base_funcs[] = { + { "log", l_hv_log }, + { "now", l_hv_now }, + { 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/lua_hv_core.cpp b/lua/lua_hv_json.cpp similarity index 83% rename from lua/lua_hv_core.cpp rename to lua/lua_hv_json.cpp index 839968a9f..2f1ecd336 100644 --- a/lua/lua_hv_core.cpp +++ b/lua/lua_hv_json.cpp @@ -1,23 +1,21 @@ -#include "hv_lua.h" - -#include -#include - extern "C" { #include #include #include } -#include "hlog.h" +#include "hv_lua.h" + +#include + #include "hstring.h" #include "json.hpp" using nlohmann::json; // This module stays C++ (nlohmann::json). All helpers are file-static; only -// hvlua_open_core is exported, with C linkage so the C core (hv_lua.c) can call -// it. No `namespace hv` wrapper is needed here. +// hvlua_open_json is exported, with C linkage so the C core (hv_lua.c) can call +// it. // ---- lua <-> json conversion (shared style with HttpLuaHandler) ---- @@ -132,29 +130,6 @@ static void json_to_lua(lua_State* L, const json& j) { } } -// ---- hv.* functions ---- - -// hv.log(...) -static int l_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; -} - -// hv.now() -> unix seconds -static int l_hv_now(lua_State* L) { - lua_pushinteger(L, (lua_Integer)time(NULL)); - return 1; -} - // hv.json.encode(value) -> string static int l_hv_json_encode(lua_State* L) { json j = lua_to_json(L, 1); @@ -177,30 +152,20 @@ static int l_hv_json_decode(lua_State* L) { return 1; } -static const luaL_Reg hv_funcs[] = { - { "log", l_hv_log }, - { "now", l_hv_now }, - { NULL, NULL } -}; - static const luaL_Reg hv_json_funcs[] = { { "encode", l_hv_json_encode }, { "decode", l_hv_json_decode }, { NULL, NULL } }; -extern "C" void hvlua_open_core(lua_State* L) { - // create/get global "hv" +// 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_setfuncs(L, hv_funcs, 0); - - // hv.json subtable luaL_newlib(L, hv_json_funcs); lua_setfield(L, -2, "json"); - lua_setglobal(L, "hv"); } From 7eac88f3ae7942df71824b6595fd8cd05ef0bf7a Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 12:33:19 +0800 Subject: [PATCH 06/33] refactor(lua): unify under hv.* namespace + hvlua_* file naming - Merge async DNS into the event module (event/ layer): hv.dns.resolve -> hv.resolveDns, alongside hv.setTimeout/setInterval/clearTimer/sleep/run/stop. - Collapse the hloop.* global into the single hv.* table; every script-facing function now lives under hv.* (loop primitives operate on the current thread's loop). - Rename files to match the hvlua_* symbol/executable prefix and mirror libhv's C-layer dirs: hv_lua.{c,h}->hvlua.{c,h}, lua_hloop.c->hvlua_event.c (event/), lua_hv_base.c->hvlua_base.c (base/), lua_hv_json.cpp->hvlua_json.cpp (cpputil/); lua_hv_dns.c removed (merged into event). Registration order base->event->json. - base module: drop hv.now (Lua has os.time), add hv.version (first), and add log levels hv.logd/logi/logw/loge with hv.log as an alias of logi. - Update examples, scripts, docs, and unittests to the hv.* API. Verified: pure-C strict compile of hvlua*.c with no C++ symbols; Makefile+CMake build (base/event as C, json as C++); all lua unittests and hvlua examples pass; hv.version/hv.log levels confirmed at runtime. --- CMakeLists.txt | 2 +- Makefile | 2 +- docs/cn/HttpLuaHandler.md | 26 +++--- examples/hvlua.cpp | 2 +- examples/lua/dns.lua | 6 +- examples/lua/sleep.lua | 16 ++-- examples/lua/timer.lua | 10 +-- examples/scripts/async.lua | 8 +- http/server/HttpLuaHandler.cpp | 2 +- lua/{hv_lua.c => hvlua.c} | 7 +- lua/{hv_lua.h => hvlua.h} | 27 +++--- lua/{lua_hv_base.c => hvlua_base.c} | 34 +++++--- lua/{lua_hloop.c => hvlua_event.c} | 105 +++++++++++++++++++++--- lua/{lua_hv_json.cpp => hvlua_json.cpp} | 4 +- lua/lua_hv_dns.c | 95 --------------------- unittest/http_lua_async_test.cpp | 4 +- unittest/lua_binding_test.cpp | 36 ++++---- 17 files changed, 196 insertions(+), 190 deletions(-) rename lua/{hv_lua.c => hvlua.c} (97%) rename lua/{hv_lua.h => hvlua.h} (79%) rename lua/{lua_hv_base.c => hvlua_base.c} (55%) rename lua/{lua_hloop.c => hvlua_event.c} (62%) rename lua/{lua_hv_json.cpp => hvlua_json.cpp} (99%) delete mode 100644 lua/lua_hv_dns.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 27be38b10..602fc941f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,7 +240,7 @@ if(WITH_EVPP) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${CPPUTIL_HEADERS} ${EVPP_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} cpputil evpp) if(WITH_LUA) - set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hv_lua.h) + set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hvlua.h) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} lua) endif() if(WITH_REDIS) diff --git a/Makefile b/Makefile index d80be1823..2cabc7246 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) LIBHV_SRCDIRS += cpputil evpp ifeq ($(WITH_LUA), yes) -LIBHV_HEADERS += lua/hv_lua.h +LIBHV_HEADERS += lua/hvlua.h LIBHV_SRCDIRS += lua endif diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index b9f21c9ed..d485d270f 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -108,30 +108,32 @@ function handle(ctx) end ``` -## hv / hloop API +## hv API 脚本运行在所属 IO 线程的 **协程** 里,因此可以用 **同步写法** 调用异步能力:调用会挂起当前请求的协程、把控制权交还事件循环,结果就绪后在同一线程恢复,全程不阻塞 loop。 -当前可用的宿主能力: +所有脚本可用能力都挂在统一的全局 `hv` 表下: ```lua -hv.log(...) -- 日志 -hv.now() -- unix 秒 +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.dns.resolve(host) -- 协程同步 DNS 解析: 返回 { ip, ... } 或 nil, err -hloop.setTimeout(ms, fn) -- 定时器 (返回句柄) -hloop.setInterval(ms, fn) -hloop.clearTimer(handle) -hloop.sleep(ms) -- 协程同步 sleep: 挂起当前协程 ms 毫秒, 不阻塞 loop +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 内不需要) ``` 示例(handler 内部“同步”写法,实际异步,loop 不阻塞): ```lua function handle(ctx) - local addrs, err = hv.dns.resolve("example.com") + local addrs, err = hv.resolveDns("example.com") if err then ctx:status(502) return ctx:json({ ok = false, error = err }) @@ -140,9 +142,9 @@ function handle(ctx) end ``` -多个请求会在同一 IO 线程上并发交错执行:某个请求在 `hv.dns.resolve` / `hloop.sleep` 处挂起时,同线程的其它请求会继续推进。注意协作式调度的语义——跨越挂起点不要对全局状态做原子性假设。 +多个请求会在同一 IO 线程上并发交错执行:某个请求在 `hv.resolveDns` / `hv.sleep` 处挂起时,同线程的其它请求会继续推进。注意协作式调度的语义——跨越挂起点不要对全局状态做原子性假设。 -> TCP/HTTP client、Redis 等高层 client 的协程绑定见 `lua/` 模块,按 `WITH_LUA` / `WITH_REDIS` 等开关编入。 +> `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/examples/hvlua.cpp b/examples/hvlua.cpp index 922651b17..385cdcef8 100644 --- a/examples/hvlua.cpp +++ b/examples/hvlua.cpp @@ -19,7 +19,7 @@ extern "C" { #include "hloop.h" #include "hlog.h" #include "EventLoop.h" -#include "hv_lua.h" +#include "hvlua.h" static void usage(const char* prog) { fprintf(stderr, "Usage: %s script.lua [args...]\n", prog); diff --git a/examples/lua/dns.lua b/examples/lua/dns.lua index c9b06ca19..77648127b 100644 --- a/examples/lua/dns.lua +++ b/examples/lua/dns.lua @@ -13,8 +13,8 @@ end local pending = #hosts for _, host in ipairs(hosts) do - hloop.setTimeout(1, function() - local addrs, err = hv.dns.resolve(host) -- synchronous-style, async underneath + hv.setTimeout(1, function() + local addrs, err = hv.resolveDns(host) -- synchronous-style, async underneath if err then hv.log("resolve", host, "failed:", err) else @@ -22,7 +22,7 @@ for _, host in ipairs(hosts) do end pending = pending - 1 if pending == 0 then - hloop.stop() + hv.stop() end end) end diff --git a/examples/lua/sleep.lua b/examples/lua/sleep.lua index 574279758..8ddc245a6 100644 --- a/examples/lua/sleep.lua +++ b/examples/lua/sleep.lua @@ -3,24 +3,24 @@ hv.log("sleep example start") --- This runs inside a coroutine, so hloop.sleep() suspends WITHOUT blocking the +-- This runs inside a coroutine, so hv.sleep() suspends WITHOUT blocking the -- event loop: the two "workers" below interleave rather than run serially. -hloop_workers_done = 0 +workers_done = 0 local function worker(name, ms) for i = 1, 3 do hv.log(name, "step", i) - hloop.sleep(ms) -- looks blocking, actually yields to the loop + hv.sleep(ms) -- looks blocking, actually yields to the loop end hv.log(name, "done") - hloop_workers_done = hloop_workers_done + 1 - if hloop_workers_done == 2 then - hloop.stop() + 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) -hloop.setTimeout(1, function() worker("A", 300) end) -hloop.setTimeout(1, function() worker("B", 500) end) +hv.setTimeout(1, function() worker("A", 300) end) +hv.setTimeout(1, function() worker("B", 500) end) diff --git a/examples/lua/timer.lua b/examples/lua/timer.lua index 67e978c4d..29203ff62 100644 --- a/examples/lua/timer.lua +++ b/examples/lua/timer.lua @@ -1,19 +1,19 @@ -- hvlua timer example -- Run: bin/hvlua examples/lua/timer.lua -hv.log("timer example start, now =", hv.now()) +hv.log("timer example start, libhv", hv.version()) local n = 0 local id -id = hloop.setInterval(500, function() +id = hv.setInterval(500, function() n = n + 1 hv.log("tick", n) if n >= 3 then - hloop.clearTimer(id) + hv.clearTimer(id) hv.log("cleared interval after 3 ticks") - hloop.setTimeout(200, function() + hv.setTimeout(200, function() hv.log("bye") - hloop.stop() + hv.stop() end) end end) diff --git a/examples/scripts/async.lua b/examples/scripts/async.lua index 1e689253a..4d06dcfb4 100644 --- a/examples/scripts/async.lua +++ b/examples/scripts/async.lua @@ -1,8 +1,8 @@ -- Async HTTP handler demo: synchronous-style code, non-blocking loop. -- --- The handler resolves a hostname via hv.dns.resolve (which yields the request +-- 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 --- hloop.sleep without blocking other requests on the same IO thread. +-- 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" @@ -13,10 +13,10 @@ function handle(ctx) -- optional artificial delay to demonstrate non-blocking concurrency local delay = tonumber(ctx:query("delay", "0")) if delay > 0 then - hloop.sleep(delay) + hv.sleep(delay) end - local addrs, err = hv.dns.resolve(host) + local addrs, err = hv.resolveDns(host) if err then ctx:status(502) return ctx:json({ ok = false, host = host, error = err }) diff --git a/http/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp index 27964f805..4f04e3709 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -18,7 +18,7 @@ extern "C" { #include "htime.h" #include "EventLoop.h" -#include "hv_lua.h" +#include "hvlua.h" namespace hv { diff --git a/lua/hv_lua.c b/lua/hvlua.c similarity index 97% rename from lua/hv_lua.c rename to lua/hvlua.c index 6a30d6a8c..4b15062b6 100644 --- a/lua/hv_lua.c +++ b/lua/hvlua.c @@ -2,7 +2,7 @@ #include #include -#include "hv_lua.h" +#include "hvlua.h" #include "hbase.h" // HV_ALLOC / HV_FREE #include "hlog.h" @@ -172,10 +172,11 @@ static lua_State* hvlua_new_state(hloop_t* loop) { lua_pushlightuserdata(L, (void*)loop); lua_setfield(L, LUA_REGISTRYINDEX, "hv.loop"); - hvlua_open_hloop(L); + // Register modules from most basic to higher-level (mirrors libhv layering): + // base -> event -> json -> (future: http, redis, mqtt, ...) hvlua_open_base(L); + hvlua_open_event(L); hvlua_open_json(L); - hvlua_open_dns(L); hloop_set_lua_state(loop, L, hvlua_state_dtor); return L; diff --git a/lua/hv_lua.h b/lua/hvlua.h similarity index 79% rename from lua/hv_lua.h rename to lua/hvlua.h index 7fa10fac8..ebb7bedae 100644 --- a/lua/hv_lua.h +++ b/lua/hvlua.h @@ -8,12 +8,14 @@ // - 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()). -// - two layers of modules: hloop.* + core scheduler are plain C (no evpp/http -// dependency); higher-level helpers like hv.json are C++. +// - 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 / hloop.* / -// hv.dns bindings are implemented in C; hv.json (nlohmann) is implemented in -// C++. C++ callers may use the hv:: aliases at the bottom. +// 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) @@ -32,7 +34,7 @@ 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 -// hloop.* / hv.* modules on creation. Returns NULL if `loop` is NULL. +// 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 @@ -85,12 +87,15 @@ hloop_t* hvlua_loop(lua_State* L); // 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). +// 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. -void hvlua_open_hloop(lua_State* L); // lua_hloop.c -> global "hloop" -void hvlua_open_base(lua_State* L); // lua_hv_base.c -> table "hv" (log/now) -void hvlua_open_json(lua_State* L); // lua_hv_json.cpp -> hv.json (nlohmann) -void hvlua_open_dns(lua_State* L); // lua_hv_dns.c -> hv.dns +// 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) +// future: hvlua_open_http, hvlua_open_redis, hvlua_open_mqtt, ... END_EXTERN_C diff --git a/lua/lua_hv_base.c b/lua/hvlua_base.c similarity index 55% rename from lua/lua_hv_base.c rename to lua/hvlua_base.c index ad8b99c25..75112b00d 100644 --- a/lua/lua_hv_base.c +++ b/lua/hvlua_base.c @@ -2,16 +2,22 @@ #include #include -#include "hv_lua.h" +#include "hvlua.h" #include -#include #include "hlog.h" +#include "hversion.h" -// hv.log(...) : join args with tabs and log at INFO level. +// 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 l_hv_log(lua_State* L) { +static int hvlua_log_at(lua_State* L, int level) { char line[1024]; size_t off = 0; int n = lua_gettop(L); @@ -32,19 +38,23 @@ static int l_hv_log(lua_State* L) { lua_pop(L, 1); // pop the string pushed by luaL_tolstring } line[off] = '\0'; - hlogi("[lua] %s", line); + logger_print(hlog, level, "[lua] %s", line); return 0; } -// hv.now() -> unix seconds -static int l_hv_now(lua_State* L) { - lua_pushinteger(L, (lua_Integer)time(NULL)); - return 1; -} +// 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[] = { - { "log", l_hv_log }, - { "now", l_hv_now }, + { "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 } }; diff --git a/lua/lua_hloop.c b/lua/hvlua_event.c similarity index 62% rename from lua/lua_hloop.c rename to lua/hvlua_event.c index ef483da61..5692042a5 100644 --- a/lua/lua_hloop.c +++ b/lua/hvlua_event.c @@ -2,18 +2,20 @@ #include #include -#include "hv_lua.h" +#include "hvlua.h" #include "hbase.h" // HV_ALLOC / HV_FREE #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 hloop.clearTimer(self). To avoid a +// 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 { @@ -99,7 +101,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea return timer; } -// hloop.setTimeout(ms, fn) -> lightuserdata handle +// 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); @@ -108,7 +110,7 @@ static int l_hloop_setTimeout(lua_State* L) { return 1; } -// hloop.setInterval(ms, fn) -> lightuserdata handle +// 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); @@ -117,7 +119,7 @@ static int l_hloop_setInterval(lua_State* L) { return 1; } -// hloop.clearTimer(handle) +// hv.clearTimer(handle) static int l_hloop_clearTimer(lua_State* L) { htimer_t* timer; LuaTimer* lt; @@ -139,7 +141,7 @@ static int l_hloop_clearTimer(lua_State* L) { return 0; } -// ---- hloop.sleep(ms): suspend the running coroutine for ms milliseconds ---- +// ---- hv.sleep(ms): suspend the running coroutine for ms milliseconds ---- typedef struct SleepCtx { HvLuaCoroutine* co; @@ -160,7 +162,7 @@ static int sleep_k(lua_State* L, int status, lua_KContext ctx) { return 0; } -// hloop.sleep(ms) +// 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); @@ -174,7 +176,78 @@ static int l_hloop_sleep(lua_State* L) { return lua_yieldk(L, 0, (lua_KContext)0, sleep_k); } -// hloop.run() / hloop.stop() +// ---- 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; +} DnsCtx; + +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; + HV_FREE(d); + + L = hvlua_coroutine_state(co); + if (L == NULL) { // coroutine gone (loop teardown); nothing to resume + 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); + + 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); + 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) { hloop_run(hvlua_loop(L)); return 0; @@ -190,12 +263,22 @@ static const luaL_Reg hloop_funcs[] = { { "setInterval", l_hloop_setInterval }, { "clearTimer", l_hloop_clearTimer }, { "sleep", l_hloop_sleep }, + { "resolveDns", l_hloop_resolveDns }, { "run", l_hloop_run }, { "stop", l_hloop_stop }, { NULL, NULL } }; -void hvlua_open_hloop(lua_State* L) { - luaL_newlib(L, hloop_funcs); - lua_setglobal(L, "hloop"); +// Register the event-loop primitives into the global "hv" table: +// hv.setTimeout / hv.setInterval / hv.clearTimer / hv.sleep / +// hv.resolveDns / hv.run / hv.stop +// These operate on the current thread's event loop. +void hvlua_open_event(lua_State* 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/lua_hv_json.cpp b/lua/hvlua_json.cpp similarity index 99% rename from lua/lua_hv_json.cpp rename to lua/hvlua_json.cpp index 2f1ecd336..376e6435d 100644 --- a/lua/lua_hv_json.cpp +++ b/lua/hvlua_json.cpp @@ -4,7 +4,7 @@ extern "C" { #include } -#include "hv_lua.h" +#include "hvlua.h" #include @@ -14,7 +14,7 @@ extern "C" { using nlohmann::json; // This module stays C++ (nlohmann::json). All helpers are file-static; only -// hvlua_open_json is exported, with C linkage so the C core (hv_lua.c) can call +// hvlua_open_json is exported, with C linkage so the C core (hvlua.c) can call // it. // ---- lua <-> json conversion (shared style with HttpLuaHandler) ---- diff --git a/lua/lua_hv_dns.c b/lua/lua_hv_dns.c deleted file mode 100644 index 9e0dea9d6..000000000 --- a/lua/lua_hv_dns.c +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include - -#include "hv_lua.h" - -#include "hbase.h" // HV_ALLOC / HV_FREE -#include "hdns.h" -#include "hsocket.h" - -// hv.dns.resolve(host) -> { ip, ip, ... } | nil, err -// -// Coroutine-synchronous: suspends the running coroutine, issues an async hdns -// query on the loop, and resumes with the resolved addresses (or nil,err) when -// the callback fires on the same loop thread. - -typedef struct DnsCtx { - HvLuaCoroutine* co; -} DnsCtx; - -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; - HV_FREE(d); - - L = hvlua_coroutine_state(co); - if (L == NULL) { // coroutine gone (loop teardown); nothing to resume - 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.dns.resolve(host) -static int l_dns_resolve(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); - - 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); - 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); -} - -static const luaL_Reg dns_funcs[] = { - { "resolve", l_dns_resolve }, - { NULL, NULL } -}; - -void hvlua_open_dns(lua_State* L) { - lua_getglobal(L, "hv"); - if (!lua_istable(L, -1)) { - lua_pop(L, 1); - lua_newtable(L); - } - luaL_newlib(L, dns_funcs); - lua_setfield(L, -2, "dns"); - lua_setglobal(L, "hv"); -} diff --git a/unittest/http_lua_async_test.cpp b/unittest/http_lua_async_test.cpp index 50ca028af..46f8ebbf9 100644 --- a/unittest/http_lua_async_test.cpp +++ b/unittest/http_lua_async_test.cpp @@ -2,7 +2,7 @@ * http_lua_async_test — Stage B integration test for coroutine-based async * Lua HTTP handlers. * - * Starts a real HttpServer whose Lua route calls hloop.sleep(ms) — a + * 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 @@ -43,7 +43,7 @@ int main() { // The handler sleeps 300ms inside the coroutine, then echoes the id. std::string script = write_script("sleep.lua", "function handle(ctx)\n" - " hloop.sleep(300)\n" + " hv.sleep(300)\n" " return ctx:json({ ok = true, id = ctx:query('id') })\n" "end\n"); diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp index c5f5bfe9b..f555df56c 100644 --- a/unittest/lua_binding_test.cpp +++ b/unittest/lua_binding_test.cpp @@ -6,11 +6,11 @@ * "probe" C function exposed to the scripts. * * Covered: - * 1. hv.now / hv.json encode+decode roundtrip - * 2. hloop.setTimeout fires the callback - * 3. hloop.setInterval + clearTimer (including clearTimer from within the + * 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. hloop.sleep suspends/resumes the coroutine (synchronous-style async) + * 4. hv.sleep suspends/resumes the coroutine (synchronous-style async) * 5. two coroutines interleave on one loop thread (concurrency, not parallel) */ @@ -26,7 +26,7 @@ extern "C" { } #include "hloop.h" -#include "hv_lua.h" +#include "hvlua.h" // A probe table the scripts write to, so C can assert what happened. static std::string g_probe; @@ -62,7 +62,7 @@ static void test_core_json() { "assert(t.a == 1)\n" "assert(t.b[1] == 2 and t.b[2] == 3)\n" "assert(t.c == 'x')\n" - "assert(type(hv.now()) == 'number')\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" @@ -74,9 +74,9 @@ static void test_core_json() { static void test_set_timeout() { run_script( - "hloop.setTimeout(10, function()\n" + "hv.setTimeout(10, function()\n" " probe('fired')\n" - " hloop.stop()\n" + " hv.stop()\n" "end)\n" ); assert(g_probe == "fired"); @@ -88,12 +88,12 @@ static void test_interval_clear() { run_script( "local n = 0\n" "local id\n" - "id = hloop.setInterval(10, function()\n" + "id = hv.setInterval(10, function()\n" " n = n + 1\n" " probe(tostring(n))\n" " if n >= 3 then\n" - " hloop.clearTimer(id)\n" - " hloop.stop()\n" + " hv.clearTimer(id)\n" + " hv.stop()\n" " end\n" "end)\n" ); @@ -104,11 +104,11 @@ static void test_interval_clear() { static void test_sleep_coroutine() { // sleep suspends the coroutine; probe order proves it resumes after wait. run_script( - "hloop.setTimeout(1, function()\n" + "hv.setTimeout(1, function()\n" " probe('a')\n" - " hloop.sleep(30)\n" + " hv.sleep(30)\n" " probe('b')\n" - " hloop.stop()\n" + " hv.stop()\n" "end)\n" ); assert(g_probe == "a,b"); @@ -122,13 +122,13 @@ static void test_two_coroutines_interleave() { "local done = 0\n" "local function worker(name, ms)\n" " probe(name..'1')\n" - " hloop.sleep(ms)\n" + " hv.sleep(ms)\n" " probe(name..'2')\n" " done = done + 1\n" - " if done == 2 then hloop.stop() end\n" + " if done == 2 then hv.stop() end\n" "end\n" - "hloop.setTimeout(1, function() worker('A', 20) end)\n" - "hloop.setTimeout(1, function() worker('B', 60) 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. From b804661cd05b7ef337cd316fa1da025969b08384 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 13:57:58 +0800 Subject: [PATCH 07/33] feat(lua): event-layer TCP/UDP IO binding (coroutine-synchronous) Add hv.connect / hv.tcpServer / hv.udpClient / hv.udpServer and conn/sock methods (read/readline/readuntil/readbytes/setUnpack/write/close/peeraddr/fd, sendto/recvfrom) to hvlua_event.c. All built directly on event/ hio (no evpp, no extra thread): each op suspends the running coroutine and resumes on the same loop thread when the hio callback fires, so scripts write synchronous-style non-blocking IO. TCP server runs on_conn(conn) in a fresh coroutine per accepted connection; conn:setUnpack maps to hio_set_unpack for framed-packet reads. lua_io_test covers TCP echo, length_field unpack, and UDP echo on a single loop (no external processes). examples/lua/{tcp,udp}_{client,server}.lua + tcp_unpack. hvlua runtime now line-buffers stdout so long-running server scripts log promptly. Verified: Makefile + CMake builds (hvlua_event.c compiled as C), all lua unittests pass, connect-failure path returns nil,err safely. --- Makefile | 3 +- docs/cn/HttpLuaHandler.md | 20 ++ examples/hvlua.cpp | 3 + examples/lua/tcp_client.lua | 28 ++ examples/lua/tcp_server.lua | 23 ++ examples/lua/tcp_unpack.lua | 39 +++ examples/lua/udp_client.lua | 20 ++ examples/lua/udp_server.lua | 15 ++ lua/hvlua_event.c | 525 +++++++++++++++++++++++++++++++++++- scripts/unittest.sh | 3 + unittest/CMakeLists.txt | 5 +- unittest/lua_io_test.cpp | 129 +++++++++ 12 files changed, 810 insertions(+), 3 deletions(-) create mode 100644 examples/lua/tcp_client.lua create mode 100644 examples/lua/tcp_server.lua create mode 100644 examples/lua/tcp_unpack.lua create mode 100644 examples/lua/udp_client.lua create mode 100644 examples/lua/udp_server.lua create mode 100644 unittest/lua_io_test.cpp diff --git a/Makefile b/Makefile index 2cabc7246..c758a8bc4 100644 --- a/Makefile +++ b/Makefile @@ -341,10 +341,11 @@ ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_LUA), yes) $(MAKE) libhv $(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) $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_async_test unittest/http_lua_async_test.cpp -Llib -lhv -pthread $(LUA_LIBS) else - $(RM) bin/lua_binding_test bin/http_lua_handler_test bin/http_lua_async_test + $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv diff --git a/docs/cn/HttpLuaHandler.md b/docs/cn/HttpLuaHandler.md index d485d270f..d77a32518 100644 --- a/docs/cn/HttpLuaHandler.md +++ b/docs/cn/HttpLuaHandler.md @@ -127,6 +127,26 @@ 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 不阻塞): diff --git a/examples/hvlua.cpp b/examples/hvlua.cpp index 385cdcef8..a3b4812bf 100644 --- a/examples/hvlua.cpp +++ b/examples/hvlua.cpp @@ -33,6 +33,9 @@ int main(int argc, char** argv) { 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); // QUIT_WHEN_NO_ACTIVE_EVENTS: exit once the script and all timers/async 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..7c4237fde --- /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.tcpServer(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("tcpServer 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/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/lua/hvlua_event.c b/lua/hvlua_event.c index 5692042a5..6a948fc1f 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -4,6 +4,8 @@ #include "hvlua.h" +#include // memset / memcpy / strcmp + #include "hbase.h" // HV_ALLOC / HV_FREE #include "hloop.h" #include "hlog.h" @@ -258,12 +260,532 @@ static int l_hloop_stop(lua_State* L) { return 0; } +// ============================================================================ +// TCP client: hv.connect(host, port [, timeout_ms]) -> conn | nil, err +// +// 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 +} 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 || 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.connect: 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; + 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; +} + +// Shared: arm the read callback + suspend. The specific hio_read* call is done +// by the caller (read once / read_until_delim / read_until_length). +static int conn_suspend_read(lua_State* L, LuaConn* c) { + hio_setcb_read(c->io, on_conn_read); + 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; + } + hio_read(c->io); + return conn_suspend_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); + unsigned int n = (unsigned int)luaL_checkinteger(L, 2); + if (c->closed || c->io == NULL) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + hio_read_until_length(c->io, n); + return conn_suspend_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"); + } + hio_read_until_delim(c->io, (unsigned char)delim[0]); + return conn_suspend_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; + } + hio_read_until_delim(c->io, '\n'); + return conn_suspend_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; + 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)) c->unpack->package_max_length = (unsigned int)lua_tointeger(L, -1); + 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"); + c->unpack->fixed_length = (unsigned int)luaL_optinteger(L, -1, 0); + 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 > PACKAGE_MAX_DELIMITER_BYTES) dlen = 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; + c->unpack->mode = UNPACK_BY_LENGTH_FIELD; + lua_getfield(L, 2, "body_offset"); + c->unpack->body_offset = (unsigned short)luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + lua_getfield(L, 2, "length_field_offset"); + c->unpack->length_field_offset = (unsigned short)luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + lua_getfield(L, 2, "length_field_bytes"); + c->unpack->length_field_bytes = (unsigned short)luaL_optinteger(L, -1, 0); + lua_pop(L, 1); + lua_getfield(L, 2, "length_adjustment"); + c->unpack->length_adjustment = (short)luaL_optinteger(L, -1, 0); + 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 +// +// 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 || 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; + } + hio_setcb_read(c->io, on_udp_read); + c->co = hvlua_suspend(L); + hio_read(c->io); + 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 }, + { "connect", l_hv_connect }, + { "tcpServer", l_hv_tcpServer }, + { "udpClient", l_hv_udpClient }, + { "udpServer", l_hv_udpServer }, { "run", l_hloop_run }, { "stop", l_hloop_stop }, { NULL, NULL } @@ -271,9 +793,10 @@ static const luaL_Reg hloop_funcs[] = { // Register the event-loop primitives into the global "hv" table: // hv.setTimeout / hv.setInterval / hv.clearTimer / hv.sleep / -// hv.resolveDns / hv.run / hv.stop +// hv.resolveDns / hv.connect / hv.tcpServer / 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); diff --git a/scripts/unittest.sh b/scripts/unittest.sh index 7d18c5b81..516c72be1 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -35,6 +35,9 @@ 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_lua_handler_test ]; then bin/http_lua_handler_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index d7e844f2d..b42dd429e 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -94,13 +94,16 @@ 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}) add_executable(http_lua_handler_test http_lua_handler_test.cpp) target_include_directories(http_lua_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server) target_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) add_executable(http_lua_async_test http_lua_async_test.cpp) target_include_directories(http_lua_async_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) target_link_libraries(http_lua_async_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test http_lua_handler_test http_lua_async_test) +set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test http_lua_handler_test http_lua_async_test) endif() # ------event: async dns------ diff --git a/unittest/lua_io_test.cpp b/unittest/lua_io_test.cpp new file mode 100644 index 000000000..8ad83835d --- /dev/null +++ b/unittest/lua_io_test.cpp @@ -0,0 +1,129 @@ +/* + * 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"); +} + +int main() { + test_tcp_echo(); + test_tcp_unpack(); + test_udp_echo(); + printf("ALL lua_io_test PASSED\n"); + return 0; +} From 61882aa733d068e3406a5dd987df9a1b05b18949 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 20:37:00 +0800 Subject: [PATCH 08/33] feat(lua): hv.http coroutine-synchronous HTTP client binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add hv.http.{get,post,put,delete,request} to the Lua runtime. Requests are issued through a per-lua_State AsyncHttpClient bound to the current loop (currentThreadEventLoopPtr), so completion callbacks fire on the same loop thread and resume the suspended coroutine directly — no cross-thread hop, single-loop model. Enabled via -DHVLUA_WITH_HTTP (WITH_LUA && WITH_HTTP). Supporting changes: - EventLoop: inherit enable_shared_from_this and add currentThreadEventLoopPtr so a raw EventLoop* (from TLS) can recover the owning EventLoopPtr to hand to AsyncHttpClient. Falls back to NULL for non-shared (stack) loops. - EventLoop::run(): null an owned (AUTO_FREE) loop_ after hloop_run returns, since hloop_run already freed it; prevents a dangling loop_ when the loop was stopped via the raw C hloop_stop() (e.g. Lua hv.stop()) instead of stop(). - AsyncHttpClient: add is_loop_owner so an externally-provided loop is not stopped/freed on destruction (mirrors evpp/TcpClient). Clear per-connection onclose before member teardown to avoid a keep-alive UAF where ~channels -> ~Channel -> close -> onclose touches the already-destroyed conn_pools map. - hvlua / example runtime now use make_shared() directly instead of hloop_new(0) + wrapper + manual TLS/free. Tests: unittest/lua_http_test.cpp (in-process server + hv.http.get) wired into Makefile, CMake and scripts/unittest.sh; examples/lua/http_client.lua demo. --- Makefile | 9 +- Makefile.in | 11 +++ evpp/EventLoop.h | 38 +++++++- examples/hvlua.cpp | 50 +++++------ examples/lua/http_client.lua | 18 ++++ http/client/AsyncHttpClient.h | 35 ++++++-- lua/hvlua.c | 5 +- lua/hvlua.h | 5 +- lua/hvlua_http.cpp | 164 ++++++++++++++++++++++++++++++++++ scripts/unittest.sh | 3 + unittest/CMakeLists.txt | 6 ++ unittest/lua_http_test.cpp | 77 ++++++++++++++++ 12 files changed, 384 insertions(+), 37 deletions(-) create mode 100644 examples/lua/http_client.lua create mode 100644 lua/hvlua_http.cpp create mode 100644 unittest/lua_http_test.cpp diff --git a/Makefile b/Makefile index c758a8bc4..0385bc12c 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,10 @@ BUILD_STATIC ?= yes # http/server examples compile sources directly (not linking libhv), so when # WITH_LUA is on they must also include the lua/ binding sources that -# HttpLuaHandler depends on. +# HttpLuaHandler depends on (and http/client, which the lua hv.http binding uses). HTTP_SERVER_EXAMPLE_SRCDIRS = $(CORE_SRCDIRS) util cpputil evpp http http/server ifeq ($(WITH_LUA), yes) -HTTP_SERVER_EXAMPLE_SRCDIRS += lua +HTTP_SERVER_EXAMPLE_SRCDIRS += lua http/client endif LIBHV_SRCDIRS = $(CORE_SRCDIRS) util @@ -344,8 +344,11 @@ 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_io_test unittest/lua_io_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 -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_async_test unittest/http_lua_async_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +ifeq ($(WITH_HTTP), 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/lua_http_test unittest/lua_http_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +endif else - $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test + $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test bin/lua_http_test endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv diff --git a/Makefile.in b/Makefile.in index 6c9b4392b..55a759bcb 100644 --- a/Makefile.in +++ b/Makefile.in @@ -164,6 +164,17 @@ 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_HTTP), yes) + CPPFLAGS += -DHVLUA_WITH_HTTP +endif +ifeq ($(WITH_REDIS), yes) + CPPFLAGS += -DHVLUA_WITH_REDIS +endif +ifeq ($(WITH_MQTT), yes) + CPPFLAGS += -DHVLUA_WITH_MQTT +endif endif CPPFLAGS += $(addprefix -D, $(DEFINES)) diff --git a/evpp/EventLoop.h b/evpp/EventLoop.h index fa8128efe..c97252456 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,16 @@ class EventLoop : public Status { setStatus(kRunning); hloop_run(loop_); setStatus(kStopped); + // 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 +317,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/examples/hvlua.cpp b/examples/hvlua.cpp index a3b4812bf..aef8d9c48 100644 --- a/examples/hvlua.cpp +++ b/examples/hvlua.cpp @@ -2,13 +2,14 @@ // // Usage: hvlua script.lua [args...] // -// Binds an hv::EventLoop to this thread (so currentThreadEventLoop and the -// hv.* client bindings work exactly as they do inside an HTTP server IO -// thread), initializes the per-loop lua_State, loads and runs the script -// (inside a coroutine so it may use synchronous-style async APIs), then runs -// the event loop so timers / async IO can complete. +// 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 -#include extern "C" { #include @@ -16,7 +17,6 @@ extern "C" { #include } -#include "hloop.h" #include "hlog.h" #include "EventLoop.h" #include "hvlua.h" @@ -38,25 +38,19 @@ int main(int argc, char** argv) { setvbuf(stdout, NULL, _IOLBF, 0); hlog_set_handler(stdout_logger); - // QUIT_WHEN_NO_ACTIVE_EVENTS: exit once the script and all timers/async - // work are done, so a script without an explicit hloop.stop() still ends. - // (No AUTO_FREE: the EventLoop wrapper owns/frees this hloop.) - hloop_t* hloop = hloop_new(HLOOP_FLAG_QUIT_WHEN_NO_ACTIVE_EVENTS); - if (hloop == NULL) { - fprintf(stderr, "hvlua: failed to create event loop\n"); - return 1; - } - - // Wrap in an EventLoop and publish it as this thread's loop, so - // currentThreadEventLoop (used by hv.http etc.) resolves during the script. - hv::EventLoop loop(hloop); - hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, &loop); + // 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(hloop); + lua_State* L = hv::hvlua_state(loop->loop()); if (L == NULL) { fprintf(stderr, "hvlua: failed to create lua state\n"); - hloop_free(&hloop); return 1; } lua_createtable(L, argc - 1, 0); @@ -66,11 +60,15 @@ int main(int argc, char** argv) { } 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 hloop.stop() or no work remains. - if (hv::hvlua_dofile(hloop, script) == 0) { - loop.run(); // sets TLS again + hloop_run + // 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 } - hloop_free(&hloop); return 0; } 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/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 31fd262fb..399bd8f86 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -104,16 +104,38 @@ struct HttpClientContext { class HV_EXPORT AsyncHttpClient : private EventLoopThread { public: - AsyncHttpClient(EventLoopPtr loop = NULL) : EventLoopThread(loop) { + AsyncHttpClient(EventLoopPtr loop = NULL) + : EventLoopThread(loop) + , is_loop_owner(loop == NULL) + { 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); + // When we own the loop (constructed with loop==NULL), stop and join the + // loop thread FIRST, so nothing runs concurrently while we tear down + // below. With an external loop, do NOT stop it — the caller owns its + // lifetime (mirrors evpp/TcpClient.h TcpClientTmpl), and in the + // single-loop model this destructor already runs on the loop thread + // (e.g. via a Lua __gc during hloop teardown). Any in-flight async DNS + // queries are owned by EventLoop::resolveDns / the C resolver and torn + // down with the loop; nothing to clean here. + if (is_loop_owner) { + 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 @@ -163,6 +185,9 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { std::map channels; // peeraddr => ConnPool std::map> conn_pools; + // Whether this client owns (and must stop) its event loop. False when an + // external loop was passed in (then the caller owns the loop's lifetime). + bool is_loop_owner; }; } diff --git a/lua/hvlua.c b/lua/hvlua.c index 4b15062b6..847b15a8b 100644 --- a/lua/hvlua.c +++ b/lua/hvlua.c @@ -173,10 +173,13 @@ static lua_State* hvlua_new_state(hloop_t* loop) { lua_setfield(L, LUA_REGISTRYINDEX, "hv.loop"); // Register modules from most basic to higher-level (mirrors libhv layering): - // base -> event -> json -> (future: http, redis, mqtt, ...) + // base -> event -> json -> http -> (future: redis, mqtt, ...) hvlua_open_base(L); hvlua_open_event(L); hvlua_open_json(L); +#ifdef HVLUA_WITH_HTTP + hvlua_open_http(L); +#endif hloop_set_lua_state(loop, L, hvlua_state_dtor); return L; diff --git a/lua/hvlua.h b/lua/hvlua.h index ebb7bedae..ac03f3b87 100644 --- a/lua/hvlua.h +++ b/lua/hvlua.h @@ -95,7 +95,10 @@ int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud); 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) -// future: hvlua_open_http, hvlua_open_redis, hvlua_open_mqtt, ... +#ifdef HVLUA_WITH_HTTP +void hvlua_open_http(lua_State* L); // hvlua_http.cpp -> hv.http (http/client AsyncHttpClient) +#endif +// future: hvlua_open_redis, hvlua_open_mqtt, ... END_EXTERN_C diff --git a/lua/hvlua_http.cpp b/lua/hvlua_http.cpp new file mode 100644 index 000000000..93dba7e4e --- /dev/null +++ b/lua/hvlua_http.cpp @@ -0,0 +1,164 @@ +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; // is_loop_owner=false: 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; + } + + 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); + } + } + + HvLuaCoroutine* co = hvlua_suspend(L); + 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); + } + }); + 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/scripts/unittest.sh b/scripts/unittest.sh index 516c72be1..a54684275 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -44,6 +44,9 @@ fi if [ -x bin/http_lua_async_test ]; then bin/http_lua_async_test fi +if [ -x bin/lua_http_test ]; then + bin/lua_http_test +fi if [ -x bin/hdns_test ]; then bin/hdns_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index b42dd429e..cded19a42 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -104,6 +104,12 @@ add_executable(http_lua_async_test http_lua_async_test.cpp) target_include_directories(http_lua_async_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) target_link_libraries(http_lua_async_test ${HV_LIBRARIES}) set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test http_lua_handler_test http_lua_async_test) +if(WITH_HTTP) +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}) +set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_http_test) +endif() endif() # ------event: async dns------ 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; +} From 6f490eb3526b93ea37fd264043a14fb03c4a6967 Mon Sep 17 00:00:00 2001 From: ithewei Date: Wed, 29 Jul 2026 22:54:50 +0800 Subject: [PATCH 09/33] refactor(evpp): hoist is_loop_owner into EventLoopThread base class Consolidate the duplicated is_loop_owner flag out of each subclass (TcpClient/TcpServer/UdpClient/UdpServer) into EventLoopThread, exposed via isLoopOwner(). The base ctor derives it from loop==NULL, and stop() guards on ownership so it is a no-op for an external, not-yet-running loop the caller owns. AsyncHttpClient, AsyncRedisClient and RedisSubscriber now query isLoopOwner() instead of carrying their own copy. --- evpp/EventLoopThread.h | 25 +++++++++++++++++++++++++ evpp/TcpClient.h | 11 +++-------- evpp/TcpServer.h | 10 +++------- evpp/UdpClient.h | 11 +++-------- evpp/UdpServer.h | 11 +++-------- http/client/AsyncHttpClient.h | 16 +--------------- lua/hvlua_http.cpp | 2 +- redis/AsyncRedisClient.cpp | 14 ++++++-------- redis/RedisSubscriber.cpp | 14 ++++++-------- 9 files changed, 51 insertions(+), 63 deletions(-) diff --git a/evpp/EventLoopThread.h b/evpp/EventLoopThread.h index c876accaa..877bbdc0e 100644 --- a/evpp/EventLoopThread.h +++ b/evpp/EventLoopThread.h @@ -17,6 +17,13 @@ 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. + is_loop_owner_ = (loop == NULL); loop_ = loop ? loop : std::make_shared(); setStatus(kInitialized); } @@ -30,6 +37,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(); } @@ -59,7 +73,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 +132,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/http/client/AsyncHttpClient.h b/http/client/AsyncHttpClient.h index 399bd8f86..64eb0dc9f 100644 --- a/http/client/AsyncHttpClient.h +++ b/http/client/AsyncHttpClient.h @@ -106,24 +106,13 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { public: AsyncHttpClient(EventLoopPtr loop = NULL) : EventLoopThread(loop) - , is_loop_owner(loop == NULL) { if (loop == NULL) { EventLoopThread::start(true); } } ~AsyncHttpClient() { - // When we own the loop (constructed with loop==NULL), stop and join the - // loop thread FIRST, so nothing runs concurrently while we tear down - // below. With an external loop, do NOT stop it — the caller owns its - // lifetime (mirrors evpp/TcpClient.h TcpClientTmpl), and in the - // single-loop model this destructor already runs on the loop thread - // (e.g. via a Lua __gc during hloop teardown). Any in-flight async DNS - // queries are owned by EventLoop::resolveDns / the C resolver and torn - // down with the loop; nothing to clean here. - if (is_loop_owner) { - EventLoopThread::stop(true); - } + 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 @@ -185,9 +174,6 @@ class HV_EXPORT AsyncHttpClient : private EventLoopThread { std::map channels; // peeraddr => ConnPool std::map> conn_pools; - // Whether this client owns (and must stop) its event loop. False when an - // external loop was passed in (then the caller owns the loop's lifetime). - bool is_loop_owner; }; } diff --git a/lua/hvlua_http.cpp b/lua/hvlua_http.cpp index 93dba7e4e..eb4678c74 100644 --- a/lua/hvlua_http.cpp +++ b/lua/hvlua_http.cpp @@ -33,7 +33,7 @@ struct LuaHttpClientBox { static int http_client_gc(lua_State* L) { LuaHttpClientBox* box = (LuaHttpClientBox*)lua_touserdata(L, 1); if (box && box->client) { - delete box->client; // is_loop_owner=false: does NOT stop the shared loop + delete box->client; // external loop (not owner): does NOT stop the shared loop box->client = NULL; } return 0; diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index 8626c10b6..5b440ce8d 100644 --- a/redis/AsyncRedisClient.cpp +++ b/redis/AsyncRedisClient.cpp @@ -60,7 +60,6 @@ struct AsyncRedisClient::Impl { TcpClientEventLoopTmpl tcp_client; std::deque > pending; RedisParser parser; - bool is_loop_owner; std::string host; int port; int connect_timeout_ms; @@ -75,10 +74,9 @@ struct AsyncRedisClient::Impl { size_t handshake_index; std::vector handshake_commands; - Impl(AsyncRedisClient* client, const EventLoopPtr& loop, bool loop_owner) + Impl(AsyncRedisClient* client, const EventLoopPtr& loop) : self(client) , tcp_client(loop) - , is_loop_owner(loop_owner) , port(6379) , connect_timeout_ms(5000) , timeout_ms(5000) @@ -99,7 +97,7 @@ struct AsyncRedisClient::Impl { if (!started || destroyed || stop_in_progress || !accept_requests || self->loop() == NULL || self->loop()->loop() == NULL) { return false; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (!self->isLoopOwner() && !self->loop()->isRunning()) { return false; } return true; @@ -157,7 +155,7 @@ struct AsyncRedisClient::Impl { if (!accept_requests || destroyed || self->loop() == NULL || self->loop()->loop() == NULL) { return ERR_CONNECT; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (!self->isLoopOwner() && !self->loop()->isRunning()) { return ERR_CONNECT; } int ret = applySettings(); @@ -444,7 +442,7 @@ struct AsyncRedisClient::Impl { AsyncRedisClient::AsyncRedisClient(EventLoopPtr loop) : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop(), loop == NULL)) { + , impl_(std::make_shared(this, EventLoopThread::loop())) { impl_->initCallbacks(); } @@ -485,7 +483,7 @@ void AsyncRedisClient::start(bool wait_threads_started) { impl_->stop_in_progress = false; impl_->started = true; impl_->accept_requests = true; - if (!impl_->is_loop_owner) { + if (!isLoopOwner()) { if (!loop() || !loop()->loop() || !loop()->isRunning()) { impl_->started = false; impl_->accept_requests = false; @@ -518,7 +516,7 @@ void AsyncRedisClient::stop(bool wait_threads_stopped) { impl_->stop_in_progress = false; return; } - if (!impl_->is_loop_owner) { + if (!isLoopOwner()) { if (loop()->isRunning()) { impl_->runCleanupOnLoopAndWait(); } diff --git a/redis/RedisSubscriber.cpp b/redis/RedisSubscriber.cpp index df52c4234..be0c9d7f5 100644 --- a/redis/RedisSubscriber.cpp +++ b/redis/RedisSubscriber.cpp @@ -53,7 +53,6 @@ struct RedisSubscriber::Impl { RedisSubscriber* self; TcpClientEventLoopTmpl tcp_client; RedisParser parser; - bool is_loop_owner; std::string host; int port; std::string password; @@ -68,10 +67,9 @@ struct RedisSubscriber::Impl { std::set channels; std::set patterns; - Impl(RedisSubscriber* subscriber, const EventLoopPtr& loop, bool loop_owner) + Impl(RedisSubscriber* subscriber, const EventLoopPtr& loop) : self(subscriber) , tcp_client(loop) - , is_loop_owner(loop_owner) , port(6379) , db(0) , handshake_pending(false) @@ -90,7 +88,7 @@ struct RedisSubscriber::Impl { if (!started || destroyed || stop_in_progress || !accept_requests || self->loop() == NULL || self->loop()->loop() == NULL) { return false; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (!self->isLoopOwner() && !self->loop()->isRunning()) { return false; } return true; @@ -142,7 +140,7 @@ struct RedisSubscriber::Impl { if (!accept_requests || destroyed || self->loop() == NULL || self->loop()->loop() == NULL) { return ERR_CONNECT; } - if (!is_loop_owner && !self->loop()->isRunning()) { + if (!self->isLoopOwner() && !self->loop()->isRunning()) { return ERR_CONNECT; } int ret = applySettings(); @@ -484,7 +482,7 @@ struct RedisSubscriber::Impl { RedisSubscriber::RedisSubscriber(EventLoopPtr loop) : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop(), loop == NULL)) { + , impl_(std::make_shared(this, EventLoopThread::loop())) { impl_->initCallbacks(); } @@ -517,7 +515,7 @@ void RedisSubscriber::start(bool wait_threads_started) { impl_->stop_in_progress = false; impl_->started = true; impl_->accept_requests = true; - if (!impl_->is_loop_owner) { + if (!isLoopOwner()) { if (!loop() || !loop()->loop() || !loop()->isRunning()) { impl_->started = false; impl_->accept_requests = false; @@ -550,7 +548,7 @@ void RedisSubscriber::stop(bool wait_threads_stopped) { impl_->stop_in_progress = false; return; } - if (!impl_->is_loop_owner) { + if (!isLoopOwner()) { if (loop()->isRunning()) { impl_->runCleanupOnLoopAndWait(); } From d2d4c2907cf2b87446630fd4bc9b65c3077ee253 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 12:00:09 +0800 Subject: [PATCH 10/33] refactor(redis): rebuild AsyncRedisClient/RedisSubscriber on TcpClient AsyncRedisClient and RedisSubscriber now inherit TcpClientTmpl (like WebSocketClient) instead of composing a TcpClientEventLoopTmpl member. The base class provides connect/reconnect/DNS/loop-ownership, so the duplicated tcp_client member, the mirrored host/port/timeout/reconnect fields, and every isLoopOwner() branch are gone; each Impl keeps only the RESP protocol layer (handshake, request pipelining, reply dispatch) driven through the inherited channel/send/startConnect/setReconnect API. This unifies external-loop semantics with TcpClient: starting on an external, not-yet-running loop now spins the client's own worker thread instead of the old redis-specific ERR_CONNECT rejection. redis_async_client_test is updated to assert the unified contract. All redis unit tests (protocol/async_client/client/batch/subscriber) pass. --- redis/AsyncRedisClient.cpp | 162 ++++++++++----------------- redis/AsyncRedisClient.h | 15 ++- redis/RedisSubscriber.cpp | 153 ++++++++++--------------- redis/RedisSubscriber.h | 11 +- unittest/redis_async_client_test.cpp | 59 +++++++--- 5 files changed, 184 insertions(+), 216 deletions(-) diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index 5b440ce8d..6ba91cfb6 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; @@ -57,12 +60,10 @@ struct AsyncRedisClient::Impl { }; AsyncRedisClient* self; - TcpClientEventLoopTmpl tcp_client; std::deque > pending; RedisParser parser; std::string host; int port; - int connect_timeout_ms; int timeout_ms; std::string password; int db; @@ -74,11 +75,9 @@ struct AsyncRedisClient::Impl { size_t handshake_index; std::vector handshake_commands; - Impl(AsyncRedisClient* client, const EventLoopPtr& loop) + explicit Impl(AsyncRedisClient* client) : self(client) - , tcp_client(loop) , port(6379) - , connect_timeout_ms(5000) , timeout_ms(5000) , db(0) , handshake_pending(false) @@ -94,17 +93,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 (!self->isLoopOwner() && !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; } @@ -124,7 +126,7 @@ struct AsyncRedisClient::Impl { } }; - tcp_client.onMessage = [this](const SocketChannelPtr&, Buffer* buf) { + self->onMessage = [this](const SocketChannelPtr&, Buffer* buf) { if (destroyed) { return; } @@ -139,56 +141,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 (!self->isLoopOwner() && !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(); } } @@ -274,11 +261,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; } @@ -304,7 +291,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; @@ -325,9 +312,9 @@ struct AsyncRedisClient::Impl { } void flushPending() { - if (!tcp_client.isConnected()) { + if (!self->isConnected()) { if (started) { - tcp_client.start(); + self->startConnect(); } return; } @@ -336,7 +323,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; @@ -385,7 +372,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; } @@ -420,8 +407,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(); } } @@ -441,8 +428,10 @@ struct AsyncRedisClient::Impl { }; AsyncRedisClient::AsyncRedisClient(EventLoopPtr loop) - : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop())) { + : TcpClientTmpl(loop) + , impl_(std::make_shared(this)) { + // preserve the historical redis default connect timeout (base default differs) + setConnectTimeout(5000); impl_->initCallbacks(); } @@ -466,44 +455,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 (!isLoopOwner()) { - 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) { @@ -511,33 +481,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 (!isLoopOwner()) { + // 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 be0c9d7f5..1f97d5033 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 { @@ -51,7 +55,6 @@ struct RedisSubscriber::Impl { }; RedisSubscriber* self; - TcpClientEventLoopTmpl tcp_client; RedisParser parser; std::string host; int port; @@ -67,9 +70,8 @@ struct RedisSubscriber::Impl { std::set channels; std::set patterns; - Impl(RedisSubscriber* subscriber, const EventLoopPtr& loop) + explicit Impl(RedisSubscriber* subscriber) : self(subscriber) - , tcp_client(loop) , port(6379) , db(0) , handshake_pending(false) @@ -85,17 +87,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 (!self->isLoopOwner() && !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; } @@ -110,7 +118,7 @@ struct RedisSubscriber::Impl { } }; - tcp_client.onMessage = [this](const SocketChannelPtr&, Buffer* buf) { + tcp->onMessage = [this](const SocketChannelPtr&, Buffer* buf) { if (destroyed) { return; } @@ -125,54 +133,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 (!self->isLoopOwner() && !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(); } } @@ -278,7 +276,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; @@ -286,17 +284,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; } @@ -331,7 +329,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; @@ -359,7 +357,7 @@ struct RedisSubscriber::Impl { void handleHandshakeReply(const RedisReply& reply) { if (reply.isError()) { - tcp_client.setReconnect(NULL); + self->setReconnect(NULL); handleClientError(ERR_RESPONSE); return; } @@ -461,8 +459,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(); } } @@ -481,8 +479,8 @@ struct RedisSubscriber::Impl { }; RedisSubscriber::RedisSubscriber(EventLoopPtr loop) - : EventLoopThread(loop) - , impl_(std::make_shared(this, EventLoopThread::loop())) { + : TcpClientTmpl(loop) + , impl_(std::make_shared(this)) { impl_->initCallbacks(); } @@ -506,36 +504,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 (!isLoopOwner()) { - 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(); - }); + TcpClientTmpl::start(wait_threads_started); } void RedisSubscriber::stop(bool wait_threads_stopped) { @@ -543,28 +524,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 (!isLoopOwner()) { + 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/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; From 8199c75e580a710ce015ece90c36df6339770cc9 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 12:00:56 +0800 Subject: [PATCH 11/33] feat(lua): hv.redis / hv.ws / hv.mqtt coroutine-synchronous bindings Add three script-facing client modules under the hv.* table, all following the single-loop coroutine-synchronous model of hv.http (client bound to the current loop; completion callbacks fire on the same thread and resume the coroutine directly, no cross-thread hop): - hv.redis: hv.redis.new{host,port,auth,db,timeout} + r:command(...) and get/set/del/incr/... sugar; RESP reply -> Lua value, error reply -> nil,err. - hv.ws: hv.ws.connect(url[,headers]) + ws:send/recv/close; message-driven, so inbound frames are buffered and ws:recv() suspends until one arrives. - hv.mqtt: hv.mqtt.connect{...} + m:publish/subscribe/unsubscribe/recv/ disconnect; binds the raw mqtt_client_t C API and installs its own dispatch callback (MqttClient::run() would block the shared loop), recv() buffered. Modules are conditionally compiled on HVLUA_WITH_HTTP/REDIS/MQTT (set by the build when WITH_HTTP/REDIS/MQTT are on) and registered by hvlua_state(). Tests: lua_redis_test (FakeRedisServer, full command + error mapping), lua_ws_test (in-process WebSocket echo server, send/recv round-trip), lua_mqtt_test (registration + connect-failure path). Example scripts under examples/lua/. All pass. --- Makefile | 11 +- examples/lua/mqtt_client.lua | 29 +++ examples/lua/redis_client.lua | 26 +++ examples/lua/ws_client.lua | 27 +++ lua/hvlua.c | 9 +- lua/hvlua.h | 8 +- lua/hvlua_mqtt.cpp | 355 ++++++++++++++++++++++++++++++++++ lua/hvlua_redis.cpp | 275 ++++++++++++++++++++++++++ lua/hvlua_ws.cpp | 256 ++++++++++++++++++++++++ unittest/lua_mqtt_test.cpp | 73 +++++++ unittest/lua_redis_test.cpp | 88 +++++++++ unittest/lua_ws_test.cpp | 82 ++++++++ 12 files changed, 1235 insertions(+), 4 deletions(-) create mode 100644 examples/lua/mqtt_client.lua create mode 100644 examples/lua/redis_client.lua create mode 100644 examples/lua/ws_client.lua create mode 100644 lua/hvlua_mqtt.cpp create mode 100644 lua/hvlua_redis.cpp create mode 100644 lua/hvlua_ws.cpp create mode 100644 unittest/lua_mqtt_test.cpp create mode 100644 unittest/lua_redis_test.cpp create mode 100644 unittest/lua_ws_test.cpp diff --git a/Makefile b/Makefile index 0385bc12c..07cfd7a66 100644 --- a/Makefile +++ b/Makefile @@ -346,9 +346,13 @@ ifeq ($(WITH_LUA), 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_async_test unittest/http_lua_async_test.cpp -Llib -lhv -pthread $(LUA_LIBS) ifeq ($(WITH_HTTP), 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/lua_http_test unittest/lua_http_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/lua_ws_test unittest/lua_ws_test.cpp -Llib -lhv -pthread $(LUA_LIBS) +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 else - $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test bin/lua_http_test + $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test bin/lua_http_test bin/lua_ws_test bin/lua_mqtt_test endif ifeq ($(WITH_REDIS), yes) $(MAKE) libhv @@ -357,8 +361,11 @@ ifeq ($(WITH_REDIS), yes) $(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 +ifeq ($(WITH_LUA), 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 else - $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test + $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test bin/lua_redis_test endif else $(RM) bin/tcpclient_dns_test diff --git a/examples/lua/mqtt_client.lua b/examples/lua/mqtt_client.lua new file mode 100644 index 000000000..10d96bdb5 --- /dev/null +++ b/examples/lua/mqtt_client.lua @@ -0,0 +1,29 @@ +-- 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) + local m, err = hv.mqtt.connect({ host = host, port = port, id = "hvlua-demo", keepalive = 60 }) + 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/ws_client.lua b/examples/lua/ws_client.lua new file mode 100644 index 000000000..40bc52673 --- /dev/null +++ b/examples/lua/ws_client.lua @@ -0,0 +1,27 @@ +-- 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() + local ws, err = hv.ws.connect(url) + 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/lua/hvlua.c b/lua/hvlua.c index 847b15a8b..73dce03c6 100644 --- a/lua/hvlua.c +++ b/lua/hvlua.c @@ -173,12 +173,19 @@ static lua_State* hvlua_new_state(hloop_t* loop) { lua_setfield(L, LUA_REGISTRYINDEX, "hv.loop"); // Register modules from most basic to higher-level (mirrors libhv layering): - // base -> event -> json -> http -> (future: redis, mqtt, ...) + // 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); diff --git a/lua/hvlua.h b/lua/hvlua.h index ac03f3b87..78c15c20f 100644 --- a/lua/hvlua.h +++ b/lua/hvlua.h @@ -97,8 +97,14 @@ void hvlua_open_event(lua_State* L); // hvlua_event.c -> hv.setTimeout/sleep/r 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 -// future: hvlua_open_redis, hvlua_open_mqtt, ... END_EXTERN_C diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp new file mode 100644 index 000000000..4ba28d714 --- /dev/null +++ b/lua/hvlua_mqtt.cpp @@ -0,0 +1,355 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" + +#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 connected; + bool closed; + bool connecting; // wait_co holds a connect() waiter (vs recv()) +}; + +// 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 (nil,"closed") when +// the connection is gone and the queue is drained. 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, "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->connected = true; + 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; + box->connected = false; + 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); + lua_pushstring(co, was_connecting ? "connect failed" : "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); + EventLoopPtr loop = currentThreadEventLoopPtr; + if (!loop) { + lua_pushnil(L); + lua_pushstring(L, "hv.mqtt: no shared event loop on this thread"); + return 2; + } + + std::string host = "127.0.0.1"; + int port = DEFAULT_MQTT_PORT; + std::string id, username, password; + int keepalive = 0, ssl = 0, clean_session = -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, "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); + + LuaMqttClient* box = (LuaMqttClient*)lua_newuserdata(L, sizeof(LuaMqttClient)); + new (&box->inbox) MqttInbox(); + box->wait_co = NULL; + box->connected = false; + box->closed = false; + box->connecting = true; + 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; + + box->wait_co = hvlua_suspend(L); + int ret = mqtt_client_connect(box->client, host.c_str(), 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; +} + +// 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) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + 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) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + 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) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + 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) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + 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) { + lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + } + 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) { + 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) { + if (luaL_newmetatable(L, MQTT_CLIENT_MT)) { + lua_pushcfunction(L, mqtt_client_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + luaL_setfuncs(L, mqtt_methods, 0); + } + 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..8f28cf64a --- /dev/null +++ b/lua/hvlua_redis.cpp @@ -0,0 +1,275 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" + +#ifdef HVLUA_WITH_REDIS + +#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; +}; + +static int redis_client_gc(lua_State* L) { + LuaRedisClient* box = (LuaRedisClient*)luaL_checkudata(L, 1, REDIS_CLIENT_MT); + if (box && box->client) { + 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->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(); +} + +// Shared implementation for r:command(...) and the sugar methods. +static int redis_do_command(lua_State* L, 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; + } + + HvLuaCoroutine* co = hvlua_suspend(L); + box->client->command(cmd, [co](const RedisResult& result) { + lua_State* cur = hvlua_coroutine_state(co); + if (cur == NULL) { hvlua_cancel(co); return; } // coroutine gone + if (result.code != 0) { + lua_pushnil(cur); + lua_pushfstring(cur, "hv.redis: request failed (%d)", result.code); + hvlua_resume(co, 2); + return; + } + if (result.reply.isError()) { + lua_pushnil(cur); + lua_pushlstring(cur, result.reply.str.data(), result.reply.str.size()); + hvlua_resume(co, 2); + return; + } + reply_push(cur, result.reply); + hvlua_resume(co, 1); + }); + return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); +} + +// r:command("GET","k") | r:command({"GET","k"}) +static int l_redis_command(lua_State* L) { + RedisCommand cmd; + if (!build_command(L, 2, &cmd)) { + lua_pushnil(L); + lua_pushstring(L, "hv.redis: empty or invalid command"); + return 2; + } + return redis_do_command(L, std::move(cmd)); +} + +// 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)); + 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); + } + return redis_do_command(L, std::move(cmd)); +} + +// 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 methods (command + verb sugar) into the table on top of L. +static void register_redis_methods(lua_State* L) { + luaL_setfuncs(L, redis_methods, 0); + 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 (luaL_newmetatable(L, REDIS_CLIENT_MT)) { + lua_pushcfunction(L, redis_client_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); // methods live on the metatable itself + register_redis_methods(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_ws.cpp b/lua/hvlua_ws.cpp new file mode 100644 index 000000000..44474504f --- /dev/null +++ b/lua/hvlua_ws.cpp @@ -0,0 +1,256 @@ +extern "C" { +#include +#include +#include +} + +#include "hvlua.h" + +#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 closed; +}; + +// Resume a pending recv() coroutine (if any) with the front queued message, or +// with (nil,"closed") when the socket is gone and the queue is drained. +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->closed) { + HvLuaCoroutine* co_tok = box->recv_co; + box->recv_co = NULL; + lua_pushnil(co); + lua_pushstring(co, "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 [, headers]) -> ws | nil, err +static int l_ws_connect(lua_State* L) { + const char* url = luaL_checkstring(L, 1); + 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. + LuaWsClient* box = (LuaWsClient*)lua_newuserdata(L, sizeof(LuaWsClient)); + new (&box->inbox) WsInbox(); + box->recv_co = NULL; + box->closed = 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 + + http_headers headers = DefaultHeaders; + if (lua_istable(L, 2)) { + 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); + } + } + + box->client->onopen = [box]() { + lua_State* co = hvlua_coroutine_state(box->recv_co); + // onopen resumes the connect() coroutine, tracked in recv_co during + // the connect phase (reused slot; no recv can be pending yet). + 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); + }; + box->client->onmessage = [box](const std::string& msg) { + box->inbox.push_back(msg); + ws_try_deliver(box); + }; + box->client->onclose = [box]() { + box->closed = true; + // Wake whoever is waiting: the connect() coroutine (never opened -> the + // handshake failed) or a pending recv(). Both get (nil,"closed"); a + // successful connect resumes earlier via onopen, so if recv_co is still + // set here during connect it means the open failed. + ws_try_deliver(box); + }; + + box->recv_co = hvlua_suspend(L); // reuse recv_co to hold the connect wait + int ret = box->client->open(url, headers); + 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->closed) { + lua_pushnil(L); lua_pushstring(L, "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->closed) { + lua_pushnil(L); lua_pushstring(L, "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 && !box->closed) { + 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) { + if (luaL_newmetatable(L, WS_CLIENT_MT)) { + lua_pushcfunction(L, ws_client_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + luaL_setfuncs(L, ws_methods, 0); + } + 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/unittest/lua_mqtt_test.cpp b/unittest/lua_mqtt_test.cpp new file mode 100644 index 000000000..75812bf6b --- /dev/null +++ b/unittest/lua_mqtt_test.cpp @@ -0,0 +1,73 @@ +/* + * 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 "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; +} + +int main() { + 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..e334f591a --- /dev/null +++ b/unittest/lua_ws_test.cpp @@ -0,0 +1,82 @@ +/* + * 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" + " 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"); + printf("ALL lua_ws_test PASSED\n"); + return 0; +} From 33774e8c1394f59d79e600bae3720a67ea5fba9a Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 14:29:31 +0800 Subject: [PATCH 12/33] fix(lua): CMake HVLUA_WITH_* parity + redis __gc reentrancy guard Two code-review findings on the lua-binding work: 1. CMake never defined HVLUA_WITH_HTTP/REDIS/MQTT (Makefile.in did). Under a CMake build (CI Windows/macOS/Android/iOS) the http/redis/ws/mqtt binding bodies were #ifdef'd out, so hv.http/redis/ws/mqtt silently became nil. Add the definitions in the WITH_LUA branch, gated on WITH_HTTP/REDIS/MQTT, mirroring Makefile.in. 2. hv.redis's __gc deleted the client without first neutralizing in-flight command callbacks. Destroying the client runs ~AsyncRedisClient -> stop -> failPending synchronously on the loop thread (i.e. from inside the __gc metamethod), firing the pending lua callback which called lua_resume -- re-entering the VM from within GC. Add a 'destroyed' flag on the box, set it before delete, and have the command callback release the suspend token (hvlua_cancel, __gc-safe) instead of resuming when destroyed. This matches the teardown-safety pattern already used by hv.ws / hv.mqtt / hvlua_event. Verified: make libhv + redis_async_client/batch/subscriber + lua_redis tests all pass. --- CMakeLists.txt | 13 +++++++++++++ lua/hvlua_redis.cpp | 20 ++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 602fc941f..28c983e66 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_HTTP) + add_definitions(-DHVLUA_WITH_HTTP) + endif() + if(WITH_REDIS) + add_definitions(-DHVLUA_WITH_REDIS) + endif() + if(WITH_MQTT) + add_definitions(-DHVLUA_WITH_MQTT) + endif() endif() if(WIN32 OR MINGW) diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp index 8f28cf64a..3ecda4309 100644 --- a/lua/hvlua_redis.cpp +++ b/lua/hvlua_redis.cpp @@ -34,12 +34,21 @@ 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) { - delete box->client; // external loop (not owner): does NOT stop the shared loop + 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; @@ -115,6 +124,7 @@ static int l_redis_new(lua_State* L) { } 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); @@ -171,7 +181,13 @@ static int redis_do_command(lua_State* L, RedisCommand&& cmd) { } HvLuaCoroutine* co = hvlua_suspend(L); - box->client->command(cmd, [co](const RedisResult& result) { + box->client->command(cmd, [co, box](const RedisResult& result) { + // If the client is being destroyed (this callback fires synchronously + // from ~AsyncRedisClient -> stop -> failPending, which runs inside the + // Lua __gc metamethod), we must NOT lua_resume — that would re-enter the + // VM from within GC. Just release the suspend token (hvlua_cancel is + // __gc-safe: luaL_unref + free, no lua_resume). + if (box->destroyed) { hvlua_cancel(co); return; } lua_State* cur = hvlua_coroutine_state(co); if (cur == NULL) { hvlua_cancel(co); return; } // coroutine gone if (result.code != 0) { From 27a50a5030135ce8802e30fac1100eaa5f17bb3c Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 16:15:09 +0800 Subject: [PATCH 13/33] feat(lua): align event API names with C++ classes + connect/listen aliases Expose the event-layer client/server entries under names mirroring the C++ classes (hv.tcpClient / hv.tcpServer / hv.udpClient / hv.udpServer) for a consistent client/server x tcp/udp matrix, and keep the idiomatic verbs hv.connect (= tcpClient) and hv.listen (= tcpServer) as aliases. Examples use the connect/listen aliases. redis keeps hv.redis.new (its 'new' is a pure constructor with lazy/queued connect + reconnect, unlike ws/mqtt connect() which suspends until the handshake completes, so the naming difference is intentional). lua_io_test unchanged: it already covers tcpServer (primary) + connect (alias) + udpServer/udpClient. All lua/redis tests pass. --- examples/lua/tcp_server.lua | 4 ++-- lua/hvlua_event.c | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/lua/tcp_server.lua b/examples/lua/tcp_server.lua index 7c4237fde..deab12e9e 100644 --- a/examples/lua/tcp_server.lua +++ b/examples/lua/tcp_server.lua @@ -3,7 +3,7 @@ local host = arg[1] or "0.0.0.0" local port = tonumber(arg[2] or "10520") -local ok, err = hv.tcpServer(host, port, function(conn) +local ok, err = hv.listen(host, port, function(conn) hv.log("accepted", conn:peeraddr()) while true do local data, rerr = conn:read() @@ -16,7 +16,7 @@ local ok, err = hv.tcpServer(host, port, function(conn) end) if not ok then - hv.loge("tcpServer failed:", err) + hv.loge("listen failed:", err) return end hv.log("echo server listening on", host .. ":" .. port) diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c index 6a948fc1f..ccb04cd6a 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -261,7 +261,8 @@ static int l_hloop_stop(lua_State* L) { } // ============================================================================ -// TCP client: hv.connect(host, port [, timeout_ms]) -> conn | nil, err +// 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 @@ -358,7 +359,7 @@ static int l_hv_connect(lua_State* 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.connect: create socket failed"); + lua_pushstring(L, "hv.tcpClient: create socket failed"); return 2; } @@ -587,6 +588,7 @@ static int l_conn_gc(lua_State* L) { // ============================================================================ // 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. @@ -782,18 +784,24 @@ static const luaL_Reg hloop_funcs[] = { { "clearTimer", l_hloop_clearTimer }, { "sleep", l_hloop_sleep }, { "resolveDns", l_hloop_resolveDns }, - { "connect", l_hv_connect }, + // 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.connect / hv.tcpServer / hv.run / hv.stop +// 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); From c4d430462ec7f6360e567263a28c8dd1887f9803 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 16:26:10 +0800 Subject: [PATCH 14/33] test(lua): use make_shared in http_lua_handler_test The test bound a stack EventLoop to TLS. It works today (the scripts finish synchronously and never reach a client binding), but is inconsistent with the other lua tests (lua_http/ws/redis/mqtt all use make_shared) and would silently trap a future script that calls hv.http/redis/ws/mqtt: currentThreadEventLoopPtr -> shared_from_this() on a stack object throws bad_weak_ptr and returns nullptr, so the binding would report 'no shared loop' instead of actually running. Switch to make_shared and store loop.get() in TLS (TLS holds a raw EventLoop*, matching EventLoop::run). ~EventLoop frees the never-run AUTO_FREE hloop via stop(), so no leak. lua_binding_test / lua_io_test are unaffected (pure hloop_new, no EventLoop object). --- unittest/http_lua_handler_test.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/unittest/http_lua_handler_test.cpp b/unittest/http_lua_handler_test.cpp index 6e3180187..7bd0cd0a2 100644 --- a/unittest/http_lua_handler_test.cpp +++ b/unittest/http_lua_handler_test.cpp @@ -160,8 +160,13 @@ 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). - hv::EventLoop loop; - hv::ThreadLocalStorage::set(hv::ThreadLocalStorage::EVENT_LOOP, &loop); + // 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(); From 68443dc1c23b992769269d69c7c39705f71cd501 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 16:47:16 +0800 Subject: [PATCH 15/33] test(lua): clearer names for the two http-lua test files Rename the two HTTP Lua handler tests so each name reflects what it verifies: - http_lua_handler_test.cpp (synchronous HttpScriptHandler unit test: .lua dispatch, .py -> 501, script-dir routing, path-traversal guard, ctx API) -> http_script_handler_test.cpp (its subject is HttpScriptHandler, the language-agnostic dispatcher) - http_lua_async_test.cpp (real HttpServer + concurrent requests proving .lua handlers run as coroutines concurrently on one IO thread) -> http_lua_handler_test.cpp Update the file bodies (headers, tmp dirs, PASSED banners) and all build wiring: Makefile, unittest/CMakeLists.txt, scripts/unittest.sh. Both tests build and pass under Make and CMake. --- Makefile | 53 ++---- scripts/unittest.sh | 6 +- unittest/CMakeLists.txt | 10 +- unittest/http_lua_async_test.cpp | 93 ----------- unittest/http_lua_handler_test.cpp | 222 ++++++++------------------ unittest/http_script_handler_test.cpp | 180 +++++++++++++++++++++ 6 files changed, 272 insertions(+), 292 deletions(-) delete mode 100644 unittest/http_lua_async_test.cpp create mode 100644 unittest/http_script_handler_test.cpp diff --git a/Makefile b/Makefile index 07cfd7a66..522842c42 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,6 @@ endif BUILD_SHARED ?= yes BUILD_STATIC ?= yes -# http/server examples compile sources directly (not linking libhv), so when -# WITH_LUA is on they must also include the lua/ binding sources that -# HttpLuaHandler depends on (and http/client, which the lua hv.http binding uses). -HTTP_SERVER_EXAMPLE_SRCDIRS = $(CORE_SRCDIRS) util cpputil evpp http http/server -ifeq ($(WITH_LUA), yes) -HTTP_SERVER_EXAMPLE_SRCDIRS += lua http/client -endif - LIBHV_SRCDIRS = $(CORE_SRCDIRS) util LIBHV_HEADERS = hv.h hconfig.h hexport.h LIBHV_HEADERS += $(BASE_HEADERS) $(SSL_HEADERS) $(EVENT_HEADERS) $(UTIL_HEADERS) @@ -220,7 +212,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" @@ -228,14 +219,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" @@ -248,13 +238,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="$(HTTP_SERVER_EXAMPLE_SRCDIRS)" 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" @@ -301,7 +291,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 @@ -332,44 +322,33 @@ 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 +endif +ifeq ($(WITH_REDIS), yes) + $(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 +endif ifeq ($(WITH_LUA), yes) - $(MAKE) libhv $(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) - $(CXX) -g -Wall -O0 -std=c++11 -DWITH_LUA $(LUA_CFLAGS) -I. -Ibase -Issl -Ievent -Icpputil -Ievpp -Ilua -Ihttp -Ihttp/server -o bin/http_lua_handler_test unittest/http_lua_handler_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_async_test unittest/http_lua_async_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 -Ihttp -Ihttp/server -o bin/http_script_handler_test unittest/http_script_handler_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/http_lua_handler_test unittest/http_lua_handler_test.cpp -Llib -lhv -pthread $(LUA_LIBS) ifeq ($(WITH_HTTP), 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/lua_http_test unittest/lua_http_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 -Ihttp -Ihttp/server -Ihttp/client -o bin/lua_ws_test unittest/lua_ws_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_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 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 -else - $(RM) bin/lua_binding_test bin/lua_io_test bin/http_lua_handler_test bin/http_lua_async_test bin/lua_http_test bin/lua_ws_test bin/lua_mqtt_test -endif 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 -ifeq ($(WITH_LUA), 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 -else - $(RM) bin/redis_protocol_test bin/redis_async_client_test bin/redis_client_test bin/redis_batch_test bin/redis_subscriber_test bin/lua_redis_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 run-unittest: unittest diff --git a/scripts/unittest.sh b/scripts/unittest.sh index a54684275..a57f5b1a0 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -38,12 +38,12 @@ 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/http_lua_async_test ]; then - bin/http_lua_async_test -fi if [ -x bin/lua_http_test ]; then bin/lua_http_test fi diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index cded19a42..1c5b075c5 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -97,13 +97,13 @@ 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}) +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}) add_executable(http_lua_handler_test http_lua_handler_test.cpp) -target_include_directories(http_lua_handler_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../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}) -add_executable(http_lua_async_test http_lua_async_test.cpp) -target_include_directories(http_lua_async_test PRIVATE .. ../base ../ssl ../event ../cpputil ../evpp ../lua ../http ../http/server ../http/client) -target_link_libraries(http_lua_async_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test http_lua_handler_test http_lua_async_test) +set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test http_script_handler_test http_lua_handler_test) if(WITH_HTTP) 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) diff --git a/unittest/http_lua_async_test.cpp b/unittest/http_lua_async_test.cpp deleted file mode 100644 index 46f8ebbf9..000000000 --- a/unittest/http_lua_async_test.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/* - * http_lua_async_test — Stage B integration test for coroutine-based async - * Lua HTTP handlers. - * - * 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 "HttpScriptHandler.h" -#include "requests.h" - -static std::string write_script(const char* name, const char* content) { - hv_mkdir_p("tmp/http_lua_async_test"); - std::string path = HPath::join("tmp/http_lua_async_test", name); - HFile file; - int ret = file.open(path.c_str(), "wb"); - assert(ret == 0); - file.write(content, strlen(content)); - file.close(); - return path; -} - -int main() { - // The handler sleeps 300ms inside the coroutine, then echoes the id. - std::string script = write_script("sleep.lua", - "function handle(ctx)\n" - " hv.sleep(300)\n" - " return ctx:json({ ok = true, id = ctx:query('id') })\n" - "end\n"); - - 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_async_test PASSED\n"); - return 0; -} diff --git a/unittest/http_lua_handler_test.cpp b/unittest/http_lua_handler_test.cpp index 7bd0cd0a2..976df7223 100644 --- a/unittest/http_lua_handler_test.cpp +++ b/unittest/http_lua_handler_test.cpp @@ -1,23 +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 "EventLoop.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"); @@ -27,154 +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() { - // 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(); + 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; +} From c44679b1b2ffae3bc5dbbd8f237c0d9a701c006a Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 16:49:05 +0800 Subject: [PATCH 16/33] build(lua): list --with-lua in configure help The configure script's module help text didn't mention the lua build option added for the Lua binding. Add the --with-lua line so ./configure --help documents it alongside --with-http/mqtt/redis. --- configure | 1 + 1 file changed, 1 insertion(+) 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) From 4a8226028d26daa2f9388be083dd154fb3a56e4b Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 16:54:26 +0800 Subject: [PATCH 17/33] docs(lua): add docs/cn/lua.md + mark lua binding done in PLAN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/cn/lua.md: user-facing reference for the hv.* Lua API — build/run, the coroutine-synchronous model, return-value convention, and every module (hv.log/json, timers/sleep, resolveDns, tcp/udp, http, ws, redis, mqtt). - docs/cn/README.md: link it under a new 'lua接口' section. - docs/PLAN.md: move 'lua binding' from Plan to Done. --- docs/PLAN.md | 4 +- docs/cn/README.md | 4 + docs/cn/lua.md | 211 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 docs/cn/lua.md 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/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..8bd4a2410 --- /dev/null +++ b/docs/cn/lua.md @@ -0,0 +1,211 @@ +# 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(独立运行时用;HTTP handler 内不需要) +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()` 挂起直到有一条消息(或连接关闭返回 `nil,"closed"`)。 + +```lua +local ws, err = hv.ws.connect("ws://127.0.0.1:8888/path" [, headers]) +ws:send("hello") -- 文本帧 +ws:send(payload, "binary") -- 二进制帧 +local msg, err = ws:recv() -- 协程同步:挂起直到收到一条消息 +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, +}) -- 协程同步:挂起到 CONNACK 或失败 + +m:subscribe("topic", 1) -- 返回 mid +m:publish("topic", "payload", 1, 0) -- topic, payload, qos, retain -> mid +m:unsubscribe("topic") +local msg = m:recv() -- { topic =, payload =, qos = } ; 关闭返回 nil,"closed" +m:disconnect() +``` + +## HTTP Lua Handler + +在 HTTP 服务端里用 Lua 脚本处理请求(`handle(ctx)`),请求在 IO 线程的协程里执行,脚本内可用上述同步写法调用异步 client。详见 [HttpLuaHandler.md](HttpLuaHandler.md)。 From 051f0833d96d9c6645837b83647b78a0ea02a2e0 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 17:25:11 +0800 Subject: [PATCH 18/33] fix(lua): address Copilot review findings on PR #857 All four review comments verified against source and fixed: 1. HttpLuaHandler push_script_env: the lua_pcall failure path popped 3 stack slots (err, env, chunk) but the stack held 4 (scripts, chunk, env, err), so the SCRIPTS_REG table leaked on the stack for every script with a runtime load error, accumulating until stack corruption. Pop 4. 2. hv.sleep: htimer_add() could return NULL (NULL loop / out of resources) and was dereferenced by hevent_set_userdata, and the coroutine would already be suspended with nothing to wake it. Create the timer before suspending and fail fast with (nil, err) if it is NULL. 3. hv.redis command: AsyncRedisClient::command may invoke the callback synchronously (e.g. loop not running -> enqueue rejected). The binding suspended the coroutine before calling command(), so a synchronous callback called lua_resume on the still-running coroutine (illegal) and the coroutine then yielded forever. Track sync vs async completion: on synchronous completion, return the results directly without yielding. 4. CMake only built lua_http_test (under WITH_HTTP); lua_ws_test / lua_mqtt_test / lua_redis_test were Make-only, so the new ws/mqtt/redis bindings had no CMake test coverage. Add them under WITH_HTTP / WITH_MQTT / WITH_REDIS. Verified: full lua/redis suite passes under both Make and CMake. --- http/server/HttpLuaHandler.cpp | 2 +- lua/hvlua_event.c | 15 +++++- lua/hvlua_redis.cpp | 87 +++++++++++++++++++++++++--------- unittest/CMakeLists.txt | 17 ++++++- 4 files changed, 95 insertions(+), 26 deletions(-) diff --git a/http/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp index 4f04e3709..03139f2e7 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -312,7 +312,7 @@ static bool push_script_env(lua_State* L, const std::string& filepath, 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, 3); // err, chunk, scripts + lua_pop(L, 4); // err, env, chunk, scripts lua_pushstring(L, err.c_str()); return false; } diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c index ccb04cd6a..2f731ff20 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -169,11 +169,22 @@ 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 = htimer_add(loop, on_sleep_timer, ms, 1); - hevent_set_userdata(s->timer, s); + s->timer = timer; + hevent_set_userdata(timer, s); return lua_yieldk(L, 0, (lua_KContext)0, sleep_k); } diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp index 3ecda4309..aaceb9fd5 100644 --- a/lua/hvlua_redis.cpp +++ b/lua/hvlua_redis.cpp @@ -9,6 +9,7 @@ extern "C" { #ifdef HVLUA_WITH_REDIS #include +#include #include #include @@ -171,6 +172,33 @@ static bool build_command(lua_State* L, int first, RedisCommand* cmd) { 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; +} + // Shared implementation for r:command(...) and the sugar methods. static int redis_do_command(lua_State* L, RedisCommand&& cmd) { LuaRedisClient* box = (LuaRedisClient*)luaL_checkudata(L, 1, REDIS_CLIENT_MT); @@ -180,31 +208,46 @@ static int redis_do_command(lua_State* L, RedisCommand&& cmd) { return 2; } - HvLuaCoroutine* co = hvlua_suspend(L); - box->client->command(cmd, [co, box](const RedisResult& result) { - // If the client is being destroyed (this callback fires synchronously - // from ~AsyncRedisClient -> stop -> failPending, which runs inside the - // Lua __gc metamethod), we must NOT lua_resume — that would re-enter the - // VM from within GC. Just release the suspend token (hvlua_cancel is - // __gc-safe: luaL_unref + free, no lua_resume). - if (box->destroyed) { hvlua_cancel(co); return; } - lua_State* cur = hvlua_coroutine_state(co); - if (cur == NULL) { hvlua_cancel(co); return; } // coroutine gone - if (result.code != 0) { - lua_pushnil(cur); - lua_pushfstring(cur, "hv.redis: request failed (%d)", result.code); - hvlua_resume(co, 2); - return; - } - if (result.reply.isError()) { - lua_pushnil(cur); - lua_pushlstring(cur, result.reply.str.data(), result.reply.str.size()); - hvlua_resume(co, 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 lua_resume. + if (box->destroyed) { hvlua_cancel(st->co); st->co = NULL; return; } + if (!st->yielded) { + // Synchronous completion: the coroutine has not yielded yet. Push + // results onto its stack and let redis_do_command return them; do + // NOT resume (it is still running). Release the unused suspend token. + st->done = true; + st->nresults = redis_push_result(st->L, result); + hvlua_cancel(st->co); + st->co = NULL; return; } - reply_push(cur, result.reply); - hvlua_resume(co, 1); + // 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; } // gone + int n = redis_push_result(cur, result); + HvLuaCoroutine* tok = st->co; + st->co = NULL; + hvlua_resume(tok, n); }); + + if (st->done) { + // Completed synchronously: results are already on L. Return them now. + return st->nresults; + } + st->yielded = true; return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); } diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 1c5b075c5..1645a5695 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -108,7 +108,22 @@ if(WITH_HTTP) 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}) -set(HTTP_LUA_UNITTEST_TARGETS ${HTTP_LUA_UNITTEST_TARGETS} lua_http_test) +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_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_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() From 0945b2be764f93b264e6e3f2a864e74e5a465ff7 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 17:59:57 +0800 Subject: [PATCH 19/33] feat(httpd): parse [script] section and register HttpService::Script The httpd example now reads a [script] section from httpd.conf and maps each URL prefix to a script directory via HttpService::Script, mirroring how the [proxy] section maps reverse-proxy routes. A request to /script/foo then runs examples/scripts/foo.lua's get/post/... or handle(ctx). - httpd.cpp: iterate ini.GetKeys("script"), ltrim the '=>' value, call g_http_service.Script(path, dir). Guarded by #ifdef WITH_LUA (Script is only compiled with WITH_LUA); without it, a non-empty [script] section logs a warning instead of silently doing nothing. - etc/httpd.conf: point /script/ at examples/scripts/ (which has handle(ctx) scripts) instead of examples/lua/ (standalone hvlua runtime scripts), and document the mapping. Verified end-to-end: built with WITH_LUA, GET/POST /script/hello run the lua get/post handlers, /script/async exercises coroutine-synchronous hv.resolveDns. --- etc/httpd.conf | 6 ++++++ examples/httpd/httpd.cpp | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) 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/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; } From a11c735f5ab2af06b8d593bdbbb6b2160e709fd9 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 21:16:43 +0800 Subject: [PATCH 20/33] fix(lua): address code-review findings on PR #857 (C1/C2/C3 + H2/H4/M1) Verified each against source before fixing. C1 (data loss / hang): l_conn_read* issued hio_read_until_* BEFORE arming the read callback + suspending. hio_read_until_length/delim invoke the read callback INLINE when buffered data already satisfies the request (hevent.c:765/789), at which point c->co is unset -> on_conn_read dropped the data and the coroutine suspended forever. Now arm callback + enter a 'reading' window first, issue the read, and if it completed synchronously return the data directly without suspending (mirrors the redis RedisCmdState pattern). Regression-tested with pipelined readbytes. C2 (UAF + double-free): a fired once-timer's htimer_t is freed by the loop after its callback, but on_lua_timer freed LuaTimer without clearing the timer's back-pointer, and the script still held the raw handle. clearTimer(stale) then read hevent_userdata of freed memory -> UAF + double-free. Add a live-timer registry; clearTimer validates the handle against it (stale -> safe no-op), and lua_timer_free unregisters + clears hevent_userdata. C3 (UAF): mqtt_client_free freed cli without closing cli->io or deleting cli->timer or clearing their hevent_userdata(cli); a later on_close / connect_timeout_cb / reconnect_timer_cb dereferenced the freed cli. Tear down io (detach close cb + close) and timer (detach + del) inside mqtt_client_free. H2 (UAF): ~WebSocketClient didn't clear channel->onclose, so ~Channel -> close() fired the onclose lambda capturing the destroying TcpClient. Clear it first (mirrors AsyncHttpClient::~AsyncHttpClient). H4 (leak): on_dns_resolved returned without hvlua_cancel when the coroutine was gone, leaking the suspend token. Cancel it. M1 (CI gap): scripts/unittest.sh ran lua_http_test but not lua_ws_test / lua_mqtt_test / lua_redis_test. Add them. Verified: full lua/redis suite (12 tests) passes under Make; clean rebuild. --- http/client/WebSocketClient.cpp | 9 +++ lua/hvlua_event.c | 130 +++++++++++++++++++++++++++++--- mqtt/mqtt_client.c | 15 ++++ scripts/unittest.sh | 8 +- 4 files changed, 151 insertions(+), 11 deletions(-) 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/lua/hvlua_event.c b/lua/hvlua_event.c index 2f731ff20..c37b68f61 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -36,7 +36,59 @@ static void lua_timer_release_ref(LuaTimer* lt) { } } +// 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) { + // 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); } @@ -100,6 +152,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea } hevent_set_userdata(timer, lt); lt->timer = timer; + lua_timer_reg_add(L, timer); // mark handle live for clearTimer validation return timer; } @@ -128,13 +181,20 @@ static int l_hloop_clearTimer(lua_State* L) { 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. + // 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); @@ -208,7 +268,8 @@ static void on_dns_resolved(hdns_t* query, const hdns_result_t* result, void* us HV_FREE(d); L = hvlua_coroutine_state(co); - if (L == NULL) { // coroutine gone (loop teardown); nothing to resume + if (L == NULL) { // coroutine gone (loop teardown); release the token + hvlua_cancel(co); return; } @@ -289,6 +350,14 @@ typedef struct LuaConn { 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 @@ -338,7 +407,18 @@ static void on_conn_connect(hio_t* io) { static void on_conn_read(hio_t* io, void* buf, int len) { LuaConn* c = (LuaConn*)hevent_userdata(io); - if (c == NULL || c->co == NULL) return; + 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); @@ -397,6 +477,9 @@ static LuaConn* conn_push_new(lua_State* L, hio_t* io) { 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); @@ -410,10 +493,33 @@ static int read_k(lua_State* L, int status, lua_KContext ctx) { return lua_gettop(L) >= 2 && lua_isnil(L, -2) ? 2 : 1; } -// Shared: arm the read callback + suspend. The specific hio_read* call is done -// by the caller (read once / read_until_delim / read_until_length). -static int conn_suspend_read(lua_State* L, LuaConn* c) { +// 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); } @@ -424,8 +530,9 @@ static int l_conn_read(lua_State* 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_suspend_read(L, c); + return conn_end_read(L, c); } // conn:readbytes(n) -> exactly n bytes (hio_read_until_length) @@ -435,8 +542,9 @@ static int l_conn_readbytes(lua_State* L) { if (c->closed || c->io == NULL) { lua_pushnil(L); lua_pushstring(L, "closed"); return 2; } + conn_begin_read(L, c); hio_read_until_length(c->io, n); - return conn_suspend_read(L, c); + return conn_end_read(L, c); } // conn:readuntil(delim) -> data up to and including the 1-byte delimiter @@ -450,8 +558,9 @@ static int l_conn_readuntil(lua_State* L) { 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_suspend_read(L, c); + return conn_end_read(L, c); } // conn:readline() -> data up to and including '\n' @@ -460,8 +569,9 @@ static int l_conn_readline(lua_State* 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_suspend_read(L, c); + return conn_end_read(L, c); } // conn:setUnpack(opts): configure automatic message unpacking so subsequent diff --git a/mqtt/mqtt_client.c b/mqtt/mqtt_client.c index 9e4566d34..cbe43fc6c 100644 --- a/mqtt/mqtt_client.c +++ b/mqtt/mqtt_client.c @@ -457,6 +457,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); diff --git a/scripts/unittest.sh b/scripts/unittest.sh index a57f5b1a0..0be55a0c0 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -47,13 +47,19 @@ 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 From b2bf8a59279e1cd5b23e3fdbbf3117bcf1697731 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 21:29:31 +0800 Subject: [PATCH 21/33] fix(mqtt): don't run/stop a caller-supplied loop (add is_loop_owner) mqtt_client_run/stop unconditionally called hloop_run/hloop_stop on cli->loop, even when the loop was supplied by the caller. Driving or stopping a loop the client doesn't own is wrong: for an external loop the caller already runs it (e.g. an IO thread or a runtime loop), so mqtt_client_run would block/double- drive it and mqtt_client_stop would stop someone else's loop. Track ownership with an is_loop_owner flag (set when mqtt_client_new creates its own loop, i.e. loop==NULL), aligned with the evpp EventLoopThread model. mqtt_client_run/stop are now no-ops for a caller-supplied loop; the self-created loop path (examples/mqtt: MqttClient with no loop) is unchanged and still auto-frees via HLOOP_FLAG_AUTO_FREE. mqtt_client_free needs no change. This is what the hv.mqtt Lua binding relies on: it binds the client to the shared runtime loop and never calls mqtt_client_run, so run/stop must leave that loop alone. Verified: libhv builds; examples/mqtt links; full lua/redis suite passes. --- mqtt/mqtt_client.c | 11 +++++++++++ mqtt/mqtt_client.h | 1 + 2 files changed, 12 insertions(+) diff --git a/mqtt/mqtt_client.c b/mqtt/mqtt_client.c index cbe43fc6c..91718d70d 100644 --- a/mqtt/mqtt_client.c +++ b/mqtt/mqtt_client.c @@ -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_); @@ -484,11 +489,17 @@ 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); } 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]; From 8250e1281438c0ba6497509cd474b15066a9282c Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 21:52:43 +0800 Subject: [PATCH 22/33] docs(evpp): document the external-loop-must-be-running contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin down the (previously implicit) contract in EventLoopThread: when an external loop is supplied to the constructor, the caller must have it already running before start(). This is what all real usages do (HttpServer/EventLoopThreadPool loops; the Lua bindings obtain the loop via currentThreadEventLoopPtr, which is only non-NULL on an already-running loop thread). If a not-yet-running external loop is passed and start() is called, start() intentionally spins its own worker thread to drive it and stop() joins that thread — a deliberate, safe fallback (exercised by redis_async_client_test), not a bug. Documenting it so the behavior isn't repeatedly misread as a lifetime defect. Comment-only; no logic change. --- evpp/EventLoopThread.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/evpp/EventLoopThread.h b/evpp/EventLoopThread.h index 877bbdc0e..f0567e745 100644 --- a/evpp/EventLoopThread.h +++ b/evpp/EventLoopThread.h @@ -23,6 +23,19 @@ class EventLoopThread : public Status { // 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); @@ -55,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()) { From 9971da31e39c126d01b05aa584a6a9fbde171a91 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 21:57:53 +0800 Subject: [PATCH 23/33] refactor(lua): hvlua_task_step returns completion; don't re-read co after finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second-review Point 1 (fragility, not a bug): hvlua_start_task called hvlua_task_step(co) then task_get(co) to decide sync-vs-async completion. But if the task finished synchronously, hvlua_task_step already released the coroutine's thread ref (luaL_unref), so re-reading co via task_get relied on 'GC won't run between these two lines' — safe in single-threaded Lua with no intervening allocation, but fragile. Make hvlua_task_step return 1 (finished) / 0 (yielded) and have hvlua_start_task use that return value instead of touching co again. The other caller (hvlua_resume) ignores the return, unchanged. Comment documents that callers must not touch co after a 'finished' return. Verified: full lua/redis suite passes (lua_binding_test covers sync + async task completion). --- lua/hvlua.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lua/hvlua.c b/lua/hvlua.c index 73dce03c6..d89895628 100644 --- a/lua/hvlua.c +++ b/lua/hvlua.c @@ -49,7 +49,10 @@ static void task_set(lua_State* co, HvLuaTask* task) { // 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. -static void hvlua_task_step(lua_State* co, int nresults) { +// @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; @@ -60,21 +63,23 @@ static void hvlua_task_step(lua_State* co, int nresults) { #endif ); if (status == LUA_YIELD) { - return; // suspended again; a resume token owns the next wakeup + 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; + 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); 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 @@ -88,9 +93,12 @@ int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud) { task->thread_ref = thread_ref; task_set(co, task); - hvlua_task_step(co, nargs); - // If the task is no longer tracked, it finished synchronously. - return task_get(co) == NULL ? 1 : 0; + // 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) { From 491041b430c75fece978650b6d83f4456f65606a Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 22:19:19 +0800 Subject: [PATCH 24/33] feat(lua): auto-reconnect for hv.ws and hv.mqtt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the underlying WebSocketClient / mqtt_client reconnect capability through the bindings, with a symmetric config-table API: hv.ws.connect(url, { headers=, ping_interval=, reconnect={min_delay, max_delay, delay_policy, max_retry} }) hv.mqtt.connect({ ..., reconnect={min_delay, max_delay, delay_policy, max_retry} }) Semantics (both): - reconnect sub-table -> reconn_setting_t, applied via setReconnect (ws, before open) / mqtt_client_set_reconnect (mqtt, before connect). - On (re)connect, connected is set and closed is reset, so recv()/send() resume working after a reconnect. - While disconnected with reconnect enabled, recv()/send() return (nil,reconnecting) — a transient signal distinct from the terminal (nil,closed) — so a script can keep waiting or bail, and send() never silently drops during a gap. - Explicit ws:close() / m:disconnect() are terminal: they disable reconnect so recv() reports closed. hv.ws.connect's 2nd arg is now an opts table (headers moved under opts.headers); existing hv.ws.connect(url) callers are unaffected. lua_ws_test extended to exercise the opts/reconnect path; full-drop reconnect behavior is covered by the C++ websocket_client_test example. Docs + example scripts updated. Verified: full lua/redis suite (12 tests) passes; example scripts run. --- docs/cn/lua.md | 29 +++++++-- examples/lua/mqtt_client.lua | 6 +- examples/lua/ws_client.lua | 5 +- lua/hvlua_mqtt.cpp | 42 ++++++++++-- lua/hvlua_ws.cpp | 122 ++++++++++++++++++++++++++--------- unittest/lua_ws_test.cpp | 17 ++++- 6 files changed, 180 insertions(+), 41 deletions(-) diff --git a/docs/cn/lua.md b/docs/cn/lua.md index 8bd4a2410..40f3a27a8 100644 --- a/docs/cn/lua.md +++ b/docs/cn/lua.md @@ -159,16 +159,29 @@ local resp5 = hv.http.request("GET", url [, body [, headers]]) ## hv.ws(WebSocket,协程同步,需 WITH_HTTP) -WebSocket 是消息驱动的,收到的消息缓存在收件箱,`ws:recv()` 挂起直到有一条消息(或连接关闭返回 `nil,"closed"`)。 +WebSocket 是消息驱动的,收到的消息缓存在收件箱,`ws:recv()` 挂起直到有一条消息。连接断开时 `recv()`/`send()` 返回 `(nil, err)`:开启了自动重连时 `err="reconnecting"`(临时断开,底层正在重连),否则 `err="closed"`(终止)。 ```lua -local ws, err = hv.ws.connect("ws://127.0.0.1:8888/path" [, headers]) +-- 第二参数为可选 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() -- 协程同步:挂起直到收到一条消息 -ws:close() +if err == "reconnecting" then ... end -- 临时断开,正在重连 +ws:close() -- 显式关闭(终止,禁用重连) ``` +> 开启重连后:重连成功会自动恢复,后续 `recv()`/`send()` 继续可用;断开期间 `send()` 会返回 `(nil,"reconnecting")` 而非静默丢弃,`recv()` 同样返回 `(nil,"reconnecting")` 让脚本自行决定继续等待还是退出。`ws:close()` 是显式终止,会禁用重连。 + ## hv.redis(协程同步,需 WITH_REDIS) ```lua @@ -197,15 +210,21 @@ 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 = m:recv() -- { topic =, payload =, qos = } ; 关闭返回 nil,"closed" -m:disconnect() +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/examples/lua/mqtt_client.lua b/examples/lua/mqtt_client.lua index 10d96bdb5..df076e3e1 100644 --- a/examples/lua/mqtt_client.lua +++ b/examples/lua/mqtt_client.lua @@ -6,7 +6,11 @@ local topic = arg[3] or "hv/lua/test" hv.setTimeout(1, function() -- connect() suspends until CONNACK (or fails with nil, err) - local m, err = hv.mqtt.connect({ host = host, port = port, id = "hvlua-demo", keepalive = 60 }) + -- 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() diff --git a/examples/lua/ws_client.lua b/examples/lua/ws_client.lua index 40bc52673..aa2a27a73 100644 --- a/examples/lua/ws_client.lua +++ b/examples/lua/ws_client.lua @@ -3,7 +3,10 @@ local url = arg[1] or "ws://127.0.0.1:8888/" hv.setTimeout(1, function() - local ws, err = hv.ws.connect(url) + -- 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() diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp index 4ba28d714..2ad2b9106 100644 --- a/lua/hvlua_mqtt.cpp +++ b/lua/hvlua_mqtt.cpp @@ -52,6 +52,7 @@ struct LuaMqttClient { bool connected; 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= }. @@ -65,8 +66,9 @@ static void mqtt_push_msg(lua_State* L, const MqttInboxItem& item) { lua_setfield(L, -2, "qos"); } -// Wake a pending recv() with the front queued message, or (nil,"closed") when -// the connection is gone and the queue is drained. No-op for a connect() waiter. +// 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); @@ -82,7 +84,7 @@ static void mqtt_try_deliver(LuaMqttClient* box) { HvLuaCoroutine* tok = box->wait_co; box->wait_co = NULL; lua_pushnil(co); - lua_pushstring(co, "closed"); + lua_pushstring(co, box->reconnect ? "reconnecting" : "closed"); hvlua_resume(tok, 2); } } @@ -95,6 +97,7 @@ static void on_mqtt(mqtt_client_t* cli, int type) { switch (type) { case MQTT_TYPE_CONNACK: box->connected = true; + 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; } @@ -125,7 +128,12 @@ static void on_mqtt(mqtt_client_t* cli, int type) { box->wait_co = NULL; box->connecting = false; lua_pushnil(co); - lua_pushstring(co, was_connecting ? "connect failed" : "closed"); + // 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; @@ -193,6 +201,7 @@ static int l_mqtt_connect(lua_State* L) { box->connected = false; 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(); @@ -211,6 +220,27 @@ static int l_mqtt_connect(lua_State* L) { } 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 } + lua_getfield(L, 1, "reconnect"); + if (lua_istable(L, -1)) { + reconn_setting_t reconn; + reconn_setting_init(&reconn); + lua_getfield(L, -1, "min_delay"); + if (lua_isinteger(L, -1)) reconn.min_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_delay"); + if (lua_isinteger(L, -1)) reconn.max_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "delay_policy"); + if (lua_isinteger(L, -1)) reconn.delay_policy = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_retry"); + if (lua_isinteger(L, -1)) reconn.max_retry_cnt = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + mqtt_client_set_reconnect(box->client, &reconn); + box->reconnect = true; + } + lua_pop(L, 1); // pop reconnect table (or nil) box->wait_co = hvlua_suspend(L); int ret = mqtt_client_connect(box->client, host.c_str(), port, ssl); @@ -313,6 +343,10 @@ static int l_mqtt_unsubscribe(lua_State* L) { 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; diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp index 44474504f..68165b15c 100644 --- a/lua/hvlua_ws.cpp +++ b/lua/hvlua_ws.cpp @@ -42,11 +42,14 @@ struct LuaWsClient { WebSocketClient* client; WsInbox inbox; // buffered inbound messages HvLuaCoroutine* recv_co; // coroutine waiting in recv(), or NULL - bool closed; + 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 (nil,"closed") when the socket is gone and the queue is drained. +// 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); @@ -62,11 +65,13 @@ static void ws_try_deliver(LuaWsClient* box) { box->recv_co = NULL; lua_pushlstring(co, msg.data(), msg.size()); hvlua_resume(co_tok, 1); - } else if (box->closed) { + } else if (!box->connected) { HvLuaCoroutine* co_tok = box->recv_co; box->recv_co = NULL; lua_pushnil(co); - lua_pushstring(co, "closed"); + // 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); } } @@ -98,7 +103,33 @@ static int ws_connect_k(lua_State* L, int status, lua_KContext ctx) { return 2; // (nil, err) } -// hv.ws.connect(url [, headers]) -> ws | nil, err +// Parse an optional { reconnect = { min_delay, max_delay, delay_policy, +// max_retry } } sub-table at opts_index into *out. Returns true if reconnect +// was requested. +static bool ws_parse_reconnect(lua_State* L, int opts_index, reconn_setting_t* out) { + if (!lua_istable(L, opts_index)) return false; + lua_getfield(L, opts_index, "reconnect"); + if (!lua_istable(L, -1)) { lua_pop(L, 1); return false; } + 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); + lua_pop(L, 1); // pop the reconnect table + return true; +} + +// 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); EventLoopPtr loop = currentThreadEventLoopPtr; @@ -112,42 +143,65 @@ static int l_ws_connect(lua_State* L) { LuaWsClient* box = (LuaWsClient*)lua_newuserdata(L, sizeof(LuaWsClient)); new (&box->inbox) WsInbox(); box->recv_co = NULL; - box->closed = false; + 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 + // opts (arg 2): headers sub-table, ping_interval, reconnect sub-table. http_headers headers = DefaultHeaders; if (lua_istable(L, 2)) { - 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_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); + } + lua_pop(L, 1); // pop headers (or nil) + 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 (ws_parse_reconnect(L, 2, &reconn)) { + box->reconnect = true; + box->client->setReconnect(&reconn); } } box->client->onopen = [box]() { - lua_State* co = hvlua_coroutine_state(box->recv_co); - // onopen resumes the connect() coroutine, tracked in recv_co during - // the connect phase (reused slot; no recv can be pending yet). - 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); + // 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: a recv() waiter (if any) + // stays parked and is woken by the next onmessage. }; box->client->onmessage = [box](const std::string& msg) { box->inbox.push_back(msg); ws_try_deliver(box); }; box->client->onclose = [box]() { - box->closed = true; - // Wake whoever is waiting: the connect() coroutine (never opened -> the - // handshake failed) or a pending recv(). Both get (nil,"closed"); a - // successful connect resumes earlier via onopen, so if recv_co is still - // set here during connect it means the open failed. + 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); }; @@ -185,8 +239,10 @@ static int l_ws_recv(lua_State* L) { lua_pushlstring(L, msg.data(), msg.size()); return 1; } - if (box->closed) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + 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); @@ -197,8 +253,12 @@ 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->closed) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + 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") { @@ -215,7 +275,11 @@ static int l_ws_send(lua_State* L) { // ws:close() static int l_ws_close(lua_State* L) { LuaWsClient* box = (LuaWsClient*)luaL_checkudata(L, 1, WS_CLIENT_MT); - if (box && box->client && !box->closed) { + 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; diff --git a/unittest/lua_ws_test.cpp b/unittest/lua_ws_test.cpp index e334f591a..851509346 100644 --- a/unittest/lua_ws_test.cpp +++ b/unittest/lua_ws_test.cpp @@ -63,6 +63,21 @@ int main() { " 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" ); @@ -76,7 +91,7 @@ int main() { hv_msleep(100); printf("hv.ws result: %s\n", g_probe.c_str()); - assert(g_probe == "opened,hello,world"); + assert(g_probe == "opened,hello,world,opened2,again"); printf("ALL lua_ws_test PASSED\n"); return 0; } From f910d59b520b4bbb10ecea302807e791b5f080a6 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 22:55:28 +0800 Subject: [PATCH 25/33] fix(lua): crash + leak fixes from runtime review (A1/A2/A3/B + UDP) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All reproduced locally before fixing; regression tests added. A1 json cyclic-ref segfault: hv.json.encode / ctx:json recursion had no depth limit — a self-referential table (t.self=t) recursed until stack overflow. Add a depth cap AND lua_checkstack per level (the Lua value stack, not just C stack, was overflowing -> crash in Lua's table internals). A2 non-UTF-8 abort (remote-triggerable): json.dump throws type_error.316 on invalid UTF-8; it was uncaught -> process abort. Reachable via an HTTP handler doing ctx:json(user_input). Wrap dump in try/catch: hv.json.encode returns (nil,err); http dump_json returns empty. (dump_json in http_content.cpp is master-level but is the remote sink, so guarded here.) A3 timer UAF: add_lua_timer stored the CALLING coroutine's lua_State in lt->L; if that coroutine was GC'd before the timer fired (e.g. a timer registered inside another timer's callback), on_lua_timer's lua_newthread(lt->L) was a use-after-free. Store the per-loop main state (hloop_lua_state), matching on_server_accept / on_udp_server_read. B lua_yieldk skips C++ destructors: lua_yieldk longjmps (liblua is C) and never unwinds C++ frames, so any non-trivial local alive at the yield leaks every call. Confined the C++ objects to a scope ending before the yield at all four sites: hvlua_http (shared_ptr), hvlua_ws (http_headers), hvlua_redis (shared_ptr + RedisCommand), hvlua_mqtt (EventLoopPtr + std::strings). Documented the rule in hvlua.h. UDP recvfrom inline-callback: l_udp_recvfrom suspended before hio_read, but hio_read can invoke the read callback inline (buffered datagram), resuming a not-yet-yielded coroutine. Reuse the TCP begin/end reading-window (reading_L) so a synchronous datagram is returned directly. Refactor: the duplicated lua<->json conversion (hvlua_json.cpp + HttpLuaHandler) is unified into lua/hvlua_json.h + hvlua_json.cpp (hv::hvlua_lua_to_json / hvlua_json_to_lua), so the A1/A2 guards live in ONE place; HttpLuaHandler reuses it. Tests: lua_binding_test gains cyclic-json / non-utf8-json / timer-in-callback+GC regressions (each crashed pre-fix). Full lua/redis suite (12) passes. --- http/http_content.cpp | 12 ++- http/server/HttpLuaHandler.cpp | 82 ++------------------ lua/hvlua.h | 10 +++ lua/hvlua_event.c | 44 ++++++++++- lua/hvlua_http.cpp | 70 +++++++++-------- lua/hvlua_json.cpp | 61 ++++++++++----- lua/hvlua_json.h | 33 ++++++++ lua/hvlua_mqtt.cpp | 137 ++++++++++++++++++--------------- lua/hvlua_redis.cpp | 63 +++++++++------ lua/hvlua_ws.cpp | 23 +++++- unittest/lua_binding_test.cpp | 51 ++++++++++++ 11 files changed, 364 insertions(+), 222 deletions(-) create mode 100644 lua/hvlua_json.h 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 03139f2e7..8f407be1c 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -19,6 +19,7 @@ extern "C" { #include "EventLoop.h" #include "hvlua.h" +#include "hvlua_json.h" // shared lua<->json conversion (single implementation) namespace hv { @@ -109,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; @@ -358,7 +286,7 @@ static void apply_result(lua_State* co, const HttpContextPtr& ctx) { const char* s = lua_tolstring(co, -1, &len); ctx->response->String(std::string(s, len)); } else if (lua_istable(co, -1)) { - Json j = lua_to_json(co, -1); + Json j = hvlua_lua_to_json(co, -1); ctx->response->Json(j); } } diff --git a/lua/hvlua.h b/lua/hvlua.h index 78c15c20f..bdb3c02fd 100644 --- a/lua/hvlua.h +++ b/lua/hvlua.h @@ -56,6 +56,16 @@ int hvlua_dostring(hloop_t* loop, const char* code); // // 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). diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c index c37b68f61..dda95688e 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -136,7 +136,15 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea luaL_checktype(L, 2, LUA_TFUNCTION); HV_ALLOC_SIZEOF(lt); - lt->L = L; + // 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->once = once; lt->in_callback = 0; @@ -773,7 +781,21 @@ 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 || c->co == NULL) return; + 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); @@ -794,9 +816,25 @@ static int l_udp_recvfrom(lua_State* 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->co = hvlua_suspend(L); + 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); } diff --git a/lua/hvlua_http.cpp b/lua/hvlua_http.cpp index eb4678c74..6c0886e28 100644 --- a/lua/hvlua_http.cpp +++ b/lua/hvlua_http.cpp @@ -93,40 +93,46 @@ static int do_request(lua_State* L, http_method method, int url_index) { return 2; } - 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); - } - } - + // 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); - 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); + { + 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); } diff --git a/lua/hvlua_json.cpp b/lua/hvlua_json.cpp index 376e6435d..9e8bd67af 100644 --- a/lua/hvlua_json.cpp +++ b/lua/hvlua_json.cpp @@ -5,23 +5,23 @@ extern "C" { } #include "hvlua.h" +#include "hvlua_json.h" // shared lua<->json conversion (also used by HttpLuaHandler) #include #include "hstring.h" -#include "json.hpp" using nlohmann::json; -// This module stays C++ (nlohmann::json). All helpers are file-static; only -// hvlua_open_json is exported, with C linkage so the C core (hvlua.c) can call -// it. +// 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. -// ---- lua <-> json conversion (shared style with HttpLuaHandler) ---- +namespace hv { -static json lua_to_json(lua_State* L, int index); +json hvlua_lua_to_json(lua_State* L, int index, int depth); -static json lua_table_to_json(lua_State* L, int index) { +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; @@ -44,7 +44,7 @@ static json lua_table_to_json(lua_State* L, int 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)); + j.push_back(hvlua_lua_to_json(L, -1, depth + 1)); lua_pop(L, 1); } return j; @@ -61,13 +61,13 @@ static json lua_table_to_json(lua_State* L, int index) { } 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); + if (!key.empty()) j[key] = hvlua_lua_to_json(L, -1, depth + 1); lua_pop(L, 1); } return j; } -static json lua_to_json(lua_State* L, int index) { +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; @@ -79,12 +79,21 @@ static json lua_to_json(lua_State* L, int index) { const char* s = lua_tolstring(L, index, &len); return std::string(s, len); } - case LUA_TTABLE: return lua_table_to_json(L, index); + 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 void json_to_lua(lua_State* L, const json& j) { +void hvlua_json_to_lua(lua_State* L, const json& j) { switch (j.type()) { case json::value_t::null: lua_pushnil(L); @@ -110,7 +119,7 @@ static void json_to_lua(lua_State* L, const json& j) { lua_createtable(L, (int)j.size(), 0); int i = 1; for (const auto& item : j) { - json_to_lua(L, item); + hvlua_json_to_lua(L, item); lua_seti(L, -2, i++); } break; @@ -119,7 +128,7 @@ static void json_to_lua(lua_State* L, const json& j) { 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()); - json_to_lua(L, it.value()); + hvlua_json_to_lua(L, it.value()); lua_settable(L, -3); } break; @@ -130,12 +139,24 @@ static void json_to_lua(lua_State* L, const json& j) { } } -// hv.json.encode(value) -> string +} // namespace hv + +// hv.json.encode(value) -> string | nil, err static int l_hv_json_encode(lua_State* L) { - json j = lua_to_json(L, 1); - std::string s = j.dump(); - lua_pushlstring(L, s.data(), s.size()); - return 1; + 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 @@ -148,7 +169,7 @@ static int l_hv_json_decode(lua_State* L) { lua_pushstring(L, "json parse error"); return 2; } - json_to_lua(L, j); + hv::hvlua_json_to_lua(L, j); return 1; } diff --git a/lua/hvlua_json.h b/lua/hvlua_json.h new file mode 100644 index 000000000..a5ecc5eb8 --- /dev/null +++ b/lua/hvlua_json.h @@ -0,0 +1,33 @@ +#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. +void hvlua_json_to_lua(lua_State* L, const nlohmann::json& j); + +} // namespace hv + +#endif // HV_LUA_JSON_H_ diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp index 2ad2b9106..bb48e6cfd 100644 --- a/lua/hvlua_mqtt.cpp +++ b/lua/hvlua_mqtt.cpp @@ -175,75 +175,84 @@ static int mqtt_connect_k(lua_State* L, int status, lua_KContext ctx) { // hv.mqtt.connect(cfg) -> client | nil, err static int l_mqtt_connect(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); - EventLoopPtr loop = currentThreadEventLoopPtr; - if (!loop) { - lua_pushnil(L); - lua_pushstring(L, "hv.mqtt: no shared event loop on this thread"); - return 2; - } - - std::string host = "127.0.0.1"; + // 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; - std::string id, username, password; - int keepalive = 0, ssl = 0, clean_session = -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, "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); + 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; + } - LuaMqttClient* box = (LuaMqttClient*)lua_newuserdata(L, sizeof(LuaMqttClient)); - new (&box->inbox) MqttInbox(); - box->wait_co = NULL; - box->connected = false; - 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 + 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); - 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 } - lua_getfield(L, 1, "reconnect"); - if (lua_istable(L, -1)) { - reconn_setting_t reconn; - reconn_setting_init(&reconn); - lua_getfield(L, -1, "min_delay"); - if (lua_isinteger(L, -1)) reconn.min_delay = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "max_delay"); - if (lua_isinteger(L, -1)) reconn.max_delay = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "delay_policy"); - if (lua_isinteger(L, -1)) reconn.delay_policy = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "max_retry"); - if (lua_isinteger(L, -1)) reconn.max_retry_cnt = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - mqtt_client_set_reconnect(box->client, &reconn); - box->reconnect = true; - } - lua_pop(L, 1); // pop reconnect table (or nil) + box = (LuaMqttClient*)lua_newuserdata(L, sizeof(LuaMqttClient)); + new (&box->inbox) MqttInbox(); + box->wait_co = NULL; + box->connected = false; + 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 } + lua_getfield(L, 1, "reconnect"); + if (lua_istable(L, -1)) { + reconn_setting_t reconn; + reconn_setting_init(&reconn); + lua_getfield(L, -1, "min_delay"); + if (lua_isinteger(L, -1)) reconn.min_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_delay"); + if (lua_isinteger(L, -1)) reconn.max_delay = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "delay_policy"); + if (lua_isinteger(L, -1)) reconn.delay_policy = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "max_retry"); + if (lua_isinteger(L, -1)) reconn.max_retry_cnt = (uint32_t)lua_tointeger(L, -1); + lua_pop(L, 1); + mqtt_client_set_reconnect(box->client, &reconn); + box->reconnect = true; + } + lua_pop(L, 1); // pop reconnect table (or nil) + } // ~loop / ~id / ~username / ~password run here, before the yield box->wait_co = hvlua_suspend(L); - int ret = mqtt_client_connect(box->client, host.c_str(), port, ssl); + int ret = mqtt_client_connect(box->client, host, port, ssl); if (ret != 0) { hvlua_cancel(box->wait_co); box->wait_co = NULL; diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp index aaceb9fd5..6d16a5c13 100644 --- a/lua/hvlua_redis.cpp +++ b/lua/hvlua_redis.cpp @@ -220,34 +220,49 @@ static int redis_do_command(lua_State* L, RedisCommand&& cmd) { 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 lua_resume. - if (box->destroyed) { hvlua_cancel(st->co); st->co = NULL; return; } - if (!st->yielded) { - // Synchronous completion: the coroutine has not yielded yet. Push - // results onto its stack and let redis_do_command return them; do - // NOT resume (it is still running). Release the unused suspend token. - st->done = true; - st->nresults = redis_push_result(st->L, result); - hvlua_cancel(st->co); + // NOTE: lua_yieldk never returns to this C++ frame (longjmp in a C-built + // Lua), so destructors of non-trivial locals in this frame are SKIPPED and + // would leak. Confine `cmd` (the RedisCommand this frame owns) and our local + // `st` ref to a scope that ends BEFORE the yield: the command callback holds + // its own `st` ref, and command() copies what it needs from cmd. + bool done; + int nresults; + { + RedisCommand local_cmd(std::move(cmd)); // owned here, destroyed at block end + box->client->command(local_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; redis_do_command returns them. Do NOT + // resume (still running). Release the unused suspend token. + 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; - 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; } // gone - int n = redis_push_result(cur, result); - HvLuaCoroutine* tok = st->co; - st->co = NULL; - hvlua_resume(tok, n); - }); + hvlua_resume(tok, n); + }); + // Snapshot sync-completion state into POD locals, then mark yielded and + // drop this frame's `st` ref (and `local_cmd`) before the yield. + done = st->done; + nresults = st->nresults; + if (!done) st->yielded = true; + st.reset(); + } - if (st->done) { + if (done) { // Completed synchronously: results are already on L. Return them now. - return st->nresults; + return nresults; } - st->yielded = true; return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); } diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp index 68165b15c..296c5f5f8 100644 --- a/lua/hvlua_ws.cpp +++ b/lua/hvlua_ws.cpp @@ -206,7 +206,28 @@ static int l_ws_connect(lua_State* L) { }; box->recv_co = hvlua_suspend(L); // reuse recv_co to hold the connect wait - int ret = box->client->open(url, headers); + // 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; diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp index f555df56c..3176db380 100644 --- a/unittest/lua_binding_test.cpp +++ b/unittest/lua_binding_test.cpp @@ -143,12 +143,63 @@ static void test_two_coroutines_interleave() { 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"); +} + +// 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_timer_registered_in_callback_gc(); printf("ALL lua_binding_test PASSED\n"); return 0; } From 994a5bbe8ce7b72774a3681eb4ee70428c66699e Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 23:00:45 +0800 Subject: [PATCH 26/33] refactor(lua): extract reconnect config parsing into hvlua_util (shared helper) hv.ws and hv.mqtt each parsed the reconnect config sub-table ({min_delay,max_delay,delay_policy,max_retry} -> reconn_setting_t) with near-identical code. Move it to a shared pure-C helper hvlua_parse_reconnect in lua/hvlua_util.{h,c} that both reuse. Pure C (C-includable) so future C or C++ bindings can share it too; C++-only helpers stay in hvlua_json.h. No behavior change; ws/mqtt/binding/io tests pass. --- lua/hvlua_mqtt.cpp | 20 +++----------------- lua/hvlua_util.c | 27 +++++++++++++++++++++++++++ lua/hvlua_util.h | 32 ++++++++++++++++++++++++++++++++ lua/hvlua_ws.cpp | 27 ++------------------------- 4 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 lua/hvlua_util.c create mode 100644 lua/hvlua_util.h diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp index bb48e6cfd..1c29feae5 100644 --- a/lua/hvlua_mqtt.cpp +++ b/lua/hvlua_mqtt.cpp @@ -5,6 +5,7 @@ extern "C" { } #include "hvlua.h" +#include "hvlua_util.h" // hvlua_parse_reconnect #ifdef HVLUA_WITH_MQTT @@ -229,26 +230,11 @@ static int l_mqtt_connect(lua_State* L) { 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 } - lua_getfield(L, 1, "reconnect"); - if (lua_istable(L, -1)) { - reconn_setting_t reconn; - reconn_setting_init(&reconn); - lua_getfield(L, -1, "min_delay"); - if (lua_isinteger(L, -1)) reconn.min_delay = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "max_delay"); - if (lua_isinteger(L, -1)) reconn.max_delay = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "delay_policy"); - if (lua_isinteger(L, -1)) reconn.delay_policy = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); - lua_getfield(L, -1, "max_retry"); - if (lua_isinteger(L, -1)) reconn.max_retry_cnt = (uint32_t)lua_tointeger(L, -1); - lua_pop(L, 1); + reconn_setting_t reconn; + if (hvlua_parse_reconnect(L, 1, &reconn)) { mqtt_client_set_reconnect(box->client, &reconn); box->reconnect = true; } - lua_pop(L, 1); // pop reconnect table (or nil) } // ~loop / ~id / ~username / ~password run here, before the yield box->wait_co = hvlua_suspend(L); diff --git a/lua/hvlua_util.c b/lua/hvlua_util.c new file mode 100644 index 000000000..28a1da121 --- /dev/null +++ b/lua/hvlua_util.c @@ -0,0 +1,27 @@ +#include "hvlua_util.h" + +#include + +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); + lua_pop(L, 1); // pop the reconnect sub-table + return 1; +} diff --git a/lua/hvlua_util.h b/lua/hvlua_util.h new file mode 100644 index 000000000..b31442904 --- /dev/null +++ b/lua/hvlua_util.h @@ -0,0 +1,32 @@ +#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 + +#ifndef lua_h +typedef struct lua_State lua_State; +#endif + +#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); + +#ifdef __cplusplus +} +#endif + +#endif // HV_LUA_UTIL_H_ diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp index 296c5f5f8..3d889903b 100644 --- a/lua/hvlua_ws.cpp +++ b/lua/hvlua_ws.cpp @@ -5,6 +5,7 @@ extern "C" { } #include "hvlua.h" +#include "hvlua_util.h" // hvlua_parse_reconnect #ifdef HVLUA_WITH_HTTP @@ -103,30 +104,6 @@ static int ws_connect_k(lua_State* L, int status, lua_KContext ctx) { return 2; // (nil, err) } -// Parse an optional { reconnect = { min_delay, max_delay, delay_policy, -// max_retry } } sub-table at opts_index into *out. Returns true if reconnect -// was requested. -static bool ws_parse_reconnect(lua_State* L, int opts_index, reconn_setting_t* out) { - if (!lua_istable(L, opts_index)) return false; - lua_getfield(L, opts_index, "reconnect"); - if (!lua_istable(L, -1)) { lua_pop(L, 1); return false; } - 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); - lua_pop(L, 1); // pop the reconnect table - return true; -} - // hv.ws.connect(url [, opts]) -> ws | nil, err // opts = { headers = {..}, ping_interval = ms, reconnect = {min_delay, max_delay, // delay_policy, max_retry} } @@ -168,7 +145,7 @@ static int l_ws_connect(lua_State* L) { if (lua_isinteger(L, -1)) box->client->setPingInterval((int)lua_tointeger(L, -1)); lua_pop(L, 1); reconn_setting_t reconn; - if (ws_parse_reconnect(L, 2, &reconn)) { + if (hvlua_parse_reconnect(L, 2, &reconn)) { box->reconnect = true; box->client->setReconnect(&reconn); } From febd2651a948070563ee22d07bd1720d15478d64 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 23:16:33 +0800 Subject: [PATCH 27/33] refactor(lua): extract client metatable registration into hvlua_new_class The __gc + __index=self + luaL_setfuncs metatable setup was byte-identical across hv.ws / hv.mqtt and near-identical in hv.redis (plus verb sugar). Consolidate into hvlua_new_class() in the shared hvlua_util helper; redis reuses it and only appends its verb closures when the mt is newly created. --- lua/hvlua_mqtt.cpp | 8 +------- lua/hvlua_redis.cpp | 16 +++++++--------- lua/hvlua_util.c | 12 ++++++++++-- lua/hvlua_util.h | 12 +++++++++--- lua/hvlua_ws.cpp | 8 +------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp index 1c29feae5..296269aca 100644 --- a/lua/hvlua_mqtt.cpp +++ b/lua/hvlua_mqtt.cpp @@ -362,13 +362,7 @@ static const luaL_Reg mqtt_funcs[] = { }; extern "C" void hvlua_open_mqtt(lua_State* L) { - if (luaL_newmetatable(L, MQTT_CLIENT_MT)) { - lua_pushcfunction(L, mqtt_client_gc); - lua_setfield(L, -2, "__gc"); - lua_pushvalue(L, -1); - lua_setfield(L, -2, "__index"); - luaL_setfuncs(L, mqtt_methods, 0); - } + hvlua_new_class(L, MQTT_CLIENT_MT, mqtt_client_gc, mqtt_methods); lua_pop(L, 1); lua_getglobal(L, "hv"); diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp index 6d16a5c13..78888fcaa 100644 --- a/lua/hvlua_redis.cpp +++ b/lua/hvlua_redis.cpp @@ -5,6 +5,7 @@ extern "C" { } #include "hvlua.h" +#include "hvlua_util.h" // hvlua_new_class #ifdef HVLUA_WITH_REDIS @@ -312,9 +313,10 @@ static const luaL_Reg redis_funcs[] = { { NULL, NULL } }; -// Register redis methods (command + verb sugar) into the table on top of L. -static void register_redis_methods(lua_State* L) { - luaL_setfuncs(L, redis_methods, 0); +// 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]); @@ -327,12 +329,8 @@ static void register_redis_methods(lua_State* L) { extern "C" void hvlua_open_redis(lua_State* L) { // Create the shared client metatable once: __gc + __index=self + methods. - if (luaL_newmetatable(L, REDIS_CLIENT_MT)) { - lua_pushcfunction(L, redis_client_gc); - lua_setfield(L, -2, "__gc"); - lua_pushvalue(L, -1); - lua_setfield(L, -2, "__index"); // methods live on the metatable itself - register_redis_methods(L); // command + verb sugar into the mt + 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); diff --git a/lua/hvlua_util.c b/lua/hvlua_util.c index 28a1da121..440b91d18 100644 --- a/lua/hvlua_util.c +++ b/lua/hvlua_util.c @@ -1,7 +1,5 @@ #include "hvlua_util.h" -#include - 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"); @@ -25,3 +23,13 @@ int hvlua_parse_reconnect(lua_State* L, int table_index, reconn_setting_t* out) 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 index b31442904..eadc41a93 100644 --- a/lua/hvlua_util.h +++ b/lua/hvlua_util.h @@ -8,9 +8,8 @@ #include "hloop.h" // reconn_setting_t -#ifndef lua_h -typedef struct lua_State lua_State; -#endif +#include // lua_State, lua_CFunction +#include // luaL_Reg #ifdef __cplusplus extern "C" { @@ -25,6 +24,13 @@ extern "C" { // 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 diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp index 3d889903b..2aa91cba5 100644 --- a/lua/hvlua_ws.cpp +++ b/lua/hvlua_ws.cpp @@ -296,13 +296,7 @@ static const luaL_Reg ws_funcs[] = { }; extern "C" void hvlua_open_ws(lua_State* L) { - if (luaL_newmetatable(L, WS_CLIENT_MT)) { - lua_pushcfunction(L, ws_client_gc); - lua_setfield(L, -2, "__gc"); - lua_pushvalue(L, -1); - lua_setfield(L, -2, "__index"); - luaL_setfuncs(L, ws_methods, 0); - } + hvlua_new_class(L, WS_CLIENT_MT, ws_client_gc, ws_methods); lua_pop(L, 1); lua_getglobal(L, "hv"); From 6210e52442ff8558b602f7b801d2a2f18d169cd2 Mon Sep 17 00:00:00 2001 From: ithewei Date: Thu, 30 Jul 2026 23:49:50 +0800 Subject: [PATCH 28/33] fix(lua): harden json, stop, and client coroutine lifetimes --- lua/hvlua_event.c | 12 +++- lua/hvlua_json.cpp | 39 ++++++++---- lua/hvlua_json.h | 5 +- lua/hvlua_mqtt.cpp | 20 +++--- lua/hvlua_redis.cpp | 116 ++++++++++++++++------------------ lua/hvlua_ws.cpp | 53 +++++++--------- unittest/lua_binding_test.cpp | 31 +++++++++ 7 files changed, 158 insertions(+), 118 deletions(-) diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c index dda95688e..47c3e420b 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -335,8 +335,18 @@ static int l_hloop_run(lua_State* 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_stop(hvlua_loop(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; } diff --git a/lua/hvlua_json.cpp b/lua/hvlua_json.cpp index 9e8bd67af..73de87ae4 100644 --- a/lua/hvlua_json.cpp +++ b/lua/hvlua_json.cpp @@ -93,52 +93,61 @@ json hvlua_lua_to_json(lua_State* L, int index, int depth) { } } -void hvlua_json_to_lua(lua_State* L, const json& j) { +static bool json_to_lua(lua_State* L, const json& j, int depth) { switch (j.type()) { case json::value_t::null: lua_pushnil(L); - break; + return true; case json::value_t::boolean: lua_pushboolean(L, j.get()); - break; + return true; case json::value_t::number_integer: lua_pushinteger(L, (lua_Integer)j.get()); - break; + return true; case json::value_t::number_unsigned: lua_pushinteger(L, (lua_Integer)j.get()); - break; + return true; case json::value_t::number_float: lua_pushnumber(L, j.get()); - break; + return true; case json::value_t::string: { const std::string& s = j.get_ref(); lua_pushlstring(L, s.data(), s.size()); - break; + 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) { - hvlua_json_to_lua(L, item); + if (!json_to_lua(L, item, depth + 1)) return false; lua_seti(L, -2, i++); } - break; + 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()); - hvlua_json_to_lua(L, it.value()); + if (!json_to_lua(L, it.value(), depth + 1)) return false; lua_settable(L, -3); } - break; + return true; } default: lua_pushnil(L); - break; + 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 @@ -169,7 +178,11 @@ static int l_hv_json_decode(lua_State* L) { lua_pushstring(L, "json parse error"); return 2; } - hv::hvlua_json_to_lua(L, j); + if (!hv::hvlua_json_to_lua(L, j)) { + lua_pushnil(L); + lua_pushstring(L, "json too deep"); + return 2; + } return 1; } diff --git a/lua/hvlua_json.h b/lua/hvlua_json.h index a5ecc5eb8..3a3d491d4 100644 --- a/lua/hvlua_json.h +++ b/lua/hvlua_json.h @@ -25,8 +25,9 @@ namespace hv { // 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. -void hvlua_json_to_lua(lua_State* L, const nlohmann::json& j); +// 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 diff --git a/lua/hvlua_mqtt.cpp b/lua/hvlua_mqtt.cpp index 296269aca..33078ae18 100644 --- a/lua/hvlua_mqtt.cpp +++ b/lua/hvlua_mqtt.cpp @@ -50,7 +50,6 @@ struct LuaMqttClient { mqtt_client_t* client; MqttInbox inbox; HvLuaCoroutine* wait_co; // coroutine waiting in connect() or recv(), or NULL - bool connected; bool closed; bool connecting; // wait_co holds a connect() waiter (vs recv()) bool reconnect; // auto-reconnect enabled @@ -97,7 +96,6 @@ static void on_mqtt(mqtt_client_t* cli, int type) { if (box == NULL) return; switch (type) { case MQTT_TYPE_CONNACK: - box->connected = true; box->closed = false; // reset for a (re)established session if (box->wait_co && box->connecting) { lua_State* co = hvlua_coroutine_state(box->wait_co); @@ -120,7 +118,6 @@ static void on_mqtt(mqtt_client_t* cli, int type) { } case MQTT_TYPE_DISCONNECT: box->closed = true; - box->connected = false; 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; } @@ -207,7 +204,6 @@ static int l_mqtt_connect(lua_State* L) { box = (LuaMqttClient*)lua_newuserdata(L, sizeof(LuaMqttClient)); new (&box->inbox) MqttInbox(); box->wait_co = NULL; - box->connected = false; box->closed = false; box->connecting = true; box->reconnect = false; @@ -255,11 +251,17 @@ static int mqtt_recv_k(lua_State* L, int status, lua_KContext 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) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + return mqtt_push_closed(L, box); } if (box->wait_co != NULL) { lua_pushnil(L); lua_pushstring(L, "hv.mqtt: recv already pending"); return 2; @@ -271,7 +273,7 @@ static int l_mqtt_recv(lua_State* L) { return 1; } if (box->closed) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + return mqtt_push_closed(L, box); } box->connecting = false; box->wait_co = hvlua_suspend(L); @@ -287,7 +289,7 @@ static int l_mqtt_publish(lua_State* L) { int qos = (int)luaL_optinteger(L, 4, 0); int retain = (int)luaL_optinteger(L, 5, 0); if (box == NULL || box->client == NULL || box->closed) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + return mqtt_push_closed(L, box); } mqtt_message_t msg; memset(&msg, 0, sizeof(msg)); @@ -309,7 +311,7 @@ static int l_mqtt_subscribe(lua_State* L) { const char* topic = luaL_checkstring(L, 2); int qos = (int)luaL_optinteger(L, 3, 0); if (box == NULL || box->client == NULL || box->closed) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + return mqtt_push_closed(L, box); } int mid = mqtt_client_subscribe(box->client, topic, qos); if (mid < 0) { @@ -324,7 +326,7 @@ 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) { - lua_pushnil(L); lua_pushstring(L, "closed"); return 2; + return mqtt_push_closed(L, box); } int mid = mqtt_client_unsubscribe(box->client, topic); if (mid < 0) { diff --git a/lua/hvlua_redis.cpp b/lua/hvlua_redis.cpp index 78888fcaa..e8e9d41a7 100644 --- a/lua/hvlua_redis.cpp +++ b/lua/hvlua_redis.cpp @@ -200,8 +200,9 @@ static int redis_push_result(lua_State* co, const RedisResult& result) { return 1; } -// Shared implementation for r:command(...) and the sugar methods. -static int redis_do_command(lua_State* L, RedisCommand&& cmd) { +// 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); @@ -221,81 +222,72 @@ static int redis_do_command(lua_State* L, RedisCommand&& cmd) { st->done = false; st->nresults = 0; - // NOTE: lua_yieldk never returns to this C++ frame (longjmp in a C-built - // Lua), so destructors of non-trivial locals in this frame are SKIPPED and - // would leak. Confine `cmd` (the RedisCommand this frame owns) and our local - // `st` ref to a scope that ends BEFORE the yield: the command callback holds - // its own `st` ref, and command() copies what it needs from cmd. - bool done; - int nresults; - { - RedisCommand local_cmd(std::move(cmd)); // owned here, destroyed at block end - box->client->command(local_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; redis_do_command returns them. Do NOT - // resume (still running). Release the unused suspend token. - 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; + 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; - hvlua_resume(tok, n); - }); - // Snapshot sync-completion state into POD locals, then mark yielded and - // drop this frame's `st` ref (and `local_cmd`) before the yield. - done = st->done; - nresults = st->nresults; - if (!done) st->yielded = true; - st.reset(); - } + 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); + }); - if (done) { - // Completed synchronously: results are already on L. Return them now. - return nresults; - } - return lua_yieldk(L, 0, (lua_KContext)0, redis_cmd_k); + 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) { - RedisCommand cmd; - if (!build_command(L, 2, &cmd)) { - lua_pushnil(L); - lua_pushstring(L, "hv.redis: empty or invalid command"); - return 2; + 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); } - return redis_do_command(L, std::move(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)); - 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; + 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); } - cmd.emplace_back(s, len); + nresults = redis_start_command(L, cmd); } - return redis_do_command(L, std::move(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. diff --git a/lua/hvlua_ws.cpp b/lua/hvlua_ws.cpp index 2aa91cba5..dbeae395f 100644 --- a/lua/hvlua_ws.cpp +++ b/lua/hvlua_ws.cpp @@ -109,38 +109,29 @@ static int ws_connect_k(lua_State* L, int status, lua_KContext ctx) { // delay_policy, max_retry} } static int l_ws_connect(lua_State* L) { const char* url = luaL_checkstring(L, 1); - EventLoopPtr loop = currentThreadEventLoopPtr; - if (!loop) { - lua_pushnil(L); - lua_pushstring(L, "hv.ws: no shared event loop on this thread"); - return 2; - } + 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. - LuaWsClient* 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 + // 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 - // opts (arg 2): headers sub-table, ping_interval, reconnect sub-table. - http_headers headers = DefaultHeaders; + // Parse headers later in the scope that ends before lua_yieldk. 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) lua_getfield(L, 2, "ping_interval"); if (lua_isinteger(L, -1)) box->client->setPingInterval((int)lua_tointeger(L, -1)); lua_pop(L, 1); @@ -165,8 +156,8 @@ static int l_ws_connect(lua_State* L) { lua_pushboolean(co, 1); // success marker for ws_connect_k hvlua_resume(tok, 1); } - // A reconnect's onopen does not resume anything: a recv() waiter (if any) - // stays parked and is woken by the next onmessage. + // 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); diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp index 3176db380..c5cdfa277 100644 --- a/unittest/lua_binding_test.cpp +++ b/unittest/lua_binding_test.cpp @@ -174,6 +174,35 @@ static void test_json_non_utf8() { 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"); +} + // 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. @@ -199,6 +228,8 @@ int main() { test_two_coroutines_interleave(); test_json_cyclic(); test_json_non_utf8(); + test_stop_before_run(); + test_json_decode_too_deep(); test_timer_registered_in_callback_gc(); printf("ALL lua_binding_test PASSED\n"); return 0; From cacf94bd4b84233b7bb6be5f874978521bede76d Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 31 Jul 2026 00:36:18 +0800 Subject: [PATCH 29/33] fix(lua): clean up pending async state on loop teardown --- docs/cn/lua.md | 2 +- http/server/HttpLuaHandler.cpp | 5 +- lua/hvlua.c | 143 ++++++++++++++++++++++++++++++++- lua/hvlua.h | 9 +++ lua/hvlua_event.c | 89 +++++++++++++++++--- lua/hvlua_util.c | 5 ++ unittest/lua_binding_test.cpp | 33 ++++++++ unittest/lua_io_test.cpp | 18 +++++ 8 files changed, 291 insertions(+), 13 deletions(-) diff --git a/docs/cn/lua.md b/docs/cn/lua.md index 40f3a27a8..ec7c4ea4d 100644 --- a/docs/cn/lua.md +++ b/docs/cn/lua.md @@ -72,7 +72,7 @@ hv.clearTimer(id) hv.sleep(1000) -- 协程同步:挂起当前协程 1000ms,loop 不阻塞 -hv.run() -- 运行当前 loop(独立运行时用;HTTP handler 内不需要) +hv.run() -- 兼容保留;loop 由宿主自动驱动,无需调用 hv.stop() -- 停止当前 loop ``` diff --git a/http/server/HttpLuaHandler.cpp b/http/server/HttpLuaHandler.cpp index 8f407be1c..fcef37e2c 100644 --- a/http/server/HttpLuaHandler.cpp +++ b/http/server/HttpLuaHandler.cpp @@ -303,8 +303,11 @@ static void on_task_done(void* ud, bool ok, lua_State* co) { bool async = task->async; delete task; + if (co == NULL) { + return; + } if (!ok) { - const char* msg = co ? lua_tostring(co, -1) : NULL; + 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; diff --git a/lua/hvlua.c b/lua/hvlua.c index d89895628..ff6921a1b 100644 --- a/lua/hvlua.c +++ b/lua/hvlua.c @@ -14,9 +14,14 @@ // 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 @@ -27,8 +32,70 @@ 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; @@ -70,6 +137,7 @@ static int hvlua_task_step(lua_State* co, int nresults) { 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; @@ -91,6 +159,7 @@ int hvlua_start_task(lua_State* L, int nargs, hvlua_done_cb on_done, void* ud) { 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 @@ -109,6 +178,7 @@ HvLuaCoroutine* hvlua_suspend(lua_State* L) { 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; } @@ -119,10 +189,11 @@ lua_State* hvlua_coroutine_state(HvLuaCoroutine* co) { void hvlua_cancel(HvLuaCoroutine* co) { if (co == NULL) return; - if (co->ref != LUA_NOREF) { + 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); } @@ -140,6 +211,7 @@ void hvlua_resume(HvLuaCoroutine* co, int nresults) { // 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 @@ -163,17 +235,84 @@ void hvlua_resume(HvLuaCoroutine* co, int nresults) { 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) { - if (L) lua_close((lua_State*)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. diff --git a/lua/hvlua.h b/lua/hvlua.h index bdb3c02fd..066bb8f07 100644 --- a/lua/hvlua.h +++ b/lua/hvlua.h @@ -25,6 +25,8 @@ typedef struct lua_State lua_State; // 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. @@ -82,6 +84,11 @@ 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); @@ -130,6 +137,8 @@ namespace hv { 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; } diff --git a/lua/hvlua_event.c b/lua/hvlua_event.c index 47c3e420b..ac17036cd 100644 --- a/lua/hvlua_event.c +++ b/lua/hvlua_event.c @@ -7,6 +7,7 @@ #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) @@ -23,6 +24,7 @@ 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; @@ -83,6 +85,8 @@ static int lua_timer_reg_has(lua_State* L, htimer_t* timer) { } 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) { @@ -93,6 +97,12 @@ static void lua_timer_free(LuaTimer* 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; @@ -146,6 +156,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea // 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; @@ -160,6 +171,7 @@ static htimer_t* add_lua_timer(lua_State* L, uint32_t timeout_ms, uint32_t repea } 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; } @@ -216,11 +228,22 @@ static int l_hloop_clearTimer(lua_State* L) { 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); @@ -252,6 +275,7 @@ static int l_hloop_sleep(lua_State* L) { 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); @@ -266,13 +290,23 @@ static int l_hloop_sleep(lua_State* L) { 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); @@ -314,12 +348,14 @@ static int l_hloop_resolveDns(lua_State* L) { 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"); @@ -331,7 +367,7 @@ static int l_hloop_resolveDns(lua_State* L) { // hv.run() / hv.stop() static int l_hloop_run(lua_State* L) { - hloop_run(hvlua_loop(L)); + (void)L; return 0; } @@ -556,10 +592,15 @@ static int l_conn_read(lua_State* L) { // conn:readbytes(n) -> exactly n bytes (hio_read_until_length) static int l_conn_readbytes(lua_State* L) { LuaConn* c = lua_check_conn(L); - unsigned int n = (unsigned int)luaL_checkinteger(L, 2); + 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); @@ -604,6 +645,7 @@ static int l_conn_readline(lua_State* L) { 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; } @@ -616,7 +658,13 @@ static int l_conn_setUnpack(lua_State* L) { c->unpack->package_max_length = DEFAULT_PACKAGE_MAX_LENGTH; lua_getfield(L, 2, "package_max_length"); - if (lua_isinteger(L, -1)) c->unpack->package_max_length = (unsigned int)lua_tointeger(L, -1); + 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"); @@ -628,7 +676,11 @@ static int l_conn_setUnpack(lua_State* L) { } else if (strcmp(mode, "fixed") == 0) { c->unpack->mode = UNPACK_BY_FIXED_LENGTH; lua_getfield(L, 2, "fixed_length"); - c->unpack->fixed_length = (unsigned int)luaL_optinteger(L, -1, 0); + 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; @@ -636,24 +688,43 @@ static int l_conn_setUnpack(lua_State* L) { c->unpack->mode = UNPACK_BY_DELIMITER; lua_getfield(L, 2, "delimiter"); delim = luaL_optlstring(L, -1, "", &dlen); - if (dlen > PACKAGE_MAX_DELIMITER_BYTES) dlen = PACKAGE_MAX_DELIMITER_BYTES; + 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"); - c->unpack->body_offset = (unsigned short)luaL_optinteger(L, -1, 0); + body_offset = luaL_optinteger(L, -1, 0); lua_pop(L, 1); lua_getfield(L, 2, "length_field_offset"); - c->unpack->length_field_offset = (unsigned short)luaL_optinteger(L, -1, 0); + field_offset = luaL_optinteger(L, -1, 0); lua_pop(L, 1); lua_getfield(L, 2, "length_field_bytes"); - c->unpack->length_field_bytes = (unsigned short)luaL_optinteger(L, -1, 0); + 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"); - c->unpack->length_adjustment = (short)luaL_optinteger(L, -1, 0); + 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"); diff --git a/lua/hvlua_util.c b/lua/hvlua_util.c index 440b91d18..808df3612 100644 --- a/lua/hvlua_util.c +++ b/lua/hvlua_util.c @@ -20,6 +20,11 @@ int hvlua_parse_reconnect(lua_State* L, int table_index, reconn_setting_t* out) 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; } diff --git a/unittest/lua_binding_test.cpp b/unittest/lua_binding_test.cpp index c5cdfa277..a9ebad045 100644 --- a/unittest/lua_binding_test.cpp +++ b/unittest/lua_binding_test.cpp @@ -26,6 +26,7 @@ extern "C" { } #include "hloop.h" +#include "hbase.h" #include "hvlua.h" // A probe table the scripts write to, so C can assert what happened. @@ -203,6 +204,36 @@ static void test_stop_before_run() { 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. @@ -229,7 +260,9 @@ int main() { 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_io_test.cpp b/unittest/lua_io_test.cpp index 8ad83835d..c505a4986 100644 --- a/unittest/lua_io_test.cpp +++ b/unittest/lua_io_test.cpp @@ -120,10 +120,28 @@ static void test_udp_echo() { 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; } From 465e03df8aa90ab2c82ee86ea42bdc095231e39d Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 31 Jul 2026 00:36:28 +0800 Subject: [PATCH 30/33] fix(event): harden loop and client teardown races --- evpp/EventLoop.h | 3 +++ mqtt/mqtt_client.c | 10 ++++++++-- redis/AsyncRedisClient.cpp | 22 ++++++++++++++++++++-- redis/RedisSubscriber.cpp | 22 ++++++++++++++++++++-- unittest/lua_mqtt_test.cpp | 16 ++++++++++++++++ 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/evpp/EventLoop.h b/evpp/EventLoop.h index c97252456..483fd34c7 100644 --- a/evpp/EventLoop.h +++ b/evpp/EventLoop.h @@ -64,6 +64,9 @@ class EventLoop : public Status, public std::enable_shared_from_this 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 diff --git a/mqtt/mqtt_client.c b/mqtt/mqtt_client.c index 91718d70d..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; @@ -494,6 +494,12 @@ void mqtt_client_run (mqtt_client_t* cli) { // 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) { diff --git a/redis/AsyncRedisClient.cpp b/redis/AsyncRedisClient.cpp index 6ba91cfb6..f6e9f8e3e 100644 --- a/redis/AsyncRedisClient.cpp +++ b/redis/AsyncRedisClient.cpp @@ -51,12 +51,20 @@ 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; @@ -186,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); @@ -204,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; }); + } } } diff --git a/redis/RedisSubscriber.cpp b/redis/RedisSubscriber.cpp index 1f97d5033..fc5465758 100644 --- a/redis/RedisSubscriber.cpp +++ b/redis/RedisSubscriber.cpp @@ -39,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 { @@ -181,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); @@ -199,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; }); + } } } diff --git a/unittest/lua_mqtt_test.cpp b/unittest/lua_mqtt_test.cpp index 75812bf6b..f976885b6 100644 --- a/unittest/lua_mqtt_test.cpp +++ b/unittest/lua_mqtt_test.cpp @@ -19,6 +19,7 @@ extern "C" { } #include "hloop.h" +#include "mqtt_client.h" #include "EventLoop.h" #include "hvlua.h" @@ -33,7 +34,22 @@ static int l_probe(lua_State* L) { 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); From 56bdb2dfec3a8e36fb8904bde8b3df31e8c0618b Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 31 Jul 2026 00:36:40 +0800 Subject: [PATCH 31/33] fix(build): align lua feature gates across make and cmake --- CMakeLists.txt | 19 ++++++++++++------- Makefile | 28 ++++++++++++++++++++++------ Makefile.in | 8 +++++++- unittest/CMakeLists.txt | 14 ++++++++++---- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 28c983e66..c4c52e16e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,13 +195,13 @@ if(WITH_LUA) # 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_HTTP) + if(WITH_EVPP AND WITH_HTTP AND WITH_HTTP_CLIENT) add_definitions(-DHVLUA_WITH_HTTP) endif() - if(WITH_REDIS) + if(WITH_EVPP AND WITH_REDIS) add_definitions(-DHVLUA_WITH_REDIS) endif() - if(WITH_MQTT) + if(WITH_EVPP AND WITH_MQTT) add_definitions(-DHVLUA_WITH_MQTT) endif() endif() @@ -249,13 +249,18 @@ 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) - if(WITH_LUA) - set(LIBHV_HEADERS ${LIBHV_HEADERS} lua/hvlua.h) - set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} lua) - endif() if(WITH_REDIS) set(LIBHV_HEADERS ${LIBHV_HEADERS} ${REDIS_HEADERS}) set(LIBHV_SRCDIRS ${LIBHV_SRCDIRS} redis) diff --git a/Makefile b/Makefile index 522842c42..8be5e3034 100644 --- a/Makefile +++ b/Makefile @@ -20,15 +20,19 @@ LIBHV_HEADERS += $(PROTOCOL_HEADERS) LIBHV_SRCDIRS += protocol endif -ifeq ($(WITH_EVPP), yes) -LIBHV_HEADERS += $(CPPUTIL_HEADERS) $(EVPP_HEADERS) -LIBHV_SRCDIRS += cpputil evpp - ifeq ($(WITH_LUA), yes) -LIBHV_HEADERS += lua/hvlua.h +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 + ifeq ($(WITH_REDIS), yes) LIBHV_HEADERS += $(REDIS_HEADERS) LIBHV_SRCDIRS += redis @@ -107,8 +111,10 @@ 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." @@ -337,12 +343,21 @@ 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) -ifeq ($(WITH_HTTP), yes) +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 @@ -350,6 +365,7 @@ 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 bash scripts/unittest.sh diff --git a/Makefile.in b/Makefile.in index 55a759bcb..51cf3c14a 100644 --- a/Makefile.in +++ b/Makefile.in @@ -166,16 +166,22 @@ ifeq ($(WITH_LUA), yes) 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_HTTP), yes) +ifeq ($(WITH_EVPP), yes) +ifeq ($(WITH_HTTP_CLIENT), yes) CPPFLAGS += -DHVLUA_WITH_HTTP 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)) CPPFLAGS += $(addprefix -I, $(INCDIRS)) diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index 1645a5695..2b62d081d 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -97,14 +97,20 @@ 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 ../lua ../http ../http/server ../http/client) target_link_libraries(http_lua_handler_test ${HV_LIBRARIES}) -set(HTTP_LUA_UNITTEST_TARGETS lua_binding_test lua_io_test http_script_handler_test http_lua_handler_test) -if(WITH_HTTP) +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}) @@ -113,13 +119,13 @@ target_include_directories(lua_ws_test PRIVATE .. ../base ../ssl ../event ../cpp 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_MQTT) +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_REDIS) +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}) From a67be10309adb0463808503ea988098bc0cf495a Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 31 Jul 2026 00:39:58 +0800 Subject: [PATCH 32/33] fix(build): complete make feature gate parity --- Makefile | 2 ++ Makefile.in | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 8be5e3034..60733f9ea 100644 --- a/Makefile +++ b/Makefile @@ -333,6 +333,7 @@ unittest: prepare libhv ifeq ($(WITH_EVPP), yes) $(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 endif +ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_REDIS), yes) $(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 @@ -340,6 +341,7 @@ ifeq ($(WITH_REDIS), yes) $(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 endif +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) diff --git a/Makefile.in b/Makefile.in index 51cf3c14a..27c7dbd95 100644 --- a/Makefile.in +++ b/Makefile.in @@ -167,10 +167,12 @@ ifeq ($(WITH_LUA), yes) # 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 From 81ce454be83efe03aa18d182e75867ed7d2ef085 Mon Sep 17 00:00:00 2001 From: ithewei Date: Fri, 31 Jul 2026 11:13:01 +0800 Subject: [PATCH 33/33] fix(lua): reject coroutine resumes during state teardown --- lua/hvlua.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/hvlua.c b/lua/hvlua.c index ff6921a1b..b29fc758e 100644 --- a/lua/hvlua.c +++ b/lua/hvlua.c @@ -184,6 +184,7 @@ HvLuaCoroutine* hvlua_suspend(lua_State* L) { 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; } @@ -201,6 +202,10 @@ 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;