Pitch #2: Protocol-based Actor Isolation

Interesting idea. It isn't really related to this proposal, but something that I would like to explore in the future is an attribute that turns off all autogenerated members -- including the default assignment operator, destructor for structs, and move/moveinit hooks in the basic type witness table. Swift internally supports arbitrary logic here, but we don't allow advanced users to write their own types with their own implementation of these hooks. I'd love to be able to write something like (disclaimer: I made no attempt to pick good names here :slight_smile:):

struct MySmartPointer {
   init(_ x: MySmartPointer) { 
     // custom copy ctor 

  init(_ x: ^MySmartPointer) { 
     // move constructor.

  operator=(_ x: MySmartPointer) { 
     // custom copy reassign operator

  operator=(_ x: ^MySmartPointer) { 
     // custom move reassign.

  deinit {
     // custom destructor

In addition to being valuable for advanced pure-swift APIs, such a thing is useful when you need to write a C API in Swift that has init/copyctor/dtor hooks. Currently the least-bad thing to do is use a class for this, which adds an extra level of indirection and a memory allocation.

This could also be way to handle move only types and other weird things that C++ can express that Swift can't.



This would make the interaction from Swift to the C much less expensive. I wonder what only a simple 'deinit' destructor on structs would do.
Having to wrap heap allocated C handles (which are cheap to just copy around) on classes and incurring in unneeded ref-counting its just taxing the performance without need.

BTW: Given its all LLVM, the abi on both sides are stable, etc.. would be possible to get a move from the C++ world to Swift AND give the possibility for Swift to automatically call the C++ destructor in case the handle is scoped destruct?? (As this combo would turn feasible to avoid unneeded heap allocations on C++ side)

With 'deinit' in structs, moves and C++ object destruction, it would be such a game changer for Swift, giving it would become the best language to interface with C and C++, which we all know, have wonderful codebases that are important to interface with (with LLVM being one of them).

I'm sure you have a much better idea, a clear picture for all this, giving the wizard you are, but i'm just trying to make it clear how important a couple of changes in the Swift language would do to the development of the ecosystem around it.

1 Like

I would add that if we can do this, then "let's" pull the HeapObject single threaded definition inside swift runtime into a separate class, so we can rig non-atomic, non-locking swift allocations to this phantom stuff, and we can build compiler safe concurrency first class into the language. Quite the perf boost!

heapobject in swift runtime

1 Like

Oh yeah, and we need to be able to override malloc too :-)

I've been thinking more about explicit conformances and the explicit "unsafe" annotation you mention. In particular, with explicit conformance being required for public types, we now have no signal to the programmer that they are taking responsibility for correct conformance. I think we need a signal here, especially for public members.

With this in mind, I think it would be good to require @unsafe ValueSemenatic in explicit conformances when the conformance does not follow trivially from composition of stored values. When all stored values conform, a normal ValueSemantic conformance would be accepted (and necessary for public types). This is similar to, but slightly different than, the @unsafeConformance approach you describe.

I think this proposal will have some impact on the "stored properties in extensions" idea that had been discussed many times in the forums. The latest (short) discussion was from 4 months ago:

For example, if String conforms to ActorSendable, then it would be an error to extend it with a stored reference-type property:

extension String {
    let newStoredProperty: NSMutableString = "a mutable string"

"Stored properties in extensions" is not a Swift feature, and definitely outside of the scope of this proposal, but I think it might be beneficial to discuss about this proposal's impact on it.

1 Like

On the topic of ValueSemantic vs UnsafeValueSemantic, maybe we can have them both by making ValueSemantic a subset of UnsafeValueSemantic.

As far as I can see, there are 2 situations where conformance to ValueSemantic is unsafe:

  1. When the conforming type is a reference type.

  2. When the conforming type is a value type, but it's not composed of only ValueSemantic types.

Situation 2 can be addressed by an error message when the compiler cannot auto-synthesise the conformance. Situation 1 can be addressed if we have a way to mark value types like how we mark reference types with AnyObject:

Maybe we can introduce another new protocol AnyValue (or ValueType or something else, bikesheddable) opposite to AnyObject, and have all value types (structs, enums, tuples) implicitly conform to it. Then, we can allow UnsafeValueSemantic written as ValueSemantic when the conforming type is a reference type:

typealias ValueSemantic = UnsafeValueSemantic where Self: AnyValue

^ This expression isn't possible in Swift today, though. So some additional compiler magic might be needed.

Alternatively, it might be more workable if ValueSemantic inherits from both AnyValue and UnsafeValueSemantic:

protocol ValueSemantic: AnyValue, UnsafeValueSemantic {}

class Foo: ValueSemantic { // error: Non-value-type 'Foo' cannot conform to value-type protocol 'ValueSemantic'
                           // fix-it: replace 'ValueSemantic' with 'UnsafeValueSemantic'
    let bar: Int

Or, a third option, make ValueSemantic a composition of AnyValue and UnsafeValueSemantic:

typealias ValueSemantic = AnyValue & UnsafeValueSemantic

class Foo: ValueSemantic { // error: Inheritance from non-protocol type 'ValueSemantic' (aka 'AnyValue & UnsafeValueSemantic')
                           // fix-it: replace 'ValueSemantic' with 'UnsafeValueSemantic'
    let bar: Int

^ These latter 2 options also need some additional compiler magic, but it's precedented by AnyObject.

FWIW, I think that many of the comments in this thread are pushing towards making ValueSemantic conformance always be explicit, instead of ever being implicitly synthesized. The current proposal (implicit synthesis for non-public types) is a bit of a middle ground, but doesn't seem very satisfactory.

It seems that following the precedent of Hashable and Codable synthesization would be cleaner. WDYT?

1 Like

Making conformance explicit would be particularly helpful when making changes to existing types (eg adding a new field). It’s currently very helpful to be told immediately that a new field you’ve just added stops automatic Equatable/Codable conformance. I would imagine it would be the same for ValueSemantic.

Otherwise, I think people will be in the situation where passing a parameter of a particular type to an Actor works (without them doing anything, or maybe even knowing why it works), they make a change and an error appears some distance away.

I think the consistency argument also has a lot of merit.

1 Like

“Moving” Objects Between Actors

is it about move-only value types(moveonly struct, etc.)?
Will it need to implement move-only value types?

It seems like the reference semantics in a value type composed of value-semantic types comes from static variables and side-effects in computed properties/functions. But then, one can argue that there isn't much value semantics in Swift. And the truly value-semantic types can be easily extended with static variables, computed properties, or functions to make them reference-semantic.

I think ValueSemantic should be implicitly synthesized, because it's not really an opt-in feature like Hashable and Codable.

I think there are 2 pieces of information communicated in a protocol conformance: capability and intention.

When the feature enabled by a protocol is opt-in, these 2 pieces of information are bound together. For example, many types are capable of being hashable and codable, but unless they declare their intention of enabling the capabilities by conforming to Hashable or Codable, they can't be hashed or encoded/decoded. Conformance to Hashable and Codable is like labeling a box with a mailing address, where the label communicates both that the box is capable of being sent by mail, and that it's intended/allowed to be sent by mail.

When the feature enabled by a protocol is not opt-in, then the intention becomes pointless. If a type has value semantics, then regardless of its conformance to ValueSemantic, it exhibits value semantics when its instances are passed and copied. An explicit conformance to ValueSemantic is like labeling a box with the word "box", where the label only confirms that the box is indeed a box.

struct Foo {
    let bar: Int

Foo is codable, but without conforming to Codable, you can't use Foo(42).encode(to: /*...*/).

Foo has value semantic, and it has value semantic with or without conformance to ValueSemantic.

1 Like

Why does it matter where it comes from? It's called ValueSemantic, not ValueImplementation :)

Yep! That's why I think reference/value split is more about how you use a thing, not the type of it.

It is related but different - I renamed that section to "Transferring" Objects Between Actors to avoid confusion, thanks!

This is a really great framing!

I see ValueSemantic as naturally an opt-in feature. As pointed out up-thread, there are types which are compositions of value semantic types that are not themselves value semantic. This implies it should be opt-in. Furthermore, something that "happens to be a composition of value semantic types" today may not be tomorrow (in a resilient API evolution sense), so "implicit" synthesis is problematic.

The proposal tried to tip toe around the second issue by making implicit synth only happen for non-public types, but that just makes the language model more complicated. I would expect to get questions about "why did my code break when I marked my type public" on stack overflow for example.

I think this is the crux of the issue - as pointed out up-thread, your Foo example may not actually be value semantic, it depends on the implementation and intention of that int. It might be a database handle after all.



I think this problem applies to other protocols to some degree too. For example,

struct DeepThought: Codable {
  let answer: 42

DeepThought is codable, with synthesised requirements. If a non-Codable instance property is added, it becomes un-Codable:

struct DeepThought: Codable {
  let answer: 42
  let question: Never

I guess the benefit of explicit conformance here is that the problem can be caught early, instead of waiting for type-checking or static analysis at the use site (e.g. passing an instance into an async function parameter in the case of ValueSemantic).

As discussed up-thread, ValueSemantic is more sensitive to static members and side effects, so I agree that requiring explicit conformance makes sense.

However, thinking from an API user's instead of author's perspective, explicit ValueSemantic conformance conceptually limits a lot of what a user can do without enforcing those limitations:

Borrowing @cukr's example:

struct TeamRef: CustomStringConvertible {
  static let numberOfTeams = 6
  // Can't allow [Int]-typed static variables here, 
  // because they enable reference semantics? 
  private static var kills: [Int] = .init(/*...*/)
  private static var deaths: [Int] = .init(/*...*/)


  var description: String {
      "kd: \(kills)/\(deaths)"

I assume that all standard library types will come with ValueSemantic conformance, and some of them such as Int and String have to be explicit because they're built on reference types. Then, can users still use them to enable reference semantics for other types, as the example above? If users are discouraged from using them for anything reference-semantic, there isn't any way to actually enforce the discouragement or nudge them towards a safe default.

Also, if the user wants to extend a ValueSemantic-conforming type they don't own with something that enables reference semantics for the type, can they still do it? For example:

extension String {
  static var somethingImportant: NSMutableString // could just be String, actually

I think the biggest problem is that we don't really have a solid definition of value semantics. When I think of value semantics, I often think in terms of if an instance and its properties (and their properties...) are passed or copied by value. Maybe I should think of this concept as a superset of value semantics.

Sorry for the long delay here... this proposal is definitely moving in the right direction. Here are a couple of comments:

  • Something like @UnsafeTransfer feels like it needs to be part of the core proposal, because the lack of an opt-out here either hampers adoption or pushes people to incorrectly introduce conformances to ActorSendable. However, the property wrapper implementation isn't actually that great, because it has ABI impact. I suggest that @unsafeTransfer be a real attribute and a core part of the proposal.

  • From a performance perspective, we should consider formalizing the notion of a "marker" protocol for ActorSendable / ValueSemantics, banning any of the dynamic features that would force us to emit a witness table in the metadata. For example, we could ban the use of such protocols in existential types and dynamic casts (no var a: ActorSendable or x as? ActorSendable in the source language) so that there is no ABI impact to the introduction and use of such protocols. This also means we don't have to worry about multiple conformances quite so much, which might help with rollout as (e.g.) lots of people add ActorSendable conformances to third-party types that will cause conflicts.

  • I don't think this proposal should be tied to value semantics at all. Yes, value semantics provide actor-sendability, but there's a natural layering here and you don't need to block progress on this proposal on value semantics. (Yes, I'm worried that discussion won't converge, given that it's been simmering for years)

  • I think the name ActorSendable isn't quite right, because (1) this could/should be more general than actors, if we go and apply this to captured local variables, and (2) it's not just about "sending" values, both due to captured local variables and also because we're currently allowing let values to be accessed from other contexts. How about Shareable, because it's safe to share values of the type across tasks/threads/actors? That could be paired with @unsafeShareable to disable the checking on a per-parameter basis.



No problem, thanks for the feedback!

Just to clarify, why is ABI impact a problem? This is part of the signature for the method, so it seems perfectly reasonable to have ABI impact. There are no retroactive adoption issues that I'm aware of. If there is an ObjC bridging related issue it seems like it could be handled in the interface layer?

Interesting, that seems totally reasonable to me. This is a bit beyond my depth though, what does making it a marker protocol mean? Does that mean extensions on it would be prohibited or something else?

The major reason to tie them together is that value semantics is the vastly most common case and is independently useful - asking people to annotate their types multiple times seems really unfortunate.

I think that Sharable is good, happy to make that change. However, I think that this is directly tied to actors, even for captured variables, because the limitations are only applied to variables captured by closures that are passed across closure boundaries. Similarly, cross-actor properties on actors have the same check, etc. All the enforcement and special behavior happens at the actor boundaries, so I think that including the work Actor is appropriate. This also reduces the problem with Sharable being such a generic name.

I would recommend ActorSharable. Does that make sense to you?

I have a bigger question for you though: how best would you like me to engage in this process? I am happy to revise and push forward the whitepaper, but I don't have the time to actually implement this, so I don't think it will ever actually get to a proposal phase. Is someone willing to be a coauthor, or is someone willing to adopt the content and become the first author and help drive this?


1 Like

I don’t have a strong opinion, but want to point out that the point of value semantics is precisely that no sharing actually happens. So this seems like a misnomer when applied to value semantic types. Sharing only actually happens for types that have internally synchronized shared state.

1 Like

This is a good point. I went through many verbs in the writeup and settled on the "Transfer" verb, which seemed to convey passing data between actors without implying "sharing" or "moving" and seemed more concrete than "sending". I'd argue for ActorTransferrable or something like that. It's a mouthful, but most people will work with ValueSemantic so it seems ok.


I'm going under the assumption that we'll have to over-annotate as @UnsafeTransfer a number of parameters of types that, later, will themselves become actor-sendable through improved language features (actor-local types and such), and having @UnsafeTransfer be an ABI no-op and source-compatible to drop it once the type is actor-sendable would help those annotations go away.

Extensions are fine. I think it means you cannot have any requirements in the protocol, can't create a value of that protocol's type, and can't check for it dynamically (e.g., with is/as?).

It doesn't have to be limited to actors. For example, given something like:

func f() async {
  let object = MyClass()
  async let a1 = object.doSomething()
  async let s2 = object.doSomethingElse()
  await (a1, a2)

The object.doSomething() and object.doSomethingElse() expressions are invoked concurrently, and we could absolutely call this an error if object is not of a "shareable" type.


1 Like