Skip to content
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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 113 additions & 0 deletions stdlib/public/Reflection/Sources/Reflection/Attribute.swift
@@ -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 {}
Expand Up @@ -16,6 +16,7 @@ list(APPEND SWIFT_REFLECTION_SWIFT_FLAGS
"-parse-stdlib")

add_swift_target_library(swiftReflection ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS_STDLIB
Attribute.swift
Case.swift
Field.swift
GenericArguments.swift
Expand Down
2 changes: 2 additions & 0 deletions stdlib/public/Reflection/Sources/_Runtime/CMakeLists.txt
Expand Up @@ -68,6 +68,8 @@ add_swift_target_library(swift_Runtime ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS_ST
ExistentialContainer.swift
Functions.swift
HeapObject.swift
ImageInspection.cpp
ImageInspection.swift
WitnessTable.swift

C_COMPILE_FLAGS
Expand Down
59 changes: 59 additions & 0 deletions stdlib/public/Reflection/Sources/_Runtime/ImageInspection.cpp
@@ -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);
Copy link
Contributor

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, when snapshot() is called (or rather, my enumerating equivalent I described in another comment), you get a snapshot of that, and just walk it.

Copy link
Member

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.

}

extern "C" SWIFT_CC(swift)
void initializeDyldLookup() {
static swift::once_t token;
swift::once(token, []{
_dyld_register_func_for_add_image(imageFunc);
Copy link
Contributor

Choose a reason for hiding this comment

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

You should use objc_addLoadImageFunc() here, and only use _dyld_register_func_for_add_image() if the former is not available. The dyld function's callback may be called before the Objective-C runtime has finished initializing its own data structures. See ImageInspectionMachO.cpp for similar code.

});
}

#endif
175 changes: 175 additions & 0 deletions stdlib/public/Reflection/Sources/_Runtime/ImageInspection.swift
@@ -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()
Copy link
Contributor

@grynspan grynspan Feb 7, 2023

Choose a reason for hiding this comment

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

This call is unsafe in production—it can race with enumerateSections(). It should only be used for tests where the set of loaded libraries is controlled. :(


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")
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
}