From f6efa087c7759def743543fdb652fbc3be21403c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rikard=20S=C3=B6derstr=C3=B6m?= Date: Sun, 24 May 2026 12:59:51 +0200 Subject: [PATCH 1/2] fix crash for NOTIFIED/FOREVER without user_data The g_assert on line 919 (approximately): g_assert (scope == GI_SCOPE_TYPE_ASYNC); ...will crash (abort) if a callback parameter has GI_SCOPE_TYPE_NOTIFIED or GI_SCOPE_TYPE_FOREVER scope AND no associated user_data parameter. While this is rare in practice, it is a correctness issue: an unexpected GLib introspection annotation causes an unrecoverable crash rather than a graceful error. GI_SCOPE_TYPE_NOTIFIED means "freed when the associated GDestroyNotify fires". If there is no user_data (and therefore no destroy parameter), there is no mechanism to free the closure. The closest correct behavior would be to treat it like ASYNC (rely on autodestroy) and emit a warning. GI_SCOPE_TYPE_FOREVER means the callback lives until the process exits. In this case no cleanup is expected or needed; the closure should be allocated without any guard. --- lgi/marshal.c | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lgi/marshal.c b/lgi/marshal.c index acf86383..c00cfea2 100644 --- a/lgi/marshal.c +++ b/lgi/marshal.c @@ -915,8 +915,22 @@ marshal_2c_callable (lua_State *L, GICallableInfo *ci, GIArgInfo *ai, *lgi_guard_create (L, lgi_closure_destroy) = user_data; nret++; } + else if (scope == GI_SCOPE_TYPE_ASYNC || scope == GI_SCOPE_TYPE_NOTIFIED) + { + /* ASYNC: callback fired once, autodestroy cleans up. + NOTIFIED without destroy param: no destroy will come; treat as + autodestroy and warn since this is unusual. */ + if (scope == GI_SCOPE_TYPE_NOTIFIED) + g_warning ("lgi: NOTIFIED-scope callback without destroy parameter;" + " treating as autodestroy"); + } + else if (scope == GI_SCOPE_TYPE_FOREVER) + { + /* Intentionally no cleanup: callback lives until process exit. */ + } else - g_assert (scope == GI_SCOPE_TYPE_ASYNC); + g_warning ("lgi: unexpected scope type %d for callback without " + "user_data; closure may leak", (int) scope); } /* Create the closure. */ From 27af9777a16d8913422d369ed87103f116c4a7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rikard=20S=C3=B6derstr=C3=B6m?= Date: Sun, 24 May 2026 13:14:49 +0200 Subject: [PATCH 2/2] Fix for GC lag during autodestroy at high callback frequency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even though the autodestroy mechanism is correct in isolation, it depends on Lua's GC running promptly after each async callback. In practice, two factors combine to make GC lag significant: (a) ffi_closure_alloc() uses the OS page allocator (mmap on Linux), which is NOT tracked by Lua's allocator. Lua sees no memory pressure from growing ffi closure memory and does not increase GC frequency in response. (b) The guard userdata (16 bytes, tracked by Lua) is tiny compared to the objects it is responsible for freeing (ffi pages, LGI wrappers, Lua closures). With Lua's default gcpause=200, GC starts a new cycle only when Lua-tracked memory doubles — which may be millions of guard allocations later. CONSEQUENCE: when read_line_async is called at high frequency (e.g. 1 Hz via awful.widget.watch in awesome-wm), the ffi closure blocks and the Lua objects reachable through them (Gio stream wrappers, Lua callback closures, accumulated stdout strings) accumulate faster than GC reclaims them. In testing, 4+ GB of Lua-tracked objects accumulated over 1 day before being freed by a manual collectgarbage("collect") call. --- lgi/callable.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lgi/callable.c b/lgi/callable.c index dd272ece..47659259 100644 --- a/lgi/callable.c +++ b/lgi/callable.c @@ -1429,6 +1429,15 @@ closure_callback (ffi_cif *cif, void *ret, void **args, void *closure_arg) /* This is NOT called by Lua, so we better leave the Lua stack we used pretty much tidied. */ lua_settop (L, stacktop); + + /* For autodestroy closures, nudge Lua's GC after resetting the stack. + ffi_closure_alloc() memory is invisible to Lua's allocator, so GC + does not naturally respond to ffi memory pressure. A single step + here ensures the guard created above is finalized promptly, which in + turn allows the callback's Lua closure chain to be freed. */ + if (closure->autodestroy) + lua_gc (block->callback.L, LUA_GCSTEP, 10); + if (L != marshal_L) lua_settop (marshal_L, 0);