Foreign reference types

Objective-C classes are imported as Swift classes with different reference counting. Objective-C blocks are imported, again, as Swift classes with different reference counting. The same is true for a variety of foreign types. I propose that some C++ types be imported as Swift classes with different reference counting: foreign reference types. And in this case, the “different reference counting” is actually “no reference counting.”

In C++ it’s very common to have types with reference semantics. Examples of such types in the Swift compiler are SILInstruction, SILBasicBlock, SILFunction, Expr, and the ASTContext to name a few. Currently, these types cannot be imported at all, because we don't have a way to materialize them or copy them around. Foreign reference types allow these C++ "reference types" to be imported with ergonomic Swift semantics, this makes them easy to use without unsafe pointers.

Foreign reference types (or custom reference types) actually have a broad set of applications beyond just C++ interop, but I won’t get into that in this post.

So what would these types actually look like? What are the rules? Foreign reference types will behave a lot like Swift classes. They will have reference semantics and be implemented as pointers under the hood. However, at least for the time being, Swift programs will not be allowed to manage their lifetime, so Swift programs will not be allowed to construct, copy, or destroy foreign reference types. This allows foreign reference types to be light-weight, trivial types. Additionally, reference types must be reference types, so we will not import any APIs that use these types as value types (i.e., a function which returns a foreign reference type by value).

If you’re like me, the easiest way to understand how this works is by looking at some code, so here you go:

// C++ API (assume non-null pointers):
struct __attribute__((swift_attr("import_as_ref"))) IntPair {
  int a = 1; int b = 2;

  static IntPair *create() { return new IntPair(); }
};

void mutateIt(IntPair &x) {
  x.a = 2;
  x.b = 4;
}
// Imported as:
class IntPair {
  var a: Int32
  var b: Int32

  class func create() -> IntPair
}

func mutateIt(_ x: IntPair)
// Swift program:
let x = IntPair.create()
mutateIt(x)
x.b = 42
print(x.a)

There are a couple of interesting things above. First, notice that we have to provide a create method to create an IntPair. We cannot construct it from Swift. Second, notice how the pointer and reference types can be used interchangeably, but IntPair is always either a pointer or reference in C++. Finally, notice how we’re able to access members of IntPair without dereferencing it, just like any other Swift class type. The above program assumes non-null pointers, but if this option were not enabled, create would return an Optional<IntPair>.

I’ve created a patch to implement these foreign reference types here. I hope to start adopting these in libSwift shortly. LibSwift will allow us to play with these types and explore how they work. After tuning the semantics, if they work well, I plan to create a proper proposal/pitch where the community can explore adopting foreign reference types as a public feature.

8 Likes

Who would be in charge of deleting the IntPair in this example (and in non-example usages)? Does this create the possibility of use after free?

The sorts of types we'd be targeting here are, in their current design in C/C++, unsafely-managed reference types. So yes, there's a possibility of use-after-free. When Swift gains the ability to express move-only types, some of these types could switch to being imported as safely-managed move-only reference types. However, others probably can never become statically managed types, because their APIs use an ownership model that's simply too dynamic to be expressed statically without major changes.

For example, the ownership model of an llvm::Instruction is that it is owned by the enclosing function. One common pattern for writing an LLVM pass is to scan the function, identify interesting instructions to replace, add the replacement instructions without touching the old ones yet, and collect all the old instructions in a list that will be deleted at the end of the pass. I'm not really sure how you would formalize that in even a very sophisticated ownership system.

1 Like

CC @dabrahams

Not to pick nits early, but a few points here:

  1. I wasn't sure exactly what you meant by “types with reference semantics,” so I went to look at examples (since @Alvae, @shabalin, @dan-zheng, @saeta, and I have been trying to formalize value semantics, how these terms are used matters to me).
  2. I think it's unlikely these types actually have reference semantics in the way we intend the term. Expr, for example, is non-copyable, and almost every non-copyable type has value semantics by our definition: given a variable of that type, you can only change its value via operations on that variable.
  3. Giving these types from inside the Swift compiler as examples of what you mean is a bit sub-optimal for the audience. For example, I took a look at SILInstruction and while it obviously has deleted copy-assignment and delete operators, many of its properties are probably a consequence of its inheritance from llvm::ilist_node<SILInstruction>… (which has four base classes of its own!), making it tough to get a read on what you're saying. It would be better if you'd give some minimal examples.

Currently, these types cannot be imported at all, because we don't have a way to materialize them or copy them around. Foreign reference types allow these C++ "reference types" to be imported with ergonomic Swift semantics, this makes them easy to use without unsafe pointers.

I'm gonna guess that what you really mean here is “non-copyable type.” But if that's not right I hope you will enlighten me.

Foreign reference types (or custom reference types) actually have a broad set of applications beyond just C++ interop, but I won’t get into that in this post.

I agree, but I doubt the specific things you're trying to handle with this initial, limited proposal should be imported as any kind of reference type. Of course, being really sure about that requires a more rigorous nailing-down of what these types have in common…

From my point-of-view, just dealing with the non-copyable subset of types that I assume you're trying to address:

  • We'll need non-copyable types eventually in Swift, so we'd better spell out their semantics in a way that isn't interop-specific.
  • C++ types with nontrivial copy constructors should be imported as non-copyable by default. All Swift types have O(1) copy/assignment operations; allowing things like std::set<std::vector<std::string>> to be implicitly copied from Swift would introduce some frightening performance cliffs.
  • Special annotation should be available to exempt things like shared_ptr that have a nontrivial but O(1) copy, so they're copyable in Swift.
  • Non-copyable types should be passable-by-value like any other type. Passing by value doesn't imply a copy or a transfer of ownership; you can think of non-copyable types as using a guaranteed calling convention, or being passed by pointer-to-immutable.
  • @escaping annotation should be available for all parameters. You can use it whenever a non-copyable parameter value might need to outlive the callee frame; typically the parameter would have to be movable for that to work.
  • Passing a non-copyable type as an @escaping parameter ends its lifetime in the calling function. If you want to keep using it, you need to use an explicit copy (if one is available).
  • Non-copyable types are passable-by-inout like any other type.
  • Imported nontrivial types that are copyable in C++ should be given an explicit copy API by the importer.

There's an implicit contract that goes with passing by & or const& in C++: unless otherwise specified, the callee is going to assume that it has a unique reference to the value. In other words, & corresponds to Swift's inout and const& corresponds Swift's pass-by-value in the common C++ programming model. The places where these assumptions do not hold are extremely rare, in part because they require awkward hoop-jumping in specification (you can see some examples in the C++ standard library). It would be a cryin' shame if we didn't take advantage of this correspondence for interop.

4 Likes

It is expected that you can and will create multiple references to the objects described by these types, so no, it would not be acceptable for those references to be non-copyable. Treating these classes as defining reference types accurately captures the reality of how they are used.

2 Likes

Can you explain in more detail why e.g. T() (or operator new + T()) cannot be imported into Swift as init()?

I wasn't suggesting that references should be non-copyable. There is a distinction in C++ between T and T& that is sometimes significant, and to broadly interoperate with C++ code we can't always erase the distinction on import, so my baseline assumption is that a non-copyable T in C++ is non-copyable in Swift and that there's a way to form some kind of reference to it, most likely an Unsafe[Mutable]Pointer<T>.

Now, it may be the case that the types being addressed by this proposal are special in some way, and treating them as reference types and erasing the distinction of the underlying T is the best answer, but as I said in my previous post I'd like someone to spell out the qualities of code that makes such a treatment appropriate so I understand what the proposal is trying to address.

A few other points:

  • The motivation stated for the proposal is that “currently, these types cannot be imported at all.” There are two ways to address that; one is (roughly) the proposal, and the other is to solve the more general problem that preserves the distinction between T and T& on the Swift side. To me, the proposal seems like sugar that will not erase the need for a more general solution; it would obviously cause problems with the accessibility of C++ overload sets and template specializations if applied everywhere. If it is meant as an incremental move so we can get more experience with imported types and see what's left to address with a more general solution, that's OK, but someone should say so.
  • I'm a little concerned that all the information about mutation that is encoded by const on the C++ side will be dropped.
  • I'm also concerned about implicitly hiding unsafe references on import to Swift. It seems to me that at the very least the C++-side annotation that does this should include the word “unsafe.”
2 Likes

Dave, thank you for all those comments! There is a lot to unpack here, and I want to make sure I address it all thoughtfully, so it may take me a while to respond.

Also, I think I was in a bit of a rush when I made the original post, so it would probably be good for me to clarify a few things.

Hopefully in the next few days I'll have time to make an updated post and address some of these comments (along with other's comments).

In the meantime, I'd like to respond to two points so that I think may be causing a lot of confusion: First,

If it is meant as an incremental move so we can get more experience with imported types and see what's left to address with a more general solution, that's OK, but someone should say so.

My intention is that this "feature" is adopted in libSwift so that we can see what types have these semantics and understand if this is the best way to handle such types. As I said in the last paragraph of my original post, I'm not intending this as a publicly available feature, right now it's just something that will allow us to explore types with these semantics in Swift. There may be a better long-term solution/feature that will come out of adopting thing in libSwift and this/future forum post(s).

Second,

Now, it may be the case that the types being addressed by this proposal are special in some way, and treating them as reference types and erasing the distinction of the underlying T is the best answer

This proposal is specifically addressing types that cannot or should not be passed as a parameter as T or returned as T. That is a heuristic that is determined by the programmer/API writer by applying the attribute import_as_ref.

... More to come soon :)

2 Likes

Not a problem; thanks for your work on this!

My intention is that this "feature" is adopted in libSwift so that we can see what types have these semantics and understand if this is the best way to handle such types.

OK, but I guess I'm looking for a crisper definition of what “these semantics” are, if you are referring to their C++ semantics… and if you mean their Swift semantics I think that may be a bit vacuous (see below).

As I said in the last paragraph of my original post, I'm not intending this as a publicly available feature, right now it's just something that will allow us to explore types with these semantics in Swift. There may be a better long-term solution/feature that will come out of adopting thing in libSwift and this/future forum post(s).

Oh, sorry I missed that. If you think such an exploration is useful, by all means I support it!

This proposal is specifically addressing types that cannot or should not be passed as a parameter as T or returned as T . That is a heuristic that is determined by the programmer/API writer by applying the attribute import_as_ref .

If what you're saying is that there may be a category of types that should be treated in Swift according to what's been proposed here and the only arbiter of that determination is a personal choice on the part of the programmer/API writer (so there is nothing technically or logically discernable that can help me decide what's in that category), then… sure, I guess it's an unfalsifiable claim and I can't really evaluate it.

I don't mean to give you a hard time here, but you /cc'd me and for better or worse this is how I function. To give useful input on a proposal I need to try to build a framework of understanding around it, so I tend to ask for things to be defined in terms that can be objectively evaluated.

2 Likes

OK, but I guess I'm looking for a crisper definition of what “these semantics” are, if you are referring to their C++ semantics… and if you mean their Swift semantics I think that may be a bit vacuous (see below)

:smiling_imp: Sorry, I was being intentionally vague there. I didn’t want to get into “these semantics” until I had time to type this whole thing out.

So, “what are those semantics.” I think it helps to think about these types in terms of memory ownership. (I know there are many formal terms/defenitions here, so forgive me while I steam roll those with my own, naive thinking.) We have the idea of value types already. These can be copied around and stored into an address (an inout parameter, for example). For many C++ “reference types” this will work well. This aligns with how C++ handles things: value types can be stored into references, and those references have reference semantics. So, for most types, the distinction between T and T& remains.

But, there are some types where the memory ownership is external or global. This is where foreign reference types come into play. For example, all SILInstructions are “owned” by a SILModule and all Exprs are “owned” by an ASTContext. Materializing Exprs in any capacity outside of an ASTContext would break things, because it would no longer be owned by an ASTContext. So, they must only be passed around indirectly. Without foreign reference types, maybe this would be via inout or unsafe pointer, but then we would have to ensure they were never dereferenced/copied, and I think it would be a generally worse experience for the programmer. Foreign reference types will allow types like this to be passed around/used indirectly in Swift programs in a way that feels natural.

There's an implicit contract that goes with passing by & or const& in C++: unless otherwise specified, the callee is going to assume that it has a unique reference to the value. In other words, & corresponds to Swift's inout and const& corresponds Swift's pass-by-value in the common C++ programming model. The places where these assumptions do not hold are extremely rare, in part because they require awkward hoop-jumping in specification (you can see some examples in the C++ standard library). It would be a cryin' shame if we didn't take advantage of this correspondence for interop.

This is where things get... complicated. I’d argue that this isn’t quite the case. For most types, especially types that behave similarly to Swift types, yes, you’re right: & corresponds to something-like- inout and const& corresponds to something-like-pass-by-value. But, there are some types for which this doesn’t really hold. Let’s go back to the example of Expr . For this type, it doesn’t really matter if it’s passed as a const& or a & (or a pointer). Because Expr s are owned by an external context, they cannot be materialized, and only one Expr may ever exist. So, if you pass by const, or pass by const ref, you’re really doing the same thing: you’re passing around the same object indirectly. Therefore there’s no technical difference here (we couldn’t lower this to a Swift pass-by-value), but is there a semantic one? Maybe. But I’d argue (and may change my mind later) that it’s not an important one.

(If John will permit me to use his example...) The Clang and Swift ASTs show this well: both of these ASTs are implemented using types that should be imported as “foreign reference types,” that is, non-copyable types that are owned by a central/external context. And both are roughly immutable. Clang decided that everything should be passed by const-ref and const-pointer and all methods should be marked as const. Swift did the opposite: knowing that everything is const after creation, it doesn’t really matter (for these types) whether you get a const pointer or not. In other words there’s no difference (techinical or semantic) between a const Expr& and an Expr & . This is something we’ll need to feel out, but it’s possible, and even likely, that most APIs don’t actually care about const when it comes to “foreign reference types,”because all types are created in the same way, “externally.”

(Of course, we’ll need to have some utilities to let us call both const and non-const overloads of a function, etc. but these are details that can be worked out later.)

Finally,

I'm also concerned about implicitly hiding unsafe references on import to Swift. It seems to me that at the very least the C++-side annotation that does this should include the word “unsafe.”

I think you’re right: we should rename this to be something like “unsafe foreign reference types.” While I don’t think that technically changes much, it puts the programmer in the correct frame of mind when using these types.

As John said earlier: there is some inherint unsaftey when using a language like C++. There is no way to get around that. We can explore ways to make it more clear that these types are unsafe (beyond just changing the name), but they will always be unsafe in some capacity.

...or will they? I’d like to also bring up the possibility of adding “safe foreign reference types” in the future. We could potentially have two layers of indirection: the first would be the foreign reference and the second would be a pointer to that reference. Imagine the second was a reference counted type (a class type in Swift), if we could somehow hook into the deallocator, we could essentially assert that the reference count was zero when the reference is deallocated. Again, this is not something I’m talking about doing now, it’s just a thought that we could potentially investigate later.

2 Likes

Hi @zoecarver — somehow I wasn't notified of this post and only just saw it…
Thanks for getting back to me.

Not a problem, as long as we (and everybody following along) eventually understand one other.

We have the idea of value types already. These can be copied around and stored into an address (an inout parameter, for example).

I'm confused already. AFAICT inout doesn't have anything to do with the ability to copy a thing or store a copy of it in an address; it is only related to whether the thing can have an address and is mutable.

For many C++ “reference types” this will work well. This aligns with how C++ handles things: value types can be stored into references, and those references have reference semantics. So, for most types, the distinction between T and T& remains.

My point was that in general, it exists, no matter what T is. Even if, for the purposes of importing T/T&/… etc. as a function argument, the distinction doesn't matter, to use any cplusplus_frobnicator<U> from Swift properly with T, it could be important that U = T or U = T&. (Of course, we haven't had the discussion in the community about how much fidelity and detail should be available when importing C++; in the absence of that I am assuming we can't discard anything)

Now I understand that you're trying to import a specific subset of C++ types into Swift, and special treatment may be warranted. Maybe there are no frobnicators that matter for these types. The only contribution I have to make at this point, in that case, is to try to get you to think about how this treatment fits into the larger, more general picture. What does the continuum of special treatments look like? How do we get this special treatment to fit into that continuum without being a one-off special case? We don't know (so far) how representative these types are of something that will be broadly seen in C++ interop.

But, there are some types where the memory ownership is external or global. This is where foreign reference types come into play. For example, all SILInstructions are “owned” by a SILModule and all Exprs are “owned” by an ASTContext. Materializing Exprs in any capacity outside of an ASTContext would break things, because it would no longer be owned by an ASTContext.

I'm going to guess that what characterizes these “owned” types is:

  • each distinct instance in memory has a lifetime that is a subset of the lifetime of its owner
  • such instances can only be created through operations on the owner
  • unlike a Swift value type, parts (these instances) of the owner are mutated outside of unique mutating operations on the owner.

Is that right? Regardless of whether it is, I'm trying to get you to write down a list something like this, that nails down what kinds of types you mean.

[Note: I go through lots of confusion here, but at the end you seem to eventually do that, at least in part].

So, they must only be passed around indirectly.

Sorry to be a pain, but what does “passed around indirectly” actually mean?

Without foreign reference types, maybe this would be via inout or unsafe pointer, but then we would have to ensure they were never dereferenced/copied,

I'm confused again. Please help me to understand.

  • You say we would need to make sure they were never copied; but earlier in the thread I guessed that the category of noncopyable C++ types is what we were dealing with, but I was apparently wrong(?)
  • There's no “dereferencing” an inout parameter.
  • There's no problem dereferencing an UnsafePointer<Noncopyable> as long as you don't copy the pointee. I realize this might not fit into the way you think about Swift but I think it is at least one consistent way to integrate noncopyability into the current semantics.
  • As I tried to say earlier, a noncopyable thing that is already in memory somewhere can be “passed by value” in Swift by an effective immutable borrow, as long as it isn't being modified concurrently through some other reference, which as I tried to say earlier, is far and away the common case.
  • As far as I understand, these C++ things are already in memory somewhere.

and I think it would be a generally worse experience for the programmer. Foreign reference types will allow types like this to be passed around/used indirectly in Swift programs in a way that feels natural.

IMO the goal of replicating the C++ programming experience in Swift does not override other goals, like the ability to use Swift's safety properties, or preserving the meaning of const-ness in C++ APIs, or preserving the performance characteristics of Swift code. IMO this is especially true when it comes to copying; as I said earlier, in C++, passing by-value creates a performance cliff; in Swift it basically never is. This is why I'm saying that passing a C++ type into a Swift function should (by default) not invoke the C++ copy constructor, and C++ types with nontrivial copy should (by default) all be imported as noncopyable Swift types.

(This is really funny… when @gribozavr started working on the interop problem I was trying to get him to use classes as a model for noncopyable C++ types, since we already have them in Swift, and class instances can't be copied. As I looked into the problem more deeply, I changed my mind and now I find myself arguing against it, at least as a general approach to all noncopyable types—but then, I'm still not clear on what subset of C++ types you're trying to address.)

For most types, especially types that behave similarly to Swift types, yes, you’re right: & corresponds to something-like- inout and const& corresponds to something-like-pass-by-value. But, there are some types for which this doesn’t really hold. Let’s go back to the example of Expr .

I'll point out again that using types from Swift's internals as examples here is problematic, because we're not all familiar with them.

For this type, it doesn’t really matter if it’s passed as a const& or a & (or a pointer).

?! Really? Seems to me the only way that could be true is if it's immutable… and, I eventually see that's what you're “roughly” talking about.

Because Expr s are owned by an external context, they cannot be materialized, and only one Expr may ever exist.

Surely you don't mean that? In any program's AST there are typically thousands of expressions that need to be represented at the same time. (I assume Expr is the AST node for expressions?)

So, if you pass by const, or pass by const ref, you’re really doing the same thing: you’re passing around the same object indirectly. Therefore there’s no technical difference here (we couldn’t lower this to a Swift pass-by-value), but is there a semantic one? Maybe. But I’d argue (and may change my mind later) that it’s not an important one.

Sorry, I'm too confused by everything that came before to understand what you're saying here.

(If John will permit me to use his example...) The Clang and Swift ASTs show this well: both of these ASTs are implemented using types that should be imported as “foreign reference types,” that is, non-copyable types that are owned by a central/external context. And both are roughly immutable. Clang decided that everything should be passed by const-ref and const-pointer and all methods should be marked as const. Swift did the opposite: knowing that everything is const after creation, it doesn’t really matter (for these types) whether you get a const pointer or not. In other words there’s no difference (techinical or semantic) between a const Expr& and an Expr & .

OK, now we're getting somewhere; this begins to approximate the list of type properties I've been asking you for. If these types are indeed immutable, they have value semantics whether you think of them as references or values (and would even if they were copyable). In general, representing them as value types would allow stronger inferences both for the Swift programmer and the Swift compiler. For example, IIUC value types can be implicitly captured in closures (and are @Sendable by default?) now. Also, there's no implication that you can subclass them in Swift.

This is something we’ll need to feel out, but it’s possible, and even likely, that most APIs don’t actually care about const when it comes to “foreign reference types,”because all types are created in the same way, “externally.”

I don't think external creation is the arbiter of whether one would care about const. I've seen plenty of C++ types whose lifetime is managed by an owner, but for which const is significant. But since you're defining what a “foreign reference type” means, you could easily define it to be so.

As John said earlier: there is some inherint unsaftey when using a language like C++. There is no way to get around that.

Depends what you mean. With a few (simple, mostly checkable) assumptions about what kinds of programming patterns clients may use, it is possible to define safe APIs in C++. Speaking for Adobe, if were to start using Swift broadly, we would want importing into Swift to eliminate the need for those assumptions and leave us with a truly safe interface. If importing into Swift undid any of the safety we had crafted into our C++ APIs, Swift would be a losing proposition for us. We will have mutable non-copyable types in C++ for which, if imported into Swift as references, would leave programmers with a net loss of safety.

Hey @dabrahams. No worries. Thanks for the lengthy reply. Lot’s to think about here. I only chose to respond to a few peices. I think these will push us forward more efficiently, and we can circle back to the other points if need be.

(This first reply is just nits and clarifications, the second one will be more "interesting".)

There's no “dereferencing” an inout parameter.

The compiler will sometimes be forced to dereference an inout, such as in the following program:

func takesValue(x: Int) { }

func takesAddress(x: inout Int) {
takesValue(x: x) // Dereferenced.
}

This is the type of thing we will have to ensure doesn’t happen if we take this approach (through diagnostics, maybe).

There's no problem dereferencing an UnsafePointer<Noncopyable> as long as you don't copy the pointee. I realize this might not fit into the way you think about Swift but I think it is at least one consistent way to integrate noncopyability into the current semantics.

I guess I wasn’t really clear. I think we’re actually saying the same thing. I am saying that this is another way we could handle this problem, but it would be a much worse experience for Swift programmers. They would not be able to use certain Swift APIs/language features for (what might be) confusing reasons. For example, this would not compile:

func takesPtr(x: UnsafePointer<T>) { let y = x.pointee }

(And this is not even touching the fact that using UnsafePointers everywhere is sub-optimal.)

I'll point out again that using types from Swift's internals as examples here is problematic, because we're not all familiar with them.

Surely you don't mean that? In any program's AST there are typically thousands of expressions that need to be represented at the same time. (I assume Expr is the AST node for expressions?)

Sorry if this was a confusing example. Yes, you’re right, Expr is the AST node for expressions. What I mean by “only one expr may ever exist” is “only one Expr may ever exist for each expression.”

If these types are indeed immutable, they have value semantics whether you think of them as references or values (and would even if they were copyable).

It is important that foreign references types are allowed to be mutable. It’s just that in this example, they aren’t. I was demonstrating how both types could be const.

But since you're defining what a “foreign reference type” means, you could easily define it to be so.

I’m not sure this is something we want to define yet. Let’s see how this plays out once we start adopting these in libSwift :)

With a few (simple, mostly checkable) assumptions about what kinds of programming patterns clients may use, it is possible to define safe APIs in C++. Speaking for Adobe, if were to start using Swift broadly, we would want importing into Swift to eliminate the need for those assumptions and leave us with a truly safe interface. If importing into Swift undid any of the safety we had crafted into our C++ APIs, Swift would be a losing proposition for us. We will have mutable non-copyable types in C++ for which, if imported into Swift as references, would leave programmers with a net loss of safety.

It would be extremely helpful to get some examples of these types. As we design interop, we’re actively looking for various kinds of APIs that we’ll need to be able to import.

(Now for pt. 2...)

IMO the goal of replicating the C++ programming experience in Swift does not override other goals, like the ability to use Swift's safety properties, or preserving the meaning of const -ness in C++ APIs, or preserving the performance characteristics of Swift code. IMO this is especially true when it comes to copying; as I said earlier, in C++, passing by-value creates a performance cliff; in Swift it basically never is. This is why I'm saying that passing a C++ type into a Swift function should (by default) not invoke the C++ copy constructor, and C++ types with nontrivial copy should (by default) all be imported as noncopyable Swift types.

(This is really funny… when @gribozavr started working on the interop problem I was trying to get him to use classes as a model for noncopyable C++ types, since we already have them in Swift, and class instances can't be copied. As I looked into the problem more deeply, I changed my mind and now I find myself arguing against it, at least as a general approach to all noncopyable types—but then, I'm still not clear on what subset of C++ types you're trying to address.)

I’d really like to understand the object model you’re proposing here. Maybe we can get on another call at some point to discuss this. At least for me, it’s much easier (and more efficient) to do that then go back and forth on the forums. But I understand that’s not the case for everyone.

As I tried to say earlier, a noncopyable thing that is already in memory somewhere can be “passed by value” in Swift by an effective immutable borrow, as long as it isn't being modified concurrently through some other reference, which as I tried to say earlier, is far and away the common case.

Hmm. Let’s assume that it isn’t being modified. Unfortunately, even if this is the case 100% of the time, this isn’t always (or even usually?) expressed in C++ APIs. How would we know that we can emit immutable borrows for Expr s in the Swift compiler (or, if it’s easier to think about, any non-copyable, immutable type that doesn’t use the const keyword anywhere in its API)?

Okay, so, let’s ignore the mutable part of this for a second and assume everything is immutable. This might help clear some things up. Let’s take “foreign reference types” and use different words to describe them. If everything was immutable, we could just call these value types that were borrowed by Swift. A “foreign reference” in this case is the same as a “shared value” from the ownership manifesto.

Do we agree that there are certain types where you would only want to use them as a shared value in Swift? There may be several reasons for this: maybe they’re non-copyable, maybe they must be unique for a given input, maybe they can only be used as a reference to memory owned externally, maybe they can only be created through operations on their (C++) owner, or maybe a combination or something else entirely. Whatever it is, do we agree that there are some types where it only makes sense to use a borrowed (or shared) value for them?

If we can agree on that, I think we’re more than half way to the finish line :grin:

1 Like

OK, now I know what you mean, but this is only a dereference to the extent you think of an inout Int as an int*. But we never talk about dereferencing an inout, beceause in the user model, it is not an int*. It is approximately a plain mutable int that will later be written back on its source, and more precisely it's a unique mutable borrow of an int value that exists somewhere in memory.

This is the type of thing we will have to ensure doesn’t happen if we take this approach (through diagnostics, maybe).

Sorry, I don't know what “this approach” is.

If you mean my suggested approach, then no, we don't have to ensure that, because these types we're importing from C++ don't exist in Swift yet, especially if they are noncopyable, so we're free to define the ABI for them, and pass-by-value can be implemented for these types as an immutable borrow. To a pure Swift programmer an immutable borrow is semantically indistinguishable from pass-by-copy, and I claim that for a type imported from C++ it's nearly always semantically indistinguishable, to the point where it should be the default.

I guess I wasn’t really clear. I think we’re actually saying the same thing. I am saying that this is another way we could handle this problem, but it would be a much worse experience for Swift programmers. They would not be able to use certain Swift APIs/language features for (what might be) confusing reasons. For example, this would not compile:

func takesPtr(x: UnsafePointer<T>) { let y = x.pointee }

And, my claim is that this shouldn't be confusing to anyone if T is non-copyable in Swift, once Swift has non-copyable types. I understand that you're working with a class of types that, because they are immutable and you can handle them by reference, we might want to represent as copyable… but again, I'm trying to get you to look at the big picture:

  • We will want to be able to define noncopyable types in Swift eventually.
  • We will certainly not be able to import all noncopyable C++ into Swift as copyable without severely compromising their semantics. Just consider std::unique_ptr and std::lock, for example.
  • To successfully interoperate with arbitrary C++ code, we will need to at least be able to represent T, T&, and T* as distinct things in Swift.

We are going to have to deal with these scenarios eventually. IMO we shouldn't dive down the rabbit hole of this special case without at least considering how it fits into the bigger picture.

(And this is not even touching the fact that using UnsafePointers everywhere is sub-optimal.)

Sorry, I agree but don't see how that's relevant here.

It is important that foreign references types are allowed to be mutable.

All I can do now is take your word for it, because I still don't understand what the boundaries of this category of types is. At this point I can't tell whether “foreign reference type” is an idea you made up or it reflects some external reality, nor whether it's fixed or fluid.

I’m not sure this is something we want to define yet. Let’s see how this plays out once we start adopting these in libSwift :)

OK, but I don't think there's any point in using a term here in the evolution forum that you're unwilling to define; I mean, how is anyone supposed to understand what you're proposing or give useful feedback? It sounds like maybe this is just, “I want to experiment with this feature idea, whose shape I am fond of because I suspect it applies well to some types I currently want to use.” If so, that's fine… but I don't see any way to contribute.

With a few (simple, mostly checkable) assumptions about what kinds of programming patterns clients may use, it is possible to define safe APIs in C++. Speaking for Adobe, if were to start using Swift broadly, we would want importing into Swift to eliminate the need for those assumptions and leave us with a truly safe interface. If importing into Swift undid any of the safety we had crafted into our C++ APIs, Swift would be a losing proposition for us. We will have mutable non-copyable types in C++ for which, if imported into Swift as references, would leave programmers with a net loss of safety.

It would be extremely helpful to get some examples of these types. As we design interop, we’re actively looking for various kinds of APIs that we’ll need to be able to import.

I mentioned std::unique_ptr earlier.
I can try to dig up some specific examples of safe APIS, but a large subset of C++ standard library APIs should qualify as safe by these criteria. Off the cuff, the assumptions are, unless otherwise specified:

  • a const reference parameter's referent
    • has a lifetime guaranteed by the client for the duration of the call
    • will not be mutated (by the client) during the duration of the call
  • a non-const reference parameter's referent will not be accessed except through that reference for the duration of the call (i.e. the client would obey Swift's law of exclusivity)
  • a returned T&'s referent will only be accessed during the same full expression as the call that returns.
  • the operations required of generic parameters are safe.

std::vector<T>::push_back is one such simple example.

1 Like

I was thinking of suggesting the same.

As I tried to say earlier, a noncopyable thing that is already in memory somewhere can be “passed by value” in Swift by an effective immutable borrow, as long as it isn't being modified concurrently through some other reference, which as I tried to say earlier, is far and away the common case.

Hmm. Let’s assume that it isn’t being modified. Unfortunately, even if this is the case 100% of the time, this isn’t always (or even usually?) expressed in C++ APIs. How would we know that we can emit immutable borrows for Expr s in the Swift compiler (or, if it’s easier to think about, any non-copyable, immutable type that doesn’t use the const keyword anywhere in its API)?

We absolutely don't know for sure. My claim is that these patterns hold so much of the time that we should assume them by default; I can support this claim in various ways but in the end there's no way to prove it. As you said, interop with C++ is going to involve some amount of unsafety. To interoperate properly with the rare C++ API that violates these patterns, some annotation might be needed.

Okay, so, let’s ignore the mutable part of this for a second and assume everything is immutable. This might help clear some things up. Let’s take “foreign reference types” and use different words to describe them. If everything was immutable, we could just call these value types that were borrowed by Swift. A “foreign reference” in this case is the same as a “shared value” from the ownership manifesto.

I think so. But I also think that's an unneeded concept, because a shared value acts exactly the same as a regular old pass-by-value (or passed-by-move, in the case of noncopyable-but-movable types) value.

Do we agree that there are certain types where you would only want to use them as a shared value in Swift?

:man_shrugging: if to a Swift programmer, it's not semantically distinguishable from any other value, then no.

There may be several reasons for this: maybe they’re non-copyable, maybe they must be unique for a given input, maybe they can only be used as a reference to memory owned externally, maybe they can only be created through operations on their (C++) owner, or maybe a combination or something else entirely. Whatever it is, do we agree that there are some types where it only makes sense to use a borrowed (or shared) value for them?

As far as I can tell, these are all descriptions of types that are, or should be, noncopyable. At an implementation level, obviously you can't pass them by making copies of the bits. But bit copies are not observable in Swift anyway. Everything that is passed by-value today could, at an implementation level, be passed as a “shared value” without changing the user model one iota. In fact that's almost exactly what we do when the implementation uses the “guaranteed” calling convention: okay, the bits of references might get copied, but the “copy constructor” of the reference, which increments the reference count, never runs. And whether the bits get copied or we have a pointer to the immutable bits is totally fungible. My point here is that:

  • Yes the “shared value” implementation technique should be used for non-copyable values
  • Because passing a copyable value by value is semantically equivalent to passing it by shared value, we don't need to expose it to users for copyable types.
  • Because we don't have noncopyable types yet, we're still free to define “shared value” as the implementation technique by which they are passed by value, and we don't need to expose it to users at all.
  • This synergizes really well with the goal of interoperating with C++ types that are expensive to copy without changing the performance characteristics of Swift code that handles them. We just make those C++ types noncopyable (and provide an explicit copy operation). Making them noncopyable doesn't make them super-hard to use, since they can still be passed by value.
1 Like

One thought I had last night while trying to get back to sleep, because C++ interop is in my dreams: another possible option for these “foreign reference types,” which might be more consistent with safety and value semantics, and would preserve mutability information, is to represent them in Swift as a kind of index into their context objects (out of which I understand they are allocated?) E.g. (making up API since I don't have a real example) if someASTCtx[someASTNodePtr].isDeclaration { ... }. Of course this approach might not be practical for any number of reasons—I still don't have a clear description of the use case here—but I think it's worth considering.

1 Like

Such a strategy would assume that all elements of the same context share the same mutability qualifiers, right? In your example, if some AST nodes are mutable while others are constant, there would be no way to "type" someASTCtx to reflect that fact.

Perhaps there should be two versions of the context, the mutable one being indexable only by mutable foreign references?

1 Like