So, our next speaker is Doug Cracker. He has deep expertise in programming languages and
compilers. He is currently working on the Swift programming languages and has in the
past, also contributed to Clang, LLVM and ICC++ standard. He works at Apple and today
he will be sharing his deep understanding of programming languages and how memory safety
can be made easier by incorporating and blending Swift with CNC++. Please welcome Doug.
Thank you. All right, I'm here to talk about Swift. We're about 10 years after the introduction
of Swift and we've been rolling it out into an established software ecosystem trying to bring
memory safety to a predominantly CNC++ world. We've learned a lot in these last 10 years.
So, I want to share some of how we've done this. We've learned along the way and some ideas
for helping the open source software ecosystem move toward more memory safety. Now, I'm not
going to try to sell you on the benefits of memory safety. A lot has been said and written on
that topic. So, this is one particular report, co-signed by a whole bunch of government
cybersecurity agencies and it makes the point that we as a software discipline need to move
from the memory unsafe languages that we have today like CNC++ toward memory safe languages.
The goal here is of course to improve software security, but there are a lot of other benefits
of memory safe languages. But the hard part here is the how, how do we actually get there?
Because where we are today is we have CNC++ everywhere. Our entire software universe is built
on top of these languages that do not provide memory safety. And there's no magic bullet that's
going to make the memory safe. Now, on the other hand, you have a lot of options for memory
safe programming languages. So, I took this list directly from the report. I'm very sorry if
you're programming language of choice isn't on here. But the idea is there are a bunch of good
options. And if you were starting from nothing, you're going to build your own software ecosystem
from the bottom up, you could do so in one of these memory safe programming languages and have memory
safety. In a sense, that's not even the hard part. The hard part is how do we get from where we are
today, over to this world, where we have better memory safety, throughout the whole open source
ecosystem. We don't know the answer, but we're looking for a roadmap. Swift itself, the language
that we rolled it out, was actually designed specifically for this transition. Swift launched into
an ecosystem that was already well established and built on top of the C family of languages,
C, C++, and Objective C. So, in this Apple ecosystem, this is the apps that run on the
iPhone, run on the Mac, and so on, we had this large software development kit,
comprised of many libraries written in, and C, and Objective C, and many, many developers that
knew how to write code for this environment. Objective C is not familiar to you. It's fine,
just think of it as C, with like small talk, wedged on top, to give it Objective
oriented programming, but it still retains all of the memory safety aspects of, let's see.
Now, in this ecosystem, we had a couple of other things to be concerned about. We needed binary
stability. So, if you're building an app for something like a phone, and you run it on today's OS,
well, it needs to work on next year's OS and in the future. And so, we had to embrace the ecosystem
we had, but move it from this C family of languages toward a memory safe future based on Swift.
The approach we took is pretty different from how programming languages are often introduced.
So, often a programming language comes with its whole big stack of, you know, libraries and
platform libraries. Everything that you need to build stuff in that programming language.
We didn't do that. When we launched Swift, it had basically one library, it standard library,
with arrays and strings and these little-level components in it, but nothing else.
Instead, we took interoperability with the C family of languages as a first-class language feature.
And the idea here is we had launched into the existing ecosystem. We had all of the libraries
that were already there, all of the code that was already there for C and Objective C,
just integrate naturally into Swift with no ceremony whatsoever.
It's interesting because here what you're doing essentially is you're only changing one variable.
We're changing the programming language, but everything that developers already knew from this ecosystem.
All the libraries they learned, all the tricks they learned to build great apps, all of that
that transferred, but now they could do it with Swift and get the benefits of memory safety and
a more ergonomic programming language. We also made sure to make it very, very easy to get started using
Swift in the code that people had. So if you had an existing project, tons of Objective C,
it doesn't matter how complicated it was, you could go add a single Swift source file to that.
And start using all of the APIs there, including using all of your own code.
This produced a really nice onboarding path where it didn't cost you more than a couple of minutes
to try out Swift. You know, write just one class in it, implement one feature in your app,
see how you like it. Very low risk. And so people could go ahead and invest time in using this new
language instead of what they already knew when it was appropriate for them. So this has worked very
well for us in the Apple platforms. So the existing C and Objective C ecosystem is moving towards Swift.
And we've seen this movement everywhere. I've talked about apps that sort of the highest level
and which we operate, but we've also been able to move lower level components. Things like
rewriting, you know, decades old C components with Swift as drop in binary compatible replacements.
So we can move memory safety forward, sort of anywhere in the system when it's time to make that
particular investment. We've learned a ton and we think that these lessons actually apply to other
ecosystems, and especially to the open source software ecosystem. And so that's what I want to
talk about today. And we've got four parts to this talk. First, I'm going to teach you a
little bit of Swift. Mostly to talk about how Swift tackles the memory safety problem because
it's a bit different than some of the other languages you might know. Then I want to go into
interoperability and what it means, how we tackled interoperability with the C family of languages
to make it possible to have this incremental approach to adopting memory safety. And I'm going
to talk about build systems. It turns out they're really important. Even when your language does
well interoperability to get build level interoperability. And finally, I'll bring it back to
memory safety because now we're talking about a memory safe language interoperating with
memory safe code as the way forward, but we have to talk about what it means to have memory
safety and that kind of hybrid environment. All right, let's talk some Swift. What is Swift? I
haven't actually said. So it is a general purpose programming language that's a joy to write.
So the goal here is to produce an approachable language. So whatever you're background in programming,
you can come to Swift, learn a couple things, see some familiar aspects, and get productive
immediately. And the language will grow to you. It has the power tools needed for experts as well,
but we have this principle of its progressive disclosure so that that comes along later in your
journey of learning Swift. The language itself is native compiled. It's great for performance.
And it's been open source since 2015. So if you want to go check us out and get hub with a Swift
language organization, there we've got all its compilers, tools, lots of libraries and so on.
And now I started my story with Apple, but Swift has grown beyond just the Apple platforms
to support all the major platforms and it's portable to other platforms as well.
So let's talk about the memory safety aspects of Swift. That's why we're here.
So memory safety in discussions, it can be a little confusing sort of what it is. And sometimes
when you listen to a memory safety talk, it sort of sounds like memory safety solves all program
programmer errors. It doesn't. That's not what it's about. I wish we could solve all program
errors. What memory safety is about is acknowledging that programmer errors will happen for sure,
but they need to not escalate into problems that can cause security issues.
Now there are aspects of language design that can eliminate classes of program errors. Those
are wonderful, right? And they can help with memory safety, but they're not the whole story. My favorite
example here is actually no pointers. So you have many options as a program language for what to do
about no pointers. In Swift our approach is, well they don't really exist. If you have a reference
to something, it's not null. If you want to represent that something may or may not be there,
you use an optional type. And so we have defined a way this problem, which is great,
but really that is static checking, right? Static checking that there's no places where you
dereference a null pointer in your program. Good that is a perfectly valid approach to memory safety
for null pointers. Java takes a different but equally valid approach. Java objects, they can all be
however, the Java system, the virtual machine, makes sure that it always checks for a null
pointer before you dereference it. And then throws a null pointer exception you can deal with later
to address that. This is also memory safety. It's dynamically checked, but it's just a safe.
Now what C does is the unsafe ground, right? There's no checking. So anything can be null
and there's no checking at runtime. And so if you dereference a null pointer and you do the right
things, you've created a security gadget that you can use to attack the program because it's
undefined behavior. The program can do anything. I call this a violation of the abstract model.
We can no longer reason about what the program is supposed to do by looking at the source code,
when there is a break of the abstract model, when there is undefined behavior. As a language,
you need to look at all the ways in which memory safety could compromise the abstract model
and lock them down either by refusing to compile the program or putting in a dynamic check
that will halt the program at runtime instead of limping on in some compromise state that can be a
security problem. When I think of memory safety in programming languages, I think of it
along about five different axes. And so there's lifetime safety. Are you accessing something
during its defined lifetime? A violation here is something like a use after free. You've freed
up some memory ending its lifetime, but then you access it later. Anything can happen. You have
no idea what's there. Bound safety is when you have a memory allocation, making sure that you're
accessing within the size of that memory allocation and no further out. Type safety is when you're
accessing a value, you need to use the same type as that memory was initialized with or some
other compatible type. So failures here are called type confusions, something like you wrote an
integer into one field of a C union and then you read a pointer out later. Well, now you've
got undefined behavior. Initialization safety is another easy one. Make sure that you've initialized
the memory before you actually read from it. Otherwise there could be garbage there that makes
your program do weird things. And then the hard one is thread safety. When your program is
concurrently executing, you need to make sure that whatever mitigations you have in place for the
other for, they still work in the presence of data races in that program. And there's a lot of
different ways you can tackle these various problems. Lifetime is perhaps the one that's most
talked about in the programming language world. So Swiss approach to lifetimes is essentially
goes in two directions. One is to build up from primitive types. So every programming language has
primitive types like integers, floating point numbers. We don't ever talk about the lifetime
safety of these things. It's an integer, right? The compiler handles it for you. It copies around
when you need. You can return them and there's no extra heap allocations to deal with. They're
very, very simple and there's safety there. So Swift builds up from this model with value types.
So all the primitive types are value types, but also things like arrays, strings, dictionaries.
Very large data models can be value types in the Swift world and Swift makes sure to copy them around
as needed. And it gives you this nice local reasoning property where, you know, when you make a copy
of a value, changing the original doesn't have any impact on the copy. It's very nice separation
of concerns you get. Swift makes that true for all the value types everywhere. And when you're talking
about something like a string and array, well the copying sounds like it could be expensive. Under
the hoods, Swift uses copy on right, so they can give this semantic behavior while still having
cheap copies. And you can build things that are essentially arbitrarily complicated with value types
and higher data models. Structs can be used to aggregate value types into more value types.
This is pretty common most programming languages, Swift also has e-dums, and so these are type
safe unions. Use this to express a choice among, you know, one of several alternatives,
all of which can have data with them, if you'd like. Now generally speaking, we pass value types
reference. Again, they're copies. There's no lifetime issues when you do that. However, we can
pass them by value with these in-out parameters. This is where things start to get interesting.
At the call site, we acknowledge the fact that you're essentially passing down the address of a variable.
Now of course, that variable has to be mutable. It's a let in in the Swift world. That means
it's immutable. You can't change it neither can anyone else so you can't pass it down by reference.
But when it's a variable, you're effectively passing down the address of mutable state.
As soon as you do that, you've introduced the possibility of lifetime issues.
And so Swift has this property that in-out parameters never alias any other reference to that value.
So when you have an in-out parameter, and it's referring to an argument, you are the only person
that can touch that memory. This exclusive access is critical for maintaining these lifetime properties.
Now this is a mix of static and dynamic analysis in the Swift language. So here's a little swap function,
just to swap two values. Now if you go ahead and swap two different local variables, it's fine.
We know that they're separate and the compiler will accept it. If you're to do something silly,
like swap A and A, well this would create an aliasing between those two arguments. So the compiler will
reject it and tell you, you have overlapping accesses here. These are not permitted.
Now if one of these is some sort of global variable, where we can't reason about everyone that's
potentially touching that memory, it'll roll over to a dynamic analysis. And we'll do a runtime
check to make sure that you don't have aliasing going into these in-out functions. This allows us to
have this lifetime safety model for this extension of primitive types. These value types that are
very easy to work with. Now Swift also supports object oriented programming. So if you're coming
from an OO language, this will be very familiar to you. We've got classes with single inheritance,
you can override methods. There's initializers that create those objects, DNitializers that
tear them down at the end, and classes have reference semantics. So you can have multiple variables
all pointing to that same object of class type on the heap. It was great for building all sorts
of data structures, of course. But it means you have to deal with lifetimes of these heap data
structures. With Swift we chose to use automatic reference counting. This is kind of in the
Goldilocks zone of sort of automatic memory management schemes, because it lets you have
lifetimes safety with very little ceremony. The programmer doesn't think about how you're doing this
behind the scenes. It just works to keep objects alive until they're no longer needed and then they're
destroyed. Now when compared against a traditional garbage collector, there's actually some really
nice engineering benefits to using automatic reference counting. It behaves deterministically. So
when the last reference to an object goes away, that object is destroyed. You don't wait until
some time later. And this can help you reason about your program. It permits local optimization.
Compiler can understand where we're doing reference counting and eliminate various reference counts.
And you can implement this with a really tiny runtime. You basically need a retain and a release
function to increment and decrement to the count and then call it a DNitializer when the thing goes away.
On our reference reference counting is also common in CNC++ libraries because it is a great
mechanism for dealing with lifetime. And so in C, it'll be manual. You have to call these,
you know, retain and release operations, but it's still the same underlying model.
Now, you do have to think about memory management a little bit with reference counting. Because
if you write a reference cycle, then unless you break that cycle, one object is keeping the other
alive. Some languages that use automatic reference counting have cycle collectors. It'll come by
identify these cycles and then destroy them. Swift does not and will not provide a cycle collector.
Because doing so basically undermines all of the engineering advantages that just talked about
with automatic reference counting. Now you have non-determinism when it happens. Now you have to have
a lot of metadata and a heavy runtime to support it so you can't run an all environments.
Instead of having a cycle collector, you know, we build into the language model the notion of
weak references. And so you can break a cycle by making one of the references weak.
And we support this in the type system. So when you have a weak variable, it must have optional
type. That's what this question mark is here. Optional is either the object is still alive and I have
a reference to it or the object died because no one was keeping it alive and now I have a meal here.
I can check this with this if let pattern where I go and try to get the objects. If it's there,
I have a strong reference to it so I'm keeping alive within the body that if. If the object is gone,
well the body of the if doesn't run and maybe I have some code in the else to deal with it.
But by building into the model, it makes it very easy to deal with reference cycles and model your
data in a way that works well with the system. That's it for lifetime safety. Down safety is really
easy. So if it has a raise, it bounds checks, it's a raise. There's not a whole lot more to say. As
long as you have the data there, bound checking is easy as a dynamic check that's often optimized
away by the compiler. Lift also checks for overflow and arithmetic operations and traps on
overflow as an additional security mitigation. Type safety again built into the design of the language.
If you have some instance of a super class and you want to check whether it's a particular
subclass, you can use the as question mark operator and that returns an optional. Either it has
the subclass instance or it wasn't there and it's no. That's safe. If you're dealing with the enums,
which are essentially type safe unions, you can just switch on them and check the various cases.
Again, just type safety built into the language. Initialization safety, very similar.
Before you get to use a value, it needs to be initialized along all paths, otherwise compile
either rejects the program. Fred safety is where things get pretty interesting. So where
Fred's, what are the threat safety problems? The fundamentally shared mutable state is the
root of all problems here in concurrent programs. Because when you have shared mutable state,
you can have a data race where someone is writing and someone else is either reading or writing
and you might not have data in a consistent state. Very easy to let this break your model.
Fortunately, this tells us what the strategies are for avoiding data races in programming language
design. You can make things immutable, right? If there's no one changing it, then it's fine.
You can read it from as many threads as you want. You can choose not to share. If you're not
sharing it across threads, then you can't have any data races because everything is in one thread.
And finally, you can make sure that if it is shared, that all access goes through some kind of synchronization
mechanism so that there's only one thread at a time that's touching that data.
Per Swift, the different lifetime mechanisms fit in with this. So value types are especially
good for concurrency because you don't have sharing. Whenever you make a copy of a value type,
you have a completely independent copy so you can't have data races on it. Even if both of those
happen to be mutable. Additionally, the value types can be passed around fairly safely,
right? Where if you have this notion of deep immutability. So if you have a let of a value type,
that thing is truly immutable. No one can change it out from under you. And so those let variables
can be accessed from any place at the same time. We know that no one is mutating. So the combination
of copying is liberally and having a deep immutability makes value types wonderful for writing
concurrent programs. References are a little bit trickier. The whole point of using reference
types in a system is that you have this shared mutable state. So Swift actually was inspired
by a length and other actor models and has actors as a first class language feature. So with
the Swift actor and actor owns and protects its state. Actors themselves can be used from many places,
but wherever you use them, you have to go through the actor's synchronization mechanism. So here
you see we're trying to withdraw something from this particular bank account. We have an
await here because you may have to wait until the actor is free to service your request to go
and actually do that operation. That's what guarantees mutual exclusion and the language and the
runtime work together to make that a safe concurrent model. It doesn't have any thread safety.
So I've just taught you Swift how wonderfully memory safe it is. Now you should all just go
and rewrite your code. No? Okay, I'm not going to tell you to do that. Maybe for some tiny
library utility? Yes, you can go ahead and just rewrite it. But rewrites are really, really,
really treacherous. Right? If you're going after trying to rewrite a library that's in use,
well, it's going to be a lot of work. And there's a lot of technical hurdles to actually
getting it right. The first one is sort of ourselves, right? The second system effect, where
it's a successful system, we build a new system and we're going to get everything right this time.
We're going to hit all the use cases, fix all the bugs, add new features, and this massive
architecture is possibly going to fall over it on itself. Even when we're doing this
noblely, like I'm going to rewrite to match the spec. Oh, the spec and the reality of the existing
library are probably not in alignment and you're going to run into trouble. And so rewrites
they're difficult and they take time and you probably can't just abandon the existing sea library
while you're doing this rewrite. You have to ship updates. You have to add features. You have to
respond to security issues. You can't just say, well, it'll be ready in five years. So you have
to keep shipping yield version. This brings up the social hurdles to just rewrite what's going on.
Because as soon as you've done this, you've basically split your world into the maintenance
of the old thing that everyone depends on and then the shiny new thing that we're off building
that people are having a lot of fun and giving conference talks about. And this can really
cause problems within a community when you've created this competition between the things
that's being used and this new rewrite. And it can leave people out. It can leave to some frustrations.
And it's not good for sort of anyone, but as a proponent of a memory safe language,
I always worry about how this reflects on the language itself, right? Because if these rewrites
go poorly, then it's going to set back to your goal of moving toward memory safe languages by
potentially years with anyone who was affected by this. My main bit of advice here is avoid creating
silos. It's silos is when you have something and it's separated from the outside world that
it doesn't work well with. And as we work toward introducing memory safety into our existing
ecosystems, be mindful that we're not creating this big silo with big walls from the outside world.
Instead, what I suggest is that we take an incremental approach, take the existing projects
that are there, right? The whole world is running on. You know, they're written in C and C++.
And yet that first line of memory safe code in there, so they can start on the path,
better memory safety. So when you do this, you've got to build system wired up. You
got to see, what does it take to actually write that first line of code and what is the experience
like? And then you move toward, well, try to write all of your new code in the memory safe language
in that existing code base. You will improve memory safety, right? You're bending the arc
toward memory safety over time, but you're not leaving anyone behind, you're not leaving any
code behind. You're evolving the existing code base. At this incremental approach, provides improvements
over time that everyone can make. Now, if you want to rewrite something in the project,
that's okay. You can do a targeted rewrite. That's fine. But have a reason that isn't just
I want to play with the new language. Make that reason be, well, this component needed to rewrite
because it doesn't meet these functional requirements. And you know what, we're going to do it in
the new language because that's what we're doing going forward. You've already established that.
All right. Let's say you've gotten that first line of memory safe code into your existing
C project, it has to talk to the rest of the code in the project. This is where language
inner operability comes in. So Swift went all in on inner operability with C and C++. Actually
embeds a full C and C++ compiler into the Swift compiler. So we use all of the M and Swift,
and we embed the client compiler. So the client can parse C headers and then Swift will translate
those into APIs that you can call directly from the Swift language. Very little ceremony,
just to get access to everything that's already out there in C. You can also mark things in Swift
as being compatible with C and those will be exported through a header that can then be used
from the C part of your code base. So this is inner operability in both directions. So we can
leverage what is already there and reinforce it, work with it, incrementally move forward.
Let's give some examples of code. So your OS headers probably are written in C.
And so you access all of your OS services with C. Maybe it's POSX, maybe it's something else,
but at the end of the day, it's going to be C. We can take this in very literally translate
into Swift. So if you import a POSX header into Swift, you're going to get those definitions,
you know, directly translated just in syntax. The compiler understands an implementation level
that these are C. So it will use the C calling conventions and type layouts and so on.
But you don't actually care. This is a Swift program where you just know that the APIs that
you were using in your C code are there now in your Swift code. These are not beautiful Swift APIs.
First thing you'll probably do in Swift is go, ugh, and write a nice abstraction barrier.
That's fine. The important part is that you can get to it from the first place with no
Zaremonie whatsoever. However, we can actually do better when we're translating C into the Swift world.
So this is the part of the core graphics library. It happens to be on Apple platforms,
but this is indicative of a lot of C libraries out there. And that, you know, it defines things
in a way that is standard to see conventions. So we have this CG color space ref pointer. And this type
is always passed around as no cake objects. We have retained in release functions because they're
using reference counting and C to handle object lifetimes. Completely reasonable thing to do.
We've got a lot of functions that all start with CG color space. They take these CG ref parameters.
There's a ton of structure here. A lot of convention that's been built up around C.
And if we use that, we can provide a much nicer Swift interface more or less automatically. Without
writing wrappers, we just tell Swift what the conventions are. And so this CG color space type
actually comes in as a Swift class. Now Swift is automatically doing reference counting for you.
It's calling those retained in release functions that you would have manually done in C automatically.
So not only did we get like nicer, nicer, more ergonomic interface here. We've got initializers
and properties and methods. It feels like great Swift. We got more safety out of this.
Because Swift is following the conventions that were established by the C library.
But safer to use the C library from Swift than it was from C.
In recent years, we've been taking this to C++. C++ is a much larger language with a whole lot more
idioms to think about. But we've been working through those a lot of them fit well with the way
Swift works. So a C++ container like a stood vector. Well, that can be treated as a Swift collection.
C++ has moved only types. Well, we can map that into the world of non-copyable types in Swift.
And what this does is it opens up more interoperability. It means that more existing libraries,
those written in C++, can be used in Swift and work well with Swift to allow us to write
more code in memory safe languages incrementally. Language of the operability is really important.
But for this to actually work, you need to get them building together. And this is not always easy.
This is sort of interesting where, you know, back when Swift started, we were launching into
one ecosystem. And we had this massive advantage. We didn't even realize.
Which is, there was really one build system that dominated there. And we were able to modify it.
Simple thing. And so what we did was we went and extended the build system so that you could mix
Swift into existing projects. Effectively, with zero configuration, you could take whatever existing
project, however messy, add some Swift code in there. And the build system would handle compiling
them separately and linking them together and help you, you know, talk across the boundaries
by leveraging that language of an interoperability. As we started looking at, you know,
going cross platform, like, ah, we need a way to make sure we can build Swift on every platform.
Let's build a package manager. Language specific package managers are awesome. You go clone some
repo and then you do just build or run or whatever. It's one command. It's amazing.
You know, we went further because we wanted to make sure, well, language level interoperability.
This is important. Let's make sure it ties into the system nicely. So we can use PKG config to
go find the headers for a C library and make them all available in Swift. And also the Swift
package manager, let's see build C and C++ sources along with your Swift. Great. We've solved
the interoperability problem. Except that we haven't, because a language specific package manager
is a silo. And if your goal is to move forward incrementally to allow C and C++ projects to go
and adopt a memory safe language, well, you don't want to tell them that the first thing they
have to do is rewrite their entire build system. Trust me, nobody wants to rewrite their entire
build system before they try your memory safe language. So you've created this massive barrier
and we sort of hadn't realized that that was the issue. And so when we did realize that this
barrier was there, we decided, okay, we're going to embrace C make. So C make well-known cross
platform build environment, it's open source and it supports a whole bunch of different languages.
There was already some basic support there for Swift. And so we work with the open source community
to go ahead and make that support real. So you can add Swift as a language to your C make project
and you can create Swift only targets, you can create mixed targets to have C or C++ and Swift.
And if you're using C make, which so many people are, it becomes a much lower investment to get
that first line of Swift code into your project. So you can start incrementally moving forward.
We have a GitHub repo up here of examples that show you how to do that if you're interested.
But the meta point here is build systems can be a huge barrier to entry and we need to deal with
that if we want to be able to move the ecosystem forward to memory safety. Language specific
package managers are awesome but they just they don't help with this particular problem.
All right. So, I've been talking a lot about interoperability. We've got C and C++. We're going to
make a talk to our memory safe language. But our goal was memory safety. So how is it that this hybrid
environment actually is or can be memory safe? First thing to do is step back. It's proponents of
a memory safe language. Remember that we can't just treat all of the C and C++ code on which the
universe depends as something fundamentally irredemally unsafe. This is really not the right attitude
to take. I can show an example of this. It actually comes from the Java ecosystem. This is a recent
Java enhancement proposal. And what it says is basically, well, we're building for the world where
we're going to treat all native code as memory unsafe. The problem here is what we know that
there's native code that's memory safe. I've just been talking about one of several native
memory safe languages. Right? And this proposal is basically creating a silo around the Java
Merciful Machine and putting every native language, including those outside of the silo.
The thing is, we can build safe interoperability between languages. So my colleague Conrad,
we'll be having a talk later in the Java room on the Swift Java interoperability and how we deal
with memory safety across the boundary there. But this is a general principle. If you know the
conventions of the other language that you're talking to, you can figure out how to interact with
it safely. And so my proposal here is we should evolve C and C++ to take those conventions that already
exist and codify them. In a way that can help improve interoperability and make it more memory safe.
I'm going to show some examples of this. So let's think about bound safety in C. So fundamentally,
bound safety in C is hard because when you have a pointer in C, there's no bounds information.
It could point to one element. It could point to 1.000 elements. That information is not there
in that pointer and you can't just add it to the pointer. So if you're bringing that into a
memory safe language, you're stuck using unsafe features. So in Swift it'll come in as unsafe
pointer. It's screaming unsafe at you for a reason. We have no idea what the bounds in that pointer
are. We have no idea what the life time of that pointer is. The thing is, if you look at this
CAPI, this was designed to provide that information. The count of the number of elements that
numbers points to is right there in the function signature and the documentation is going to say
or at least imply that that's the count for the function. So what if we actually just said that?
So this counted by attribute here is saying that the memory pointed to by numbers has count
elements in it. Those are the bounds. Simple annotation moving documentation into code.
Now, on the C side, if we have this, we can improve the C code by doing bounds checking on
that pointer based on the count that we're given, incrementally improving the memory safety of
C itself. So this is the feature that's implemented experimentally in Clang. You can go check
out this URL if you're interested. It's the bounds safety flag and it helps incrementally improve
safety of C, but also communicates critical information about the conventions here that are
needed that you have to follow to make this API safe. And so one of the things that let's us do is
if we honor that convention from the swift side, well, now we can fuse these numbers and count values
that like separately together describe the information you need to be bound safe into a single
parameter on the swift side of an unsafe buffer pointer. It's still unsafe because we know nothing
about lifetime at this point, but we at least know the bounds and they're there with the
pointer together. So we've created that information that wasn't there before or wasn't there
in a mechanically verifiable way. Again, leverage safety by describing this convention, we can
improve safety on both sides. Let's talk again about automatic reference counting. I showed you
a little example before of taking this convention of automatic reference counting forward. Let's
dig into another example here. So this is from the Knome G lib library, so one of the lower level
libraries with the Knome Toolkit. And there's this G variant type that uses reference counting,
like so many things in these Knome libraries. And, you know, it's this G variant type, there's a
ref in an unref, there's some couple of other operations. We could bring this into swift just with
a whole bunch of unsafe pointers, but we would rather state what the convention is. And so we can
state with this macro here, the swift shared reference macro, that in fact G variant is meant to be
used as a ref counted type. Variant ref's in variant unrest or the retain and the release operations
respectively. And by doing this, we've stated what the convention is. So in swift, we can bring
this in as a class. Now, there's other things we may have to annotate, so this G variant new double.
What does that return? Like, did it return something where you're responsible for
incrementing the reference count before you destroy it or not? We don't know. And so this is again
another annotation to say, well, it returns a retained reference. So it's returning at plus one,
or it's already retained at for you, you need to go and do the release yourself when you're done
with it. By establishing the convention, right, which should be in the documentation anyway to be clear,
but by putting in the signature here, we can actually have swift fall that convention and
provide safe access to this type. Now, I've done one more thing here. I'd notice I've marked the
the return pointer as non-null, right? As I said before, swift doesn't have null pointers. And so
if we have a pointer coming in from C, we have to assume it could be null, so we treat it as an optional
type. By marking this as non-null, we're stating that no, this will never be a null pointer
when coming out of C, and so we can use a non-optional type. So these two macros essentially
let us state in C the convention's on reference counting so that we can make the swift code
use these APIs directly. Now, extra translation layer and get memory safe behavior out of it.
Biler will automatically put in the retains and releases based on those C functions you provided,
giving you memory safe access to this underlying C. If we want to go a little further,
we can add some annotations and say, hey, actually be really nice if, you know, that new
double function was treated like an initializer to create the class the way you would probably want to
in swift code. And then you can turn, you know, the various functions into methods or properties,
just to give a nice swift interface to make it easier to use. So you get safety and you get nice
ergonomics just from this, you know, adding annotations into the C header, describing the
conventions that we're already there. There's other lifetime safety attributes in Clang to
color of other situations, right? Reference counting isn't everything. There's this no escape
attribute, which basically says, if you have a parameter that's no escape, then that pointer will
only be used within the body of that function. It's not going to be saved somewhere in access later.
This is really important information because it says, it's okay to pass down a pointer to some
temporary, it's on the stack, for example, whether you're going to remove afterward. There's also
the much more complicated lifetime bound attribute that's newer, that describes the relationship
between sort of the lifetime of a pointer parameter that comes in and says that the result of the
function points somewhere into the data structure that came from that pointer, right? So it establishes
this important link. Now, the important thing about these attributes being there is we can
use them on the swift side, but they can also be used to improve C and C++. Because if you've added
these annotations, some static analysis tool for C and C++ can go and make sure that those actually
do hold. Make sure that you don't escape the pointer that you said was no escape or that you're
following these lifetime rules. Adding these to a header, therefore improves the world for
C and improves the world for swift. And so if we go and add this no escape attribute to numbers,
which is documenting the behavior we already expected there, well, we can bring this into swift
in an even better form. No one's safe here anymore because we get to bring this parameter in as a
span. Span is like a reference to some kind of contiguous memory where we have the count,
we know the bounds information and we know the lifetime of it, right? And so we can make sure that
it doesn't, isn't used past the lifetime of that particular entity. So the CPI is now far safer
to be more verified from the C side and it's safe to use from the swift side. Now, I just talked
about a whole lot of modifications to genome headers. You can't usually do that with your system
headers. You can't just go and rewrite them. And so one of the things we found here is if you can't
modify the headers, you don't own them, can't get the maintainer, take your patch, don't want to
wait for the updates to roll out. We have this separate mechanism. We call API notes. And so this is
actually a YAML file that you can sit on the side and it can come with your own project to layer
this convention information on top of a C library that you use. And so I didn't actually touch
my canone headers when I was playing with this demo project. I just wrote API notes directly
to go and layer conventions on top of my existing system headers for genome to get this nice
safe access to the existing C library. Now, this is not a solution that will work forever. The
right answer is to get those annotations into the headers at some point. But let's do incrementally
move forward by translating the documentation of conventions into code that everyone can use.
So this kind of safe interoperability. We're talking across the language boundary to
provide safety. This is going to require coordination. And so our general software community
is going to have to actually work on this together and it's going to require work like real
work from a lot of different places. So some of that work is going to have to be in the C
++ communities and their committees that evolve the language to find a standard way to
codify these conventions so they can be rolled out throughout C libraries. And we shouldn't be codifying
them with swift shared reference because this isn't just about swift. This is about documenting
the memory safety conventions that exist. And so things like nullability, these lifetime
annotations, these bound annotations. We should standardize those in C and C++ and start adopting
them throughout our system libraries. Now we need to do this in a way that benefits C and C++.
So these should come with optional runtime checking for things like bounds when you add bounds
annotations or static analysis tools that can verify these so that when you go and say okay I'm
going to add these annotations to my library to make it better it makes the C code based incrementally
better. So we improve what we have and we build a bridge to the memory safe future where we can
use those annotations to provide better safer interfaces in the memory safe language. So I think on
the memory safe language side we need to prioritize thinking about interoperability with the
C family of languages, making sure that's safe and easy to do both at a language level and at
the build system level. And when these changes come along to C and C++ to add this convention
interoperation we need to go and honor that on the on the side of the memory safe language.
So now that we have this convention information we can reason about what it takes to use this
safely. And then say okay this whole program right where we've documented all the conventions
and we have checking we can reason about its memory safety properties. This is an incremental step forward.
When we do this we can get a faster rollout of memory safe languages because we've compartmentalized the
problem. So a couple of parting thoughts here. The goal here is to incrementally move memory safety
forward and we want to be able to take sort of any part of the system and when we put in some work
to the memory safety whether it's you know bringing up support for memory safe language,
rewriting a piece or showing up the secret that's there by documenting conventions. That moves
memory safety forward for that ecosystem. As we do this be mindful to avoid creating silos.
Don't don't build up a memory safe silo over here and say well we can't work with the rest of this
world. It's not memory safe. Try to keep it moving together so because this is going to take
everyone in the industry to move forward. And we have to work across these language barriers to improve
safety throughout. There's no way we can build up any one silo that's big enough to cover an
entire ecosystem. So we have to keep working across languages maintaining safety across these boundaries
to make progress. I've talked a lot about Swift. If you're interested you can check out Swift.org
for some resources and learning it. There's a Swift room here at Fossom. So it's up on the fourth floor
in this building. I think we start three o'clock this afternoon. We've got some great talks and demos
on embedded Swift, Swift on server, all sorts of other interesting Swift talks. In the Java
in the free Java room there will be a Conrad's talk on Swift and Java interoperability and how to
get safety along the funders. All right well thank you very much. If you have any questions please come
talk to me after.