New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Reflection] Implement Runtime Attributes #63168
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Swift | ||
import _Runtime | ||
|
||
/// A namespace used for working with runtime attributes. | ||
@available(SwiftStdlib 5.9, *) | ||
@frozen | ||
public enum Attribute { | ||
/// Get all the instances of a runtime attribute wherever it's attached to. | ||
/// | ||
/// Example: | ||
/// | ||
/// @runtimeMetadata | ||
/// struct Field { | ||
/// let name: String | ||
/// | ||
/// init<T, U>(attachedTo: KeyPath<T, U>, _ name: String) { | ||
/// self.name = name | ||
/// } | ||
/// } | ||
/// | ||
/// struct Dog { | ||
/// @Field("dog_breed") | ||
/// let breed: String | ||
/// } | ||
/// | ||
/// let fields = Attribute.allInstances(of: Field.self) | ||
/// | ||
/// for field in fields { | ||
/// print(field.name) // "dog_breed" | ||
/// } | ||
/// | ||
/// - Parameters: | ||
/// - type: The type of the attribute that is attached to various sources. | ||
/// - Returns: A sequence of attribute instances of `type` in no particular | ||
/// order. | ||
@available(SwiftStdlib 5.9, *) | ||
public static func allInstances<T>(of type: T.Type) -> AttributeInstances<T> { | ||
let meta = Metadata(T.self) | ||
|
||
guard meta.kind == .struct || | ||
meta.kind == .enum || | ||
meta.kind == .class else { | ||
return AttributeInstances([]) | ||
} | ||
|
||
return ImageInspection.withAttributeCache { | ||
let attrDescriptor = meta.type.descriptor.base | ||
|
||
guard let fnPtrs = $0[attrDescriptor] else { | ||
return AttributeInstances([]) | ||
} | ||
|
||
return AttributeInstances(fnPtrs) | ||
} | ||
} | ||
} | ||
|
||
/// A sequence wrapper over some runtime attribute instances. | ||
/// | ||
/// Instances of `AttributeInstances` are created with the | ||
/// `Attribute.allInstances(of:)` function. | ||
@available(SwiftStdlib 5.9, *) | ||
@frozen | ||
public struct AttributeInstances<T> { | ||
@usableFromInline | ||
let fnPtrs: [UnsafeRawPointer] | ||
|
||
@usableFromInline | ||
var index = 0 | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
init(_ fnPtrs: [UnsafeRawPointer]) { | ||
self.fnPtrs = fnPtrs | ||
} | ||
} | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
extension AttributeInstances: IteratorProtocol { | ||
@available(SwiftStdlib 5.9, *) | ||
@inlinable | ||
public mutating func next() -> T? { | ||
while index < fnPtrs.endIndex { | ||
let fnPtr = fnPtrs[index] | ||
index += 1 | ||
|
||
typealias AttributeFn = @convention(thin) () -> T? | ||
|
||
let fn = unsafeBitCast(fnPtr, to: AttributeFn.self) | ||
|
||
guard let attribute = fn() else { | ||
continue | ||
} | ||
|
||
return attribute | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
extension AttributeInstances: Sequence {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
#include <stdint.h> | ||
|
||
#include "swift/Runtime/Config.h" | ||
#include "swift/Threading/Once.h" | ||
|
||
extern "C" | ||
void registerAttributes(const void *section, intptr_t size); | ||
|
||
//===----------------------------------------------------------------------===// | ||
// Mach-O Image Inspection | ||
//===----------------------------------------------------------------------===// | ||
|
||
#if defined(__MACH__) | ||
|
||
#include <mach-o/dyld.h> | ||
#include <mach-o/getsect.h> | ||
|
||
#if __POINTER_WIDTH__ == 64 | ||
typedef mach_header_64 mach_header_platform; | ||
#else | ||
typedef mach_header mach_header_platform; | ||
#endif | ||
|
||
void lookupSection(const struct mach_header *header, const char *segment, | ||
const char *section, | ||
void (*registerFunc)(const void *, intptr_t)) { | ||
unsigned long size = 0; | ||
|
||
auto sectionData = getsectiondata( | ||
reinterpret_cast<const mach_header_platform *>(header), segment, section, | ||
&size); | ||
|
||
registerFunc(sectionData, size); | ||
} | ||
|
||
void imageFunc(const struct mach_header *header, intptr_t size) { | ||
lookupSection(header, "__TEXT", "__swift5_rattrs", registerAttributes); | ||
} | ||
|
||
extern "C" SWIFT_CC(swift) | ||
void initializeDyldLookup() { | ||
static swift::once_t token; | ||
swift::once(token, []{ | ||
_dyld_register_func_for_add_image(imageFunc); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should use |
||
}); | ||
} | ||
|
||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the Swift project authors | ||
// Licensed under Apache License v2.0 with Runtime Library Exception | ||
// | ||
// See https://swift.org/LICENSE.txt for license information | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import Swift | ||
import _SwiftRuntimeShims | ||
|
||
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) | ||
import SwiftShims | ||
#endif | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
@frozen | ||
public enum ImageInspection {} | ||
|
||
//===----------------------------------------------------------------------===// | ||
// Mach-O Image Lookup | ||
//===----------------------------------------------------------------------===// | ||
|
||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) | ||
@_silgen_name("initializeDyldLookup") | ||
func initializeDyldLookup() | ||
#endif | ||
|
||
//===----------------------------------------------------------------------===// | ||
// ELF and COFF Image Lookup | ||
//===----------------------------------------------------------------------===// | ||
|
||
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) | ||
func enumerateSections<K: Hashable, V>( | ||
_ body: (MetadataSections) -> ( | ||
MetadataSectionRange, | ||
(UnsafeRawPointer, Int) -> () | ||
) | ||
) { | ||
swift_enumerateAllMetadataSections({ sections, _ in | ||
guard let sections = sections else { | ||
return true | ||
} | ||
|
||
let metadataSections = sections.assumingMemoryBound( | ||
to: MetadataSections.self | ||
) | ||
|
||
guard metadataSections.version >= 3 else { | ||
return false | ||
} | ||
|
||
let (range, register) = body(metadataSections) | ||
|
||
register(range.start, range.length) | ||
|
||
return true | ||
}, nil) | ||
} | ||
#endif | ||
|
||
//===----------------------------------------------------------------------===// | ||
// Runtime Attributes | ||
//===----------------------------------------------------------------------===// | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
struct AttributeCache { | ||
#if !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS)) | ||
var imageCount = 0 | ||
#endif | ||
var map: [ContextDescriptor: [UnsafeRawPointer]] = [:] | ||
} | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
var attributeCache: Lock<AttributeCache> = .create(with: .init()) | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
extension ImageInspection { | ||
@available(SwiftStdlib 5.9, *) | ||
public static func withAttributeCache<T>( | ||
_ body: @Sendable ([ContextDescriptor: [UnsafeRawPointer]]) throws -> T | ||
) rethrows -> T { | ||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) | ||
initializeDyldLookup() | ||
|
||
return try attributeCache.withLock { | ||
try body($0.map) | ||
} | ||
#else | ||
return try attributeCache.withLock { | ||
// If our cached image count is less than what's being reported by the | ||
// Swift runtime, then we need to reset our cache and enumerate all of | ||
// the images again for updated attributes. | ||
let currentImageCount = swift_getMetadataSectionCount() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This call is unsafe in production—it can race with |
||
|
||
if $0.imageCount < currentImageCount { | ||
$0.map.removeAll(keepingCapacity: true) | ||
|
||
// We enumerate inside the lock because we need to ensure that the | ||
// thread who gets this lock first is able to at least initialize the | ||
// cache before other threads. | ||
enumerateSections { | ||
($0.swift5_runtime_attributes, registerAttributes(_:_:)) | ||
} | ||
|
||
$0.imageCount = currentImageCount | ||
} | ||
|
||
return try body($0.map) | ||
} | ||
#endif | ||
} | ||
} | ||
|
||
@available(SwiftStdlib 5.9, *) | ||
@_cdecl("registerAttributes") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This puts the function in the global C namespace with the name "registerAttributes". Did you want "swift_registerAttributes" instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Internally, yes, but these symbols are not exported. This is only an issue if a dependency of this library declares some symbol named this. I can change this if you want. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my other comments. I think we can refactor this code to not need this function at all. I'd be happy to offer an additional commit to this PR, or to open a separate PR, to show what I mean. :) |
||
func registerAttributes(_ section: UnsafeRawPointer, _ size: Int) { | ||
var address = section | ||
let end = address + size | ||
|
||
while address < end { | ||
// Flags (always 0 for right now) | ||
address += MemoryLayout<Int32>.size | ||
|
||
let attributeAddr = address.relativeIndirectableAddress( | ||
as: ContextDescriptor.self | ||
) | ||
|
||
let attributeDescriptor = ContextDescriptor(attributeAddr) | ||
|
||
// Attribute Context Descriptor | ||
address += MemoryLayout<Int32>.size | ||
|
||
let numberOfInstances = address.loadUnaligned(as: UInt32.self) | ||
|
||
// Number of records this attribute has | ||
address += MemoryLayout<Int32>.size | ||
|
||
var fnPtrs: [UnsafeRawPointer] = [] | ||
fnPtrs.reserveCapacity(Int(numberOfInstances)) | ||
|
||
for _ in 0 ..< numberOfInstances { | ||
// The type this attribute was on (not always an actual type) | ||
address += MemoryLayout<Int32>.size | ||
|
||
var fnRecord = address.relativeDirectAddress(as: UnsafeRawPointer.self) | ||
fnRecord += MemoryLayout<RelativeDirectPointer<Void>>.size * 3 | ||
|
||
let fnPtr = fnRecord.relativeDirectAddress(as: UnsafeRawPointer.self) | ||
fnPtrs.append(fnPtr) | ||
|
||
// Function pointer to attribute initializer | ||
address += MemoryLayout<Int32>.size | ||
} | ||
|
||
let copyFnPtrs = fnPtrs | ||
|
||
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) | ||
attributeCache.withLock { | ||
$0.map[attributeDescriptor, default: []].append(contentsOf: copyFnPtrs) | ||
} | ||
#else | ||
// For non-Darwin platforms, we have to iterate the images and register | ||
// the attributes inside of the attribute cache. So, we're already in the | ||
// lock by the time this function gets called, so it is safe to access the | ||
// value in our lock without locking. | ||
attributeCache.withUnsafeValue { | ||
$0.map[attributeDescriptor, default: []].append(contentsOf: copyFnPtrs) | ||
} | ||
#endif | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the right thing to do here is have a
ConcurrentReadableArray<std::pair<const void *, size_t>>
that you add to each time_dyld_register_func_for_add_image()
calls its callback. Then, whensnapshot()
is called (or rather, my enumerating equivalent I described in another comment), you get a snapshot of that, and just walk it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a better approach to me as well.