Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
163 changes: 105 additions & 58 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,109 @@
import PackageDescription

let package = Package(
name: "SnapshotPreviews",
platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "PreviewGallery",
type: .static, // Replace this to build dynamic
targets: ["PreviewGallery"]),
// Test library to import in your XCTest target.
// This is the only library that depends on XCTest.framework
.library(
name: "SnapshottingTests",
type: .static, // Replace this to build dynamic
targets: ["SnapshottingTests"]),
// Link the main app to this target to use custom snapshot settings
// This lib does not get inserted when running tests to avoid
// duplicate symbols.
.library(
name: "SnapshotPreferences",
targets: ["SnapshotPreferences"]),
// Core functionality for snapshotting exported from the internal package
.library(
name: "SnapshotPreviewsCore",
targets: ["SnapshotPreviewsCore"]),
// Dynamic library that your main app will have inserted to generate previews
.library(
name: "Snapshotting",
type: .dynamic,
targets: ["Snapshotting"]),
],
dependencies: [
.package(url: "https://github.com/swhitty/FlyingFox.git", exact: "0.16.0"),
.package(url: "https://github.com/EmergeTools/AccessibilitySnapshot.git", exact: "1.0.2"),
.package(url: "https://github.com/EmergeTools/SimpleDebugger.git", exact: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// Target that provides the XCTest
.target(name: "SnapshottingTestsObjc", dependencies: [.product(name: "SimpleDebugger", package: "SimpleDebugger", condition: .when(platforms: [.iOS, .macOS, .macCatalyst]))]),
.target(name: "SnapshottingTests", dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"]),
.target(name: "SnapshotSharedModels"),
// Core functionality
.target(name: "SnapshotPreviewsCore", dependencies: ["PreviewsSupport", "SnapshotSharedModels", .product(name: "AccessibilitySnapshotCore", package: "AccessibilitySnapshot", condition: .when(platforms: [.iOS, .macCatalyst]))]),
.target(name: "SnapshotPreferences", dependencies: ["SnapshotSharedModels"]),
// Inserted dylib
.target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]),
// Swift code in the inserted dylib
.target(name: "SnapshottingSwift", dependencies: ["SnapshotPreviewsCore", .product(name: "FlyingFox", package: "FlyingFox")]),
.target(name: "PreviewGallery", dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]),
.binaryTarget(
name: "PreviewsSupport",
path: "PreviewsSupport/PreviewsSupport.xcframework"),
.testTarget(
name: "SnapshotPreviewsTests",
dependencies: ["SnapshotPreviewsCore"]),
],
cxxLanguageStandard: .cxx11
name: "SnapshotPreviews",
platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v9)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "PreviewGallery",
type: .static, // Replace this to build dynamic
targets: ["PreviewGallery"]
),
// Test library to import in your XCTest target.
// This is the only library that depends on XCTest.framework
.library(
name: "SnapshottingTests",
type: .static, // Replace this to build dynamic
targets: ["SnapshottingTests"]
),
// Link the main app to this target to use custom snapshot settings
// This lib does not get inserted when running tests to avoid
// duplicate symbols.
.library(
name: "SnapshotPreferences",
targets: ["SnapshotPreferences"]
),
// Core functionality for snapshotting exported from the internal package
.library(
name: "SnapshotPreviewsCore",
targets: ["SnapshotPreviewsCore"]
),
// Dynamic library that your main app will have inserted to generate previews
.library(
name: "Snapshotting",
type: .dynamic,
targets: ["Snapshotting"]
),
],
dependencies: [
.package(url: "https://github.com/swhitty/FlyingFox.git", exact: "0.16.0"),
.package(
url: "https://github.com/EmergeTools/AccessibilitySnapshot.git",
exact: "1.0.2"
),
.package(
url: "https://github.com/EmergeTools/SimpleDebugger.git",
exact: "1.0.0"
),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// Target that provides the XCTest
.target(
name: "SnapshottingTestsObjc",
dependencies: [
.product(
name: "SimpleDebugger",
package: "SimpleDebugger",
condition: .when(platforms: [.iOS, .macOS, .macCatalyst])
)
]
),
.target(
name: "SnapshottingTests",
dependencies: ["SnapshotPreviewsCore", "SnapshottingTestsObjc"]
),
.target(name: "SnapshotSharedModels"),
// Core functionality
.target(
name: "SnapshotPreviewsCore",
dependencies: [
"PreviewsSupport", "SnapshotSharedModels",
.product(
name: "AccessibilitySnapshotCore",
package: "AccessibilitySnapshot",
condition: .when(platforms: [.iOS, .macCatalyst])
),
]
),
.target(
name: "SnapshotPreferences",
dependencies: ["SnapshotSharedModels"]
),
// Inserted dylib
.target(name: "Snapshotting", dependencies: ["SnapshottingSwift"]),
// Swift code in the inserted dylib
.target(
name: "SnapshottingSwift",
dependencies: [
"SnapshotPreviewsCore",
.product(name: "FlyingFox", package: "FlyingFox"),
]
),
.target(
name: "PreviewGallery",
dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]
),
.binaryTarget(
name: "PreviewsSupport",
path: "PreviewsSupport/PreviewsSupport.xcframework"
),
.testTarget(
name: "SnapshotPreviewsTests",
dependencies: ["SnapshotPreviewsCore", "SnapshotPreferences"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I reformatted this file via swiftlint (can revert if you want) and added some dependencies for our testTarget.

),
],
cxxLanguageStandard: .cxx11
)
184 changes: 93 additions & 91 deletions Sources/SnapshotPreviewsCore/ConformanceLookup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,107 +8,109 @@
import Foundation
import MachO

private func getTypeName(descriptor: UnsafePointer<TargetModuleContextDescriptor>) -> String? {
let flags = descriptor.pointee.flags
var parentName: String? = nil
if descriptor.pointee.parent != 0 {
let parent = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.parent)!).advanced(by: Int(descriptor.pointee.parent))
if abs(descriptor.pointee.parent) % 2 == 1 {
return nil
}
parentName = getTypeName(descriptor: parent.assumingMemoryBound(to: TargetModuleContextDescriptor.self))
}
switch flags.kind {
case .Module, .Enum, .Struct, .Class:
let name = UnsafeRawPointer(descriptor)
.advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.name)!)
.advanced(by: Int(descriptor.pointee.name))
.assumingMemoryBound(to: CChar.self)
let typeName = String(cString: name)
if let parentName = parentName {
return "\(parentName).\(typeName)"
}
return typeName
default:
return parentName
}
}

public typealias LookupResult = (name: String, accessor: () -> UInt64, proto: String)

private func parseConformance(conformance: UnsafePointer<ProtocolConformanceDescriptor>) -> LookupResult? {
let flags = conformance.pointee.conformanceFlags

guard case .DirectTypeDescriptor = flags.kind else {
return nil
}

guard conformance.pointee.protocolDescriptor % 2 == 1 else {
return nil
}
let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1)
let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout<ProtocolConformanceDescriptor>.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset)
let address = jumpPtr.load(as: UInt64.self)

// Address will be 0 if the protocol is not available (such as only defined on a newer OS)
guard address != 0 else {
return nil
public class ConformanceLookup {
public static func getPreviewTypes() -> [LookupResult] {
let images = _dyld_image_count()
var types = [LookupResult]()
for i in 0..<images {
let imageName = String(cString: _dyld_get_image_name(i))
// System frameworks on the simulator are in Xcode.app/Contents/** (Although Xcode could be renamed like Xcode-beta.app so don't check for that specifically)
guard !imageName.contains(".simruntime") && !imageName.contains(".app/Contents/Developer") && !imageName.starts(with: "/usr/lib/") else {
continue
}

let header = _dyld_get_image_header(i)!
var size: UInt = 0
let sectStart = UnsafeRawPointer(
getsectiondata(
UnsafeRawPointer(header).assumingMemoryBound(to: mach_header_type.self),
"__TEXT",
"__swift5_proto",
&size))?.assumingMemoryBound(to: Int32.self)
if var sectData = sectStart {
for _ in 0..<Int(size)/MemoryLayout<Int32>.size {
let conformance = UnsafeRawPointer(sectData)
.advanced(by: Int(sectData.pointee))
.assumingMemoryBound(to: ProtocolConformanceDescriptor.self)
if let result = parseConformance(conformance: conformance) {
types.append(result)
}
sectData = sectData.successor()
}
}
}
return types
}
let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))!
let proto = protoPtr.load(as: ProtocolDescriptor.self)
let namePtr = protoPtr.advanced(by: MemoryLayout<ProtocolDescriptor>.offset(of: \.name)!).advanced(by: Int(proto.name))
let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self))
guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else {

private static func parseConformance(conformance: UnsafePointer<ProtocolConformanceDescriptor>) -> LookupResult? {
let flags = conformance.pointee.conformanceFlags

guard case .DirectTypeDescriptor = flags.kind else {
return nil
}

guard conformance.pointee.protocolDescriptor % 2 == 1 else {
return nil
}
let descriptorOffset = Int(conformance.pointee.protocolDescriptor & ~1)
let jumpPtr = UnsafeRawPointer(conformance).advanced(by: MemoryLayout<ProtocolConformanceDescriptor>.offset(of: \.protocolDescriptor)!).advanced(by: descriptorOffset)
let address = jumpPtr.load(as: UInt64.self)

// Address will be 0 if the protocol is not available (such as only defined on a newer OS)
guard address != 0 else {
return nil
}
let protoPtr = UnsafeRawPointer(bitPattern: UInt(address))!
let proto = protoPtr.load(as: ProtocolDescriptor.self)
let namePtr = protoPtr.advanced(by: MemoryLayout<ProtocolDescriptor>.offset(of: \.name)!).advanced(by: Int(proto.name))
let protocolName = String(cString: namePtr.assumingMemoryBound(to: CChar.self))
guard ["PreviewProvider", "PreviewRegistry"].contains(protocolName) else {
return nil
}

let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout<ProtocolConformanceDescriptor>.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor))

let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self)
if let name = getTypeName(descriptor: descriptor),
[ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) {
let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction))
let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self)
return (name, accessFunction, protocolName)
}
return nil
}

let typeDescriptorPointer = UnsafeRawPointer(conformance).advanced(by: MemoryLayout<ProtocolConformanceDescriptor>.offset(of: \.nominalTypeDescriptor)!).advanced(by: Int(conformance.pointee.nominalTypeDescriptor))

let descriptor = typeDescriptorPointer.assumingMemoryBound(to: TargetModuleContextDescriptor.self)
if let name = getTypeName(descriptor: descriptor),
[ContextDescriptorKind.Class, ContextDescriptorKind.Struct, ContextDescriptorKind.Enum].contains(descriptor.pointee.flags.kind) {
let accessFunctionPointer = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.accessFunction)!).advanced(by: Int(descriptor.pointee.accessFunction))
let accessFunction = unsafeBitCast(accessFunctionPointer, to: (@convention(c) () -> UInt64).self)
return (name, accessFunction, protocolName)

private static func getTypeName(descriptor: UnsafePointer<TargetModuleContextDescriptor>) -> String? {
let flags = descriptor.pointee.flags
var parentName: String? = nil
if descriptor.pointee.parent != 0 {
let parent = UnsafeRawPointer(descriptor).advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.parent)!).advanced(by: Int(descriptor.pointee.parent))
if abs(descriptor.pointee.parent) % 2 == 1 {
return nil
}
parentName = getTypeName(descriptor: parent.assumingMemoryBound(to: TargetModuleContextDescriptor.self))
}
switch flags.kind {
case .Module, .Enum, .Struct, .Class:
let name = UnsafeRawPointer(descriptor)
.advanced(by: MemoryLayout<TargetModuleContextDescriptor>.offset(of: \.name)!)
.advanced(by: Int(descriptor.pointee.name))
.assumingMemoryBound(to: CChar.self)
let typeName = String(cString: name)
if let parentName = parentName {
return "\(parentName).\(typeName)"
}
return typeName
default:
return parentName
}
}
return nil
}

#if arch(i386) || arch(arm) || arch(arm64_32)
typealias mach_header_type = mach_header
#else
typealias mach_header_type = mach_header_64
#endif

public func getPreviewTypes() -> [LookupResult] {
let images = _dyld_image_count()
var types = [LookupResult]()
for i in 0..<images {
let imageName = String(cString: _dyld_get_image_name(i))
// System frameworks on the simulator are in Xcode.app/Contents/** (Although Xcode could be renamed like Xcode-beta.app so don't check for that specifically)
guard !imageName.contains(".simruntime") && !imageName.contains(".app/Contents/Developer") && !imageName.starts(with: "/usr/lib/") else {
continue
}

let header = _dyld_get_image_header(i)!
var size: UInt = 0
let sectStart = UnsafeRawPointer(
getsectiondata(
UnsafeRawPointer(header).assumingMemoryBound(to: mach_header_type.self),
"__TEXT",
"__swift5_proto",
&size))?.assumingMemoryBound(to: Int32.self)
if var sectData = sectStart {
for _ in 0..<Int(size)/MemoryLayout<Int32>.size {
let conformance = UnsafeRawPointer(sectData)
.advanced(by: Int(sectData.pointee))
.assumingMemoryBound(to: ProtocolConformanceDescriptor.self)
if let result = parseConformance(conformance: conformance) {
types.append(result)
}
sectData = sectData.successor()
}
}
}
return types
}
Loading