mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
[SerialExecutor] SerialExecutor.checkIsolated() to check its own tracking for isolation checks (#71172)
This commit is contained in:
committed by
GitHub
parent
233c1675cf
commit
86f5441294
@@ -155,8 +155,12 @@ public:
|
||||
return reinterpret_cast<DefaultActor*>(Identity);
|
||||
}
|
||||
|
||||
bool hasSerialExecutorWitnessTable() const {
|
||||
return !isGeneric() && !isDefaultActor();
|
||||
}
|
||||
|
||||
const SerialExecutorWitnessTable *getSerialExecutorWitnessTable() const {
|
||||
assert(!isGeneric() && !isDefaultActor());
|
||||
assert(hasSerialExecutorWitnessTable());
|
||||
auto table = Implementation & WitnessTableMask;
|
||||
return reinterpret_cast<const SerialExecutorWitnessTable*>(table);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// This source file is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
|
||||
// Copyright (c) 2014 - 2024 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
|
||||
@@ -47,6 +47,38 @@ bool useLegacySwiftValueUnboxingInCasting();
|
||||
/// if present.
|
||||
bool useLegacySwiftObjCHashing();
|
||||
|
||||
/// Legacy semantics allowed for the `swift_task_reportUnexpectedExecutor` to
|
||||
/// only log a warning. This changes in future releases and this function
|
||||
/// will fatal error always.
|
||||
///
|
||||
/// Similarly, the internal runtime function
|
||||
/// `swift_task_isCurrentExecutor(expected)` was previously allowed to return
|
||||
/// `false`. In future releases it will call into `checkIsolated`, and CRASH
|
||||
/// when previously it would have returned false.
|
||||
///
|
||||
/// Because some applications were running with "isolation warnings" and
|
||||
/// those call into the `isCurrentExecutor` API and expected warnings to be
|
||||
/// logged, but they ignored those warnings we cannot make them crashing,
|
||||
/// and must check if the app was built against a new.
|
||||
///
|
||||
/// Old behavior:
|
||||
/// - `swift_task_isCurrentExecutorImpl` cannot crash and does NOT invoke
|
||||
/// `SerialExecutor.checkIsolated`
|
||||
/// - `swift_task_isCurrentExecutorImpl` does not invoke `checkIsolated`
|
||||
/// - logging a warning on concurrency violation is allowed
|
||||
///
|
||||
/// New behavior:
|
||||
/// - always fatal error in `swift_task_reportUnexpectedExecutor`
|
||||
/// - `swift_task_isCurrentExecutorImpl` will crash when it would have returned
|
||||
/// false
|
||||
/// - `swift_task_isCurrentExecutorImpl` does invoke `checkIsolated` when other
|
||||
/// checks failed
|
||||
///
|
||||
/// This can be overridden by using `SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL=1`
|
||||
/// or `SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=crash|nocrash`
|
||||
SWIFT_RUNTIME_STDLIB_SPI
|
||||
bool swift_bincompat_useLegacyNonCrashingExecutorChecks();
|
||||
|
||||
} // namespace bincompat
|
||||
|
||||
} // namespace runtime
|
||||
|
||||
@@ -715,6 +715,11 @@ void swift_task_enqueue(Job *job, SerialExecutorRef executor);
|
||||
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
|
||||
void swift_task_enqueueGlobal(Job *job);
|
||||
|
||||
/// Invoke an executor's `checkIsolated` or otherwise equivalent API,
|
||||
/// that will crash if the current executor is NOT the passed executor.
|
||||
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
|
||||
void swift_task_checkIsolated(SerialExecutorRef executor);
|
||||
|
||||
/// A count in nanoseconds.
|
||||
using JobDelay = unsigned long long;
|
||||
|
||||
@@ -729,6 +734,9 @@ void swift_task_enqueueGlobalWithDeadline(long long sec, long long nsec,
|
||||
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
|
||||
void swift_task_enqueueMainExecutor(Job *job);
|
||||
|
||||
/// WARNING: This method is expected to CRASH when caller is not on the
|
||||
/// expected executor.
|
||||
///
|
||||
/// Return true if the caller is running in a Task on the passed Executor.
|
||||
SWIFT_EXPORT_FROM(swift_Concurrency) SWIFT_CC(swift)
|
||||
bool swift_task_isOnExecutor(
|
||||
@@ -781,6 +789,12 @@ SWIFT_CC(swift) void (*swift_task_enqueueGlobalWithDeadline_hook)(
|
||||
int clock, Job *job,
|
||||
swift_task_enqueueGlobalWithDeadline_original original);
|
||||
|
||||
typedef SWIFT_CC(swift) void (*swift_task_checkIsolated_original)(SerialExecutorRef executor);
|
||||
SWIFT_EXPORT_FROM(swift_Concurrency)
|
||||
SWIFT_CC(swift) void (*swift_task_checkIsolated_hook)(
|
||||
SerialExecutorRef executor, swift_task_checkIsolated_original original);
|
||||
|
||||
|
||||
typedef SWIFT_CC(swift) bool (*swift_task_isOnExecutor_original)(
|
||||
HeapObject *executor,
|
||||
const Metadata *selfType,
|
||||
|
||||
@@ -22,10 +22,12 @@
|
||||
#include "../CompatibilityOverride/CompatibilityOverride.h"
|
||||
#include "swift/ABI/Actor.h"
|
||||
#include "swift/ABI/Task.h"
|
||||
#include "TaskPrivate.h"
|
||||
#include "swift/Basic/ListMerger.h"
|
||||
#include "swift/Concurrency/Actor.h"
|
||||
#include "swift/Runtime/AccessibleFunction.h"
|
||||
#include "swift/Runtime/Atomic.h"
|
||||
#include "swift/Runtime/Bincompat.h"
|
||||
#include "swift/Runtime/Casting.h"
|
||||
#include "swift/Runtime/DispatchShims.h"
|
||||
#include "swift/Threading/Mutex.h"
|
||||
@@ -313,39 +315,178 @@ bool _task_serialExecutor_isSameExclusiveExecutionContext(
|
||||
const Metadata *selfType,
|
||||
const SerialExecutorWitnessTable *wtable);
|
||||
|
||||
// We currently still support "legacy mode" in which isCurrentExecutor is NOT
|
||||
// allowed to crash, because it is used to power "log warnings" data race
|
||||
// detector. This mode is going away in Swift 6, but until then we allow this.
|
||||
// This override exists primarily to be able to test both code-paths.
|
||||
enum IsCurrentExecutorCheckMode: unsigned {
|
||||
/// The default mode when an app was compiled against "new" enough SDK.
|
||||
/// It allows crashing in isCurrentExecutor, and calls into `checkIsolated`.
|
||||
Default_UseCheckIsolated_AllowCrash,
|
||||
/// Legacy mode; Primarily to support old applications which used data race
|
||||
/// detector with "warning" mode, which is no longer supported. When such app
|
||||
/// is re-compiled against a new SDK, it will see crashes in what was
|
||||
/// previously warnings; however, until until recompiled, warnings will be
|
||||
/// used, and `checkIsolated` cannot be invoked.
|
||||
Legacy_NoCheckIsolated_NonCrashing,
|
||||
};
|
||||
static IsCurrentExecutorCheckMode isCurrentExecutorMode =
|
||||
Default_UseCheckIsolated_AllowCrash;
|
||||
|
||||
|
||||
// Shimming call to Swift runtime because Swift Embedded does not have
|
||||
// these symbols defined
|
||||
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
|
||||
#if !SWIFT_CONCURRENCY_EMBEDDED
|
||||
swift::runtime::bincompat::
|
||||
swift_bincompat_useLegacyNonCrashingExecutorChecks();
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check override of executor checking mode.
|
||||
static void checkIsCurrentExecutorMode(void *context) {
|
||||
auto useLegacyMode =
|
||||
swift_bincompat_useLegacyNonCrashingExecutorChecks();
|
||||
|
||||
// Potentially, override the platform detected mode, primarily used in tests.
|
||||
#if SWIFT_STDLIB_HAS_ENVIRON
|
||||
const char *modeStr = getenv("SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE");
|
||||
if (!modeStr)
|
||||
return;
|
||||
|
||||
if (strcmp(modeStr, "nocrash") == 0) {
|
||||
useLegacyMode = Legacy_NoCheckIsolated_NonCrashing;
|
||||
} else if (strcmp(modeStr, "crash") == 0) {
|
||||
useLegacyMode = Default_UseCheckIsolated_AllowCrash;
|
||||
} // else, just use the platform detected mode
|
||||
#endif // SWIFT_STDLIB_HAS_ENVIRON
|
||||
isCurrentExecutorMode = useLegacyMode ? Legacy_NoCheckIsolated_NonCrashing
|
||||
: Default_UseCheckIsolated_AllowCrash;
|
||||
}
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef executor) {
|
||||
static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef expectedExecutor) {
|
||||
auto current = ExecutorTrackingInfo::current();
|
||||
|
||||
// To support old applications on apple platforms which assumed this call
|
||||
// does not crash, try to use a more compatible mode for those apps.
|
||||
static swift::once_t checkModeToken;
|
||||
swift::once(checkModeToken, checkIsCurrentExecutorMode, nullptr);
|
||||
|
||||
if (!current) {
|
||||
// TODO(ktoso): checking the "is main thread" is not correct, main executor can be not main thread, relates to rdar://106188692
|
||||
return executor.isMainExecutor() && isExecutingOnMainThread();
|
||||
// We have no current executor, i.e. we are running "outside" of Swift
|
||||
// Concurrency. We could still be running on a thread/queue owned by
|
||||
// the expected executor however, so we need to try a bit harder before
|
||||
// we fail.
|
||||
|
||||
// Are we expecting the main executor and are using the main thread?
|
||||
if (expectedExecutor.isMainExecutor() && isExecutingOnMainThread()) {
|
||||
// Due to compatibility with pre-checkIsolated code, we cannot remove
|
||||
// this special handling. CheckIsolated can handle this if the expected
|
||||
// executor is the main queue / main executor, however, if we cannot call
|
||||
// checkIsolated we cannot rely on it to handle this.
|
||||
// TODO: consider removing this branch when `useCrashingCheckIsolated=true`
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, as last resort, let the expected executor check using
|
||||
// external means, as it may "know" this thread is managed by it etc.
|
||||
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
|
||||
swift_task_checkIsolated(expectedExecutor);
|
||||
// checkIsolated did not crash, so we are on the right executor, after all!
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto currentExecutor = current->getActiveExecutor();
|
||||
if (currentExecutor == executor) {
|
||||
SerialExecutorRef currentExecutor = current->getActiveExecutor();
|
||||
|
||||
// Fast-path: the executor is exactly the same memory address;
|
||||
// We assume executors do not come-and-go appearing under the same address,
|
||||
// and treat pointer equality of executors as good enough to assume the executor.
|
||||
if (currentExecutor == expectedExecutor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (executor.isComplexEquality()) {
|
||||
// Fast-path, specialize the common case of comparing two main executors.
|
||||
if (currentExecutor.isMainExecutor() && expectedExecutor.isMainExecutor()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the expected executor is "default" then we should have matched
|
||||
// by pointer equality already with the current executor.
|
||||
if (expectedExecutor.isDefaultActor()) {
|
||||
// If the expected executor is a default actor, it makes no sense to try
|
||||
// the 'checkIsolated' call, it must be equal to the other actor, or it is
|
||||
// not the same isolation domain.
|
||||
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedExecutor.isMainExecutor() && !currentExecutor.isMainExecutor()) {
|
||||
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
|
||||
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected MainActor executor");
|
||||
return false;
|
||||
} else if (!expectedExecutor.isMainExecutor() && currentExecutor.isMainExecutor()) {
|
||||
// TODO: Invoke checkIsolated() on "main" SerialQueue once it implements `checkIsolated`, otherwise messages will be sub-par and hard to address
|
||||
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption; Expected not-MainActor executor");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedExecutor.isComplexEquality()) {
|
||||
if (!swift_compareWitnessTables(
|
||||
reinterpret_cast<const WitnessTable*>(currentExecutor.getSerialExecutorWitnessTable()),
|
||||
reinterpret_cast<const WitnessTable*>(executor.getSerialExecutorWitnessTable()))) {
|
||||
reinterpret_cast<const WitnessTable*>(expectedExecutor.getSerialExecutorWitnessTable()))) {
|
||||
// different witness table, we cannot invoke complex equality call
|
||||
return false;
|
||||
}
|
||||
|
||||
// Avoid passing nulls to Swift for the isSame check:
|
||||
if (!currentExecutor.getIdentity() || !executor.getIdentity()) {
|
||||
if (!currentExecutor.getIdentity() || !expectedExecutor.getIdentity()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _task_serialExecutor_isSameExclusiveExecutionContext(
|
||||
auto result = _task_serialExecutor_isSameExclusiveExecutionContext(
|
||||
currentExecutor.getIdentity(),
|
||||
executor.getIdentity(),
|
||||
expectedExecutor.getIdentity(),
|
||||
swift_getObjectType(currentExecutor.getIdentity()),
|
||||
executor.getSerialExecutorWitnessTable());
|
||||
expectedExecutor.getSerialExecutorWitnessTable());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// This provides a last-resort check by giving the expected SerialExecutor the
|
||||
// chance to perform a check using some external knowledge if perhaps we are,
|
||||
// after all, on this executor, but the Swift concurrency runtime was just not
|
||||
// aware.
|
||||
//
|
||||
// Unless handled in `swift_task_checkIsolated` directly, this should call
|
||||
// through to the executor's `SerialExecutor.checkIsolated`.
|
||||
//
|
||||
// This call is expected to CRASH, unless it has some way of proving that
|
||||
// we're actually indeed running on this executor.
|
||||
//
|
||||
// For example, when running outside of Swift concurrency tasks, but trying to
|
||||
// `MainActor.assumeIsolated` while executing DIRECTLY on the main dispatch
|
||||
// queue, this allows Dispatch to check for this using its own tracking
|
||||
// mechanism, and thus allow the assumeIsolated to work correctly, even though
|
||||
// the code executing is not even running inside a Task.
|
||||
//
|
||||
// Note that this only works because the closure in assumeIsolated is
|
||||
// synchronous, and will not cause suspensions, as that would require the
|
||||
// presence of a Task.
|
||||
// compat_invoke_swift_task_checkIsolated(expectedExecutor);
|
||||
if (isCurrentExecutorMode == Default_UseCheckIsolated_AllowCrash) {
|
||||
swift_task_checkIsolated(expectedExecutor);
|
||||
// The checkIsolated call did not crash, so we are on the right executor.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Using legacy mode, if no explicit executor match worked, we assume `false`
|
||||
assert(isCurrentExecutorMode == Legacy_NoCheckIsolated_NonCrashing);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -353,7 +494,14 @@ static bool swift_task_isCurrentExecutorImpl(SerialExecutorRef executor) {
|
||||
/// 0 - no logging
|
||||
/// 1 - warn on each instance
|
||||
/// 2 - fatal error
|
||||
static unsigned unexpectedExecutorLogLevel = 2;
|
||||
///
|
||||
/// NOTE: The default behavior on Apple platforms depends on the SDK version
|
||||
/// an application was linked to. Since Swift 6 the default is to crash,
|
||||
/// and the logging behavior is no longer available.
|
||||
static unsigned unexpectedExecutorLogLevel =
|
||||
swift_bincompat_useLegacyNonCrashingExecutorChecks()
|
||||
? 1 // legacy apps default to the logging mode, and cannot use `checkIsolated`
|
||||
: 2; // new apps will only crash upon concurrency violations, and will call into `checkIsolated`
|
||||
|
||||
static void checkUnexpectedExecutorLogLevel(void *context) {
|
||||
#if SWIFT_STDLIB_HAS_ENVIRON
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
/// swift_task_enqueueGlobalImpl
|
||||
/// swift_task_enqueueGlobalWithDelayImpl
|
||||
/// swift_task_enqueueMainExecutorImpl
|
||||
/// swift_task_checkIsolatedImpl
|
||||
/// as well as any cooperative-executor-specific functions in the runtime.
|
||||
///
|
||||
///===------------------------------------------------------------------===///
|
||||
@@ -136,6 +137,13 @@ static void insertDelayedJob(Job *newJob, JobDeadline deadline) {
|
||||
*position = newJob;
|
||||
}
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
|
||||
_task_serialExecutor_checkIsolated(
|
||||
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
|
||||
executor.getSerialExecutorWitnessTable());
|
||||
}
|
||||
|
||||
/// Insert a job into the cooperative global queue with a delay.
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
/// swift_task_enqueueGlobalImpl
|
||||
/// swift_task_enqueueGlobalWithDelayImpl
|
||||
/// swift_task_enqueueMainExecutorImpl
|
||||
/// swift_task_checkIsolated
|
||||
/// as well as any Dispatch-specific functions for the runtime.
|
||||
///
|
||||
///===------------------------------------------------------------------===///
|
||||
|
||||
#if SWIFT_CONCURRENCY_ENABLE_DISPATCH
|
||||
#include "swift/Runtime/HeapObject.h"
|
||||
#include <dispatch/dispatch.h>
|
||||
#if defined(_WIN32)
|
||||
#include <Windows.h>
|
||||
@@ -233,7 +235,7 @@ static void swift_task_enqueueGlobalImpl(Job *job) {
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_enqueueGlobalWithDelayImpl(JobDelay delay,
|
||||
Job *job) {
|
||||
Job *job) {
|
||||
assert(job && "no job provided");
|
||||
|
||||
dispatch_function_t dispatchFunction = &__swift_run_job;
|
||||
@@ -383,3 +385,56 @@ void swift::swift_task_enqueueOnDispatchQueue(Job *job,
|
||||
auto queue = reinterpret_cast<dispatch_queue_t>(_queue);
|
||||
dispatchEnqueue(queue, job, (dispatch_qos_class_t)priority, queue);
|
||||
}
|
||||
|
||||
/// Recognize if the SerialExecutor is specifically a `DispatchSerialQueue`
|
||||
/// by comparing witness tables and return it if true.
|
||||
static dispatch_queue_s *getAsDispatchSerialQueue(SerialExecutorRef executor) {
|
||||
if (!executor.hasSerialExecutorWitnessTable()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto executorWitnessTable = reinterpret_cast<const WitnessTable *>(
|
||||
executor.getSerialExecutorWitnessTable());
|
||||
auto serialQueueWitnessTable = reinterpret_cast<const WitnessTable *>(
|
||||
_swift_task_getDispatchQueueSerialExecutorWitnessTable());
|
||||
|
||||
if (swift_compareWitnessTables(executorWitnessTable,
|
||||
serialQueueWitnessTable)) {
|
||||
return reinterpret_cast<dispatch_queue_s *>(executor.getIdentity());
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/// If the executor is a `DispatchSerialQueue` we're able to invoke the
|
||||
/// dispatch's precondition API directly -- this is more efficient than going
|
||||
/// through the runtime call to end up calling the same API, and also allows us
|
||||
/// to perform this assertion on earlier platforms, where the `checkIsolated`
|
||||
/// requirement/witness was not shipping yet.
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
|
||||
// If it is the main executor, compare with the Main queue
|
||||
if (executor.isMainExecutor()) {
|
||||
dispatch_assert_queue(dispatch_get_main_queue());
|
||||
return;
|
||||
}
|
||||
|
||||
// if able to, use the checkIsolated implementation in Swift
|
||||
if (executor.hasSerialExecutorWitnessTable()) {
|
||||
_task_serialExecutor_checkIsolated(
|
||||
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
|
||||
executor.getSerialExecutorWitnessTable());
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto queue = getAsDispatchSerialQueue(executor)) {
|
||||
// if the executor was not SerialExecutor for some reason but we're able
|
||||
// to get a queue from it anyway, use the assert directly on it.
|
||||
dispatch_assert_queue(queue); // TODO(concurrency): could we report a better message here somehow?
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, we have no way to check, so report an error
|
||||
// TODO: can we swift_getTypeName(swift_getObjectType(executor.getIdentity()), false).data safely in the message here?
|
||||
swift_Concurrency_fatalError(0, "Incorrect actor executor assumption");
|
||||
}
|
||||
|
||||
@@ -100,6 +100,43 @@ public protocol SerialExecutor: Executor {
|
||||
/// perspective–to execute code assuming one on the other.
|
||||
@available(SwiftStdlib 5.9, *)
|
||||
func isSameExclusiveExecutionContext(other: Self) -> Bool
|
||||
|
||||
/// Last resort "fallback" isolation check, called when the concurrency runtime
|
||||
/// is comparing executors e.g. during ``assumeIsolated()`` and is unable to prove
|
||||
/// serial equivalence between the expected (this object), and the current executor.
|
||||
///
|
||||
/// During executor comparison, the Swift concurrency runtime attempts to compare
|
||||
/// current and expected executors in a few ways (including "complex" equality
|
||||
/// between executors (see ``isSameExclusiveExecutionContext(other:)``), and if all
|
||||
/// those checks fail, this method is invoked on the expected executor.
|
||||
///
|
||||
/// This method MUST crash if it is unable to prove that the current execution
|
||||
/// context belongs to this executor. At this point usual executor comparison would
|
||||
/// have already failed, though the executor may have some external tracking of
|
||||
/// threads it owns, and may be able to prove isolation nevertheless.
|
||||
///
|
||||
/// A default implementation is provided that unconditionally crashes the
|
||||
/// program, and prevents calling code from proceeding with potentially
|
||||
/// not thread-safe execution.
|
||||
///
|
||||
/// - Warning: This method must crash and halt program execution if unable
|
||||
/// to prove the isolation of the calling context.
|
||||
@available(SwiftStdlib 6.0, *)
|
||||
func checkIsolated()
|
||||
|
||||
}
|
||||
|
||||
@available(SwiftStdlib 6.0, *)
|
||||
extension SerialExecutor {
|
||||
|
||||
@available(SwiftStdlib 6.0, *)
|
||||
public func checkIsolated() {
|
||||
#if !$Embedded
|
||||
fatalError("Unexpected isolation context, expected to be executing on \(Self.self)")
|
||||
#else
|
||||
Builtin.int_trap()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// An executor that may be used as preferred executor by a task.
|
||||
@@ -311,16 +348,26 @@ extension UnownedTaskExecutor: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the current task is running on the expected executor.
|
||||
/// Returns either `true` or will CRASH if called from a different executor
|
||||
/// than the passed `executor`.
|
||||
///
|
||||
/// This method will attempt to verify the current executor against `executor`,
|
||||
/// and as a last-resort call through to `SerialExecutor.checkIsolated`.
|
||||
///
|
||||
/// This method will never return `false`. It either can verify we're on the
|
||||
/// correct executor, or will crash the program. It should be used in
|
||||
/// isolation correctness guaranteeing APIs.
|
||||
///
|
||||
/// Generally, Swift programs should be constructed such that it is statically
|
||||
/// known that a specific executor is used, for example by using global actors or
|
||||
/// custom executors. However, in some APIs it may be useful to provide an
|
||||
/// additional runtime check for this, especially when moving towards Swift
|
||||
/// concurrency from other runtimes which frequently use such assertions.
|
||||
///
|
||||
/// - Parameter executor: The expected executor.
|
||||
@_spi(ConcurrencyExecutors)
|
||||
@available(SwiftStdlib 5.9, *)
|
||||
@_silgen_name("swift_task_isOnExecutor")
|
||||
@_silgen_name("swift_task_isOnExecutor") // This function will CRASH rather than return `false`!
|
||||
public func _taskIsOnExecutor<Executor: SerialExecutor>(_ executor: Executor) -> Bool
|
||||
|
||||
@_spi(ConcurrencyExecutors)
|
||||
@@ -361,6 +408,13 @@ internal func _task_serialExecutor_isSameExclusiveExecutionContext<E>(current cu
|
||||
currentExecutor.isSameExclusiveExecutionContext(other: executor)
|
||||
}
|
||||
|
||||
@available(SwiftStdlib 6.0, *)
|
||||
@_silgen_name("_task_serialExecutor_checkIsolated")
|
||||
internal func _task_serialExecutor_checkIsolated<E>(executor: E)
|
||||
where E: SerialExecutor {
|
||||
executor.checkIsolated()
|
||||
}
|
||||
|
||||
/// Obtain the executor ref by calling the executor's `asUnownedSerialExecutor()`.
|
||||
/// The obtained executor ref will have all the user-defined flags set on the executor.
|
||||
@available(SwiftStdlib 5.9, *)
|
||||
|
||||
@@ -106,11 +106,12 @@ extension Actor {
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: This method will CRASH in new runtime versions,
|
||||
// if it would have previously returned `false`.
|
||||
// It will call through to SerialExecutor.checkIsolated` as a last resort.
|
||||
let expectationCheck = _taskIsCurrentExecutor(self.unownedExecutor.executor)
|
||||
|
||||
// TODO: offer information which executor we actually got
|
||||
precondition(expectationCheck,
|
||||
// TODO: figure out a way to get the typed repr out of the unowned executor
|
||||
"Incorrect actor executor assumption; Expected '\(self.unownedExecutor)' executor. \(message())",
|
||||
file: file, line: line)
|
||||
}
|
||||
@@ -247,8 +248,6 @@ extension Actor {
|
||||
}
|
||||
|
||||
guard _taskIsCurrentExecutor(self.unownedExecutor.executor) else {
|
||||
// TODO: offer information which executor we actually got
|
||||
// TODO: figure out a way to get the typed repr out of the unowned executor
|
||||
let msg = "Incorrect actor executor assumption; Expected '\(self.unownedExecutor)' executor. \(message())"
|
||||
/// TODO: implement the logic in-place perhaps rather than delegating to precondition()?
|
||||
assertionFailure(msg, file: file, line: line) // short-cut so we get the exact same failure reporting semantics
|
||||
|
||||
@@ -90,6 +90,17 @@ SWIFT_CC(swift)
|
||||
void (*swift::swift_task_enqueueMainExecutor_hook)(
|
||||
Job *job, swift_task_enqueueMainExecutor_original original) = nullptr;
|
||||
|
||||
SWIFT_CC(swift)
|
||||
void (*swift::swift_task_checkIsolated_hook)(
|
||||
SerialExecutorRef executor,
|
||||
swift_task_checkIsolated_original original) = nullptr;
|
||||
|
||||
extern "C" SWIFT_CC(swift)
|
||||
bool _task_serialExecutor_checkIsolated(
|
||||
HeapObject *executor,
|
||||
const Metadata *selfType,
|
||||
const SerialExecutorWitnessTable *wtable);
|
||||
|
||||
#if SWIFT_CONCURRENCY_COOPERATIVE_GLOBAL_EXECUTOR
|
||||
#include "CooperativeGlobalExecutor.inc"
|
||||
#elif SWIFT_CONCURRENCY_ENABLE_DISPATCH
|
||||
@@ -132,6 +143,13 @@ void swift::swift_task_enqueueGlobalWithDeadline(
|
||||
swift_task_enqueueGlobalWithDeadlineImpl(sec, nsec, tsec, tnsec, clock, job);
|
||||
}
|
||||
|
||||
void swift::swift_task_checkIsolated(SerialExecutorRef executor) {
|
||||
if (swift_task_checkIsolated_hook)
|
||||
swift_task_checkIsolated_hook(executor, swift_task_checkIsolatedImpl);
|
||||
else
|
||||
swift_task_checkIsolatedImpl(executor);
|
||||
}
|
||||
|
||||
// Implemented in Swift because we need to obtain the user-defined flags on the executor ref.
|
||||
//
|
||||
// We could inline this with effort, though.
|
||||
@@ -154,6 +172,12 @@ TaskExecutorRef _task_executor_getTaskExecutorRef(
|
||||
const SerialExecutorWitnessTable *wtable);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
|
||||
/// WARNING: This method is expected to CRASH in new runtimes, and cannot be
|
||||
/// used to implement "log warnings" mode. We would need a new entry point to
|
||||
/// implement a "only log warnings" actor isolation checking mode, and it would
|
||||
/// no be able handle more complex situations, as `SerialExecutor.checkIsolated`
|
||||
/// is able to (by calling into dispatchPrecondition on old runtimes).
|
||||
SWIFT_CC(swift)
|
||||
static bool swift_task_isOnExecutorImpl(HeapObject *executor,
|
||||
const Metadata *selfType,
|
||||
|
||||
@@ -61,3 +61,10 @@ static void swift_task_enqueueMainExecutorImpl(Job *job) {
|
||||
swift_reportError(0, "operation unsupported without libdispatch: "
|
||||
"swift_task_enqueueMainExecutor");
|
||||
}
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_checkIsolatedImpl(SerialExecutorRef executor) {
|
||||
_task_serialExecutor_checkIsolated(
|
||||
executor.getIdentity(), swift_getObjectType(executor.getIdentity()),
|
||||
executor.getSerialExecutorWitnessTable());
|
||||
}
|
||||
|
||||
@@ -54,9 +54,7 @@ extension DistributedActor {
|
||||
let unownedExecutor = self.unownedExecutor
|
||||
let expectationCheck = _taskIsCurrentExecutor(unownedExecutor._executor)
|
||||
|
||||
// TODO: offer information which executor we actually got
|
||||
precondition(expectationCheck,
|
||||
// TODO: figure out a way to get the typed repr out of the unowned executor
|
||||
"Incorrect actor executor assumption; Expected '\(self.unownedExecutor)' executor. \(message())",
|
||||
file: file, line: line)
|
||||
}
|
||||
@@ -103,8 +101,6 @@ extension DistributedActor {
|
||||
|
||||
let unownedExecutor = self.unownedExecutor
|
||||
guard _taskIsCurrentExecutor(unownedExecutor._executor) else {
|
||||
// TODO: offer information which executor we actually got
|
||||
// TODO: figure out a way to get the typed repr out of the unowned executor
|
||||
let msg = "Incorrect actor executor assumption; Expected '\(unownedExecutor)' executor. \(message())"
|
||||
/// TODO: implement the logic in-place perhaps rather than delegating to precondition()?
|
||||
assertionFailure(msg, file: file, line: line) // short-cut so we get the exact same failure reporting semantics
|
||||
@@ -187,6 +183,12 @@ extension DistributedActor {
|
||||
}
|
||||
}
|
||||
|
||||
/// WARNING: This function will CRASH rather than return `false` in new runtimes
|
||||
///
|
||||
/// It eventually calls into `SerialExecutor.checkIsolated` which allows even
|
||||
/// for non Task code to assume isolation in certain situations, however this
|
||||
/// API cannot be made "return false", and instead will always crash if it
|
||||
/// were to return false.
|
||||
@available(SwiftStdlib 5.1, *)
|
||||
@usableFromInline
|
||||
@_silgen_name("swift_task_isCurrentExecutor")
|
||||
|
||||
@@ -255,6 +255,17 @@ bool useLegacySwiftObjCHashing() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// FIXME(concurrency): Once the release is announced, adjust the logic detecting the SDKs
|
||||
bool swift_bincompat_useLegacyNonCrashingExecutorChecks() {
|
||||
#if BINARY_COMPATIBILITY_APPLE
|
||||
return true; // For now, legacy behavior on Apple OSes
|
||||
#elif SWIFT_TARGET_OS_DARWIN
|
||||
return true; // For now, use legacy behavior on open-source builds for Apple platforms
|
||||
#else
|
||||
return false; // Always use the new behavior on non-Apple OSes
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace bincompat
|
||||
|
||||
} // namespace runtime
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// RUN: %target-run-simple-swift(-parse-as-library -Xfrontend -disable-availability-checking) | %FileCheck %s
|
||||
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: concurrency
|
||||
// REQUIRES: concurrency_runtime
|
||||
|
||||
// REQUIRES: libdispatch
|
||||
|
||||
// UNSUPPORTED: back_deployment_runtime
|
||||
// UNSUPPORTED: back_deploy_concurrency
|
||||
// UNSUPPORTED: use_os_stdlib
|
||||
// UNSUPPORTED: freestanding
|
||||
|
||||
final class NaiveQueueExecutor: SerialExecutor {
|
||||
init() {}
|
||||
|
||||
public func enqueue(_ job: consuming ExecutorJob) {
|
||||
job.runSynchronously(on: self.asUnownedSerialExecutor())
|
||||
}
|
||||
|
||||
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
|
||||
UnownedSerialExecutor(ordinary: self)
|
||||
}
|
||||
|
||||
func checkIsolated() {
|
||||
print("checkIsolated: pretend it is ok!")
|
||||
}
|
||||
}
|
||||
|
||||
actor ActorOnNaiveQueueExecutor {
|
||||
let executor: NaiveQueueExecutor
|
||||
|
||||
init() {
|
||||
self.executor = NaiveQueueExecutor()
|
||||
}
|
||||
|
||||
nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||
self.executor.asUnownedSerialExecutor()
|
||||
}
|
||||
|
||||
nonisolated func checkPreconditionIsolated() async {
|
||||
print("Before preconditionIsolated")
|
||||
self.preconditionIsolated()
|
||||
print("After preconditionIsolated")
|
||||
|
||||
print("Before assumeIsolated")
|
||||
self.assumeIsolated { iso in
|
||||
print("Inside assumeIsolated")
|
||||
}
|
||||
print("After assumeIsolated")
|
||||
}
|
||||
}
|
||||
|
||||
@main struct Main {
|
||||
static func main() async {
|
||||
if #available(SwiftStdlib 6.0, *) {
|
||||
let actor = ActorOnNaiveQueueExecutor()
|
||||
await actor.checkPreconditionIsolated()
|
||||
// CHECK: Before preconditionIsolated
|
||||
// CHECK-NEXT: checkIsolated: pretend it is ok!
|
||||
// CHECK-NEXT: After preconditionIsolated
|
||||
|
||||
// CHECK-NEXT: Before assumeIsolated
|
||||
// CHECK-NEXT: checkIsolated: pretend it is ok!
|
||||
// CHECK-NEXT: Inside assumeIsolated
|
||||
// CHECK-NEXT: After assumeIsolated
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// RUN: %target-run-simple-swift(-parse-as-library -Xfrontend -disable-availability-checking) | %FileCheck %s
|
||||
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: concurrency
|
||||
// REQUIRES: concurrency_runtime
|
||||
|
||||
// REQUIRES: libdispatch
|
||||
|
||||
// UNSUPPORTED: back_deployment_runtime
|
||||
// UNSUPPORTED: back_deploy_concurrency
|
||||
// UNSUPPORTED: use_os_stdlib
|
||||
// UNSUPPORTED: freestanding
|
||||
|
||||
final class NaiveQueueExecutor: SerialExecutor {
|
||||
init() {}
|
||||
|
||||
public func enqueue(_ job: consuming ExecutorJob) {
|
||||
job.runSynchronously(on: self.asUnownedSerialExecutor())
|
||||
}
|
||||
|
||||
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
|
||||
UnownedSerialExecutor(ordinary: self)
|
||||
}
|
||||
|
||||
func checkIsolated() {
|
||||
print("checkIsolated: pretend it is ok!")
|
||||
}
|
||||
}
|
||||
|
||||
actor ActorOnNaiveQueueExecutor {
|
||||
let executor: NaiveQueueExecutor
|
||||
|
||||
init() {
|
||||
self.executor = NaiveQueueExecutor()
|
||||
}
|
||||
|
||||
nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||
self.executor.asUnownedSerialExecutor()
|
||||
}
|
||||
|
||||
nonisolated func checkPreconditionIsolated() async {
|
||||
print("Before preconditionIsolated")
|
||||
self.preconditionIsolated()
|
||||
print("After preconditionIsolated")
|
||||
|
||||
print("Before assumeIsolated")
|
||||
self.assumeIsolated { iso in
|
||||
print("Inside assumeIsolated")
|
||||
}
|
||||
print("After assumeIsolated")
|
||||
}
|
||||
}
|
||||
|
||||
@main struct Main {
|
||||
static func main() async {
|
||||
if #available(SwiftStdlib 6.0, *) {
|
||||
let actor = ActorOnNaiveQueueExecutor()
|
||||
await actor.checkPreconditionIsolated()
|
||||
// CHECK: Before preconditionIsolated
|
||||
// CHECK-NEXT: checkIsolated: pretend it is ok!
|
||||
// CHECK-NEXT: After preconditionIsolated
|
||||
|
||||
// CHECK-NEXT: Before assumeIsolated
|
||||
// CHECK-NEXT: checkIsolated: pretend it is ok!
|
||||
// CHECK-NEXT: Inside assumeIsolated
|
||||
// CHECK-NEXT: After assumeIsolated
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// RUN: %empty-directory(%t)
|
||||
// RUN: %target-build-swift -Xfrontend -disable-availability-checking %import-libdispatch -parse-as-library %s -o %t/a.out
|
||||
// RUN: %target-codesign %t/a.out
|
||||
// RUN: %target-run %t/a.out
|
||||
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: concurrency
|
||||
// REQUIRES: concurrency_runtime
|
||||
|
||||
// REQUIRES: libdispatch
|
||||
|
||||
// UNSUPPORTED: back_deployment_runtime
|
||||
// UNSUPPORTED: back_deploy_concurrency
|
||||
// UNSUPPORTED: use_os_stdlib
|
||||
// UNSUPPORTED: freestanding
|
||||
|
||||
// FIXME(concurrency): rdar://119743909 fails in optimize tests.
|
||||
// UNSUPPORTED: swift_test_mode_optimize
|
||||
// UNSUPPORTED: swift_test_mode_optimize_size
|
||||
|
||||
import StdlibUnittest
|
||||
import Dispatch
|
||||
|
||||
// FIXME(concurrency): Dispatch should provide such implementation
|
||||
extension DispatchQueue { // which includes DispatchSerialQueue, when a platform has it
|
||||
public func checkIsolated() {
|
||||
dispatchPrecondition(condition: .onQueue(self))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// We only do the executor dance because missing 'asUnownedSerialExecutor'
|
||||
/// on DispatchSerialQueue on some platforms, so we can't make this test
|
||||
/// reliably just use a queue as the executor directly.
|
||||
final class NaiveQueueExecutor: SerialExecutor {
|
||||
let queue: DispatchQueue
|
||||
|
||||
init(queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
}
|
||||
|
||||
public func enqueue(_ unowned: UnownedJob) {
|
||||
queue.sync {
|
||||
unowned.runSynchronously(on: self.asUnownedSerialExecutor())
|
||||
}
|
||||
}
|
||||
|
||||
public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
|
||||
UnownedSerialExecutor(ordinary: self)
|
||||
}
|
||||
|
||||
func checkIsolated() {
|
||||
self.queue.checkIsolated()
|
||||
}
|
||||
}
|
||||
|
||||
actor ActorOnNaiveQueueExecutor {
|
||||
let queue: DispatchQueue
|
||||
let executor: NaiveQueueExecutor
|
||||
|
||||
init() {
|
||||
self.queue = DispatchQueue(label: "MyQueue")
|
||||
self.executor = NaiveQueueExecutor(queue: queue)
|
||||
}
|
||||
|
||||
nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||
self.executor.asUnownedSerialExecutor()
|
||||
}
|
||||
|
||||
nonisolated func checkPreconditionIsolated() async {
|
||||
print("Before queue.sync {}")
|
||||
self.queue.sync {
|
||||
print("Before preconditionIsolated")
|
||||
self.preconditionIsolated()
|
||||
print("After preconditionIsolated")
|
||||
|
||||
print("Before dispatchPrecondition")
|
||||
dispatchPrecondition(condition: .onQueue(self.queue))
|
||||
print("After preconditionIsolated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actor Other {
|
||||
func checkUnexpected(actor: some Actor) {
|
||||
actor.assumeIsolated { isolatedActor in
|
||||
print("OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(dispatch): DispatchSerialQueue conformance to SerialExecutor not available on all platforms yet
|
||||
//actor ActorOnQueue {
|
||||
// let queue: DispatchSerialQueue
|
||||
//
|
||||
// init() {
|
||||
// self.queue = DispatchSerialQueue(label: "MyQueue")
|
||||
// }
|
||||
//
|
||||
// nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||
// self.queue.asUnownedSerialExecutor()
|
||||
// }
|
||||
//
|
||||
// nonisolated func checkPreconditionIsolated() async {
|
||||
// print("Before queue.sync {}")
|
||||
// self.queue.sync {
|
||||
// print("Before preconditionIsolated")
|
||||
// self.queue.preconditionIsolated()
|
||||
// print("After preconditionIsolated")
|
||||
//
|
||||
// print("Before dispatchPrecondition")
|
||||
// dispatchPrecondition(condition: .onQueue(self.queue))
|
||||
// print("After preconditionIsolated")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@main struct Main {
|
||||
static func main() async {
|
||||
let tests = TestSuite("AssertPreconditionActorExecutorCheckIsolated")
|
||||
|
||||
if #available(SwiftStdlib 6.0, *) {
|
||||
let actorOnQueue = ActorOnNaiveQueueExecutor()
|
||||
|
||||
tests.test("\(ActorOnNaiveQueueExecutor.self): queue.sync { preconditionIsolated() } ") {
|
||||
await actorOnQueue.checkPreconditionIsolated()
|
||||
}
|
||||
|
||||
tests.test("\(Other.self): wrongly assume default actor on another actor's custom executor") {
|
||||
expectCrashLater() // this will call into dispatch precondition, no message
|
||||
let other = Other()
|
||||
await other.checkUnexpected(actor: actorOnQueue)
|
||||
}
|
||||
|
||||
tests.test("\(Other.self): correctly assume default actor on same default actor") {
|
||||
let other = Other()
|
||||
await other.checkUnexpected(actor: other)
|
||||
}
|
||||
|
||||
// FIXME(dispatch): DispatchSerialQueue conformance to SerialExecutor not available on all platforms yet
|
||||
// tests.test("\(ActorOnQueue.self): queue.sync { preconditionIsolated() } ") {
|
||||
// await ActorOnQueue().checkPreconditionIsolated()
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
await runAllTestsAsync()
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ actor MainFriend {
|
||||
}
|
||||
}
|
||||
|
||||
func checkAssumeSomeone(someone: Someone) /* synchronous */ {
|
||||
func checkAssumeSomeone(someone: SomeoneOnDefaultExecutor) /* synchronous */ {
|
||||
// someone.something // can't access, would need a hop but we can't
|
||||
someone.assumeIsolated { someone in
|
||||
let something = someone.something
|
||||
@@ -47,7 +47,7 @@ func checkAssumeSomeone(someone: Someone) /* synchronous */ {
|
||||
}
|
||||
}
|
||||
|
||||
actor Someone {
|
||||
actor SomeoneOnDefaultExecutor {
|
||||
func callCheckMainActor(echo: MainActorEcho) {
|
||||
checkAssumeMainActor(echo: echo)
|
||||
}
|
||||
@@ -62,12 +62,12 @@ actor Someone {
|
||||
}
|
||||
|
||||
actor SomeonesFriend {
|
||||
let someone: Someone
|
||||
let someone: SomeoneOnDefaultExecutor
|
||||
nonisolated var unownedExecutor: UnownedSerialExecutor {
|
||||
self.someone.unownedExecutor
|
||||
}
|
||||
|
||||
init(someone: Someone) {
|
||||
init(someone: SomeoneOnDefaultExecutor) {
|
||||
self.someone = someone
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ actor SomeonesFriend {
|
||||
}
|
||||
|
||||
actor CompleteStranger {
|
||||
let someone: Someone
|
||||
init(someone: Someone) {
|
||||
let someone: SomeoneOnDefaultExecutor
|
||||
init(someone: SomeoneOnDefaultExecutor) {
|
||||
self.someone = someone
|
||||
}
|
||||
|
||||
@@ -118,36 +118,36 @@ final class MainActorEcho {
|
||||
#if !os(WASI)
|
||||
tests.test("MainActor.assumeIsolated: wrongly assume the main executor, from actor on other executor") {
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected 'MainActor' executor.")
|
||||
await Someone().callCheckMainActor(echo: echo)
|
||||
await SomeoneOnDefaultExecutor().callCheckMainActor(echo: echo)
|
||||
}
|
||||
#endif
|
||||
|
||||
// === some Actor -------------------------------------------------------
|
||||
|
||||
let someone = Someone()
|
||||
let someone = SomeoneOnDefaultExecutor()
|
||||
#if !os(WASI)
|
||||
tests.test("assumeOnActorExecutor: wrongly assume someone's executor, from 'main() async'") {
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.SomeoneOnDefaultExecutor.")
|
||||
checkAssumeSomeone(someone: someone)
|
||||
}
|
||||
|
||||
tests.test("assumeOnActorExecutor: wrongly assume someone's executor, from MainActor method") {
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.SomeoneOnDefaultExecutor.")
|
||||
checkAssumeSomeone(someone: someone)
|
||||
}
|
||||
#endif
|
||||
|
||||
tests.test("assumeOnActorExecutor: assume someone's executor, from Someone") {
|
||||
tests.test("assumeOnActorExecutor: assume someone's executor, from SomeoneOnDefaultExecutor") {
|
||||
await someone.callCheckSomeone()
|
||||
}
|
||||
|
||||
tests.test("assumeOnActorExecutor: assume someone's executor, from actor on the Someone.unownedExecutor") {
|
||||
tests.test("assumeOnActorExecutor: assume someone's executor, from actor on the SomeoneOnDefaultExecutor.unownedExecutor") {
|
||||
await SomeonesFriend(someone: someone).callCheckSomeone()
|
||||
}
|
||||
|
||||
#if !os(WASI)
|
||||
tests.test("assumeOnActorExecutor: wrongly assume the main executor, from actor on other executor") {
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.Someone.")
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected same executor as a.SomeoneOnDefaultExecutor.")
|
||||
await CompleteStranger(someone: someone).callCheckSomeone()
|
||||
}
|
||||
#endif
|
||||
|
||||
94
test/Concurrency/Runtime/data_race_detection_crash.swift
Normal file
94
test/Concurrency/Runtime/data_race_detection_crash.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
// RUN: %empty-directory(%t)
|
||||
// RUN: %target-build-swift %import-libdispatch -Xfrontend -disable-availability-checking -enable-actor-data-race-checks -parse-as-library %s -o %t/a.out -module-name main
|
||||
// RUN: %target-codesign %t/a.out
|
||||
|
||||
// NOTE: This test specifically tests the crashing behavior of `checkIsolated`,
|
||||
// because this behavior is currently disabled
|
||||
// RUN: env %env-SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=crash %target-run %t/a.out
|
||||
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: concurrency
|
||||
// REQUIRES: libdispatch
|
||||
|
||||
// rdar://76038845
|
||||
// REQUIRES: concurrency_runtime
|
||||
// UNSUPPORTED: back_deployment_runtime
|
||||
// UNSUPPORTED: single_threaded_concurrency
|
||||
|
||||
import StdlibUnittest
|
||||
import _Concurrency
|
||||
import Dispatch
|
||||
|
||||
// For sleep
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
@MainActor func onMainActor() {
|
||||
print("I'm on the main actor!")
|
||||
}
|
||||
|
||||
func promiseMainThread(_ fn: @escaping @MainActor () -> Void) -> (() -> Void) {
|
||||
typealias Fn = () -> Void
|
||||
return unsafeBitCast(fn, to: Fn.self)
|
||||
}
|
||||
|
||||
func launchTask(_ fn: @escaping () -> Void) {
|
||||
if #available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 8.0, *) {
|
||||
DispatchQueue.global().async {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func launchFromMainThread() {
|
||||
launchTask(promiseMainThread(onMainActor))
|
||||
}
|
||||
|
||||
actor MyActor {
|
||||
var counter = 0
|
||||
|
||||
func onMyActor() {
|
||||
dispatchPrecondition(condition: .notOnQueue(DispatchQueue.main))
|
||||
counter = counter + 1
|
||||
}
|
||||
|
||||
func getTaskOnMyActor() -> (() -> Void) {
|
||||
return {
|
||||
self.onMyActor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// These tests now eventually end up calling `dispatch_assert_queue`,
|
||||
/// after the introduction of checkIsolated in `swift_task_isCurrentExecutorImpl`
|
||||
@main struct Main {
|
||||
static func main() {
|
||||
if #available(SwiftStdlib 5.9, *) {
|
||||
let tests = TestSuite("data_race_detection")
|
||||
|
||||
tests.test("Expect MainActor") {
|
||||
expectCrashLater()
|
||||
print("Launching a main-actor task")
|
||||
launchFromMainThread()
|
||||
sleep(1)
|
||||
}
|
||||
|
||||
tests.test("Expect same executor") {
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption")
|
||||
Task.detached {
|
||||
let actor = MyActor()
|
||||
let actorFn = await actor.getTaskOnMyActor()
|
||||
print("Launching an actor-instance task")
|
||||
launchTask(actorFn)
|
||||
}
|
||||
|
||||
sleep(2)
|
||||
}
|
||||
|
||||
runAllTests()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
// RUN: %empty-directory(%t)
|
||||
// RUN: %target-build-swift %import-libdispatch -Xfrontend -disable-availability-checking -enable-actor-data-race-checks -parse-as-library %s -o %t/a.out -module-name main
|
||||
// RUN: %target-codesign %t/a.out
|
||||
// RUN: env %env-SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL=1 %target-run %t/a.out 2>&1 | %FileCheck %s
|
||||
|
||||
// We specifically test for legacy behavior here, apps compiled against old SDKs
|
||||
// will be able to have this behavior, however new apps will not. We use the
|
||||
// overrides to test the logic for legacy code remains functional.
|
||||
//
|
||||
// RUN: env %env-SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL=1 %env-SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=nocrash %target-run %t/a.out 2>&1 | %FileCheck %s
|
||||
|
||||
// REQUIRES: executable_test
|
||||
// REQUIRES: concurrency
|
||||
@@ -61,14 +66,14 @@ actor MyActor {
|
||||
struct Runner {
|
||||
static func main() async {
|
||||
print("Launching a main-actor task")
|
||||
// CHECK: warning: data race detected: @MainActor function at main/data_race_detection.swift:25 was not called on the main thread
|
||||
// CHECK: warning: data race detected: @MainActor function at main/data_race_detection_legacy_warning.swift:30 was not called on the main thread
|
||||
launchFromMainThread()
|
||||
sleep(1)
|
||||
|
||||
let actor = MyActor()
|
||||
let actorFn = await actor.getTaskOnMyActor()
|
||||
print("Launching an actor-instance task")
|
||||
// CHECK: warning: data race detected: actor-isolated function at main/data_race_detection.swift:54 was not called on the same actor
|
||||
// CHECK: warning: data race detected: actor-isolated function at main/data_race_detection_legacy_warning.swift:59 was not called on the same actor
|
||||
launchTask(actorFn)
|
||||
|
||||
sleep(1)
|
||||
@@ -29,7 +29,7 @@ import Distributed
|
||||
let system = LocalTestingDistributedActorSystem()
|
||||
|
||||
tests.test("5.7 actor, no availability executor property => no custom executor") {
|
||||
expectCrashLater(withMessage: "Fatal error: Incorrect actor executor assumption; Expected 'MainActor' executor.")
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected MainActor executor")
|
||||
try! await FiveSevenActor_NothingExecutor(actorSystem: system).test(x: 42)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import Distributed
|
||||
}
|
||||
|
||||
tests.test("5.7 actor, 5.9 executor property => no custom executor") {
|
||||
expectCrashLater(withMessage: "Fatal error: Incorrect actor executor assumption; Expected 'MainActor' executor.")
|
||||
expectCrashLater(withMessage: "Incorrect actor executor assumption; Expected MainActor executor")
|
||||
try! await FiveSevenActor_FiveNineExecutor(actorSystem: system).test(x: 42)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,19 +86,19 @@ extension ActorTest : @preconcurrency P {
|
||||
//--- Crash1.swift
|
||||
import Types
|
||||
print(await runTest(Test.self))
|
||||
// CHECK: error: data race detected: @MainActor function at Types/Types.swift:16 was not called on the main thread
|
||||
// CHECK: Incorrect actor executor assumption; Expected MainActor executor
|
||||
|
||||
//--- Crash2.swift
|
||||
import Types
|
||||
print(await runAccessors(Test.self))
|
||||
// CHECK: error: data race detected: @MainActor function at Types/Types.swift:15 was not called on the main thread
|
||||
// CHECK: Incorrect actor executor assumption; Expected MainActor executor
|
||||
|
||||
//--- Crash3.swift
|
||||
import Types
|
||||
print(await runTest(ActorTest.self))
|
||||
// CHECK: error: data race detected: actor-isolated function at Types/Types.swift:33 was not called on the same actor
|
||||
// CHECK: Incorrect actor executor assumption
|
||||
|
||||
//--- Crash4.swift
|
||||
import Types
|
||||
print(await runAccessors(ActorTest.self))
|
||||
// CHECK: error: data race detected: actor-isolated function at Types/Types.swift:30 was not called on the same actor
|
||||
// CHECK: Incorrect actor executor assumption
|
||||
|
||||
@@ -275,3 +275,13 @@ Added: _swift_task_pushTaskExecutorPreference
|
||||
Added: _$ss26withTaskExecutorPreference_9isolation9operationxSch_pSg_ScA_pSgYixyYaq_YKXEtYaq_YKs5ErrorR_r0_lF
|
||||
// async function pointer to Swift.withTaskExecutorPreference<A, B where B: Swift.Error>(_: Swift.TaskExecutor?, isolation: isolated Swift.Actor?, operation: () async throws(B) -> A) async throws(B) -> A
|
||||
Added: _$ss26withTaskExecutorPreference_9isolation9operationxSch_pSg_ScA_pSgYixyYaq_YKXEtYaq_YKs5ErrorR_r0_lFTu
|
||||
|
||||
// === SerialExecutor.checkIsolated()
|
||||
Added: _swift_task_checkIsolated
|
||||
Added: _swift_task_checkIsolated_hook
|
||||
// (extension in Swift):Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScfsE13checkIsolatedyyF
|
||||
// dispatch thunk of Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScf13checkIsolatedyyFTj
|
||||
// method descriptor for Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScf13checkIsolatedyyFTq
|
||||
@@ -255,3 +255,6 @@ Added: __swift_exceptionPersonality
|
||||
Added: _swift_willThrowTypedImpl
|
||||
Added: __swift_willThrowTypedImpl
|
||||
Added: __swift_enableSwizzlingOfAllocationAndRefCountingFunctions_forInstrumentsOnly
|
||||
|
||||
// Runtime bincompat functions for Concurrency runtime to detect legacy mode
|
||||
Added: _swift_bincompat_useLegacyNonCrashingExecutorChecks
|
||||
@@ -275,3 +275,13 @@ Added: _swift_task_pushTaskExecutorPreference
|
||||
Added: _$ss26withTaskExecutorPreference_9isolation9operationxSch_pSg_ScA_pSgYixyYaq_YKXEtYaq_YKs5ErrorR_r0_lF
|
||||
// async function pointer to Swift.withTaskExecutorPreference<A, B where B: Swift.Error>(_: Swift.TaskExecutor?, isolation: isolated Swift.Actor?, operation: () async throws(B) -> A) async throws(B) -> A
|
||||
Added: _$ss26withTaskExecutorPreference_9isolation9operationxSch_pSg_ScA_pSgYixyYaq_YKXEtYaq_YKs5ErrorR_r0_lFTu
|
||||
|
||||
// === SerialExecutor.checkIsolated()
|
||||
Added: _swift_task_checkIsolated
|
||||
Added: _swift_task_checkIsolated_hook
|
||||
// (extension in Swift):Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScfsE13checkIsolatedyyF
|
||||
// dispatch thunk of Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScf13checkIsolatedyyFTj
|
||||
// method descriptor for Swift.SerialExecutor.checkIsolated() -> ()
|
||||
Added: _$sScf13checkIsolatedyyFTq
|
||||
@@ -255,3 +255,6 @@ Added: __swift_exceptionPersonality
|
||||
Added: _swift_willThrowTypedImpl
|
||||
Added: __swift_willThrowTypedImpl
|
||||
Added: __swift_enableSwizzlingOfAllocationAndRefCountingFunctions_forInstrumentsOnly
|
||||
|
||||
// Runtime bincompat functions for Concurrency runtime to detect legacy mode
|
||||
Added: _swift_bincompat_useLegacyNonCrashingExecutorChecks
|
||||
@@ -84,6 +84,13 @@ swift_task_enqueueGlobal_override(Job *job,
|
||||
Ran = true;
|
||||
}
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static void
|
||||
swift_task_checkIsolated_override(SerialExecutorRef executor,
|
||||
swift_task_checkIsolated_original original) {
|
||||
Ran = true;
|
||||
}
|
||||
|
||||
SWIFT_CC(swift)
|
||||
static void swift_task_enqueueGlobalWithDelay_override(
|
||||
unsigned long long delay, Job *job,
|
||||
@@ -130,6 +137,8 @@ protected:
|
||||
swift_task_enqueueGlobalWithDelay_override;
|
||||
swift_task_enqueueMainExecutor_hook =
|
||||
swift_task_enqueueMainExecutor_override;
|
||||
swift_task_checkIsolated_hook =
|
||||
swift_task_checkIsolated_override;
|
||||
#ifdef RUN_ASYNC_MAIN_DRAIN_QUEUE_TEST
|
||||
swift_task_asyncMainDrainQueue_hook =
|
||||
swift_task_asyncMainDrainQueue_override_fn;
|
||||
@@ -182,6 +191,11 @@ TEST_F(CompatibilityOverrideConcurrencyTest,
|
||||
swift_task_enqueueGlobalWithDelay(0, &fakeJob);
|
||||
}
|
||||
|
||||
TEST_F(CompatibilityOverrideConcurrencyTest,
|
||||
test_swift_task_checkIsolated) {
|
||||
swift_task_checkIsolated(SerialExecutorRef::generic());
|
||||
}
|
||||
|
||||
TEST_F(CompatibilityOverrideConcurrencyTest,
|
||||
test_swift_task_enqueueMainExecutor) {
|
||||
swift_task_enqueueMainExecutor(&fakeJob);
|
||||
|
||||
Reference in New Issue
Block a user