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..dbd8871 100644 --- a/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py +++ b/LLDBPlugin/touchlab_kotlin_lldb/types/summary.py @@ -1,8 +1,41 @@ +from typing import Optional + import lldb from .select_provider import select_provider -from .base import get_type_info, obj_header_pointer, single_pointer -from ..util import log, evaluate +from .base import get_type_info, obj_header_pointer, objc_bridged_obj_header, single_pointer +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 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( + '(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: + 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) def kotlin_object_type_summary(valobj: lldb.SBValue, internal_dict): @@ -13,20 +46,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)