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 frobnicator
s 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.