From a7ba9e01b8bda63d2349d3fa2e8be3c707ff7db6 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 14:51:37 -0500 Subject: [PATCH 01/10] Remove C++-based section discovery logic. This PR removes the C++ _section_ discovery logic that's used to look up Swift's type metadata sections. Instead, we reuse the new logic used for test content sections (written in Swift.) --- Sources/Testing/Discovery+Platform.swift | 143 ++++++-- Sources/Testing/ExitTests/ExitTest.swift | 13 +- Sources/Testing/Test+Discovery+Legacy.swift | 40 +-- Sources/Testing/Test+Discovery.swift | 11 +- Sources/_TestingInternals/Discovery.cpp | 331 +++--------------- Sources/_TestingInternals/include/Discovery.h | 45 +-- 6 files changed, 205 insertions(+), 378 deletions(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index d85554498..cc3d6632b 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -22,22 +22,38 @@ struct SectionBounds: Sendable { static var allTestContent: some RandomAccessCollection { _testContentSectionBounds() } + + /// All type metadata section bounds found in the current process. + static var allTypeMetadata: some RandomAccessCollection { + _typeMetadataSectionBounds() + } } #if !SWT_NO_DYNAMIC_LINKING #if SWT_TARGET_OS_APPLE // MARK: - Apple implementation +/// A type describing the different sections that we collect. +private struct _AllSectionBounds: Sendable { + /// Test content section bounds. + var testContent = [SectionBounds]() + + /// Type metadata section bounds. + var typeMetadata = [SectionBounds]() +} + /// An array containing all of the test content section bounds known to the /// testing library. -private let _sectionBounds = Locked<[SectionBounds]>(rawValue: []) +private let _sectionBounds = Locked(rawValue: _AllSectionBounds()) /// A call-once function that initializes `_sectionBounds` and starts listening /// for loaded Mach headers. private let _startCollectingSectionBounds: Void = { // Ensure _sectionBounds is initialized before we touch libobjc or dyld. _sectionBounds.withLock { sectionBounds in - sectionBounds.reserveCapacity(Int(_dyld_image_count())) + let imageCount = Int(_dyld_image_count()) + sectionBounds.testContent.reserveCapacity(imageCount) + sectionBounds.typeMetadata.reserveCapacity(imageCount) } func addSectionBounds(from mh: UnsafePointer) { @@ -55,12 +71,32 @@ private let _startCollectingSectionBounds: Void = { // If this image contains the Swift section we need, acquire the lock and // store the section's bounds. - var size = CUnsignedLong(0) - if let start = getsectiondata(mh, "__DATA_CONST", "__swift5_tests", &size), size > 0 { - _sectionBounds.withLock { sectionBounds in + let testContentSectionBounds: SectionBounds? = { + var size = CUnsignedLong(0) + if let start = getsectiondata(mh, "__DATA_CONST", "__swift5_tests", &size), size > 0 { let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size)) - let sb = SectionBounds(imageAddress: mh, buffer: buffer) - sectionBounds.append(sb) + return SectionBounds(imageAddress: mh, buffer: buffer) + } + return nil + }() + + let typeMetadataSectionBounds: SectionBounds? = { + var size = CUnsignedLong(0) + if let start = getsectiondata(mh, "__TEXT", "__swift5_types", &size), size > 0 { + let buffer = UnsafeRawBufferPointer(start: start, count: Int(clamping: size)) + return SectionBounds(imageAddress: mh, buffer: buffer) + } + return nil + }() + + if testContentSectionBounds != nil || typeMetadataSectionBounds != nil { + _sectionBounds.withLock { sectionBounds in + if let testContentSectionBounds { + sectionBounds.testContent.append(testContentSectionBounds) + } + if let typeMetadataSectionBounds { + sectionBounds.typeMetadata.append(typeMetadataSectionBounds) + } } } } @@ -76,13 +112,22 @@ private let _startCollectingSectionBounds: Void = { #endif }() -/// The Apple-specific implementation of ``SectionBounds/all``. +/// The Apple-specific implementation of ``SectionBounds/allTestContent``. /// /// - Returns: An array of structures describing the bounds of all known test /// content sections in the current process. private func _testContentSectionBounds() -> [SectionBounds] { _startCollectingSectionBounds - return _sectionBounds.rawValue + return _sectionBounds.rawValue.testContent +} + +/// The Apple-specific implementation of ``SectionBounds/allTypeMetadata``. +/// +/// - Returns: An array of structures describing the bounds of all known type +/// metadata sections in the current process. +private func _typeMetadataSectionBounds() -> [SectionBounds] { + _startCollectingSectionBounds + return _sectionBounds.rawValue.typeMetadata } #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) @@ -90,29 +135,34 @@ private func _testContentSectionBounds() -> [SectionBounds] { private import SwiftShims // For MetadataSections -/// The ELF-specific implementation of ``SectionBounds/all``. +/// Get all Swift metadata sections of a given name that have been loaded into +/// the current process. /// -/// - Returns: An array of structures describing the bounds of all known test -/// content sections in the current process. -private func _testContentSectionBounds() -> [SectionBounds] { +/// - Parameters: +/// - sectionRangeKeyPath: A key path to the field of ``MetadataSections`` +/// containing the bounds of the section of interest. +/// +/// - Returns: An array of structures describing the bounds of all known +/// sections in the current process matching `sectionRangeKeyPath`. +private func _sectionBounds(for sectionRangeKeyPath: KeyPath) -> [SectionBounds] { var result = [SectionBounds]() withUnsafeMutablePointer(to: &result) { result in swift_enumerateAllMetadataSections({ sections, context in let version = sections.load(as: UInt.self) - guard version >= 4 else { + guard sectionRangeKeyPath != \.swift5_tests || version >= 4 else { // This structure is too old to contain the swift5_tests field. return true } - let sections = sections.load(as: MetadataSections.self) - let result = context.assumingMemoryBound(to: [SectionBounds].self) - - let start = UnsafeRawPointer(bitPattern: sections.swift5_tests.start) - let size = Int(clamping: sections.swift5_tests.length) + let range = sections.load(as: MetadataSections.self)[keyPath: sectionRangeKeyPath] + let start = UnsafeRawPointer(bitPattern: range.start) + let size = Int(clamping: range.length) if let start, size > 0 { let buffer = UnsafeRawBufferPointer(start: start, count: size) let sb = SectionBounds(imageAddress: sections.baseAddress, buffer: buffer) + + let result = context.assumingMemoryBound(to: [SectionBounds].self) result.pointee.append(sb) } @@ -123,6 +173,22 @@ private func _testContentSectionBounds() -> [SectionBounds] { return result } +/// The ELF-specific implementation of ``SectionBounds/allTestContent``. +/// +/// - Returns: An array of structures describing the bounds of all known test +/// content sections in the current process. +private func _testContentSectionBounds() -> [SectionBounds] { + _sectionBounds(for: \.swift5_tests) +} + +/// The ELF-specific implementation of ``SectionBounds/allTypeMetadata``. +/// +/// - Returns: An array of structures describing the bounds of all known type +/// metadata sections in the current process. +private func _typeMetadataSectionBounds() -> [SectionBounds] { + _sectionBounds(for: \.swift5_type_metadata) +} + #elseif os(Windows) // MARK: - Windows implementation @@ -183,28 +249,45 @@ private func _findSection(named sectionName: String, in hModule: HMODULE) -> Sec } } -/// The Windows-specific implementation of ``SectionBounds/all``. +/// The Windows-specific implementation of ``SectionBounds/allTestContent``. /// /// - Returns: An array of structures describing the bounds of all known test /// content sections in the current process. private func _testContentSectionBounds() -> [SectionBounds] { HMODULE.all.compactMap { _findSection(named: ".sw5test", in: $0) } } + +/// The Windows-specific implementation of ``SectionBounds/allTypeMetadata``. +/// +/// - Returns: An array of structures describing the bounds of all known type +/// metadata sections in the current process. +private func _typeMetadataSectionBounds() -> [SectionBounds] { + HMODULE.all.compactMap { _findSection(named: ".sw5tymd", in: $0) } +} #else -/// The fallback implementation of ``SectionBounds/all`` for platforms that -/// support dynamic linking. +/// The fallback implementation of ``SectionBounds/allTestContent`` for +/// platforms that support dynamic linking. /// /// - Returns: The empty array. private func _testContentSectionBounds() -> [SectionBounds] { #warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") return [] } + +/// The fallback implementation of ``SectionBounds/allTypeMetadata`` for +/// platforms that support dynamic linking. +/// +/// - Returns: The empty array. +private func _typeMetadataSectionBounds() -> [SectionBounds] { +#warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") + return [] +} #endif #else // MARK: - Statically-linked implementation -/// The common implementation of ``SectionBounds/all`` for platforms that do not -/// support dynamic linking. +/// The common implementation of ``SectionBounds/allTestContent`` for platforms +/// that do not support dynamic linking. /// /// - Returns: A structure describing the bounds of the test content section /// contained in the same image as the testing library itself. @@ -214,4 +297,16 @@ private func _testContentSectionBounds() -> CollectionOfOne { let sb = SectionBounds(imageAddress: nil, buffer: buffer) return CollectionOfOne(sb) } + +/// The common implementation of ``SectionBounds/allTypeMetadata`` for platforms +/// that do not support dynamic linking. +/// +/// - Returns: A structure describing the bounds of the type metadata section +/// contained in the same image as the testing library itself. +private func _typeMetadataSectionBounds() -> CollectionOfOne { + let (sectionBegin, sectionEnd) = SWTTypeMetadataSectionBounds + let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin)) + let sb = SectionBounds(imageAddress: nil, buffer: buffer) + return CollectionOfOne(sb) +} #endif diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 01810a7ca..96d636b11 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -250,19 +250,16 @@ extension ExitTest { if result == nil { // Call the legacy lookup function that discovers tests embedded in types. - enumerateTypes(withNamesContaining: exitTestContainerTypeNameMagic) { _, type, stop in - guard let type = type as? any __ExitTestContainer.Type else { - return - } - if type.__sourceLocation == sourceLocation { - result = ExitTest( + result = types(withNamesContaining: exitTestContainerTypeNameMagic).lazy + .compactMap { $0 as? any __ExitTestContainer.Type } + .first { $0.__sourceLocation == sourceLocation } + .map { type in + ExitTest( __expectedExitCondition: type.__expectedExitCondition, sourceLocation: type.__sourceLocation, body: type.__body ) - stop = true } - } } return result diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index b65d72b39..ce57b431d 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -49,35 +49,23 @@ let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__" // MARK: - -/// The type of callback called by ``enumerateTypes(withNamesContaining:_:)``. -/// -/// - Parameters: -/// - imageAddress: A pointer to the start of the image. This value is _not_ -/// equal to the value returned from `dlopen()`. On platforms that do not -/// support dynamic loading (and so do not have loadable images), this -/// argument is unspecified. -/// - type: A Swift type. -/// - stop: An `inout` boolean variable indicating whether type enumeration -/// should stop after the function returns. Set `stop` to `true` to stop -/// type enumeration. -typealias TypeEnumerator = (_ imageAddress: UnsafeRawPointer?, _ type: Any.Type, _ stop: inout Bool) -> Void - -/// Enumerate all types known to Swift found in the current process whose names +/// Get all types known to Swift found in the current process whose names /// contain a given substring. /// /// - Parameters: /// - nameSubstring: A string which the names of matching classes all contain. -/// - body: A function to invoke, once per matching type. -func enumerateTypes(withNamesContaining nameSubstring: String, _ typeEnumerator: TypeEnumerator) { - withoutActuallyEscaping(typeEnumerator) { typeEnumerator in - withUnsafePointer(to: typeEnumerator) { context in - swt_enumerateTypes(withNamesContaining: nameSubstring, .init(mutating: context)) { imageAddress, type, stop, context in - let typeEnumerator = context!.load(as: TypeEnumerator.self) - let type = unsafeBitCast(type, to: Any.Type.self) - var stop2 = false - typeEnumerator(imageAddress, type, &stop2) - stop.pointee = stop2 +/// +/// - Returns: A sequence of Swift types whose names contain `nameSubstring`. +func types(withNamesContaining nameSubstring: String) -> some Sequence { + SectionBounds.allTypeMetadata.lazy + .map { sb in + var count = 0 + let start = swt_copyTypes(in: sb.buffer.baseAddress!, sb.buffer.count, withNamesContaining: nameSubstring, count: &count) + defer { + free(start) + } + return start.withMemoryRebound(to: Any.Type.self, capacity: count) { start in + Array(UnsafeBufferPointer(start: start, count: count)) } - } - } + }.joined() } diff --git a/Sources/Testing/Test+Discovery.swift b/Sources/Testing/Test+Discovery.swift index 9a187c917..751a7de85 100644 --- a/Sources/Testing/Test+Discovery.swift +++ b/Sources/Testing/Test+Discovery.swift @@ -56,14 +56,11 @@ extension Test: TestContent { } if discoveryMode != .newOnly && generators.isEmpty { - enumerateTypes(withNamesContaining: testContainerTypeNameMagic) { imageAddress, type, _ in - guard let type = type as? any __TestContainer.Type else { - return + generators += types(withNamesContaining: testContainerTypeNameMagic).lazy + .compactMap { $0 as? any __TestContainer.Type } + .map { type in + { @Sendable in await type.__tests } } - generators.append { @Sendable in - await type.__tests - } - } } // *Now* we call all the generators and return their results. diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index 8af5e1690..c7de11c3f 100644 --- a/Sources/_TestingInternals/Discovery.cpp +++ b/Sources/_TestingInternals/Discovery.cpp @@ -16,13 +16,19 @@ #if defined(__APPLE__) extern "C" const char testContentSectionBegin __asm("section$start$__DATA_CONST$__swift5_tests"); extern "C" const char testContentSectionEnd __asm("section$end$__DATA_CONST$__swift5_tests"); +extern "C" const char typeMetadataSectionBegin __asm__("section$start$__TEXT$__swift5_types"); +extern "C" const char typeMetadataSectionEnd __asm__("section$end$__TEXT$__swift5_types"); #elif defined(__wasi__) extern "C" const char testContentSectionBegin __asm__("__start_swift5_tests"); extern "C" const char testContentSectionEnd __asm__("__stop_swift5_tests"); +extern "C" const char typeMetadataSectionBegin __asm__("__start_swift5_type_metadata"); +extern "C" const char typeMetadataSectionEnd __asm__("__stop_swift5_type_metadata"); #else #warning Platform-specific implementation missing: Runtime test discovery unavailable (static) static const char testContentSectionBegin = 0; static const char& testContentSectionEnd = testContentSectionBegin; +static const char typeMetadataSectionBegin = 0; +static const char& typeMetadataSectionEnd = testContentSectionBegin; #endif /// The bounds of the test content section statically linked into the image @@ -31,6 +37,13 @@ const void *_Nonnull const SWTTestContentSectionBounds[2] = { &testContentSectionBegin, &testContentSectionEnd }; + +/// The bounds of the type metadata section statically linked into the image +/// containing Swift Testing. +const void *_Nonnull const SWTTypeMetadataSectionBounds[2] = { + &typeMetadataSectionBegin, + &typeMetadataSectionEnd +}; #endif #pragma mark - Legacy test discovery @@ -40,19 +53,12 @@ const void *_Nonnull const SWTTestContentSectionBounds[2] = { #include #include #include +#include #include #include #include #include -#if defined(__APPLE__) && !defined(SWT_NO_DYNAMIC_LINKING) -#include -#include -#include -#include -#include -#endif - /// Enumerate over all Swift type metadata sections in the current process. /// /// - Parameters: @@ -264,298 +270,41 @@ struct SWTTypeMetadataRecord { } }; -#if !defined(SWT_NO_DYNAMIC_LINKING) -#if defined(__APPLE__) -#pragma mark - Apple implementation - -/// Get a copy of the currently-loaded type metadata sections list. -/// -/// - Returns: A list of type metadata sections in images loaded into the -/// current process. The order of the resulting list is unspecified. -/// -/// On ELF-based platforms, the `swift_enumerateAllMetadataSections()` function -/// exported by the runtime serves the same purpose as this function. -static SWTSectionBoundsList getSectionBounds(void) { - /// This list is necessarily mutated while a global libobjc- or dyld-owned - /// lock is held. Hence, code using this list must avoid potentially - /// re-entering either library (otherwise it could potentially deadlock.) - /// - /// To see how the Swift runtime accomplishes the above goal, see - /// `ConcurrentReadableArray` in that project's Concurrent.h header. Since the - /// testing library is not tasked with the same performance constraints as - /// Swift's runtime library, we just use a `std::vector` guarded by an unfair - /// lock. - static constinit SWTSectionBoundsList *sectionBounds = nullptr; - static constinit os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; - - static constinit dispatch_once_t once = 0; - dispatch_once_f(&once, nullptr, [] (void *) { - sectionBounds = reinterpret_cast *>(std::malloc(sizeof(SWTSectionBoundsList))); - ::new (sectionBounds) SWTSectionBoundsList(); - sectionBounds->reserve(_dyld_image_count()); - - objc_addLoadImageFunc([] (const mach_header *mh) { -#if __LP64__ - auto mhn = reinterpret_cast(mh); -#else - auto mhn = mh; -#endif - - // Ignore this Mach header if it is in the shared cache. On platforms that - // support it (Darwin), most system images are contained in this range. - // System images can be expected not to contain test declarations, so we - // don't need to walk them. - if (mhn->flags & MH_DYLIB_IN_CACHE) { - return; - } +#pragma mark - - // If this image contains the Swift section we need, acquire the lock and - // store the section's bounds. - unsigned long size = 0; - auto start = getsectiondata(mhn, SEG_TEXT, "__swift5_types", &size); - if (start && size > 0) { - os_unfair_lock_lock(&lock); { - sectionBounds->emplace_back(mhn, start, size); - } os_unfair_lock_unlock(&lock); - } - }); - }); - - // After the first call sets up the loader hook, all calls take the lock and - // make a copy of whatever has been loaded so far. - SWTSectionBoundsList result; - result.reserve(_dyld_image_count()); - os_unfair_lock_lock(&lock); { - result = *sectionBounds; - } os_unfair_lock_unlock(&lock); - result.shrink_to_fit(); - return result; -} +void **swt_copyTypesWithNamesContaining(const void *sectionBegin, size_t sectionSize, const char *nameSubstring, size_t *outCount) { + SWTSectionBounds sb = { nullptr, sectionBegin, sectionSize }; + std::vector> result; -template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - bool stop = false; - for (const auto& sb : getSectionBounds()) { - body(sb, &stop); - if (stop) { - break; + for (const auto& record : sb) { + auto contextDescriptor = record.getContextDescriptor(); + if (!contextDescriptor) { + // This type metadata record is invalid (or we don't understand how to + // get its context descriptor), so skip it. + continue; + } else if (contextDescriptor->isGeneric()) { + // Generic types cannot be fully instantiated without generic + // parameters, which is not something we can know abstractly. + continue; } - } -} - -#elif defined(_WIN32) -#pragma mark - Windows implementation - -/// Find the section with the given name in the given module. -/// -/// - Parameters: -/// - hModule: The module to inspect. -/// - sectionName: The name of the section to look for. Long section names are -/// not supported. -/// -/// - Returns: A pointer to the start of the given section along with its size -/// in bytes, or `std::nullopt` if the section could not be found. If the -/// section was emitted by the Swift toolchain, be aware it will have leading -/// and trailing bytes (`sizeof(uintptr_t)` each.) -static std::optional> findSection(HMODULE hModule, const char *sectionName) { - if (!hModule) { - return std::nullopt; - } - // Get the DOS header (to which the HMODULE directly points, conveniently!) - // and check it's sufficiently valid for us to walk. - auto dosHeader = reinterpret_cast(hModule); - if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE || dosHeader->e_lfanew <= 0) { - return std::nullopt; - } - - // Check the NT header. Since we don't use the optional header, skip it. - auto ntHeader = reinterpret_cast(reinterpret_cast(dosHeader) + dosHeader->e_lfanew); - if (!ntHeader || ntHeader->Signature != IMAGE_NT_SIGNATURE) { - return std::nullopt; - } - - auto sectionCount = ntHeader->FileHeader.NumberOfSections; - auto section = IMAGE_FIRST_SECTION(ntHeader); - for (size_t i = 0; i < sectionCount; i++, section += 1) { - if (section->VirtualAddress == 0) { + // Check that the type's name passes. This will be more expensive than the + // checks above, but should be cheaper than realizing the metadata. + const char *typeName = contextDescriptor->getName(); + bool nameOK = typeName && nullptr != std::strstr(typeName, nameSubstring); + if (!nameOK) { continue; } - auto start = reinterpret_cast(reinterpret_cast(dosHeader) + section->VirtualAddress); - size_t size = std::min(section->Misc.VirtualSize, section->SizeOfRawData); - if (start && size > 0) { - // FIXME: Handle longer names ("/%u") from string table - auto thisSectionName = reinterpret_cast(section->Name); - if (0 == std::strncmp(sectionName, thisSectionName, IMAGE_SIZEOF_SHORT_NAME)) { - return SWTSectionBounds { hModule, start, size }; - } + if (void *typeMetadata = contextDescriptor->getMetadata()) { + result.push_back(typeMetadata); } } - return std::nullopt; -} - -template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - // Find all the modules loaded in the current process. We assume there aren't - // more than 1024 loaded modules (as does Microsoft sample code.) - std::array hModules; - DWORD byteCountNeeded = 0; - if (!EnumProcessModules(GetCurrentProcess(), &hModules[0], hModules.size() * sizeof(HMODULE), &byteCountNeeded)) { - return; + auto resultCopy = reinterpret_cast(std::calloc(sizeof(void *), result.size())); + if (resultCopy) { + std::uninitialized_move(result.begin(), result.end(), resultCopy); + *outCount = result.size(); } - size_t hModuleCount = std::min(hModules.size(), static_cast(byteCountNeeded) / sizeof(HMODULE)); - - // Look in all the loaded modules for Swift type metadata sections and store - // them in a side table. - // - // This two-step process is more complicated to read than a single loop would - // be but it is safer: the callback will eventually invoke developer code that - // could theoretically unload a module from the list we're enumerating. (Swift - // modules do not support unloading, so we'll just not worry about them.) - SWTSectionBoundsList sectionBounds; - sectionBounds.reserve(hModuleCount); - for (size_t i = 0; i < hModuleCount; i++) { - if (auto sb = findSection(hModules[i], ".sw5tymd")) { - sectionBounds.push_back(*sb); - } - } - - // Pass each discovered section back to the body callback. - // - // NOTE: we ignore the leading and trailing uintptr_t values: they're both - // always set to zero so we'll skip them in the callback, and in the future - // the toolchain might not emit them at all in which case we don't want to - // skip over real section data. - bool stop = false; - for (const auto& sb : sectionBounds) { - body(sb, &stop); - if (stop) { - break; - } - } -} - -#elif defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__ANDROID__) -#pragma mark - ELF implementation - -/// Specifies the address range corresponding to a section. -struct MetadataSectionRange { - uintptr_t start; - size_t length; -}; - -/// Identifies the address space ranges for the Swift metadata required by the -/// Swift runtime. -struct MetadataSections { - uintptr_t version; - std::atomic baseAddress; - - void *unused0; - void *unused1; - - MetadataSectionRange swift5_protocols; - MetadataSectionRange swift5_protocol_conformances; - MetadataSectionRange swift5_type_metadata; - MetadataSectionRange swift5_typeref; - MetadataSectionRange swift5_reflstr; - MetadataSectionRange swift5_fieldmd; - MetadataSectionRange swift5_assocty; - MetadataSectionRange swift5_replace; - MetadataSectionRange swift5_replac2; - MetadataSectionRange swift5_builtin; - MetadataSectionRange swift5_capture; - MetadataSectionRange swift5_mpenum; - MetadataSectionRange swift5_accessible_functions; -}; - -/// A function exported by the Swift runtime that enumerates all metadata -/// sections loaded into the current process. -SWT_IMPORT_FROM_STDLIB void swift_enumerateAllMetadataSections( - bool (* body)(const MetadataSections *sections, void *context), - void *context -); - -template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - swift_enumerateAllMetadataSections([] (const MetadataSections *sections, void *context) { - bool stop = false; - - const auto& body = *reinterpret_cast(context); - MetadataSectionRange section = sections->swift5_type_metadata; - if (section.start && section.length > 0) { - SWTSectionBounds sb = { - sections->baseAddress.load(), - reinterpret_cast(section.start), - section.length - }; - body(sb, &stop); - } - - return !stop; - }, const_cast(&body)); -} -#else -#warning Platform-specific implementation missing: Runtime test discovery unavailable (dynamic) -template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) {} -#endif - -#else -#pragma mark - Statically-linked implementation - -#if defined(__APPLE__) -extern "C" const char sectionBegin __asm__("section$start$__TEXT$__swift5_types"); -extern "C" const char sectionEnd __asm__("section$end$__TEXT$__swift5_types"); -#elif defined(__wasi__) -extern "C" const char sectionBegin __asm__("__start_swift5_type_metadata"); -extern "C" const char sectionEnd __asm__("__stop_swift5_type_metadata"); -#else -#warning Platform-specific implementation missing: Runtime test discovery unavailable (static) -static const char sectionBegin = 0; -static const char& sectionEnd = sectionBegin; -#endif - -template -static void enumerateTypeMetadataSections(const SectionEnumerator& body) { - SWTSectionBounds sb = { - nullptr, - §ionBegin, - static_cast(std::distance(§ionBegin, §ionEnd)) - }; - bool stop = false; - body(sb, &stop); -} -#endif - -#pragma mark - - -void swt_enumerateTypesWithNamesContaining(const char *nameSubstring, void *context, SWTTypeEnumerator body) { - enumerateTypeMetadataSections([=] (const SWTSectionBounds& sectionBounds, bool *stop) { - for (const auto& record : sectionBounds) { - auto contextDescriptor = record.getContextDescriptor(); - if (!contextDescriptor) { - // This type metadata record is invalid (or we don't understand how to - // get its context descriptor), so skip it. - continue; - } else if (contextDescriptor->isGeneric()) { - // Generic types cannot be fully instantiated without generic - // parameters, which is not something we can know abstractly. - continue; - } - - // Check that the type's name passes. This will be more expensive than the - // checks above, but should be cheaper than realizing the metadata. - const char *typeName = contextDescriptor->getName(); - bool nameOK = typeName && nullptr != std::strstr(typeName, nameSubstring); - if (!nameOK) { - continue; - } - - if (void *typeMetadata = contextDescriptor->getMetadata()) { - body(sectionBounds.imageAddress, typeMetadata, stop, context); - } - } - }); + return resultCopy; } diff --git a/Sources/_TestingInternals/include/Discovery.h b/Sources/_TestingInternals/include/Discovery.h index 6432a798c..2cfd63339 100644 --- a/Sources/_TestingInternals/include/Discovery.h +++ b/Sources/_TestingInternals/include/Discovery.h @@ -73,34 +73,35 @@ SWT_IMPORT_FROM_STDLIB void swift_enumerateAllMetadataSections( /// when Swift files import the `_TestingInternals` C++ module. SWT_EXTERN const void *_Nonnull const SWTTestContentSectionBounds[2]; -#pragma mark - Legacy test discovery - -/// The type of callback called by `swt_enumerateTypes()`. +/// The bounds of the type metadata section statically linked into the image +/// containing Swift Testing. /// -/// - Parameters: -/// - imageAddress: A pointer to the start of the image. This value is _not_ -/// equal to the value returned from `dlopen()`. On platforms that do not -/// support dynamic loading (and so do not have loadable images), this -/// argument is unspecified. -/// - typeMetadata: A type metadata pointer that can be bitcast to `Any.Type`. -/// - stop: A pointer to a boolean variable indicating whether type -/// enumeration should stop after the function returns. Set `*stop` to -/// `true` to stop type enumeration. -/// - context: An arbitrary pointer passed by the caller to -/// `swt_enumerateTypes()`. -typedef void (* SWTTypeEnumerator)(const void *_Null_unspecified imageAddress, void *typeMetadata, bool *stop, void *_Null_unspecified context); +/// - Note: This symbol is _declared_, but not _defined_, on platforms with +/// dynamic linking because the `SWT_NO_DYNAMIC_LINKING` C++ macro (not the +/// Swift compiler conditional of the same name) is not consistently declared +/// when Swift files import the `_TestingInternals` C++ module. +SWT_EXTERN const void *_Nonnull const SWTTypeMetadataSectionBounds[2]; -/// Enumerate all types known to Swift found in the current process. +#pragma mark - Legacy test discovery + +/// Copy all types known to Swift found in the given type metadata section with +/// a name containing the given substring. /// /// - Parameters: +/// - sectionBegin: The address of the first byte of the Swift type metadata +/// section. +/// - sectionSize: The size, in bytes, of the Swift type metadata section. /// - nameSubstring: A string which the names of matching classes all contain. -/// - context: An arbitrary pointer to pass to `body`. -/// - body: A function to invoke, once per matching type. -SWT_EXTERN void swt_enumerateTypesWithNamesContaining( +/// - outCount: On return, the number of type metadata pointers returned. +/// +/// - Returns: A pointer to an array of type metadata pointers. The caller is +/// responsible for freeing this memory with `free()` when done. +SWT_EXTERN void *_Nonnull *_Nonnull swt_copyTypesWithNamesContaining( + const void *sectionBegin, + size_t sectionSize, const char *nameSubstring, - void *_Null_unspecified context, - SWTTypeEnumerator body -) SWT_SWIFT_NAME(swt_enumerateTypes(withNamesContaining:_:_:)); + size_t *outCount +) SWT_SWIFT_NAME(swt_copyTypes(in:_:withNamesContaining:count:)); SWT_ASSUME_NONNULL_END From 2255fb9af5d4b720b9f40cf257b4c2f25cc25ce9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 15:23:34 -0500 Subject: [PATCH 02/10] Fix typo in ELF branch --- Sources/Testing/Discovery+Platform.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index cc3d6632b..c432c72e3 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -154,8 +154,9 @@ private func _sectionBounds(for sectionRangeKeyPath: KeyPath 0 { From d55ae77222dedc84f25fd1205f99cd1bb49a8c79 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 15:43:59 -0500 Subject: [PATCH 03/10] Simplify further and try to work around a compiler crash on Linux --- Sources/Testing/Discovery+Platform.swift | 178 +++++++++----------- Sources/Testing/Discovery.swift | 2 +- Sources/Testing/Test+Discovery+Legacy.swift | 2 +- 3 files changed, 85 insertions(+), 97 deletions(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index c432c72e3..8132825a1 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -18,14 +18,25 @@ struct SectionBounds: Sendable { /// The in-memory representation of the section. nonisolated(unsafe) var buffer: UnsafeRawBufferPointer - /// All test content section bounds found in the current process. - static var allTestContent: some RandomAccessCollection { - _testContentSectionBounds() + /// An enumeration describing the different sections discoverable by the + /// testing library. + enum Kind: Equatable, Hashable, CaseIterable { + /// The test content metadata section. + case testContent + + /// The type metadata section. + case typeMetadata } - /// All type metadata section bounds found in the current process. - static var allTypeMetadata: some RandomAccessCollection { - _typeMetadataSectionBounds() + /// All section bounds of the given kind found in the current process. + /// + /// - Parameters: + /// - kind: Which kind of metadata section to return. + /// + /// - Returns: A sequence of structures describing the bounds of metadata + /// sections of the given kind found in the current process. + static func all(_ kind: Kind) -> some RandomAccessCollection { + _sectionBounds(kind) } } @@ -44,7 +55,7 @@ private struct _AllSectionBounds: Sendable { /// An array containing all of the test content section bounds known to the /// testing library. -private let _sectionBounds = Locked(rawValue: _AllSectionBounds()) +private let _sectionBounds = Locked<[SectionBounds.Kind: [SectionBounds]]>() /// A call-once function that initializes `_sectionBounds` and starts listening /// for loaded Mach headers. @@ -52,8 +63,9 @@ private let _startCollectingSectionBounds: Void = { // Ensure _sectionBounds is initialized before we touch libobjc or dyld. _sectionBounds.withLock { sectionBounds in let imageCount = Int(_dyld_image_count()) - sectionBounds.testContent.reserveCapacity(imageCount) - sectionBounds.typeMetadata.reserveCapacity(imageCount) + for kind in SectionBounds.Kind.allCases { + sectionBounds[kind, default: []].reserveCapacity(imageCount) + } } func addSectionBounds(from mh: UnsafePointer) { @@ -92,10 +104,10 @@ private let _startCollectingSectionBounds: Void = { if testContentSectionBounds != nil || typeMetadataSectionBounds != nil { _sectionBounds.withLock { sectionBounds in if let testContentSectionBounds { - sectionBounds.testContent.append(testContentSectionBounds) + sectionBounds[.testContent, default: []].append(testContentSectionBounds) } if let typeMetadataSectionBounds { - sectionBounds.typeMetadata.append(typeMetadataSectionBounds) + sectionBounds[.typeMetadata, default: []].append(typeMetadataSectionBounds) } } } @@ -112,22 +124,16 @@ private let _startCollectingSectionBounds: Void = { #endif }() -/// The Apple-specific implementation of ``SectionBounds/allTestContent``. +/// The Apple-specific implementation of ``SectionBounds/all(_:)``. +/// +/// - Parameters: +/// - kind: Which kind of metadata section to return. /// /// - Returns: An array of structures describing the bounds of all known test /// content sections in the current process. -private func _testContentSectionBounds() -> [SectionBounds] { +private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { _startCollectingSectionBounds - return _sectionBounds.rawValue.testContent -} - -/// The Apple-specific implementation of ``SectionBounds/allTypeMetadata``. -/// -/// - Returns: An array of structures describing the bounds of all known type -/// metadata sections in the current process. -private func _typeMetadataSectionBounds() -> [SectionBounds] { - _startCollectingSectionBounds - return _sectionBounds.rawValue.typeMetadata + return _sectionBounds.rawValue[kind, default: []] } #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) @@ -135,59 +141,50 @@ private func _typeMetadataSectionBounds() -> [SectionBounds] { private import SwiftShims // For MetadataSections -/// Get all Swift metadata sections of a given name that have been loaded into -/// the current process. +/// The ELF-specific implementation of ``SectionBounds/all(_:)``. /// /// - Parameters: -/// - sectionRangeKeyPath: A key path to the field of ``MetadataSections`` -/// containing the bounds of the section of interest. +/// - kind: Which kind of metadata section to return. /// -/// - Returns: An array of structures describing the bounds of all known -/// sections in the current process matching `sectionRangeKeyPath`. -private func _sectionBounds(for sectionRangeKeyPath: KeyPath) -> [SectionBounds] { - var result = [SectionBounds]() +/// - Returns: An array of structures describing the bounds of all known test +/// content sections in the current process. +private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { + struct Context { + var kind: SectionBounds.Kind + var result = [SectionBounds]() + } + var context = Context(kind: kind) - withUnsafeMutablePointer(to: &result) { result in + withUnsafeMutablePointer(to: &context) { result in swift_enumerateAllMetadataSections({ sections, context in + let context = context.assumingMemoryBound(to: Context.self) + let version = sections.load(as: UInt.self) - guard sectionRangeKeyPath != \.swift5_tests || version >= 4 else { + guard context.pointee.kind != .testContent || version >= 4 else { // This structure is too old to contain the swift5_tests field. return true } let sections = sections.load(as: MetadataSections.self) - let range = sections[keyPath: sectionRangeKeyPath] - let start = UnsafeRawPointer(bitPattern: range.start) - let size = Int(clamping: range.length) - if let start, size > 0 { + let range = switch context.pointee.kind { + case .testContent: + sections.swift5_tests + case .typeMetadata: + sections.swift5_type_metadata + } + if let start = UnsafeRawPointer(bitPattern: range.start), + let size = Int(clamping: range.length), size > 0 { let buffer = UnsafeRawBufferPointer(start: start, count: size) let sb = SectionBounds(imageAddress: sections.baseAddress, buffer: buffer) - let result = context.assumingMemoryBound(to: [SectionBounds].self) - result.pointee.append(sb) + context.pointee.result.append(sb) } return true - }, result) + }, context) } - return result -} - -/// The ELF-specific implementation of ``SectionBounds/allTestContent``. -/// -/// - Returns: An array of structures describing the bounds of all known test -/// content sections in the current process. -private func _testContentSectionBounds() -> [SectionBounds] { - _sectionBounds(for: \.swift5_tests) -} - -/// The ELF-specific implementation of ``SectionBounds/allTypeMetadata``. -/// -/// - Returns: An array of structures describing the bounds of all known type -/// metadata sections in the current process. -private func _typeMetadataSectionBounds() -> [SectionBounds] { - _sectionBounds(for: \.swift5_type_metadata) + return context.result } #elseif os(Windows) @@ -250,62 +247,53 @@ private func _findSection(named sectionName: String, in hModule: HMODULE) -> Sec } } -/// The Windows-specific implementation of ``SectionBounds/allTestContent``. +/// The Windows-specific implementation of ``SectionBounds/all(_:)``. +/// +/// - Parameters: +/// - kind: Which kind of metadata section to return. /// /// - Returns: An array of structures describing the bounds of all known test /// content sections in the current process. -private func _testContentSectionBounds() -> [SectionBounds] { - HMODULE.all.compactMap { _findSection(named: ".sw5test", in: $0) } -} - -/// The Windows-specific implementation of ``SectionBounds/allTypeMetadata``. -/// -/// - Returns: An array of structures describing the bounds of all known type -/// metadata sections in the current process. -private func _typeMetadataSectionBounds() -> [SectionBounds] { - HMODULE.all.compactMap { _findSection(named: ".sw5tymd", in: $0) } +private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { + let sectionName = switch kind { + case .testContent: + ".sw5test" + case .typeMetadata: + ".sw5tymd" + } + return HMODULE.all.compactMap { _findSection(named: sectionName, in: $0) } } #else -/// The fallback implementation of ``SectionBounds/allTestContent`` for -/// platforms that support dynamic linking. +/// The fallback implementation of ``SectionBounds/all(_:)`` for platforms that +/// support dynamic linking. /// -/// - Returns: The empty array. -private func _testContentSectionBounds() -> [SectionBounds] { - #warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") - return [] -} - -/// The fallback implementation of ``SectionBounds/allTypeMetadata`` for -/// platforms that support dynamic linking. +/// - Parameters: +/// - kind: Ignored. /// /// - Returns: The empty array. -private func _typeMetadataSectionBounds() -> [SectionBounds] { -#warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") +private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { + #warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") return [] } #endif #else // MARK: - Statically-linked implementation -/// The common implementation of ``SectionBounds/allTestContent`` for platforms -/// that do not support dynamic linking. +/// The common implementation of ``SectionBounds/all(_:)`` for platforms that do +/// not support dynamic linking. /// -/// - Returns: A structure describing the bounds of the test content section -/// contained in the same image as the testing library itself. -private func _testContentSectionBounds() -> CollectionOfOne { - let (sectionBegin, sectionEnd) = SWTTestContentSectionBounds - let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin)) - let sb = SectionBounds(imageAddress: nil, buffer: buffer) - return CollectionOfOne(sb) -} - -/// The common implementation of ``SectionBounds/allTypeMetadata`` for platforms -/// that do not support dynamic linking. +/// - Parameters: +/// - kind: Which kind of metadata section to return. /// /// - Returns: A structure describing the bounds of the type metadata section /// contained in the same image as the testing library itself. -private func _typeMetadataSectionBounds() -> CollectionOfOne { - let (sectionBegin, sectionEnd) = SWTTypeMetadataSectionBounds +private func _sectionBounds(_ kind: SectionBounds.Kind) -> CollectionOfOne { + let (sectionBegin, sectionEnd) = switch kind { + case .testContent: + SWTTestContentSectionBounds + case .typeMetadata: + SWTTypeMetadataSectionBounds + } let buffer = UnsafeRawBufferPointer(start: sectionBegin, count: max(0, sectionEnd - sectionBegin)) let sb = SectionBounds(imageAddress: nil, buffer: buffer) return CollectionOfOne(sb) diff --git a/Sources/Testing/Discovery.swift b/Sources/Testing/Discovery.swift index b2fc7825c..6343b92f4 100644 --- a/Sources/Testing/Discovery.swift +++ b/Sources/Testing/Discovery.swift @@ -157,7 +157,7 @@ extension TestContent where Self: ~Copyable { /// is used with move-only types (specifically ``ExitTest``) and /// `Sequence.Element` must be copyable. static func enumerateTestContent(withHint hint: TestContentAccessorHint? = nil, _ body: TestContentEnumerator) { - let testContentRecords = SectionBounds.allTestContent.lazy.flatMap(_testContentRecords(in:)) + let testContentRecords = SectionBounds.all(.testContent).lazy.flatMap(_testContentRecords(in:)) var stop = false for (imageAddress, record) in testContentRecords { diff --git a/Sources/Testing/Test+Discovery+Legacy.swift b/Sources/Testing/Test+Discovery+Legacy.swift index ce57b431d..d330104ef 100644 --- a/Sources/Testing/Test+Discovery+Legacy.swift +++ b/Sources/Testing/Test+Discovery+Legacy.swift @@ -57,7 +57,7 @@ let exitTestContainerTypeNameMagic = "__🟠$exit_test_body__" /// /// - Returns: A sequence of Swift types whose names contain `nameSubstring`. func types(withNamesContaining nameSubstring: String) -> some Sequence { - SectionBounds.allTypeMetadata.lazy + SectionBounds.all(.typeMetadata).lazy .map { sb in var count = 0 let start = swt_copyTypes(in: sb.buffer.baseAddress!, sb.buffer.count, withNamesContaining: nameSubstring, count: &count) From 1d4d5371799b933a76767a04a5a6ea61dcc0bfe4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 15:50:35 -0500 Subject: [PATCH 04/10] Linux, why do you do this to me? --- Sources/Testing/Discovery+Platform.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 8132825a1..05f7442c3 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -155,7 +155,7 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { } var context = Context(kind: kind) - withUnsafeMutablePointer(to: &context) { result in + withUnsafeMutablePointer(to: &context) { context in swift_enumerateAllMetadataSections({ sections, context in let context = context.assumingMemoryBound(to: Context.self) From af9183f2742801838c76d8124bc6c022c0473e89 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 15:54:50 -0500 Subject: [PATCH 05/10] Why don't you try building it at your desk, Jonathan? --- Sources/Testing/Discovery+Platform.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 05f7442c3..9d2c4d000 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -133,7 +133,7 @@ private let _startCollectingSectionBounds: Void = { /// content sections in the current process. private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { _startCollectingSectionBounds - return _sectionBounds.rawValue[kind, default: []] + return _sectionBounds.rawValue[kind]! } #elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) @@ -172,8 +172,9 @@ private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { case .typeMetadata: sections.swift5_type_metadata } - if let start = UnsafeRawPointer(bitPattern: range.start), - let size = Int(clamping: range.length), size > 0 { + let start = UnsafeRawPointer(bitPattern: range.start) + let size = Int(clamping: range.length) + if let start, size > 0 { let buffer = UnsafeRawBufferPointer(start: start, count: size) let sb = SectionBounds(imageAddress: sections.baseAddress, buffer: buffer) From c66d60cfc8abb7fe3eba619314bc78b10513e590 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 16:00:45 -0500 Subject: [PATCH 06/10] Remove unused struct --- Sources/Testing/Discovery+Platform.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 9d2c4d000..1c358c2f3 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -44,15 +44,6 @@ struct SectionBounds: Sendable { #if SWT_TARGET_OS_APPLE // MARK: - Apple implementation -/// A type describing the different sections that we collect. -private struct _AllSectionBounds: Sendable { - /// Test content section bounds. - var testContent = [SectionBounds]() - - /// Type metadata section bounds. - var typeMetadata = [SectionBounds]() -} - /// An array containing all of the test content section bounds known to the /// testing library. private let _sectionBounds = Locked<[SectionBounds.Kind: [SectionBounds]]>() From 2fa5ff9e2d761a57aa538bc83e2d23045a05d025 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 16:42:59 -0500 Subject: [PATCH 07/10] Clamp _dyld_image_count() juuuuust in case --- Sources/Testing/Discovery+Platform.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 1c358c2f3..02a6dd1ed 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -53,7 +53,7 @@ private let _sectionBounds = Locked<[SectionBounds.Kind: [SectionBounds]]>() private let _startCollectingSectionBounds: Void = { // Ensure _sectionBounds is initialized before we touch libobjc or dyld. _sectionBounds.withLock { sectionBounds in - let imageCount = Int(_dyld_image_count()) + let imageCount = Int(clamping: _dyld_image_count()) for kind in SectionBounds.Kind.allCases { sectionBounds[kind, default: []].reserveCapacity(imageCount) } From 4917705cc6b8df31c3e66ce16e84797f5c4e5e42 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 9 Jan 2025 16:44:44 -0500 Subject: [PATCH 08/10] Defaults not needed on these lines --- Sources/Testing/Discovery+Platform.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Discovery+Platform.swift b/Sources/Testing/Discovery+Platform.swift index 02a6dd1ed..7fcb94fe5 100644 --- a/Sources/Testing/Discovery+Platform.swift +++ b/Sources/Testing/Discovery+Platform.swift @@ -95,10 +95,10 @@ private let _startCollectingSectionBounds: Void = { if testContentSectionBounds != nil || typeMetadataSectionBounds != nil { _sectionBounds.withLock { sectionBounds in if let testContentSectionBounds { - sectionBounds[.testContent, default: []].append(testContentSectionBounds) + sectionBounds[.testContent]!.append(testContentSectionBounds) } if let typeMetadataSectionBounds { - sectionBounds[.typeMetadata, default: []].append(typeMetadataSectionBounds) + sectionBounds[.typeMetadata]!.append(typeMetadataSectionBounds) } } } From 719b3c3ed7308d128af4cfd3712f593b4311cb08 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 10 Jan 2025 13:41:58 -0500 Subject: [PATCH 09/10] Update Porting.md --- Documentation/Porting.md | 146 ++++++++++++++++++++++----------------- 1 file changed, 83 insertions(+), 63 deletions(-) diff --git a/Documentation/Porting.md b/Documentation/Porting.md index 39d1d8e88..ce179d53d 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -66,7 +66,7 @@ platform-specific attention. > These errors are produced when the configuration you're trying to build has > conflicting requirements (for example, attempting to enable support for pipes > without also enabling support for file I/O.) You should be able to resolve -> these issues by updating Package.swift and/or CompilerSettings.cmake. +> these issues by updating `Package.swift` and/or `CompilerSettings.cmake`. Most platform dependencies can be resolved through the use of platform-specific API. For example, Swift Testing uses the C11 standard [`timespec`](https://en.cppreference.com/w/c/chrono/timespec) @@ -123,69 +123,110 @@ Once the header is included, we can call `GetDateTime()` from `Clock.swift`: ## Runtime test discovery When porting to a new platform, you may need to provide a new implementation for -`enumerateTypeMetadataSections()` in `Discovery.cpp`. Test discovery is -dependent on Swift metadata discovery which is an inherently platform-specific -operation. - -_Most_ platforms will be able to reuse the implementation used by Linux and -Windows that calls an internal Swift runtime function to enumerate available -metadata. If you are porting Swift Testing to Classic, this function won't be -available, so you'll need to write a custom implementation instead. Assuming -that the Swift compiler emits section information into the resource fork on -Classic, you could use the [Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf) +`_sectionBounds(_:)` in `Discovery+Platform.swift`. Test discovery is dependent +on Swift metadata discovery which is an inherently platform-specific operation. + +_Most_ platforms in use today use the ELF image format and will be able to reuse +the implementation used by Linux. + +Classic does not use the ELF image format, so you'll need to write a custom +implementation of `_sectionBounds(_:)` instead. Assuming that the Swift compiler +emits section information into the resource fork on Classic, you would use the +[Resource Manager](https://developer.apple.com/library/archive/documentation/mac/pdf/MoreMacintoshToolbox.pdf) to load that information: ```diff ---- a/Sources/_TestingInternals/Discovery.cpp -+++ b/Sources/_TestingInternals/Discovery.cpp +--- a/Sources/Testing/Discovery+Platform.swift ++++ b/Sources/Testing/Discovery+Platform.swift // ... -+#elif defined(macintosh) -+template -+static void enumerateTypeMetadataSections(const SectionEnumerator& body) { -+ ResFileRefNum refNum; -+ if (noErr == GetTopResourceFile(&refNum)) { -+ ResFileRefNum oldRefNum = refNum; -+ do { -+ UseResFile(refNum); -+ Handle handle = Get1NamedResource('swft', "\p__swift5_types"); -+ if (handle && *handle) { -+ auto imageAddress = reinterpret_cast(static_cast(refNum)); -+ SWTSectionBounds sb = { imageAddress, *handle, GetHandleSize(handle) }; -+ bool stop = false; -+ body(sb, &stop); -+ if (stop) { -+ break; -+ } -+ } -+ } while (noErr == GetNextResourceFile(refNum, &refNum)); -+ UseResFile(oldRefNum); ++#elseif os(Classic) ++private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { ++ let resourceName: Str255 = switch kind { ++ case .testContent: ++ "__swift5_tests" ++ case .typeMetadata: ++ "__swift5_types" ++ } ++ ++ let oldRefNum = CurResFile() ++ defer { ++ UseResFile(oldRefNum) ++ } ++ ++ var refNum = ResFileRefNum(0) ++ guard noErr == GetTopResourceFile(&refNum) else { ++ return [] + } ++ ++ var result = [SectionBounds]() ++ repeat { ++ UseResFile(refNum) ++ guard let handle = Get1NamedResource(ResType("swft"), resourceName) else { ++ continue ++ } ++ let sb = SectionBounds( ++ imageAddress: UnsafeRawPointer(bitPattern: UInt(refNum)), ++ start: handle.pointee!, ++ size: GetHandleSize(handle) ++ ) ++ result.append(sb) ++ } while noErr == GetNextResourceFile(refNum, &refNum)) ++ return result +} #else - #warning Platform-specific implementation missing: Runtime test discovery unavailable (dynamic) - template - static void enumerateTypeMetadataSections(const SectionEnumerator& body) {} + private func _sectionBounds(_ kind: SectionBounds.Kind) -> [SectionBounds] { + #warning("Platform-specific implementation missing: Runtime test discovery unavailable (dynamic)") + return [] + } #endif ``` +You will also need to update the `makeTestContentRecordDecl()` function in the +`TestingMacros` target to emit the correct `@_section` attribute for your +platform. If your platform uses the ELF image format and supports the +`dl_iterate_phdr()` function, add it to the existing `#elseif os(Linux) || ...` +case. Otherwise, add a new case for your platform: + +```diff +--- a/Sources/TestingMacros/Support/TestContentGeneration.swift ++++ b/Sources/TestingMacros/Support/TestContentGeneration.swift + // ... ++ #elseif os(Classic) ++ @_section(".rsrc,swft,__swift5_tests") + #else + @__testing(warning: "Platform-specific implementation missing: test content section name unavailable") + #endif +``` + +Keep in mind that this code is emitted by the `@Test` and `@Suite` macros +directly into test authors' test targets, so you will not be able to use +compiler conditionals defined in the Swift Testing package (including those that +start with `"SWT_"`). + ## Runtime test discovery with static linkage If your platform does not support dynamic linking and loading, you will need to use static linkage instead. Define the `"SWT_NO_DYNAMIC_LINKING"` compiler -conditional for your platform in both Package.swift and CompilerSettings.cmake, -then define the `sectionBegin` and `sectionEnd` symbols in Discovery.cpp: +conditional for your platform in both `Package.swift` and +`CompilerSettings.cmake`, then define the symbols `testContentSectionBegin`, +`testContentSectionEnd`, `typeMetadataSectionBegin`, and +`typeMetadataSectionEnd` in `Discovery.cpp`. ```diff diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp // ... +#elif defined(macintosh) -+extern "C" const char sectionBegin __asm__("..."); -+extern "C" const char sectionEnd __asm__("..."); ++extern "C" const char testContentSectionBegin __asm__("..."); ++extern "C" const char testContentSectionEnd __asm__("..."); ++extern "C" const char typeMetadataSectionBegin __asm__("..."); ++extern "C" const char typeMetadataSectionEnd __asm__("..."); #else #warning Platform-specific implementation missing: Runtime test discovery unavailable (static) - static const char sectionBegin = 0; - static const char& sectionEnd = sectionBegin; + static const char testContentSectionBegin = 0; + static const char& testContentSectionEnd = testContentSectionBegin; + static const char typeMetadataSectionBegin = 0; + static const char& typeMetadataSectionEnd = testContentSectionBegin; #endif ``` @@ -195,27 +236,6 @@ respectively. Their linker-level names will be platform-dependent: refer to the linker documentation for your platform to determine what names to place in the `__asm__` attribute applied to each. -If you can't use `__asm__` on your platform, you can declare these symbols as -C++ references to linker-defined symbols: - -```diff -diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp - // ... -+#elif defined(macintosh) -+extern "C" const char __linker_defined_begin_symbol; -+extern "C" const char __linker_defined_end_symbol; -+static const auto& sectionBegin = __linker_defined_begin_symbol; -+static const auto& sectionEnd = __linker_defined_end_symbol; - #else - #warning Platform-specific implementation missing: Runtime test discovery unavailable (static) - static const char sectionBegin = 0; - static const char& sectionEnd = sectionBegin; - #endif -``` - -The names of `__linker_defined_begin_symbol` and `__linker_defined_end_symbol` -in this example are, as with the shorter implementation, platform-dependent. - ## C++ stub implementations Some symbols defined in C and C++ headers, especially "complex" macros, cannot From b19cbe001e47bfd511dab94c5e4cf12d4711bee4 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Fri, 10 Jan 2025 15:21:33 -0500 Subject: [PATCH 10/10] Update Sources/_TestingInternals/Discovery.cpp Co-authored-by: Stuart Montgomery --- Sources/_TestingInternals/Discovery.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/Discovery.cpp b/Sources/_TestingInternals/Discovery.cpp index c7de11c3f..08ffc5f47 100644 --- a/Sources/_TestingInternals/Discovery.cpp +++ b/Sources/_TestingInternals/Discovery.cpp @@ -28,7 +28,7 @@ extern "C" const char typeMetadataSectionEnd __asm__("__stop_swift5_type_metadat static const char testContentSectionBegin = 0; static const char& testContentSectionEnd = testContentSectionBegin; static const char typeMetadataSectionBegin = 0; -static const char& typeMetadataSectionEnd = testContentSectionBegin; +static const char& typeMetadataSectionEnd = typeMetadataSectionBegin; #endif /// The bounds of the test content section statically linked into the image