# Pretty printers for Odin types in gdb and lldb # # gdb: source /base/runtime/odin_debugger.py # lldb: command script import /base/runtime/odin_debugger.py # # gdb also loads it by itself from the `.debug_gdb_scripts` section of an ELF binary built with `-debug`, # once that binary's directory is trusted, with `add-auto-load-safe-path `, and lldb from the dSYM # of a macOS binary built with `-debug`, once `settings set target.load-script-from-symbol-file true` # # Shown: `string`, `string16`, slices, dynamic arrays (and fixed capacity ones), `#soa` slices and dynamic arrays, # maps as their entries (a map of zero sized values as its keys), unions as the variant they hold, or `nil`, # and `any` as the value it holds. # Types are recognised by their fields rather than their names, so named types, such as # `Table :: map[string]int`, are shown the same way. # # gdb: front ends which mishandle the "map" display hint, such as Microsoft's C/C++ extension for VS Code, # need `set odin-map-hint off`, which names each entry by its key instead. # # A map's debug type does not describe its memory: `data` points to a struct whose fields give the # `key`, `value` and `hash` types and how keys and values are packed into cells, `key_cell` and `value_cell` # (see `init_map_internal_debug_types` in the compiler and `Raw_Map` in `base:runtime`). # A union's members are its variants, `v`, and the `tag` itself, except for a `Maybe` of a pointer, # which is `v0` alone and nil when the pointer is (see `lb_debug_union`). # An `any`'s `id` is an enum named by each type's canonical name, which finds the type of what `data` points to; # a `typeid` is the hash of that name, which picks the type when a debugger shortens the name (lldb does, from a PDB). # Every target Odin supports is little-endian. import re MAX_STRING_BYTES = 64*1024 MAP_METADATA_FIELDS = ["key", "value", "hash", "key_cell", "value_cell"] # the kind and size of each basic type, by its name in `typeid`; None is the size of a pointer ODIN_BASIC_TYPES = { "i8": ("int", 1), "i16": ("int", 2), "i32": ("int", 4), "i64": ("int", 8), "i128": ("int", 16), "u8": ("uint", 1), "u16": ("uint", 2), "u32": ("uint", 4), "u64": ("uint", 8), "u128": ("uint", 16), "i16le": ("int", 2), "i32le": ("int", 4), "i64le": ("int", 8), "i128le": ("int", 16), "u16le": ("uint", 2), "u32le": ("uint", 4), "u64le": ("uint", 8), "u128le": ("uint", 16), "f16": ("float", 2), "f32": ("float", 4), "f64": ("float", 8), "f16le": ("float", 2), "f32le": ("float", 4), "f64le": ("float", 8), "f16be": ("float", 2), "f32be": ("float", 4), "f64be": ("float", 8), "bool": ("bool", 1), "b8": ("bool", 1), "b16": ("uint", 2), "b32": ("uint", 4), "b64": ("uint", 8), "rune": ("rune", 4), "int": ("int", None), "uint": ("uint", None), "uintptr": ("uint", None), "rawptr": ("rawptr", None), "cstring": ("cstring", None), "cstring16": ("cstring16", None), } def typeid_hash(name): # SipHash-2-4 of a type's canonical name, which is its `typeid` (see `type_hash_canonical_type`) mask = (1 << 64) - 1 k0 = 0xa6592ea25e04ac3c k1 = 0xba3cba04ed28a9ae v = [0x736f6d6570736575 ^ k0, 0x646f72616e646f6d ^ k1, 0x6c7967656e657261 ^ k0, 0x7465646279746573 ^ k1] def rotl(x, k): return ((x << k) | (x >> (64 - k))) & mask def rounds(n): for _ in range(n): v[0] = (v[0] + v[1]) & mask; v[1] = rotl(v[1], 13); v[1] ^= v[0]; v[0] = rotl(v[0], 32) v[2] = (v[2] + v[3]) & mask; v[3] = rotl(v[3], 16); v[3] ^= v[2] v[0] = (v[0] + v[3]) & mask; v[3] = rotl(v[3], 21); v[3] ^= v[0] v[2] = (v[2] + v[1]) & mask; v[1] = rotl(v[1], 17); v[1] ^= v[2]; v[2] = rotl(v[2], 32) data = name.encode("utf-8") full = len(data) & ~7 last = bytearray(data[full:]) + bytearray(8 - (len(data) - full)) last[7] = len(data) & 0xff for block in [data[i:i+8] for i in range(0, full, 8)] + [bytes(last)]: m = int.from_bytes(block, "little") v[3] ^= m rounds(2) v[0] ^= m v[2] ^= 0xff rounds(4) h = v[0] ^ v[1] ^ v[2] ^ v[3] return h if h != 0 else 1 def is_map_metadata(field_names): # the members of a zero sized value can be missing if not all(n in MAP_METADATA_FIELDS for n in field_names): return False return all(n in field_names for n in ("key", "hash", "key_cell")) def odin_kind(type_name, field_names, data_is_pointer, data_is_array, data_target_fields): if field_names == ["data", "len"] and type_name in ("string", "string16"): return type_name if field_names == ["data", "id"] and type_name == "any": return "any" if field_names == ["data", "len", "allocator"] and is_map_metadata(data_target_fields): return "map" if field_names == ["data", "len", "cap", "allocator"] and data_is_pointer: return "dynamic" if field_names == ["data", "len"] and data_is_pointer: return "slice" if field_names == ["data", "len"] and data_is_array: return "fixed" if field_names[-3:] == ["__$len", "__$cap", "allocator"]: return "soa_dynamic" if field_names[-1:] == ["__$len"]: return "soa_slice" return None def soa_fields(field_names): return [n for n in field_names if n not in ("__$len", "__$cap", "allocator", "_")] def is_odin_union(field_names): variants = [n for n in field_names if n != "tag"] return len(variants) != 0 and all(re.fullmatch(r"v[0-9]+", n) for n in variants) def union_variant(field_names, tag): # the member holding the union's value, None when it is nil; `tag` is None for a `Maybe` of a pointer if tag is None: return "v0" name = "v%d" % tag if name in field_names: return name if tag == 0: return None raise ValueError("invalid tag %d" % tag) def cell_layout(elem_size, cell_size, cell_data_size): # A cell is the element type itself when packing needs no padding, # else `struct{v: [n]T, _: [padding]u8}`, where `cell_data_size` is the size of `v` if cell_size == elem_size: return elem_size, 1, 0 per_cell = cell_data_size // elem_size if per_cell == 1: return cell_size, 1, 0 return elem_size, per_cell, cell_size - per_cell*elem_size def cell_address(base, layout, index): stride, per_cell, padding = layout return base + index*stride + (index // per_cell)*padding def map_capacity(data, length): if data == 0: return 0 capacity = 1 << (data & 63) if length <= 0 or length > capacity: return 0 if capacity > 1 << 40: return 0 return capacity def map_slots(data, length, key_layout, value_layout, hash_size, read_memory): capacity = map_capacity(data, length) if capacity == 0: return keys = data & ~63 values = cell_address(keys, key_layout, capacity) hashes = cell_address(values, value_layout, capacity) tombstone = 1 << (8*hash_size - 1) found = 0 chunk = 1024 for start in range(0, capacity, chunk): count = min(chunk, capacity - start) raw = read_memory(hashes + start*hash_size, count*hash_size) for j in range(count): h = int.from_bytes(raw[j*hash_size:(j+1)*hash_size], "little") if h != 0 and h & tombstone == 0: yield cell_address(keys, key_layout, start+j), cell_address(values, value_layout, start+j) found += 1 if found == length: return def array_count(data, length, capacity=None, read_memory=None): # how many elements to show, none for a variable not yet initialised, whose data cannot be read if data == 0: return 0 if length <= 0: return 0 if capacity is not None and length > capacity: return 0 if read_memory is not None: try: read_memory(data, 1) except Exception: return 0 return length def read_text(read_memory, address, count, unit, encoding): if address == 0 or count <= 0: return "" size = min(count*unit, MAX_STRING_BYTES) text = read_memory(address, size).decode(encoding, "replace") if size < count*unit: text += "..." return text ################################################################################ # gdb try: import gdb except ImportError: gdb = None if gdb is not None: # NOTE: gdb 14 and later fetch the children of a `gdb.ValuePrinter` one at a time, through `num_children` and `child` _GdbPrinter = getattr(gdb, "ValuePrinter", object) class _GdbMapHintParameter(gdb.Parameter): """Whether Odin maps use the "map" display hint; when off, each entry is named by its key.""" set_doc = "Set whether Odin maps use the \"map\" display hint." show_doc = "Show whether Odin maps use the \"map\" display hint." def __init__(self): super().__init__("odin-map-hint", gdb.COMMAND_DATA, gdb.PARAM_BOOLEAN) self.value = True try: gdb.parameter("odin-map-hint") except RuntimeError: _gdb_map_hint_parameter = _GdbMapHintParameter() def _gdb_map_hint(): try: return bool(gdb.parameter("odin-map-hint")) except RuntimeError: return True def _gdb_read(address, size): return bytes(gdb.selected_inferior().read_memory(address, size)) def _gdb_fields(t): return {f.name: f.type for f in t.fields()} def _gdb_kind(t): if t.code != gdb.TYPE_CODE_STRUCT: return None fields = t.fields() names = [f.name for f in fields] data_is_pointer = False data_is_array = False target_fields = [] if names and names[0] == "data": data = fields[0].type.strip_typedefs() data_is_pointer = data.code == gdb.TYPE_CODE_PTR data_is_array = data.code == gdb.TYPE_CODE_ARRAY if data_is_pointer: target = data.target().strip_typedefs() if target.code == gdb.TYPE_CODE_STRUCT: target_fields = [f.name for f in target.fields()] return odin_kind(t.name, names, data_is_pointer, data_is_array, target_fields) def _gdb_cell_layout(elem, cell): cell_data_size = 0 if cell.sizeof != elem.sizeof: cell_data_size = _gdb_fields(cell.strip_typedefs())["v"].sizeof return cell_layout(elem.sizeof, cell.sizeof, cell_data_size) def _gdb_type(*names): for name in names: try: return gdb.lookup_type(name) except gdb.error: continue return None def _gdb_odin_type(name): basic = ODIN_BASIC_TYPES.get(name) if basic is None: return _gdb_type(name, "struct " + name, "union " + name, "enum " + name) kind, size = basic if size is None: size = gdb.lookup_type("void").pointer().sizeof if kind == "rawptr": return gdb.lookup_type("void").pointer() if kind == "cstring": return gdb.lookup_type("char").pointer() if kind == "cstring16": return _gdb_type("char16_t", "unsigned short").pointer() if kind == "float": return _gdb_type({2: "_Float16", 4: "float", 8: "double"}[size]) if kind == "bool": return _gdb_type("_Bool", "bool") if kind == "rune": return _gdb_type("char32_t", "unsigned int") ints = {1: "char", 2: "short", 4: "int", 8: "long long", 16: "__int128"} if kind == "uint": return _gdb_type("unsigned " + ints[size]) return _gdb_type("signed char" if size == 1 else ints[size]) def _gdb_is_short(t): t = t.strip_typedefs() if t.code in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_ENUM, gdb.TYPE_CODE_FLT, gdb.TYPE_CODE_PTR, gdb.TYPE_CODE_CHAR, gdb.TYPE_CODE_BOOL): return True return _gdb_kind(t) in ("string", "string16") class _GdbString(_GdbPrinter): def __init__(self, val, kind): self._val = val self._unit, self._encoding = (1, "utf-8") if kind == "string" else (2, "utf-16-le") def to_string(self): return read_text(_gdb_read, int(self._val["data"]), int(self._val["len"]), self._unit, self._encoding) def display_hint(self): return "string" class _GdbArray(_GdbPrinter): # A dynamic array also shows its allocator, after its elements def __init__(self, val, kind): self._val = val self._kind = kind self._data = val["data"] length = int(val["len"]) if kind == "fixed": capacity = self._data.type.strip_typedefs().range()[1] + 1 self._count = array_count(1, length, capacity) else: capacity = int(val["cap"]) if kind == "dynamic" else None self._count = array_count(int(self._data), length, capacity, _gdb_read) def to_string(self): if self._kind == "dynamic": return "len=%d cap=%d" % (int(self._val["len"]), int(self._val["cap"])) return "len=%d" % int(self._val["len"]) def display_hint(self): if self._kind == "dynamic": return None return "array" def num_children(self): if self._kind == "dynamic": return self._count + 1 return self._count def child(self, index): if index == self._count: return "allocator", self._val["allocator"] if self._kind == "fixed": return "[%d]" % index, self._data[index] return "[%d]" % index, (self._data + index).dereference() def children(self): for i in range(self.num_children()): yield self.child(i) class _GdbSoa(_GdbPrinter): # Each field as an array of the length, then a dynamic array's allocator def __init__(self, val, kind): self._val = val self._kind = kind t = val.type.strip_typedefs() self._fields = soa_fields([f.name for f in t.fields()]) length = int(val["__$len"]) capacity = int(val["__$cap"]) if kind == "soa_dynamic" else None first = int(val[self._fields[0]]) if self._fields else 0 self._count = array_count(first, length, capacity, _gdb_read) def to_string(self): if self._kind == "soa_dynamic": return "len=%d cap=%d" % (int(self._val["__$len"]), int(self._val["__$cap"])) return "len=%d" % int(self._val["__$len"]) def num_children(self): fields = len(self._fields) if self._count > 0 else 0 if self._kind == "soa_dynamic": return fields + 1 return fields def child(self, index): if self._count == 0 or index == len(self._fields): return "allocator", self._val["allocator"] name = self._fields[index] pointer = self._val[name] array = pointer.type.strip_typedefs().target().array(0, self._count - 1) return name, pointer.cast(array.pointer()).dereference() def children(self): for i in range(self.num_children()): yield self.child(i) class _GdbMap(_GdbPrinter): def __init__(self, val): self._val = val f = _gdb_fields(val["data"].type.strip_typedefs().target().strip_typedefs()) self._key_type = f["key"] self._value_type = f.get("value") key_layout = _gdb_cell_layout(f["key"], f["key_cell"]) value_layout = (0, 1, 0) if self._value_type is not None: value_layout = _gdb_cell_layout(self._value_type, f["value_cell"]) data = int(val["data"]) self._count = int(val["len"]) if map_capacity(data, self._count) == 0: self._count = 0 self._slots = [] self._scan = map_slots(data, self._count, key_layout, value_layout, f["hash"].sizeof, _gdb_read) self._set = self._value_type is None or self._value_type.sizeof == 0 self._hint = _gdb_map_hint() self._pairs = self._hint or not _gdb_is_short(self._key_type) def to_string(self): return "len=%d" % int(self._val["len"]) def display_hint(self): if self._set: return "array" if self._hint: return "map" return None def _slot(self, entry): while len(self._slots) <= entry: try: self._slots.append(next(self._scan)) except (StopIteration, gdb.MemoryError): return None return self._slots[entry] def num_children(self): if self._set or not self._pairs: return self._count return 2*self._count def child(self, index): entry = index if not self._set and self._pairs: entry = index//2 slot = self._slot(entry) if slot is None: return None key = gdb.Value(slot[0]).cast(self._key_type.pointer()).dereference() if self._set: return "[%d]" % entry, key value = gdb.Value(slot[1]).cast(self._value_type.pointer()).dereference() if self._hint: if index % 2 == 0: return "key", key return "value", value if self._pairs: if index % 2 == 0: return "[%d].key" % entry, key return "[%d].value" % entry, value return "[%s]" % key.format_string(), value def children(self): for i in range(self.num_children()): c = self.child(i) if c is None: return yield c class _GdbUnion(_GdbPrinter): def __init__(self, val): self._val = val def to_string(self): names = [f.name for f in self._val.type.strip_typedefs().fields()] tag = int(self._val["tag"]) if "tag" in names else None try: name = union_variant(names, tag) except ValueError as e: return "<%s>" % e if name is None or (tag is None and int(self._val[name]) == 0): return "nil" return self._val[name] class _GdbAny(_GdbPrinter): # The value it holds, after the name of its type; an unknown type shows the `any` itself def __init__(self, val): self._val = val def to_string(self): data = int(self._val["data"]) name = self._val["id"].format_string() if data == 0 or int(self._val["id"]) == 0: return "nil" t = _gdb_odin_type(name) if t is None: return self._val.format_string(raw=True) value = gdb.Value(data).cast(t.pointer()).dereference() return "%s %s" % (name, value.format_string()) def _gdb_lookup(val): t = val.type.strip_typedefs() if t.code == gdb.TYPE_CODE_UNION and is_odin_union([f.name for f in t.fields()]): return _GdbUnion(val) kind = _gdb_kind(t) if kind in ("string", "string16"): return _GdbString(val, kind) if kind == "any": return _GdbAny(val) if kind == "map": return _GdbMap(val) if kind in ("soa_slice", "soa_dynamic"): return _GdbSoa(val, kind) if kind is not None: return _GdbArray(val, kind) return None # NOTE: loaded from a binary's `.debug_gdb_scripts` section, the printers belong to that binary if gdb.current_objfile() is not None: gdb.current_objfile().pretty_printers.append(_gdb_lookup) else: gdb.pretty_printers.append(_gdb_lookup) ################################################################################ # lldb try: import lldb except ImportError: lldb = None if lldb is not None: def _lldb_kind(sbtype): t = sbtype.GetCanonicalType() if t.GetTypeClass() not in (lldb.eTypeClassStruct, lldb.eTypeClassClass): return None names = [t.GetFieldAtIndex(i).GetName() for i in range(t.GetNumberOfFields())] data_is_pointer = False data_is_array = False target_fields = [] if names and names[0] == "data": data = t.GetFieldAtIndex(0).GetType().GetCanonicalType() data_is_pointer = data.IsPointerType() data_is_array = data.IsArrayType() if data_is_pointer: target = data.GetPointeeType().GetCanonicalType() target_fields = [target.GetFieldAtIndex(i).GetName() for i in range(target.GetNumberOfFields())] return odin_kind(t.GetName(), names, data_is_pointer, data_is_array, target_fields) def _lldb_reader(valobj): process = valobj.GetProcess() def read(address, size): error = lldb.SBError() data = process.ReadMemory(address, size, error) if error.Fail(): raise MemoryError(error.GetCString()) return data return read def _lldb_fields(t): t = t.GetCanonicalType() return {t.GetFieldAtIndex(i).GetName(): t.GetFieldAtIndex(i).GetType() for i in range(t.GetNumberOfFields())} def _lldb_cell_layout(elem, cell): cell_data_size = 0 if cell.GetByteSize() != elem.GetByteSize(): cell_data_size = _lldb_fields(cell)["v"].GetByteSize() return cell_layout(elem.GetByteSize(), cell.GetByteSize(), cell_data_size) def _lldb_odin_type(target, name, id): # the type a `typeid` names, None when it cannot be found basic = ODIN_BASIC_TYPES.get(name) if basic is None: types = target.FindTypes(name) for i in range(types.GetSize()): t = types.GetTypeAtIndex(i) if typeid_hash(t.GetName()) == id: return t return None kind, size = basic if size is None: size = target.GetAddressByteSize() if kind == "rawptr": return target.GetBasicType(lldb.eBasicTypeVoid).GetPointerType() if kind == "cstring": return target.GetBasicType(lldb.eBasicTypeChar).GetPointerType() if kind == "cstring16": return target.GetBasicType(lldb.eBasicTypeChar16).GetPointerType() basics = { ("int", 1): lldb.eBasicTypeSignedChar, ("int", 2): lldb.eBasicTypeShort, ("int", 4): lldb.eBasicTypeInt, ("int", 8): lldb.eBasicTypeLongLong, ("int", 16): lldb.eBasicTypeInt128, ("uint", 1): lldb.eBasicTypeUnsignedChar, ("uint", 2): lldb.eBasicTypeUnsignedShort, ("uint", 4): lldb.eBasicTypeUnsignedInt, ("uint", 8): lldb.eBasicTypeUnsignedLongLong, ("uint", 16): lldb.eBasicTypeUnsignedInt128, ("float", 2): lldb.eBasicTypeHalf, ("float", 4): lldb.eBasicTypeFloat, ("float", 8): lldb.eBasicTypeDouble, ("bool", 1): lldb.eBasicTypeBool, ("rune", 4): lldb.eBasicTypeChar32, } basic_type = basics.get((kind, size)) if basic_type is None: return None return target.GetBasicType(basic_type) def lldb_is_string(sbtype, internal_dict): return _lldb_kind(sbtype) in ("string", "string16") def lldb_is_array(sbtype, internal_dict): return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed") def lldb_is_soa(sbtype, internal_dict): return _lldb_kind(sbtype) in ("soa_slice", "soa_dynamic") def lldb_is_map(sbtype, internal_dict): return _lldb_kind(sbtype) == "map" def lldb_is_any(sbtype, internal_dict): return _lldb_kind(sbtype) == "any" def lldb_is_container(sbtype, internal_dict): return _lldb_kind(sbtype) in ("slice", "dynamic", "fixed", "map", "soa_slice", "soa_dynamic") def lldb_is_union(sbtype, internal_dict): t = sbtype.GetCanonicalType() return t.GetTypeClass() == lldb.eTypeClassUnion and is_odin_union(list(_lldb_fields(t))) def _lldb_union_value(valobj): # the variant the union holds, None when it is nil; raises ValueError for an invalid tag v = valobj.GetNonSyntheticValue() names = list(_lldb_fields(v.GetType())) tag = v.GetChildMemberWithName("tag").GetValueAsUnsigned() if "tag" in names else None name = union_variant(names, tag) if name is None: return None value = v.GetChildMemberWithName(name) if tag is None and value.GetValueAsUnsigned() == 0: return None value.SetPreferSyntheticValue(True) return value def _lldb_any_value(valobj): # the name of the type and the value an `any` holds; the value is None when it is nil, and when its type is unknown v = valobj.GetNonSyntheticValue() data = v.GetChildMemberWithName("data").GetValueAsUnsigned() id = v.GetChildMemberWithName("id") name = id.GetValue() or "" if data == 0 or id.GetValueAsUnsigned() == 0: return "nil", None t = _lldb_odin_type(valobj.GetTarget(), name, id.GetValueAsUnsigned()) if t is None: return name, None if name not in ODIN_BASIC_TYPES: name = t.GetName() value = valobj.CreateValueFromAddress("value", data, t) value.SetPreferSyntheticValue(True) return name, value def lldb_union_summary(valobj, internal_dict): try: value = _lldb_union_value(valobj) except ValueError as e: return "<%s>" % e if value is None: return "nil" # NOTE: a struct has neither, so it is shown by its fields, the union's children return value.GetSummary() or value.GetValue() or "" def lldb_any_summary(valobj, internal_dict): name, value = _lldb_any_value(valobj) if value is None: return name return ("%s %s" % (name, value.GetSummary() or value.GetValue() or "")).strip() def lldb_string_summary(valobj, internal_dict): v = valobj.GetNonSyntheticValue() unit, encoding = (1, "utf-8") if _lldb_kind(v.GetType()) == "string" else (2, "utf-16-le") data = v.GetChildMemberWithName("data").GetValueAsUnsigned() count = v.GetChildMemberWithName("len").GetValueAsSigned() try: text = read_text(_lldb_reader(v), data, count, unit, encoding) except MemoryError: return "" return '"%s"' % text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") def lldb_len_summary(valobj, internal_dict): v = valobj.GetNonSyntheticValue() kind = _lldb_kind(v.GetType()) if kind in ("soa_slice", "soa_dynamic"): summary = "len=%d" % v.GetChildMemberWithName("__$len").GetValueAsSigned() if kind == "soa_dynamic": summary += " cap=%d" % v.GetChildMemberWithName("__$cap").GetValueAsSigned() return summary summary = "len=%d" % v.GetChildMemberWithName("len").GetValueAsSigned() if kind == "dynamic": summary += " cap=%d" % v.GetChildMemberWithName("cap").GetValueAsSigned() return summary class LldbArraySynth: # A dynamic array also shows its allocator, after its elements def __init__(self, valobj, internal_dict): self.valobj = valobj def update(self): data = self.valobj.GetChildMemberWithName("data") t = data.GetType().GetCanonicalType() length = self.valobj.GetChildMemberWithName("len").GetValueAsSigned() self.allocator = None if t.IsArrayType(): self.elem = t.GetArrayElementType() self.address = data.GetLoadAddress() capacity = t.GetByteSize() // max(1, self.elem.GetByteSize()) self.count = array_count(1, length, capacity) else: self.elem = t.GetPointeeType() self.address = data.GetValueAsUnsigned() capacity = None if self.valobj.GetChildMemberWithName("cap").IsValid(): capacity = self.valobj.GetChildMemberWithName("cap").GetValueAsSigned() self.allocator = self.valobj.GetChildMemberWithName("allocator") self.count = array_count(self.address, length, capacity, _lldb_reader(self.valobj)) return False def num_children(self): if self.allocator is not None: return self.count + 1 return self.count def get_child_index(self, name): if name == "allocator" and self.allocator is not None: return self.count try: return int(name.strip("[]")) except ValueError: return -1 def get_child_at_index(self, index): if index == self.count: return self.allocator return self.valobj.CreateValueFromAddress("[%d]" % index, self.address + index*self.elem.GetByteSize(), self.elem) def has_children(self): return True class LldbSoaSynth: # Each field as an array of the length, then a dynamic array's allocator def __init__(self, valobj, internal_dict): self.valobj = valobj def update(self): v = self.valobj kind = _lldb_kind(v.GetType()) self.fields = soa_fields(list(_lldb_fields(v.GetType()))) length = v.GetChildMemberWithName("__$len").GetValueAsSigned() capacity = None self.allocator = None if kind == "soa_dynamic": capacity = v.GetChildMemberWithName("__$cap").GetValueAsSigned() self.allocator = v.GetChildMemberWithName("allocator") first = v.GetChildMemberWithName(self.fields[0]).GetValueAsUnsigned() if self.fields else 0 self.count = array_count(first, length, capacity, _lldb_reader(v)) return False def num_children(self): fields = len(self.fields) if self.count > 0 else 0 if self.allocator is not None: return fields + 1 return fields def get_child_index(self, name): fields = len(self.fields) if self.count > 0 else 0 if name == "allocator" and self.allocator is not None: return fields if name in self.fields[:fields]: return self.fields.index(name) return -1 def get_child_at_index(self, index): if self.count == 0 or index == len(self.fields): return self.allocator name = self.fields[index] pointer = self.valobj.GetChildMemberWithName(name) array = pointer.GetType().GetCanonicalType().GetPointeeType().GetArrayType(self.count) return self.valobj.CreateValueFromAddress(name, pointer.GetValueAsUnsigned(), array) def has_children(self): return True class LldbMapSynth: # Each entry is a child named by its key; a key without a short form, such as a struct, # gives two children per entry instead, `[i].key` and `[i].value`; a map of zero sized values shows its keys def __init__(self, valobj, internal_dict): self.valobj = valobj def update(self): data = self.valobj.GetChildMemberWithName("data") f = _lldb_fields(data.GetType().GetCanonicalType().GetPointeeType()) self.key_type = f["key"] self.value_type = f.get("value") key_layout = _lldb_cell_layout(f["key"], f["key_cell"]) value_layout = (0, 1, 0) if self.value_type is not None: value_layout = _lldb_cell_layout(self.value_type, f["value_cell"]) raw = data.GetValueAsUnsigned() self.count = self.valobj.GetChildMemberWithName("len").GetValueAsSigned() if map_capacity(raw, self.count) == 0: self.count = 0 self.slots = [] self.scan = map_slots(raw, self.count, key_layout, value_layout, f["hash"].GetByteSize(), _lldb_reader(self.valobj)) self.set = self.value_type is None or self.value_type.GetByteSize() == 0 key = self.key_type.GetCanonicalType() self.pairs = not (self.set or key.GetTypeClass() in (lldb.eTypeClassBuiltin, lldb.eTypeClassEnumeration, lldb.eTypeClassPointer) or lldb_is_string(key, None)) return False def _slot(self, entry): while len(self.slots) <= entry: try: self.slots.append(next(self.scan)) except (StopIteration, MemoryError): return None return self.slots[entry] def num_children(self): return 2*self.count if self.pairs else self.count def get_child_index(self, name): return -1 def get_child_at_index(self, index): entry = index//2 if self.pairs else index slot = self._slot(entry) if slot is None: return None key, value = slot if self.set: return self.valobj.CreateValueFromAddress("[%d]" % entry, key, self.key_type) if self.pairs: if index % 2 == 0: return self.valobj.CreateValueFromAddress("[%d].key" % entry, key, self.key_type) return self.valobj.CreateValueFromAddress("[%d].value" % entry, value, self.value_type) k = self.valobj.CreateValueFromAddress("key", key, self.key_type) text = k.GetSummary() or k.GetValue() or str(entry) return self.valobj.CreateValueFromAddress("[%s]" % text, value, self.value_type) def has_children(self): return True class LldbUnionSynth: # The children of the variant the union holds def __init__(self, valobj, internal_dict): self.valobj = valobj def update(self): try: self.value = _lldb_union_value(self.valobj) except ValueError: self.value = None if self.value is not None and self.value.GetSummary() and not self.value.IsSynthetic(): # a string, which its summary shows whole self.value = None return False def num_children(self): return self.value.GetNumChildren() if self.value is not None else 0 def get_child_index(self, name): return self.value.GetIndexOfChildWithName(name) if self.value is not None else -1 def get_child_at_index(self, index): return self.value.GetChildAtIndex(index) def has_children(self): return self.value is not None and self.value.MightHaveChildren() class LldbAnySynth: # The children of the value it holds, or of the `any` itself when that value's type is unknown def __init__(self, valobj, internal_dict): self.valobj = valobj def update(self): name, value = _lldb_any_value(self.valobj) self.value = value if value is None and name != "nil": self.value = self.valobj.GetNonSyntheticValue() if self.value is not None and self.value.GetSummary() and not self.value.IsSynthetic(): # a string, which its summary shows whole self.value = None return False def num_children(self): return self.value.GetNumChildren() if self.value is not None else 0 def get_child_index(self, name): return self.value.GetIndexOfChildWithName(name) if self.value is not None else -1 def get_child_at_index(self, index): return self.value.GetChildAtIndex(index) def has_children(self): return self.value is not None and self.value.MightHaveChildren() def __lldb_init_module(debugger, internal_dict): # NOTE: commands go through the interpreter with a result object, as `SBDebugger.HandleCommand` writes to # the debugger's output, which lldb-dap uses for its protocol, so editors using it lose the session. # `--recognizer-function` needs LLDB 15; before it, types are matched by their names, so named types are not. m = __name__ rules = [ ("type summary add -w odin -F %s.lldb_string_summary" % m, "lldb_is_string", "^string(16)?$"), ("type summary add -w odin -e -F %s.lldb_len_summary" % m, "lldb_is_container", "^([[][]]|[[]dynamic|map[[]|#soa)"), ("type synthetic add -w odin -l %s.LldbArraySynth" % m, "lldb_is_array", "^([[][]]|[[]dynamic)"), ("type synthetic add -w odin -l %s.LldbSoaSynth" % m, "lldb_is_soa", "^#soa[[]([]]|dynamic)"), ("type synthetic add -w odin -l %s.LldbMapSynth" % m, "lldb_is_map", "^map[[]"), ("type summary add -w odin -e -F %s.lldb_union_summary" % m, "lldb_is_union", "^union[{]"), ("type synthetic add -w odin -l %s.LldbUnionSynth" % m, "lldb_is_union", "^union[{]"), ("type summary add -w odin -e -F %s.lldb_any_summary" % m, "lldb_is_any", "^any$"), ("type synthetic add -w odin -l %s.LldbAnySynth" % m, "lldb_is_any", "^any$"), ] interpreter = debugger.GetCommandInterpreter() def run(command): result = lldb.SBCommandReturnObject() interpreter.HandleCommand(command, result) return result.Succeeded() for command, recognizer, regex in rules: if not run("%s --recognizer-function %s.%s" % (command, m, recognizer)): run('%s -x "%s"' % (command, regex)) run("type category enable odin")