From 5ece9cbcc439ba3956fb5c807467e657374002c5 Mon Sep 17 00:00:00 2001 From: Wellington Pereira Date: Wed, 20 May 2026 20:15:21 -0300 Subject: [PATCH 1/2] Fix: ObjC-bridged Kotlin fields rendered as `None` --- LLDBPlugin/touchlab_kotlin_lldb/types/base.py | 16 ++++++++ .../touchlab_kotlin_lldb/types/proxy.py | 17 +++++--- .../touchlab_kotlin_lldb/types/summary.py | 40 +++++++++++++++++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/types/base.py b/LLDBPlugin/touchlab_kotlin_lldb/types/base.py index 6da8c4b..f86f67c 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/base.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/base.py @@ -76,6 +76,22 @@ def obj_header_pointer(valobj: lldb.SBValue) -> lldb.SBValue: return single_pointer(valobj).Cast(obj_header_type()) +def objc_bridged_obj_header(cast_value: lldb.SBValue) -> Optional[lldb.SBValue]: + """If cast_value points at an ObjC instance wrapping a Kotlin object, + return the corresponding ObjHeader*. Otherwise, return None.""" + if cast_value.unsigned == 0: + return None + bridged = evaluate( + 'void* __result = 0; (ObjHeader*)Kotlin_ObjCExport_refFromObjC((void*){:#x}, &__result)', + cast_value.unsigned, + ) + if bridged is None or not bridged.IsValid() or bridged.error.Fail(): + return None + if bridged.unsigned == 0 or bridged.unsigned == cast_value.unsigned: + return None + return bridged + + def get_runtime_type(variable): return strip_quotes(evaluate("(char *)Konan_DebugGetTypeName({:#x})", variable.unsigned).summary) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/types/proxy.py b/LLDBPlugin/touchlab_kotlin_lldb/types/proxy.py index c0bdef3..3f67df9 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/proxy.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/proxy.py @@ -6,7 +6,7 @@ from .KonanNotInitializedObjectSyntheticProvider import KonanNotInitializedObjectSyntheticProvider from .KonanBaseSyntheticProvider import KonanBaseSyntheticProvider from .KonanZerroSyntheticProvider import KonanZerroSyntheticProvider -from .base import get_type_info, obj_header_pointer, single_pointer +from .base import get_type_info, obj_header_pointer, objc_bridged_obj_header, single_pointer from .select_provider import select_provider @@ -20,11 +20,16 @@ def __getattr__(self, item): cast_value = obj_header_pointer(self._valobj) type_info = get_type_info(cast_value) - if not type_info: - self._proxy = KonanNotInitializedObjectSyntheticProvider(self._valobj) - return - - self._proxy = select_provider(cast_value, type_info) + if type_info: + self._proxy = select_provider(cast_value, type_info) + else: + bridged = objc_bridged_obj_header(cast_value) + bridged_type_info = get_type_info(bridged) if bridged is not None else None + if bridged_type_info: + self._proxy = select_provider(bridged, bridged_type_info) + else: + self._proxy = KonanNotInitializedObjectSyntheticProvider(self._valobj) + return return getattr(self._proxy, item) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py b/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py index 4304952..4461e5c 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py @@ -1,10 +1,27 @@ import lldb from .select_provider import select_provider -from .base import get_type_info, obj_header_pointer, single_pointer +from .base import get_type_info, obj_header_pointer, objc_bridged_obj_header, single_pointer from ..util import log, evaluate +def _objc_bridge_summary_fallback(cast_value: lldb.SBValue) -> str: + """Build a summary for a Kotlin object that the Kotlin runtime can't + describe via Konan_DebugObjectToUtf8Array (typically a Kotlin wrapper + around a Swift class implementing a Kotlin interface). It looks up the + underlying ObjC pointer via Kotlin_ObjCExport_refToObjC so the user + sees something useful instead of `None`.""" + if cast_value.unsigned == 0: + return "@0x0" + objc_ptr = evaluate( + '(void*)Kotlin_ObjCExport_refToObjC((ObjHeader*){:#x})', + cast_value.unsigned, + ) + if objc_ptr is not None and objc_ptr.error.Success() and objc_ptr.unsigned != 0: + return "objc@{:#x}".format(objc_ptr.unsigned) + return "@{:#x}".format(cast_value.unsigned) + + def kotlin_object_type_summary(valobj: lldb.SBValue, internal_dict): """Hook that is run by lldb to display a Kotlin object.""" log(lambda: "kotlin_object_type_summary({:#x}: {}: {})".format(valobj.unsigned, valobj.name, valobj.type.name)) @@ -13,20 +30,35 @@ def kotlin_object_type_summary(valobj: lldb.SBValue, internal_dict): type_info = internal_dict["type_info"] if "type_info" in internal_dict.keys() else get_type_info(cast_value) if not type_info: + bridged = objc_bridged_obj_header(cast_value) + if bridged is not None: + bridged_type_info = get_type_info(bridged) + if bridged_type_info: + log(lambda: "kotlin_object_type_summary: bridged ObjC->Kotlin {:#x}->{:#x}".format( + cast_value.unsigned, bridged.unsigned)) + provider = select_provider(bridged, bridged_type_info) + provider.update() + bridged_summary = provider.to_string() + if bridged_summary is not None: + return bridged_summary + return _objc_bridge_summary_fallback(bridged) return cast_value.GetValue() provider = select_provider(cast_value, type_info) log(lambda: "kotlin_object_type_summary({:#x} - {})".format(cast_value.unsigned, type(provider).__name__)) provider.update() - return provider.to_string() + summary = provider.to_string() + if summary is None and cast_value.unsigned != 0: + return _objc_bridge_summary_fallback(cast_value) + return summary def kotlin_objc_class_summary(objc_obj: lldb.SBValue, internal_dict): - # """Hook that is run by lldb to display a Kotlin ObjC class wrapper.""" + """Hook that is run by lldb to display a Kotlin ObjC class wrapper.""" objc_obj = single_pointer(objc_obj) konan_obj = evaluate( 'void* __result = 0; (ObjHeader*)Kotlin_ObjCExport_refFromObjC((void*){:#x}, &__result)', objc_obj.unsigned ) - return kotlin_object_type_summary(konan_obj, internal_dict) \ No newline at end of file + return kotlin_object_type_summary(konan_obj, internal_dict) From b1edc81e660fd2687f3d3f9c65fbe8b3d8347689 Mon Sep 17 00:00:00 2001 From: Sam Hill Date: Tue, 26 May 2026 22:39:34 -0400 Subject: [PATCH 2/2] Add class name to summary --- .../touchlab_kotlin_lldb/types/summary.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py b/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py index 4461e5c..dbd8871 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py @@ -1,16 +1,29 @@ +from typing import Optional + import lldb from .select_provider import select_provider from .base import get_type_info, obj_header_pointer, objc_bridged_obj_header, single_pointer -from ..util import log, evaluate +from ..util import log, evaluate, strip_quotes + + +def _objc_class_name(objc_ptr: int) -> Optional[str]: + """Return the ObjC class name for an instance pointer, or None if it can't + be read. Used to label a bridged Kotlin object with the class the user + actually wrote (e.g. their Swift type) instead of a bare address.""" + name = evaluate('(const char*)object_getClassName((void*){:#x})', objc_ptr) + if name is None or not name.IsValid() or name.error.Fail(): + return None + return strip_quotes(name.summary) or None def _objc_bridge_summary_fallback(cast_value: lldb.SBValue) -> str: """Build a summary for a Kotlin object that the Kotlin runtime can't describe via Konan_DebugObjectToUtf8Array (typically a Kotlin wrapper around a Swift class implementing a Kotlin interface). It looks up the - underlying ObjC pointer via Kotlin_ObjCExport_refToObjC so the user - sees something useful instead of `None`.""" + underlying ObjC pointer via Kotlin_ObjCExport_refToObjC and the instance's + ObjC class name so the user sees something like `SwiftCallback objc@0x…` + instead of `None`.""" if cast_value.unsigned == 0: return "@0x0" objc_ptr = evaluate( @@ -18,6 +31,9 @@ def _objc_bridge_summary_fallback(cast_value: lldb.SBValue) -> str: cast_value.unsigned, ) if objc_ptr is not None and objc_ptr.error.Success() and objc_ptr.unsigned != 0: + class_name = _objc_class_name(objc_ptr.unsigned) + if class_name: + return "{} objc@{:#x}".format(class_name, objc_ptr.unsigned) return "objc@{:#x}".format(objc_ptr.unsigned) return "@{:#x}".format(cast_value.unsigned)