Hi, Sergey, thanks for the patch! LGTM On 6/4/26 12:30, Sergey Kaplun wrote: > Our GDB extension already has dumpers for TValues. But sometimes it > may be useful to dump GC objects (GCobj) without stack context. This > patch adds additional wrappers around dumpers for GC objects to get the > corresponding GC object from a TValue. Also, the lj-gco command is > introduced. It allows dumping GC objects without stack context. The > output format is the same as for the lj-tv command. > > Part of tarantool/tarantool#4808 > --- > src/luajit_dbg.py | 182 ++++++++++++------ > .../debug-extension-tests.py | 83 ++++++-- > 2 files changed, 190 insertions(+), 75 deletions(-) > > diff --git a/src/luajit_dbg.py b/src/luajit_dbg.py > index 300d65e9..f5868e61 100644 > --- a/src/luajit_dbg.py > +++ b/src/luajit_dbg.py > @@ -882,44 +882,29 @@ def lightudV(tv): > > # Dumpers. > > +# GCobj dumpers. > > -def dump_lj_tnil(tv): > - return 'nil' > - > - > -def dump_lj_tfalse(tv): > - return 'false' > - > - > -def dump_lj_ttrue(tv): > - return 'true' > - > - > -def dump_lj_tlightud(tv): > - return 'light userdata @ {}'.format(strx64(lightudV(tv))) > - > - > -def dump_lj_tstr(tv): > +def dump_lj_gco_str(gcobj): > return 'string {body} @ {address}'.format( > - body=strdata(gcval(tv['gcr'])), > - address=strx64(gcval(tv['gcr'])) > + body=strdata(gcobj), > + address=strx64(gcobj) > ) > > > -def dump_lj_tupval(tv): > - return 'upvalue @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_upval(gcobj): > + return 'upvalue @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tthread(tv): > - return 'thread @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_thread(gcobj): > + return 'thread @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tproto(tv): > - return 'proto @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_proto(gcobj): > + return 'proto @ {}'.format(strx64(gcobj)) > > > -def dump_lj_tfunc(tv): > - func = dbg.cast('struct GCfuncC *', gcval(tv['gcr'])) > +def dump_lj_gco_func(gcobj): > + func = dbg.cast('struct GCfuncC *', gcobj) > ffid = func['ffid'] > > if ffid == 0: > @@ -936,20 +921,20 @@ def dump_lj_tfunc(tv): > return 'fast function #{}'.format(int(ffid)) > > > -def dump_lj_ttrace(tv): > - trace = dbg.cast('struct GCtrace *', gcval(tv['gcr'])) > +def dump_lj_gco_trace(gcobj): > + trace = dbg.cast('struct GCtrace *', gcobj) > return 'trace {traceno} @ {addr}'.format( > traceno=strx64(trace['traceno']), > addr=strx64(trace) > ) > > > -def dump_lj_tcdata(tv): > - return 'cdata @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_cdata(gcobj): > + return 'cdata @ {}'.format(strx64(gcobj)) > > > -def dump_lj_ttab(tv): > - table = dbg.cast('GCtab *', gcval(tv['gcr'])) > +def dump_lj_gco_tab(gcobj): > + table = dbg.cast('GCtab *', gcobj) > return 'table @ {gcr} (asize: {asize}, hmask: {hmask})'.format( > gcr=strx64(table), > asize=table['asize'], > @@ -957,41 +942,94 @@ def dump_lj_ttab(tv): > ) > > > -def dump_lj_tudata(tv): > - return 'userdata @ {}'.format(strx64(gcval(tv['gcr']))) > +def dump_lj_gco_udata(gcobj): > + return 'userdata @ {}'.format(strx64(gcobj)) > + > + > +def dump_lj_gco_invalid(gcobj): > + return 'not valid type @ {}'.format(strx64(gcobj)) > + > + > +# TValue dumpers > + > +def dump_lj_tv_nil(tv): > + return 'nil' > + > + > +def dump_lj_tv_false(tv): > + return 'false' > + > + > +def dump_lj_tv_true(tv): > + return 'true' > + > + > +def dump_lj_tv_lightud(tv): > + return 'light userdata @ {}'.format(strx64(lightudV(tv))) > + > + > +# Generate wrappers for TValues containing GCobj. > +gco_fn_dumpers = [ > + fn for fn in globals().keys() if fn.startswith('dump_lj_gco') > +] > +for fn_name in gco_fn_dumpers: > + wrapped_fn_name = fn_name.replace('gco', 'tv') > + # Lambda takes `fn_name` as a reference, so the additional > + # lambda is needed to fixate the correct wrapper. > + globals()[wrapped_fn_name] = (lambda f: ( > + lambda tv: globals()[f](gcval(tv['gcr'])) > + ))(fn_name) > > > -def dump_lj_tnumx(tv): > +def dump_lj_tv_numx(tv): > if tvisint(tv): > return 'integer {}'.format(dbg.cast('int32_t', tv['i'])) > else: > return 'number {}'.format(dbg.cast('double', tv['n'])) > > > -def dump_lj_invalid(tv): > - return 'not valid type @ {}'.format(strx64(gcval(tv['gcr']))) > - > - > -dumpers = { > - 'LJ_TNIL': dump_lj_tnil, > - 'LJ_TFALSE': dump_lj_tfalse, > - 'LJ_TTRUE': dump_lj_ttrue, > - 'LJ_TLIGHTUD': dump_lj_tlightud, > - 'LJ_TSTR': dump_lj_tstr, > - 'LJ_TUPVAL': dump_lj_tupval, > - 'LJ_TTHREAD': dump_lj_tthread, > - 'LJ_TPROTO': dump_lj_tproto, > - 'LJ_TFUNC': dump_lj_tfunc, > - 'LJ_TTRACE': dump_lj_ttrace, > - 'LJ_TCDATA': dump_lj_tcdata, > - 'LJ_TTAB': dump_lj_ttab, > - 'LJ_TUDATA': dump_lj_tudata, > - 'LJ_TNUMX': dump_lj_tnumx, > +gco_dumpers = { > + 'LJ_TSTR': dump_lj_gco_str, > + 'LJ_TUPVAL': dump_lj_gco_upval, > + 'LJ_TTHREAD': dump_lj_gco_thread, > + 'LJ_TPROTO': dump_lj_gco_proto, > + 'LJ_TFUNC': dump_lj_gco_func, > + 'LJ_TTRACE': dump_lj_gco_trace, > + 'LJ_TCDATA': dump_lj_gco_cdata, > + 'LJ_TTAB': dump_lj_gco_tab, > + 'LJ_TUDATA': dump_lj_gco_udata, > } > > > +tv_dumpers = { > + 'LJ_TNIL': dump_lj_tv_nil, > + 'LJ_TFALSE': dump_lj_tv_false, > + 'LJ_TTRUE': dump_lj_tv_true, > + 'LJ_TLIGHTUD': dump_lj_tv_lightud, > + 'LJ_TSTR': dump_lj_tv_str, # noqa: F821 # Generated. > + 'LJ_TUPVAL': dump_lj_tv_upval, # noqa: F821 # Generated. > + 'LJ_TTHREAD': dump_lj_tv_thread, # noqa: F821 # Generated. > + 'LJ_TPROTO': dump_lj_tv_proto, # noqa: F821 # Generated. > + 'LJ_TFUNC': dump_lj_tv_func, # noqa: F821 # Generated. > + 'LJ_TTRACE': dump_lj_tv_trace, # noqa: F821 # Generated. > + 'LJ_TCDATA': dump_lj_tv_cdata, # noqa: F821 # Generated. > + 'LJ_TTAB': dump_lj_tv_tab, # noqa: F821 # Generated. > + 'LJ_TUDATA': dump_lj_tv_udata, # noqa: F821 # Generated. > + 'LJ_TNUMX': dump_lj_tv_numx, > +} > + > + > +def dump_gcobj(gcobj): > + return gco_dumpers.get( > + typenames(i2notu32(gcobj['gch']['gct'])), dump_lj_gco_invalid > + )(gcobj) > + > + > def dump_tvalue(tvalue): > - return dumpers.get(typenames(itypemap(tvalue)), dump_lj_invalid)(tvalue) > + return tv_dumpers.get( > + typenames(itypemap(tvalue)), > + dump_lj_tv_invalid # noqa: F821 # Generated. > + )(tvalue) > > > def dump_framelink_slot_address(fr): > @@ -1011,7 +1049,7 @@ def dump_framelink(L, fr): > p='P' if frame_typep(fr) & FRAME_P else '' > ), > d=dbg.cast('TValue *', fr) - dbg.cast('TValue *', frame_prev(fr)), > - f=dump_lj_tfunc(fr - LJ_FR2), > + f=dump_lj_tv_func(fr - LJ_FR2), # noqa: F821 # Generated. > ) > > > @@ -1141,6 +1179,35 @@ The command requires no args and dumps current GC stats: > )) > > > +class LJDumpGCobj(dbg.LJBase): > + ''' > +lj-gco > + > +The command receives a pointer to (GCobj address) and dumps > +the type and some info related to it. > + > +* LJ_TSTR: string @ > +* LJ_TUPVAL: upvalue @ > +* LJ_TTHREAD: thread @ > +* LJ_TPROTO: proto @ > +* LJ_TFUNC: > + : Lua function @ , upvalues, > + : C function > + : fast function # > +* LJ_TTRACE: trace @ > +* LJ_TCDATA: cdata @ > +* LJ_TTAB: table @ (asize: , hmask: ) > +* LJ_TUDATA: userdata @ > + > +Whether the type of the given address differs from the listed above, then > +error message occurs. > + ''' > + > + def execute(self, arg): > + gcobj = dbg.cast('GCobj *', dbg.eval(arg)) > + dbg.write('{}\n'.format(dump_gcobj(gcobj))) > + > + > class LJDumpStack(dbg.LJBase): > ''' > lj-stack [] > @@ -1302,6 +1369,7 @@ def load(event=None): > dbg.initialize_extension({ > 'lj-arch': LJDumpArch, > 'lj-gc': LJGC, > + 'lj-gco': LJDumpGCobj, > 'lj-stack': LJDumpStack, > 'lj-state': LJState, > 'lj-str': LJDumpString, > diff --git a/test/tarantool-debugger-tests/debug-extension-tests.py b/test/tarantool-debugger-tests/debug-extension-tests.py > index 06a118ff..7e2b5ac4 100644 > --- a/test/tarantool-debugger-tests/debug-extension-tests.py > +++ b/test/tarantool-debugger-tests/debug-extension-tests.py > @@ -138,6 +138,17 @@ class TestCaseBase(unittest.TestCase): > self.assertRegex(self.output, self.pattern.strip()) > > > +# LLDB + Clang on macOS can't produce debug info for the C-defined > +# macros. Thus, we hardcoded its value manually. > +def gcval(arg): > + if sys.platform == 'darwin': > + # Assume GC64 build only. > + LJ_GCVMASK = '(((uint64_t)1 << 47) - 1)' > + return '(((' + arg + ')->gcr).gcptr64 & ' + LJ_GCVMASK + ')' > + else: > + return 'gcval(' + arg + ')' > + > + > class TestLoad(TestCaseBase): > extension_cmds = '' > location = 'lj_cf_print' > @@ -145,6 +156,7 @@ class TestLoad(TestCaseBase): > pattern = ( > r'lj-arch command initialized\n' > r'lj-gc command initialized\n' > + r'lj-gco command initialized\n' > r'lj-stack command initialized\n' > r'lj-state command initialized\n' > r'lj-str command initialized\n' > @@ -223,6 +235,31 @@ class TestLJStackFunc(TestCaseBase): > pattern = STACK_RX > > > +# Sorted in LJT order. > +GCO_ARGS = ( > + '"hello",\n' > + 'coroutine.create(function() end),\n' > + 'function() end,\n' > + 'require,\n' > + 'print,\n' > + 'ffi.new("int*"),\n' > + '{1},\n' > + 'newproxy(),\n' > +) > + > + > +GCO_RX = ( > + r'string \"hello\" @ ' + RX_ADDR + r'\n' > + r'thread @ ' + RX_ADDR + r'\n' > + r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n' > + r'C function @ ' + RX_ADDR + r'\n' > + r'fast function #[0-9]+\n' > + r'cdata @ ' + RX_ADDR + r'\n' > + r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n' > + r'userdata @ ' + RX_ADDR + r'\n' > +) > + > + > class TestLJTV(TestCaseBase): > location = 'lj_cf_print' > extension_cmds = ( > @@ -249,15 +286,8 @@ class TestLJTV(TestCaseBase): > ' nil,\n' > ' false,\n' > ' true,\n' > - ' debug.upvalueid(print, 1), \n' # lightuserdata > - ' "hello",\n' > - ' coroutine.create(function() end),\n' > - ' function() end,\n' > - ' require,\n' > - ' print,\n' > - ' ffi.new("int*"),\n' > - ' {1},\n' > - ' newproxy(),\n' > + ' debug.upvalueid(print, 1), \n' + # lightuserdata > + GCO_ARGS + > ' 1,\n' > ' 1.1\n' > ')\n' > @@ -267,15 +297,8 @@ class TestLJTV(TestCaseBase): > r'nil\n' > r'false\n' > r'true\n' > - r'light userdata @ ' + RX_ADDR + r'\n' > - r'string \"hello\" @ ' + RX_ADDR + r'\n' > - r'thread @ ' + RX_ADDR + r'\n' > - r'Lua function @ ' + RX_ADDR + r', [0-9]+ upvalues, .+:[0-9]+\n' > - r'C function @ ' + RX_ADDR + r'\n' > - r'fast function #[0-9]+\n' > - r'cdata @ ' + RX_ADDR + r'\n' > - r'table @ ' + RX_ADDR + r' \(asize: \d+, hmask: ' + RX_HASH + r'\)\n' > - r'userdata @ ' + RX_ADDR + r'\n' > + r'light userdata @ ' + RX_ADDR + r'\n' + > + GCO_RX + > RX_INT + r' .*1.*\n' > r'number 1.1\d+\n' > ) > @@ -312,6 +335,30 @@ class TestLJTab(TestCaseBase): > ) > > > +class TestLJGCo(TestCaseBase): > + location = 'lj_cf_print' > + extension_cmds = ( > + 'lj-gco ' + gcval('L->base + 0') + '\n' > + 'lj-gco ' + gcval('L->base + 1') + '\n' > + 'lj-gco ' + gcval('L->base + 2') + '\n' > + 'lj-gco ' + gcval('L->base + 3') + '\n' > + 'lj-gco ' + gcval('L->base + 4') + '\n' > + 'lj-gco ' + gcval('L->base + 5') + '\n' > + 'lj-gco ' + gcval('L->base + 6') + '\n' > + 'lj-gco ' + gcval('L->base + 7') + '\n' > + ) > + > + lua_script = ( > + 'local ffi = require("ffi")\n' > + 'print(\n' + > + GCO_ARGS + > + ' 1\n' # Stub for the pattern. > + ')\n' > + ) > + > + pattern = GCO_RX > + > + > for test_cls in TestCaseBase.__subclasses__(): > test_cls.test = lambda self: self.check() >