Sendable escape hatch with runtime checks and clear API

In adopting Swift Concurrency, there are cases where a @unchecked Sendable wrapper type is the best/only solution.

For me, this is mostly about interfacing with legacy Objective-C APIs where I do not expect Swift Concurrency support in the near future to be available. sending and other compiler hints help in many cases, but in practice there are still situations where a non-sendable value must cross isolation boundaries.

The following is a wrapper type that provides such an escape hatch while still offering some runtime checks and an API that encourages sound handling of operations that the compiler cannot check:

/// A wrapper type to make non-sendable values sendable by providing constraint access.
final class Immutable<Value>: @unchecked Sendable {
	private var value: Value
	/// An internal status to enforce some of the access constraints at runtime.
	///
	/// - A non-negative status indicates the concurrent immutable access count.
	/// - A value of `-1` indicates that the value has been consumed and this wrapper may no longer be accessed.
	private let accessStatus = Mutex(0)
	
	init(_ value: Value) {
		self.value = value
	}
	
	/// Provides immutable access to the wrapped value.
	///
	/// The wrapped value is passed to `body` where it may not be mutated.
	/// Multiple concurrent immutable accesses are permitted.
	///
	/// > Important: The fact that `body` does not mutate the value is neither checked at compile time nor at runtime; failing to fulfill this constraint is a serious programming error.
	///
	/// A precondition verifies that the value has not been consumed.
	///
	/// - Parameter body: If the closure has a return value, that value is also used as the return value of the `withUncheckedImmutableAccess(_:)` method.
	///   Any errors thrown by the closure are rethrown.
	func withUncheckedImmutableAccess<E, T>(
		_ body: (Value) throws(E) -> T
	) throws(E) -> T {
		self.accessStatus.withLock {
			precondition($0 >= 0, "Cannot acquire immutable access of consumed value.")
			$0 += 1
		}
		defer {
			self.accessStatus.withLock { $0 -= 1 }
		}
		return try body(self.value)
	}
	
	/// Returns the wrapped value and prohibits any further access of the value using the wrapper.
	///
	/// A precondition verifies that the value is currently not accessed and has not been consumed.
	func consume() -> Value {
		self.accessStatus.withLock {
			precondition($0 > -1, "Cannot consume value that has already been consumed.")
			precondition($0 == 0, "Cannot consume value: access count \($0) is non-zero.")
			$0 = -1
		}
		return self.value
	}
}

(I am using Mutex from github.com/swhitty/swift-mutex as OSAllocatedUnfairLock or Synchronization.Mutex are not available on all of my deployment targets.)

The idea is to pretend that a value is immutable and thus make it Sendable.

This immutability constraint cannot be upheld by the compiler, and it is possible that some other code still has a reference to the value (for example, when it is an NSObject) and mutates it from outside the wrapper. Or that a user of the wrapper misuses the API and performs mutations inside withUncheckedImmutableAccess { … }.

Still, I think this wrapper improves clarity at site of use and some runtime checking, which is better than a simple @unchecked Sendable wrapper providing public access to its wrapped property.

Example usage:

let size = storage.someObject.withUncheckedImmutableAccess { $0.size }
let someObject = storage.someObject.consume()

// after `consume()`, these will stop program execution:
let size2 = storage.someObject.withUncheckedImmutableAccess { $0.size }
let someObject2 = storage.someObject.consume()

In the example above the Immutable<T> is a property for simplicity, but in my usage I mostly need this wrapper for return values. consume() makes it clear I do not expect further use of the wrapper. withUncheckedImmutableAccess { … } makes it clear that no mutation should happen to the value within the given closure.

A possible extension would be to allow mutation, but only when the access count is 0:

extension Immutable {
	/// Provides mutable access to the wrapped value.
	///
	/// The wrapped value is passed to `body` where it may be mutated.
	/// Mutation is allowed as `body` is guaranteed to have exclusive access to the value.
	/// In effect, this operation is the same as consuming the value, mutating it outside of this wrapper, and wrapping it again.
	///
	/// A precondition verifies that the value is currently not accessed and has not been consumed.
	///
	/// - Parameter body: If the closure has a return value, that value is also used as the return value of the `withExclusiveAccess(_:)` method.
	///   Any errors thrown by the closure are rethrown.
	func withExclusiveAccess<E, T>(
		_ body: (inout Value) throws(E) -> T
	) throws(E) -> T {
		self.accessStatus.withLock {
			precondition($0 == 0, "Cannot acquire exclusive access: access status \($0) is non-zero.")
			$0 = -2
		}
		defer {
			self.accessStatus.withLock { $0 = 0 }
		}
		return try body(&self.value)
	}
}

I would be interested to hear if anyone has ideas of how to further improve this type.