|
| 1 | +import re |
| 2 | +from typing import List, Optional |
| 3 | + |
| 4 | +import lldb |
| 5 | + |
| 6 | +from .types.base import KOTLIN_CATEGORY |
| 7 | +from .types.summary import kotlin_objc_class_summary |
| 8 | +from .types.proxy import KonanObjcProxyTypeProvider |
| 9 | +from .cache import LLDBCache |
| 10 | +from .util.log import log |
| 11 | + |
| 12 | +# `_Konan_init_MyMod` (framework) or `_Konan_init_MyApp_kexe` (executable); group 1 = module name. |
| 13 | +_KONAN_INIT_RE = re.compile(r'^_Konan_init_([0-9a-zA-Z_]+?)(_kexe)?$') |
| 14 | +_KOTLIN_RUNTIME_MARKER = 'Kotlin_initRuntimeIfNeeded' |
| 15 | +_KOTLIN_BASE_OBJC_SYMBOL = '_OBJC_CLASS_RO_$_KotlinBase' |
| 16 | +# `name` pointer offset in the 64-bit ObjC class_ro_t; all current Apple targets are 64-bit. |
| 17 | +_OBJC_CLASS_RO_NAME_OFFSET = 6 * 4 |
| 18 | +# Skipping these by PATH ALONE — before any symbol API realizes their symtab — is the launch-time win. |
| 19 | +_SYSTEM_PATH_MARKERS = ('/usr/lib', '/System/') |
| 20 | +# Cap ObjC-prefix retries: the read fails until dyld rebases the class_ro_t name pointer. |
| 21 | +_MAX_PREFIX_ATTEMPTS = 16 |
| 22 | + |
| 23 | + |
| 24 | +def _module_key(module: lldb.SBModule) -> str: |
| 25 | + # Fall back to path so two no-UUID modules don't collide on one key. |
| 26 | + return module.GetUUIDString() or module.GetFileSpec().fullpath or '' |
| 27 | + |
| 28 | + |
| 29 | +def _is_candidate_module(module: lldb.SBModule) -> bool: |
| 30 | + directory = module.GetFileSpec().GetDirectory() or '' |
| 31 | + return not any(marker in directory for marker in _SYSTEM_PATH_MARKERS) |
| 32 | + |
| 33 | + |
| 34 | +def _is_kotlin_module(module: lldb.SBModule) -> bool: |
| 35 | + return len(module.FindSymbols(_KOTLIN_RUNTIME_MARKER)) > 0 |
| 36 | + |
| 37 | + |
| 38 | +def _kotlin_module_names(module: lldb.SBModule) -> List[str]: |
| 39 | + names: List[str] = [] |
| 40 | + for symbol in module.symbols: |
| 41 | + match = _KONAN_INIT_RE.match(symbol.name or '') |
| 42 | + if match is None: |
| 43 | + continue |
| 44 | + module_name = match.group(1) |
| 45 | + if module_name == 'stdlib': |
| 46 | + continue |
| 47 | + names.append(module_name) |
| 48 | + return names |
| 49 | + |
| 50 | + |
| 51 | +def _read_objc_class_prefix( |
| 52 | + target: lldb.SBTarget, |
| 53 | + process: lldb.SBProcess, |
| 54 | + base_symbols: lldb.SBSymbolContextList, |
| 55 | +) -> Optional[str]: |
| 56 | + for symbol_context in base_symbols: |
| 57 | + error = lldb.SBError() |
| 58 | + symbol_addr = symbol_context.symbol.addr.GetLoadAddress(target) |
| 59 | + name_addr = process.ReadPointerFromMemory(symbol_addr + _OBJC_CLASS_RO_NAME_OFFSET, error) |
| 60 | + if not error.success: |
| 61 | + continue |
| 62 | + base_class_name = process.ReadCStringFromMemory(name_addr, 128, error) |
| 63 | + if not error.success or not base_class_name: |
| 64 | + continue |
| 65 | + # `or None`: an empty prefix would build `^`, matching every type. |
| 66 | + return base_class_name.removesuffix('Base') or None |
| 67 | + return None |
| 68 | + |
| 69 | + |
| 70 | +def _register_specifiers(target: lldb.SBTarget, specifiers: List[lldb.SBTypeNameSpecifier]): |
| 71 | + category = target.debugger.GetCategory(KOTLIN_CATEGORY) |
| 72 | + for type_specifier in specifiers: |
| 73 | + category.AddTypeSummary( |
| 74 | + type_specifier, |
| 75 | + lldb.SBTypeSummary.CreateWithFunctionName( |
| 76 | + '{}.{}'.format(kotlin_objc_class_summary.__module__, kotlin_objc_class_summary.__name__), |
| 77 | + lldb.eTypeOptionHideValue, |
| 78 | + ), |
| 79 | + ) |
| 80 | + category.AddTypeSynthetic( |
| 81 | + type_specifier, |
| 82 | + lldb.SBTypeSynthetic.CreateWithClassName( |
| 83 | + '{}.{}'.format(KonanObjcProxyTypeProvider.__module__, KonanObjcProxyTypeProvider.__name__), |
| 84 | + ), |
| 85 | + ) |
| 86 | + |
| 87 | + |
| 88 | +def _try_register_objc_prefix( |
| 89 | + target: lldb.SBTarget, |
| 90 | + process: lldb.SBProcess, |
| 91 | + cache: 'LLDBCache', |
| 92 | + module: lldb.SBModule, |
| 93 | + key: str, |
| 94 | +) -> bool: |
| 95 | + """Register `^<prefix>`. True when handled (registered / no base class / cap hit), False to retry.""" |
| 96 | + base_symbols = module.FindSymbols(_KOTLIN_BASE_OBJC_SYMBOL) |
| 97 | + if not base_symbols: |
| 98 | + return True # no exported ObjC base class; module-name formatters suffice |
| 99 | + |
| 100 | + prefix = _read_objc_class_prefix(target, process, base_symbols) |
| 101 | + if prefix: |
| 102 | + _register_specifiers(target, [lldb.SBTypeNameSpecifier('^{}'.format(prefix), lldb.eMatchTypeRegex)]) |
| 103 | + log(lambda: 'Registered ObjC prefix ^{} for {}.'.format(prefix, module.GetFileSpec().GetFilename())) |
| 104 | + return True |
| 105 | + |
| 106 | + attempts = cache.pending_prefix.get(key, 0) + 1 |
| 107 | + cache.pending_prefix[key] = attempts |
| 108 | + if attempts >= _MAX_PREFIX_ATTEMPTS: |
| 109 | + log(lambda: 'Gave up reading ObjC prefix for {} after {} stops; ' |
| 110 | + '^<prefix> formatting unavailable.'.format(module.GetFileSpec().GetFilename(), attempts)) |
| 111 | + return True |
| 112 | + return False |
| 113 | + |
| 114 | + |
| 115 | +def _register_kotlin_module( |
| 116 | + target: lldb.SBTarget, |
| 117 | + process: lldb.SBProcess, |
| 118 | + cache: 'LLDBCache', |
| 119 | + module: lldb.SBModule, |
| 120 | + key: str, |
| 121 | +) -> bool: |
| 122 | + """First sight of a Kotlin module: register `^<module>\\.` now, then attempt the ObjC prefix.""" |
| 123 | + names = _kotlin_module_names(module) |
| 124 | + if not names: |
| 125 | + log(lambda: 'Kotlin marker present but no module names for {}; skipping.'.format( |
| 126 | + module.GetFileSpec().GetFilename())) |
| 127 | + return True |
| 128 | + _register_specifiers(target, [ |
| 129 | + lldb.SBTypeNameSpecifier('^{}\\.'.format(name), lldb.eMatchTypeRegex) for name in names |
| 130 | + ]) |
| 131 | + return _try_register_objc_prefix(target, process, cache, module, key) |
| 132 | + |
| 133 | + |
| 134 | +def scan_and_register_modules(execution_context: lldb.SBExecutionContext): |
| 135 | + """Lazily register Kotlin formatters at stop time. Replaces the old global `_Konan_init_*` |
| 136 | + regex breakpoint that realized every module's symtab at launch. Side effect only — never |
| 137 | + influences whether the process stops.""" |
| 138 | + target = execution_context.target |
| 139 | + if not target.IsValid(): |
| 140 | + return |
| 141 | + process = target.GetProcess() |
| 142 | + if not process.IsValid(): |
| 143 | + return |
| 144 | + |
| 145 | + cache = LLDBCache.instance() |
| 146 | + for i in range(target.GetNumModules()): |
| 147 | + module = target.GetModuleAtIndex(i) |
| 148 | + key = _module_key(module) |
| 149 | + |
| 150 | + if key in cache.registered_module_keys: |
| 151 | + continue |
| 152 | + |
| 153 | + if key in cache.pending_prefix: # known Kotlin module, prefix still settling |
| 154 | + if _try_register_objc_prefix(target, process, cache, module, key): |
| 155 | + cache.pending_prefix.pop(key, None) |
| 156 | + cache.registered_module_keys.add(key) |
| 157 | + continue |
| 158 | + |
| 159 | + if not _is_candidate_module(module) or not _is_kotlin_module(module): |
| 160 | + cache.registered_module_keys.add(key) |
| 161 | + continue |
| 162 | + |
| 163 | + if _register_kotlin_module(target, process, cache, module, key): |
| 164 | + cache.pending_prefix.pop(key, None) |
| 165 | + cache.registered_module_keys.add(key) |
0 commit comments