Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/macOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@ jobs:
swift test \
-c debug \
--build-path .build-test-debug \
--filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|GenericSpecializerAPITests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)'
--filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)'

Comment on lines 89 to 93
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says CI should pass --no-parallel to swift test, but these invocations still run with default parallelism. If disabling parallelism is required to avoid cross-suite races, add --no-parallel here (or update the PR description if that’s no longer the approach).

Copilot uses AI. Check for mistakes.
- name: Build and run tests in release mode
run: |
swift test \
-c release \
--build-path .build-test-release \
--filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|GenericSpecializerAPITests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)'
--filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)'
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import MachOKit
import MachOReading
import MachOExtensions

public protocol RelativeDirectPointerIntPairProtocol<Pointee>: RelativeDirectPointerProtocol {
typealias Integer = Value.RawValue
associatedtype Value: RawRepresentable where Value.RawValue: FixedWidthInteger
var relativeOffsetPlusInt: Offset { get }
}

extension RelativeDirectPointerIntPairProtocol {
public var relativeOffset: Offset {
relativeOffsetPlusInt & ~mask
}

public var mask: Offset {
Offset(MemoryLayout<Offset>.alignment - 1)
}

public var intValue: Integer {
numericCast(relativeOffsetPlusInt & mask)
}

public var value: Value {
return Value(rawValue: intValue)!
}
}
4 changes: 4 additions & 0 deletions Sources/MachOPointers/RelativePointers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ public typealias RelativeDirectPointer<Pointee: Resolvable> = TargetRelativeDire

public typealias RelativeDirectRawPointer = TargetRelativeDirectPointer<AnyResolvable, RelativeOffset>

public typealias RelativeDirectPointerIntPair<Pointee: Resolvable, Integer: RawRepresentable> = TargetRelativeDirectPointerIntPair<Pointee, RelativeOffset, Integer> where Integer.RawValue: FixedWidthInteger

public typealias RelativeDirectRawPointerIntPair<Integer: RawRepresentable> = TargetRelativeDirectPointerIntPair<AnyResolvable, RelativeOffset, Integer> where Integer.RawValue: FixedWidthInteger

public typealias RelativeIndirectPointer<Pointee: Resolvable, IndirectType: RelativeIndirectType> = TargetRelativeIndirectPointer<Pointee, RelativeOffset, IndirectType> where Pointee == IndirectType.Resolved

public typealias RelativeIndirectRawPointer = TargetRelativeIndirectPointer<AnyResolvable, RelativeOffset, Pointer<AnyResolvable>>
Expand Down
11 changes: 11 additions & 0 deletions Sources/MachOPointers/TargetRelativeDirectPointerIntPair.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import MachOReading
import MachOResolving
import MachOExtensions

public struct TargetRelativeDirectPointerIntPair<Pointee: Resolvable, Offset: FixedWidthInteger & SignedInteger & Sendable, Value: RawRepresentable>: RelativeDirectPointerIntPairProtocol where Value.RawValue: FixedWidthInteger {
public let relativeOffsetPlusInt: Offset

public init(relativeOffsetPlusInt: Offset) {
self.relativeOffsetPlusInt = relativeOffsetPlusInt
}
}
30 changes: 27 additions & 3 deletions Sources/MachOSwiftSection/MachOFile+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extension MachOFile.Swift: SwiftSectionRepresentable {

public var contextDescriptors: [ContextDescriptorWrapper] {
get throws {
return try _readRelativeDescriptors(from: .__swift5_types, in: machO) + (try? _readRelativeDescriptors(from: .__swift5_types2, in: machO))
return try _readTypeMetadataRecords(from: .__swift5_types, in: machO) + (try? _readTypeMetadataRecords(from: .__swift5_types2, in: machO))
}
}

Expand All @@ -59,7 +59,7 @@ extension MachOFile.Swift: SwiftSectionRepresentable {

public var protocolDescriptors: [ProtocolDescriptor] {
get throws {
return try _readRelativeDescriptors(from: .__swift5_protos, in: machO)
return try _readProtocolRecords(from: .__swift5_protos, in: machO)
}
}

Expand Down Expand Up @@ -116,7 +116,31 @@ extension MachOFile.Swift {
section.offset
}
let data: [AnyLocatableLayoutWrapper<RelativeDirectPointer<Descriptor>>] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / pointerSize)
return data.compactMap { try? $0.layout.resolve(from: $0.offset, in: machO) }
return try data.map { try $0.layout.resolve(from: $0.offset, in: machO) }
}

private func _readTypeMetadataRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOFile) throws -> [ContextDescriptorWrapper] {
let section = try machO.section(for: swiftMachOSection)
let offset = if let cache = machO.cache {
section.address - cache.mainCacheHeader.sharedRegionStart.cast()
} else {
section.offset
}
let recordSize = MemoryLayout<TypeMetadataRecord.Layout>.size
let records: [TypeMetadataRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize)
return try records.compactMap { try $0.contextDescriptor(in: machO) }
}

private func _readProtocolRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOFile) throws -> [ProtocolDescriptor] {
let section = try machO.section(for: swiftMachOSection)
let offset = if let cache = machO.cache {
section.address - cache.mainCacheHeader.sharedRegionStart.cast()
} else {
section.offset
}
let recordSize = MemoryLayout<ProtocolRecord.Layout>.size
let records: [ProtocolRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize)
return try records.compactMap { try $0.protocolDescriptor(in: machO) }
}
}

Expand Down
26 changes: 24 additions & 2 deletions Sources/MachOSwiftSection/MachOImage+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extension MachOImage.Swift: SwiftSectionRepresentable {

public var contextDescriptors: [ContextDescriptorWrapper] {
get throws {
return try _readRelativeDescriptors(from: .__swift5_types, in: machO) + (try? _readRelativeDescriptors(from: .__swift5_types2, in: machO))
return try _readTypeMetadataRecords(from: .__swift5_types, in: machO) + (try? _readTypeMetadataRecords(from: .__swift5_types2, in: machO))
}
}

Expand All @@ -59,7 +59,7 @@ extension MachOImage.Swift: SwiftSectionRepresentable {

public var protocolDescriptors: [ProtocolDescriptor] {
get throws {
return try _readRelativeDescriptors(from: .__swift5_protos, in: machO)
return try _readProtocolRecords(from: .__swift5_protos, in: machO)
}
}

Expand Down Expand Up @@ -114,4 +114,26 @@ extension MachOImage.Swift {
let data: [AnyLocatableLayoutWrapper<RelativeDirectPointer<Descriptor>>] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / pointerSize)
return try data.map { try $0.layout.resolve(from: $0.offset, in: machO) }
}

private func _readTypeMetadataRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOImage) throws -> [ContextDescriptorWrapper] {
let section = try machO.section(for: swiftMachOSection)
let vmaddrSlide = try required(machO.vmaddrSlide)
let start = try required(UnsafeRawPointer(bitPattern: section.address + vmaddrSlide))
let offset = start.bitPattern.int - machO.ptr.bitPattern.int
let recordSize = MemoryLayout<TypeMetadataRecord.Layout>.size
let records: [TypeMetadataRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize)
return try records.compactMap { try $0.contextDescriptor(in: machO) }
}

private func _readProtocolRecords(from swiftMachOSection: MachOSwiftSectionName, in machO: MachOImage) throws -> [ProtocolDescriptor] {
let section = try machO.section(for: swiftMachOSection)
let vmaddrSlide = try required(machO.vmaddrSlide)
let start = try required(UnsafeRawPointer(bitPattern: section.address + vmaddrSlide))
let offset = start.bitPattern.int - machO.ptr.bitPattern.int
let recordSize = MemoryLayout<ProtocolRecord.Layout>.size
let records: [ProtocolRecord] = try machO.readWrapperElements(offset: offset, numberOfElements: section.size / recordSize)
return try records.compactMap { try $0.protocolDescriptor(in: machO) }
}
}


34 changes: 34 additions & 0 deletions Sources/MachOSwiftSection/Models/Protocol/ProtocolRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation
import MachOKit
import MachOFoundation

/// Mirrors `TargetProtocolRecord` from
/// `swift/include/swift/ABI/Metadata.h:2766`. One entry per 4-byte slot of
/// `__swift5_protos`.
///
/// The C++ declaration stores a single
/// `RelativeContextPointerIntPair<Runtime, bool, TargetProtocolDescriptor>`
/// (`MetadataRef.h:109` — a `RelativeIndirectablePointerIntPair` with
/// `nullable=true`). The low bit is the indirect flag handled by the pointer
/// itself; the next bit ("reserved for future use", see
/// `Metadata.h:2769`) is exposed via `Bit` and currently ignored by the
/// runtime (`MetadataLookup.cpp:821` only calls `getPointer()`).
public struct ProtocolRecord: ResolvableLocatableLayoutWrapper {
public struct Layout: LayoutProtocol {
public let `protocol`: RelativeIndirectablePointerIntPair<ProtocolDescriptor?, Bit, Pointer<ProtocolDescriptor?>>
}

public let offset: Int
public var layout: Layout

public init(layout: Layout, offset: Int) {
self.offset = offset
self.layout = layout
}
}

extension ProtocolRecord {
public func protocolDescriptor<MachO: MachOSwiftSectionRepresentableWithCache>(in machO: MachO) throws -> ProtocolDescriptor? {
try layout.protocol.resolve(from: offset(of: \.protocol), in: machO)
}
}
52 changes: 52 additions & 0 deletions Sources/MachOSwiftSection/Models/Type/TypeMetadataRecord.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation
import MachOKit
import MachOFoundation

/// Mirrors `TargetTypeMetadataRecord` from
/// `swift/include/swift/ABI/Metadata.h:2720`. One entry per 4-byte slot of
/// `__swift5_types` / `__swift5_types2`.
///
/// In C++ the record is a union over two arms, both
/// `RelativeDirectPointerIntPair<…, TypeReferenceKind>` with identical
/// in-memory layout, so a single storage field is enough; the
/// `TypeReferenceKind` tag picks which arm to resolve at access time.
public struct TypeMetadataRecord: ResolvableLocatableLayoutWrapper {
public struct Layout: LayoutProtocol {
public let nominalTypeDescriptor: RelativeDirectPointerIntPair<ContextDescriptorWrapper, TypeReferenceKind>
}

public let offset: Int
public var layout: Layout

public init(layout: Layout, offset: Int) {
self.offset = offset
self.layout = layout
}
}

extension TypeMetadataRecord {
public var typeKind: TypeReferenceKind {
return layout.nominalTypeDescriptor.value
}

/// Resolves the referenced context descriptor, branching on
/// `TypeReferenceKind` the same way Swift runtime does in
/// `TargetTypeMetadataRecord::getContextDescriptor()`
/// (`swift/include/swift/ABI/Metadata.h:2743`). ObjC kinds are never
/// populated in this section (see the comment at Metadata.h:2751); return
/// `nil` for them to mirror the runtime's `nullptr` fallback.
public func contextDescriptor<MachO: MachOSwiftSectionRepresentableWithCache>(in machO: MachO) throws -> ContextDescriptorWrapper? {
let fieldOffset = offset(of: \.nominalTypeDescriptor)
let relativeOffset = layout.nominalTypeDescriptor.relativeOffset
switch typeKind {
case .directTypeDescriptor:
let pointer = RelativeDirectPointer<ContextDescriptorWrapper>(relativeOffset: relativeOffset)
return try pointer.resolve(from: fieldOffset, in: machO)
case .indirectTypeDescriptor:
let pointer = RelativeIndirectPointer<ContextDescriptorWrapper, Pointer<ContextDescriptorWrapper>>(relativeOffset: relativeOffset)
return try pointer.resolve(from: fieldOffset, in: machO)
case .directObjCClassName, .indirectObjCClass:
return nil
}
}
}
1 change: 1 addition & 0 deletions Sources/MachOSymbols/SymbolIndexStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ public final class SymbolIndexStore: SharedCache<SymbolIndexStore.Storage>, @unc

private(set) var symbolsByOffset: OrderedDictionary<Int, [Symbol]> = [:]

@Mutex
private(set) var demangledNodeBySymbol: [Symbol: Node] = [:]
Comment on lines +155 to 156
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While @Mutex correctly synchronizes access to demangledNodeBySymbol to prevent data races, using a property wrapper on a Dictionary (a value type) can have performance implications. In Swift, any mutation to a property-wrapped value type (like the subscript assignment in setDemangledNode) typically involves a read-modify-write cycle that copies the entire collection if it's not uniquely referenced. For a large symbol index, this could become a bottleneck. Additionally, the check-then-set pattern in demangledNode(for:in:) is not atomic across the mutex boundary, which might lead to redundant demangling operations. If the @Mutex implementation provides a withLock method on its projected value, using it would allow for more efficient, atomic updates.


Comment on lines +155 to 157
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Mutex on demangledNodeBySymbol alone may not eliminate the race if call sites mutate/read the Dictionary via subscripting (which can go through an in-place _modify access). To guarantee the lock is always held (as SharedCache does for storageByIdentifier), access this property via the wrapper’s withLockUnchecked (e.g. _demangledNodeBySymbol.withLockUnchecked { ... }) for both reads in demangledNode(for:in:) and writes in setDemangledNode(_:,for:) / setDemangledNodeBySymbol(_:).

Copilot uses AI. Check for mistakes.
private(set) var thunkAttributeMembersByKindAndTypeName: [Node.Kind: [String: [ThunkAttributeMember]]] = [:]
Expand Down
7 changes: 1 addition & 6 deletions Tests/SwiftInterfaceTests/GenericSpecializationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,7 @@ final class GenericSpecializationTests: MachOImageTests, @unchecked Sendable {
)
try #expect(#require(metadata.value.resolve().struct).fieldOffsets() == [0, 8, 16])
}
}

// MARK: - GenericSpecializer API Tests

@Suite
struct GenericSpecializerAPITests {

@Test func makeRequest() async throws {
let machO = MachOImage.current()
Comment on lines 74 to 77
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions marking the merged GenericSpecialization test suite as .serialized, but this file still has no @Suite(.serialized) annotation on the suite type. If serialization is still intended (to avoid intra-suite parallelism), add the attribute; otherwise update the PR description to match the implementation.

Copilot uses AI. Check for mistakes.

Expand Down
Loading