mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
480 lines
15 KiB
Swift
480 lines
15 KiB
Swift
// RUN: %empty-directory(%t)
|
|
// RUN: %target-build-swift %s -target %target-swift-5.1-abi-triple -o %t/voucher_propagation
|
|
// RUN: %target-codesign %t/voucher_propagation
|
|
// RUN: env MallocStackLogging=1 %target-run %t/voucher_propagation
|
|
|
|
// REQUIRES: executable_test
|
|
// REQUIRES: concurrency
|
|
|
|
// Use objc_interop as a proxy for voucher support in the OS.
|
|
// REQUIRES: objc_interop
|
|
|
|
// REQUIRES: concurrency_runtime
|
|
// UNSUPPORTED: back_deployment_runtime
|
|
|
|
// REQUIRES: OS=macosx
|
|
|
|
import Darwin
|
|
import Dispatch // expected-warning {{add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'Dispatch'}}
|
|
import StdlibUnittest
|
|
|
|
// These are an attempt to simulate some kind of async work, and the
|
|
// computations being performed are not intended to actually make any kind of
|
|
// sense.
|
|
actor Accumulator {
|
|
let label: String
|
|
var x = 0
|
|
|
|
init(label: String) {
|
|
self.label = label
|
|
}
|
|
|
|
func add(_ n: Int, _ voucher: voucher_t?) async {
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(voucher, currentVoucher)
|
|
print("\(n) \(label)", voucher: currentVoucher)
|
|
os_release(currentVoucher)
|
|
x += n
|
|
usleep(1000)
|
|
}
|
|
|
|
func add<T: Sequence>(sequence: T, _ voucher: voucher_t?) async -> Int where T.Element == Int {
|
|
for n in sequence {
|
|
await add(n, voucher)
|
|
}
|
|
return x
|
|
}
|
|
|
|
func get() -> Int { x }
|
|
}
|
|
|
|
actor Combiner {
|
|
var accumulators: [Accumulator] = []
|
|
|
|
func add(accumulator: Accumulator) {
|
|
accumulators.append(accumulator)
|
|
}
|
|
|
|
func sum() async -> Int {
|
|
var sum = 0
|
|
for a in accumulators {
|
|
fputs("Accumulating from \(ptrstr(a))\n", stderr)
|
|
sum += await a.get()
|
|
}
|
|
return sum
|
|
}
|
|
}
|
|
|
|
actor Counter {
|
|
var n = 0
|
|
|
|
func increment() {
|
|
n += 1
|
|
}
|
|
|
|
func get() -> Int { n }
|
|
}
|
|
|
|
@available(SwiftStdlib 6.1, *)
|
|
actor ActorWithSelfIsolatedDeinit {
|
|
let expectedVoucher: voucher_t?
|
|
let group: DispatchGroup
|
|
|
|
init(expectedVoucher: voucher_t?, group: DispatchGroup) {
|
|
self.expectedVoucher = expectedVoucher
|
|
self.group = group
|
|
}
|
|
|
|
isolated deinit {
|
|
expectTrue(isCurrentExecutor(self.unownedExecutor))
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(expectedVoucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
group.leave()
|
|
}
|
|
}
|
|
|
|
@globalActor actor AnotherActor: GlobalActor {
|
|
static let shared = AnotherActor()
|
|
|
|
func performTesting(_ work: @Sendable () -> Void) {
|
|
work()
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 6.1, *)
|
|
actor ActorWithDeinitIsolatedOnAnother {
|
|
let expectedVoucher: voucher_t?
|
|
let group: DispatchGroup
|
|
|
|
init(expectedVoucher: voucher_t?, group: DispatchGroup) {
|
|
self.expectedVoucher = expectedVoucher
|
|
self.group = group
|
|
}
|
|
|
|
@AnotherActor
|
|
deinit {
|
|
expectTrue(isCurrentExecutor(AnotherActor.shared.unownedExecutor))
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(expectedVoucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
group.leave()
|
|
}
|
|
}
|
|
|
|
@available(SwiftStdlib 6.1, *)
|
|
class ClassWithIsolatedDeinit {
|
|
let expectedVoucher: voucher_t?
|
|
let group: DispatchGroup
|
|
|
|
init(expectedVoucher: voucher_t?, group: DispatchGroup) {
|
|
self.expectedVoucher = expectedVoucher
|
|
self.group = group
|
|
}
|
|
|
|
@AnotherActor
|
|
deinit {
|
|
expectTrue(isCurrentExecutor(AnotherActor.shared.unownedExecutor))
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(expectedVoucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
group.leave()
|
|
}
|
|
}
|
|
|
|
// Make a nice string for a pointer, like what %p would produce in printf.
|
|
func ptrstr<T>(_ ptr: T) -> String {
|
|
"0x" + String(unsafeBitCast(ptr, to: UInt.self), radix: 16)
|
|
}
|
|
|
|
// Print a voucher to stderr with an optional label.
|
|
func print(_ s: String = "", voucher: voucher_t?) {
|
|
fputs("\(s) - \(ptrstr(voucher))\n", stderr)
|
|
}
|
|
|
|
// Look up a symbol using dlsym and cast the result to an arbitrary type.
|
|
func lookup<T>(_ name: String) -> T {
|
|
let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
|
|
let result = dlsym(RTLD_DEFAULT, name)
|
|
return unsafeBitCast(result, to: T.self)
|
|
}
|
|
|
|
// We'll use os_activity calls to make our test vouchers. The calls aren't in
|
|
// the headers so we need to look them up dynamically.
|
|
let _os_activity_create = lookup("_os_activity_create")
|
|
as @convention(c) (UnsafeRawPointer, UnsafePointer<CChar>, UnsafeRawPointer,
|
|
UInt32) -> voucher_t?
|
|
let OS_ACTIVITY_NONE = lookup("_os_activity_none") as UnsafeRawPointer
|
|
let OS_ACTIVITY_FLAG_DETACHED = 1 as UInt32
|
|
|
|
// Look up the voucher calls we'll be using. Vouchers are ObjC objects, but we
|
|
// want total control over their memory management, so we'll treat them as raw
|
|
// pointers instead, and manually manage their memory.
|
|
typealias voucher_t = UnsafeMutableRawPointer
|
|
let voucher_copy = lookup("voucher_copy") as @convention(c) () -> voucher_t?
|
|
let voucher_adopt = lookup("voucher_adopt") as @convention(c) (voucher_t?)
|
|
-> voucher_t?
|
|
let os_retain = lookup("os_retain") as @convention(c) (voucher_t?) -> voucher_t?
|
|
let os_release = lookup("os_release") as @convention(c) (voucher_t?) -> Void
|
|
|
|
let isCurrentExecutor = lookup("swift_task_isCurrentExecutor") as @convention(thin) (UnownedSerialExecutor) -> Bool
|
|
|
|
// Run some async code with test vouchers. Wait for the async code to complete,
|
|
// then verify that the vouchers aren't leaked.
|
|
func withVouchers(call: @Sendable @escaping (voucher_t?, voucher_t?, voucher_t?)
|
|
async -> Void) {
|
|
// We'll store weak pointers to the vouchers to verify that they don't leak.
|
|
// If the voucher was deallocated after we're done, then the weak references
|
|
// will be nil. We can't use Swift `weak` references since we're treating
|
|
// vouchers as raw pointers. We'll use the ObjC runtime APIs directly. Those
|
|
// require a stable address for the storage, so we'll allocate that manually.
|
|
let weakPtrsAllocation = UnsafeMutablePointer<AnyObject?>.allocate(capacity: 3)
|
|
weakPtrsAllocation.initialize(repeating: nil, count: 3)
|
|
let weakPtrs = AutoreleasingUnsafeMutablePointer<AnyObject?>(weakPtrsAllocation)
|
|
|
|
// Helper function to store three vouchers into the weak storage.
|
|
func storeWeak(vouchers: [voucher_t?]) {
|
|
for (i, voucher) in vouchers.enumerated() {
|
|
objc_storeWeak(weakPtrs + i, unsafeBitCast(voucher, to: AnyObject?.self))
|
|
}
|
|
}
|
|
|
|
// Make convenient pointers to the individual weak variables.
|
|
let weak1 = AutoreleasingUnsafeMutablePointer<AnyObject?>(weakPtrs + 0)
|
|
let weak2 = AutoreleasingUnsafeMutablePointer<AnyObject?>(weakPtrs + 1)
|
|
let weak3 = AutoreleasingUnsafeMutablePointer<AnyObject?>(weakPtrs + 2)
|
|
do {
|
|
let v1 = _os_activity_create(#dsohandle, "Swift Test Voucher 1", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED)
|
|
let v2 = _os_activity_create(#dsohandle, "Swift Test Voucher 2", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED)
|
|
let v3 = _os_activity_create(#dsohandle, "Swift Test Voucher 3", OS_ACTIVITY_NONE, OS_ACTIVITY_FLAG_DETACHED)
|
|
|
|
print("Created v1", voucher: v1)
|
|
print("Created v2", voucher: v2)
|
|
print("Created v3", voucher: v3)
|
|
|
|
// Place the vouchers in the weak variables and verify that it worked.
|
|
storeWeak(vouchers: [v1, v2, v3])
|
|
autoreleasepool {
|
|
expectNotNil(objc_loadWeak(weak1))
|
|
expectNotNil(objc_loadWeak(weak2))
|
|
expectNotNil(objc_loadWeak(weak3))
|
|
}
|
|
|
|
// Start the async call in the background, waiting for it to complete.
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
Task {
|
|
await call(v1, v2, v3)
|
|
|
|
// Clear any voucher that the call adopted.
|
|
adopt(voucher: nil)
|
|
group.leave() // expected-complete-tns-warning {{capture of 'group' with non-Sendable type 'DispatchGroup' in a '@Sendable' closure}}
|
|
}
|
|
group.wait()
|
|
|
|
// Release what should be the last reference to the vouchers.
|
|
os_release(v1)
|
|
os_release(v2)
|
|
os_release(v3)
|
|
}
|
|
func now() -> UInt64 {
|
|
return clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
|
|
}
|
|
// The vouchers may take a moment to be destroyed, as background threads
|
|
// finish what they're doing. Wait for the voucher weak references to become
|
|
// nil. We'll give them ten seconds, and otherwise assume they've leaked.
|
|
let start = now();
|
|
var allNil = false;
|
|
while !allNil && now() - start < 10_000_000_000 {
|
|
autoreleasepool {
|
|
allNil = objc_loadWeak(weak1) == nil
|
|
&& objc_loadWeak(weak2) == nil
|
|
&& objc_loadWeak(weak3) == nil
|
|
}
|
|
}
|
|
|
|
// If any weak reference contains non-nil at this point, a voucher has leaked.
|
|
expectNil(objc_loadWeak(weak1))
|
|
expectNil(objc_loadWeak(weak2))
|
|
expectNil(objc_loadWeak(weak3))
|
|
|
|
// Clean up the weak references.
|
|
storeWeak(vouchers: [nil, nil, nil])
|
|
weakPtrsAllocation.deallocate()
|
|
}
|
|
|
|
// Adopt the given voucher on the current thread. This takes the voucher at +0
|
|
// and handles the memory management around voucher_adopt.
|
|
func adopt(voucher: voucher_t?) {
|
|
os_release(voucher_adopt(os_retain(voucher)))
|
|
}
|
|
|
|
let tests = TestSuite("Voucher Propagation")
|
|
|
|
if #available(SwiftStdlib 5.1, *) {
|
|
tests.test("simple voucher propagation") {
|
|
withVouchers { v1, v2, v3 in
|
|
let a = Accumulator(label: "a: ")
|
|
adopt(voucher: v1)
|
|
await a.add(42, v1)
|
|
}
|
|
}
|
|
|
|
tests.test("voucher propagation with a simple async let") {
|
|
withVouchers { v1, v2, v3 in
|
|
let a1 = Accumulator(label: "a1: ")
|
|
let a2 = Accumulator(label: "a2: ")
|
|
|
|
adopt(voucher: v1)
|
|
async let r1: () = a1.add(42, v1)
|
|
adopt(voucher: v2)
|
|
async let r2: () = a2.add(42, v2)
|
|
_ = await (r1, r2)
|
|
}
|
|
}
|
|
|
|
tests.test("Task {} voucher propagation") {
|
|
withVouchers { v1, v2, v3 in
|
|
let a = Accumulator(label: "a: ")
|
|
adopt(voucher: v1)
|
|
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
Task {
|
|
await a.add(42, v1)
|
|
group.leave()
|
|
}
|
|
group.wait()
|
|
}
|
|
}
|
|
|
|
tests.test("Task.detached {} voucher propagation") {
|
|
withVouchers { v1, v2, v3 in
|
|
let a = Accumulator(label: "a: ")
|
|
adopt(voucher: v1)
|
|
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
Task.detached {
|
|
// Task.detached should NOT propagate vouchers, so tell add to look for
|
|
// nil.
|
|
await a.add(42, nil)
|
|
group.leave()
|
|
}
|
|
group.wait()
|
|
}
|
|
}
|
|
|
|
tests.test("complex voucher propagation") {
|
|
withVouchers { v1, v2, v3 in
|
|
fputs("Hello, whirled.\n", stderr)
|
|
|
|
let a1 = Accumulator(label: "a1: ")
|
|
let a2 = Accumulator(label: "a2: ")
|
|
|
|
adopt(voucher: v1)
|
|
|
|
@Sendable
|
|
func go(_ log: String, _ v: voucher_t?, _ a: Accumulator, _ n: Int) async -> Int {
|
|
print("Starting \(log): ", voucher: v)
|
|
return await a.add(sequence: (n*10)..<(n*10+10), v)
|
|
}
|
|
|
|
adopt(voucher: v1)
|
|
async let r1 = go("1 a1", v1, a1, 1)
|
|
adopt(voucher: v2)
|
|
async let r2 = go("2 a2", v2, a2, 2)
|
|
|
|
async let r3 = go("3 a1", v2, a1, 3)
|
|
adopt(voucher: v1)
|
|
async let r4 = go("4 a2", v1, a2, 4)
|
|
|
|
adopt(voucher: v1)
|
|
async let r5 = go("5 a1", v1, a1, 5)
|
|
async let r6 = go("6 a2", v1, a2, 6)
|
|
|
|
adopt(voucher: v2)
|
|
async let r7 = go("7 a1", v2, a1, 7)
|
|
async let r8 = go("8 a2", v2, a2, 8)
|
|
|
|
adopt(voucher: v3)
|
|
async let r9 = go("9 a1", v3, a1, 9)
|
|
async let r10 = go("10 a2", v3, a2, 10)
|
|
fputs("async let results: \((await r1, await r2, await r3, await r4, await r5, await r6, await r7, await r8, await r9, await r10))\n", stderr)
|
|
|
|
fputs("\(await a2.get())\n", stderr)
|
|
|
|
let combiner = Combiner()
|
|
async let add1: () = await combiner.add(accumulator: a1)
|
|
async let add2: () = await combiner.add(accumulator: a2)
|
|
_ = await (add1, add2)
|
|
fputs("combiner 1 - \(await combiner.sum())\n", stderr)
|
|
async let add3: () = await combiner.add(accumulator: a1)
|
|
async let add4: () = await combiner.add(accumulator: a2)
|
|
_ = await (add3, add4)
|
|
fputs("combiner 2 - \(await combiner.sum())\n", stderr)
|
|
for n in 0..<10 {
|
|
async let a = combiner.sum()
|
|
async let b = combiner.sum()
|
|
fputs("combiner loop \(n) - \(await a + b)\n", stderr)
|
|
}
|
|
}
|
|
}
|
|
|
|
tests.test("voucher propagation with mixed concurrency/dispatch code") {
|
|
withVouchers {v1, v2, v3 in
|
|
let a = Accumulator(label: "a: ")
|
|
let group = DispatchGroup()
|
|
let n = Counter()
|
|
let limit = 100
|
|
group.enter()
|
|
@Sendable func detachedTask() async {
|
|
let voucher = [v1, v2, v3][await n.get() % 3]
|
|
adopt(voucher: voucher)
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(voucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
DispatchQueue.global().async {
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(voucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
|
|
Task {
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(voucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
|
|
async let g: () = withTaskGroup(of: Void.self, body: { group in
|
|
for _ in 0..<10 {
|
|
group.async {
|
|
let currentVoucher = voucher_copy()
|
|
expectEqual(voucher, currentVoucher)
|
|
os_release(currentVoucher)
|
|
}
|
|
}
|
|
})
|
|
async let add: () = a.add(42, voucher)
|
|
_ = await (g, add)
|
|
|
|
if await n.get() >= limit {
|
|
group.leave() // expected-warning 2{{capture of 'group' with non-Sendable type 'DispatchGroup' in a '@Sendable' closure}}
|
|
} else {
|
|
await n.increment()
|
|
await detachedTask()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await detachedTask()
|
|
group.wait()
|
|
}
|
|
}
|
|
|
|
if #available(SwiftStdlib 6.1, *) {
|
|
tests.test("voucher propagation in isolated deinit [fast path]") {
|
|
withVouchers { v1, v2, v3 in
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
group.enter()
|
|
group.enter()
|
|
Task {
|
|
await AnotherActor.shared.performTesting {
|
|
adopt(voucher: v1)
|
|
_ = ClassWithIsolatedDeinit(expectedVoucher: v1, group: group)
|
|
}
|
|
await AnotherActor.shared.performTesting {
|
|
adopt(voucher: v2)
|
|
_ = ActorWithSelfIsolatedDeinit(expectedVoucher: v2, group: group)
|
|
}
|
|
await AnotherActor.shared.performTesting {
|
|
adopt(voucher: v3)
|
|
_ = ActorWithDeinitIsolatedOnAnother(expectedVoucher: v3, group: group)
|
|
}
|
|
}
|
|
group.wait()
|
|
}
|
|
}
|
|
|
|
tests.test("voucher propagation in isolated deinit [slow path]") {
|
|
withVouchers { v1, v2, v3 in
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
group.enter()
|
|
Task {
|
|
do {
|
|
adopt(voucher: v1)
|
|
_ = ActorWithDeinitIsolatedOnAnother(expectedVoucher: v1, group: group)
|
|
}
|
|
do {
|
|
adopt(voucher: v2)
|
|
_ = ClassWithIsolatedDeinit(expectedVoucher: v2, group: group)
|
|
}
|
|
}
|
|
group.wait()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
runAllTests()
|