Keep up with the latest enhancements to C, Objective-C, and C++ made possible by advancements in the Clang compiler and LLVM. Find out about new static analyzer features and how to use them to improve your code. Learn how to take advantage of new optimizations for code size.
So, over the past year, we've been working really hard on adding some great new features to the compiler.
I'm Jessica, and today, me and my colleagues, JF and Devin, are going to share some of those great features with all of you.
So, we've got a lot of ground to cover today.
We're going to talk about new platform support, some low-level code size optimizations, some language-level code size optimizations, some great new diagnostics, and then we're going to finish up with some great new static analyzer checks that will help you find bugs in your code. So, let's get started with new platform support.
Specifically, I'd like to talk about the Series 4 Watch.
The Series 4 watch usually a fully 64-bit chip.
But yet, all of the App Store apps are 32 bit.
The curious thing about this is that on day one, all of your apps worked seamlessly on the Series 4 Watch.
This seems kind of like magic. You didn't have to recompile anything.
But yet, all of the apps worked. How did this happen? Now, it would be awesome if I could get the front row or any row of the audience to give me a drum roll like this, but if you don't want to it's fine.
Da da da da da da da da da, the answer is Bitcode. And I explained absolutely nothing. So, allow me to explain this to you. Let's take some source code, your favorite language. What we're going to do is we're going to hand that off to the compiler.
Now, normally what you would do here is you would continue the standard compilation process, and you would get like a binary or something, but we're not going to do that. What we're going to do instead is we're going to stop the compilation process early, and we're going to produce LLVM Bitcode.
So, what's cool about LLVM Bitcode is it encodes an intermediate state in the compiler.
And what you can do with this intermediate state is you can actually pick up the compilation process where you left it off.
So, that's what we do, except this time we do it in the App Store.
By doing this, we could take one Bitcode for one app and produce two different apps from it, one to run on a 32-bit chip, and one to run on the fancy new 64-bit chip as well.
Now, there kind of is a problem here.
The problem is is that the compiler doesn't actually know that you're going to be executing code on a 64-bit device, and if it knew that, it could actually leverage that information to optimize the app even more.
And so to get around that, what we do is we collect Bitcode for the 64-bit chip as well.
This allows us to create a really, really fast app just from the Bitcode.
Now, this is pretty cool. This is like-- this is one of the things that makes compilers kind of magical. It's pretty awesome, eh? Now, let's move onto Code Size Improvements. This is one of my favorite topics.
We've been hard at work having the compiler actually produce as small of code as possible for you.
Now, code size is pretty important because bigger code means bigger, slower downloads, and bigger apps take up more space on users' devices, and you notice this kind of thing.
So, to support users that actually want to prioritize code size over all other metrics, we added a new optimization level to Xcode.
And that optimization level is -Oz.
And I'm going to keep saying -Oz because I'm Canadian, so we might as well call it Oh Canada.
What I'd like to show you today is an example of the kind of optimization that appears at -Oz, but before doing that, I'm going to do my best to tell all of you how a compiler works in a couple of minutes flat.
So, when you compile some code, the initial representation is very, very target independent. It's source code.
When you put that into the compiler, it gets lowered to an IR, an intermediate representation.
This intermediate representation is still mostly target independent, but it does have some target-dependent features built in.
It looks kind of like a generic assembly.
At this point, you could just stop the compilation process and spit out some Bitcode, but we're not going to do that. We're going to keep going.
That representation is further lowered to what we call MIR, which is short for Machine IR.
At the end of this process, the Machine IR looks almost identical to the assembly for the target, in this case arm 64 assembly.
We could put the code side by side, for example.
Now, in the optimization that we're going to talk about, we're going to be working with the Machine IR, but for the sake of familiarity, I'm just going to do all of our examples in assembly, because that's less scary.
What I'd like to tell you about is the code size optimization called Function Outlining.
Function Outlining is one of those optimizations that's going to do everything it can to save you some size.
It's very, very late in the compiler. It doesn't actually rely on any sort of a source code language.
The best way to explain what this does is by example.
So, let's say that you have some assembly like this.
hasse and kakutani are two random functions in some random program.
Now, what's interesting about hasse and kakutani is they have some identical instructions.
What we can do with this is we could take those identical instructions and factor them out into a new function.
After we factor them out into a new function, what we can do is we can replace the sequences that we found with calls or branches.
As a result of this, we end up with a smaller program.
And how much smaller? Well, up to 25 percent on our test programs. Now, some of you might be wondering, okay, well where do these savings come from? Is it because of copied and pasted code? Is it because your code needs to be refactored? Well, no, there's actually something a bit deeper happening here. If you do have a bunch of code copied and pasted around, that will impact outlining behavior, but that's not really the most important thing.
Let's look at another example.
Say you have this function.
It doesn't matter what this function does, but what I'd like to address is what happens if we take this function and we put it into the compiler. We get some assembly. Well, you might get something that looks kind of like this.
Once again, you don't really need to understand this assembly, but what I'd like to draw attention to is the instructions at the beginning and the end of the function.
These are called the function prologue and epilogue.
These instructions don't correspond to any individual lines of source code.
These are instructions that are inserted by the compiler to meet some sort of system requirements.
So, these types of things, like these stores and these loads could appear in many places throughout your program, and this is the kind of thing that the outliner can actually leverage to reduce the size of the overall program.
There are some gotchas to do with this though.
First off, when you outline, you change the control flow of your program.
Here, you might initially have ulam called collatz, but then you could outline the call to collatz.
The thing about what happens here is we've changed the control of flow of the program.
So, the problem with this is what happens if you crash inside collatz? Well, what's going to happen is you're going to go and you're going to take your program and you're going to throw it into LLDB, and you're going to see the added outline function in your back trace. So, this is something you have to watch out for if you're actually outlining code. Another thing is that outlining can increase the execution time of your program.
You're adding calls, and calls could have an execution time overhead.
And this is actually okay though because -Oz prioritizes size over everything else. When you're going through -Oz, you're saying, make it small.
Because of this, we don't recommend that you compile performance sensitive code with -Oz.
If execution time is king in your program, -Oz is not the best thing to use.
However, we do recommend that you use instruments.
What instruments is going to tell you is where the hot spots in your program are, and this can allow you to make the best decisions with respect to optimization for your app.
The compiler has lots of different optimization levels, and those optimization levels all prioritize different things.
For example, -Oz prioritizes size at all cost, and as a result, you might have some slightly slower execution time. But on the other end of the spectrum, you have -O3. -O3 is going to prioritize the execution time of your program at all costs, and as a result, you might get a larger program. -Os is the default optimization level in Xcode because it has a good balance between speed and size.
But, you know, you might have different optimization needs, and so you can use instruments to figure this out. The compiler also offers some extra optimizations that I don't have a lot of time to talk about but I would like to cover a little bit about anyway.
It offers PGO, which is Profile-Guided Optimization.
PGO is pretty cool because it allows you to actually execute your program and then collect information about how your program runs. Then you can use that to guide the compiler when you compile it again.
It also offers LTO, which is Link-Time Optimization.
What's cool about Link-Time Optimization is at the cost of some compile time, what you can do is you can tell the compiler, okay, let's wait until we have every single file in the program and use that to say provide better inlining and outlining.
Optimizations like inlining and outlining do better when they have more context, so LTO could help there.
You can also combine these extra optimizations with the existing optimization levels to get some really, really good actual performance.
Because I don't have a lot of time to actually cover what these things do, I recommend that you check out the previous What's New in LLVM talk so that you can learn a bit more about them. After all that, you might be wondering, okay, how do I enable -Oz? Well, just go to your project's build settings and select -Oz as the optimization level.
You can also enable -Oz or other optimization levels on specific files in Xcode by going to your project's build phases, going to the compile sources list, and setting the compiler flags. All right.
So, I just told you a lot of stuff. You're probably wondering, okay, how does this impact my app's code size? How do I figure out this kind of information? Well, to figure out code size, I recommend a little tool called size.
I use this tool a lot. It's a nice little terminal app.
So, size is going to give you some nice low-level binary information about your app.
It is not going to tell you the actual total size of your app, because it doesn't include things like assets, for example. If you have like a giant picture in your app, and you have like a hello world of code, the compiler can't help you with that. But, let's say you want to use size.
Here's how you do it. It's easy. You just say size, and here's the path to my binary.
What it's going to do is it's going to give you some nice output like this.
What it's going to do is it's going to tell you the size of each segment in your binary, and it's going to tell you the overall size of the binary as well.
But, the thing is, is each segment in a binary is actually comprised of many sections.
I only care about executable instructions in this example. So, what I'm going to do then, is I'm going to give size a couple extra flags.
Those extra flags are the -l and the -m flags.
When I do this, what size is going to do is it's going to give me a per section breakdown.
If I want to find out more about executable instructions, then I can just look at the text section. So, I hope that all of this helps you with gaining some more insight into the code size of your apps. Now, I'm going to leave, and I'm going to hand it off to my fellow Canadian, JF, who's going to tell you about some language-level Code Size Improvements. Thanks, Jessica.
So, I'm Jeff, and I'm going to tell you about Language-Level Optimization. So, Jessica told you about low-level, kind of assembly-ish optimizations, and I'm going to tell you about the stuff that happens when you use the language itself, right, so the constructs that you write your code in. I'm going to tell you about four of those optimizations today that also affect code size.
The first one is related to objective C, when you use blocks, right. So blocks has a bunch of associated metadata that the compiler generates for you.
It has metadata as well as helper functions, and we'll go into a few examples to see what that looks like.
So, say you write some code, and it looks something like this.
So, you'll notice that I have two blocks in two different functions, and the code doesn't really matter, but the important bit to notice is that the blocks do completely different things, right. The code has nothing to do with each other, but they have fairly similar struct in that the capture for the blocks is similar. In this case, I capture two arc-strong pointers.
Now, I told you that there is metadata associated with each block. What does that look like? Well, this is the metadata that we're talking about, right. So the compiler generates this automatically for you when you use blocks to track a bunch of information about the blocks and give them the behavior that the language guarantees.
What you'll notice is there's a block size in there. There's a copy helper and a destroy helper, which are two methods that we'll go into in a bit, and there's a block method signature as well as the block layout info. All right. And if you look at the example that I have on the screen, this is actually the synthesized code that the compiler generates. Now, that looks like a bunch of gobbledygook, but what really matters is that the struct itself is the same, so we can duplicate in a lot of cases. Now, you'll notice in this case we can't because despite capturing two arc-strong pointers, there's other captures in there, and the block size itself is different.
All right. So, we can't merge these cases, right. But in general, there's some cases where we can end up merging these things, right. You'll notice in this example though, we have the functions, right, so the copy helper and destroy helper can be merged, right. And you'll notice that as of Xcode 11, we do merge them, right. So, those are the same. What does that mean? Well, the copy helper is there to help you when you move a block around, right, and destroy helper is there to help you when the block goes away, right. And the code that the compiler synthesizes when you do this looks about something like this.
All right, so in the example I had, you'll remember I said we have two strong arc pointers, and the code we generate is something like that, and we copy it around. You retain, and when you destroy it, you release. Now, there's a bunch of other things that blocks end up having to do when you copy or destroy. You might have C++ objects, in which case you have to color copy constructor.
You might have-- and the destructors, right. You might have some weak arc pointers. You might have nontrivial C types or something like that, right. So, there's a bunch of other stuff that needs to happen, but basically you write code using blocks, and when the compiler detects that there is redundancy, we try to eliminate as much of it as we can.
Now, how much does this pay off? We found that in objective C applications, it's roughly a 2 to 7 percent code size reduction, right, and that comes for free. It's just enabled by default.
The second optimization I'm going to tell you about is related to Instance Variables of Direct Subclasses of NSObject. Now, that's a mouthful, and I've got to give you an example and explain what that actually means, all right. So let's say I'm writing a card game, and I write code that looks something like this. The key thing to remember here is I'm deriving directly from NSObject, right. When I write the objective C code, my properties corresponds to instance variables, right. So there's instance variables generated automatically to back up the properties that I have here, right. Now, the class itself that I wrote, the compiler looks at it and generates a struct that looks something like this roughly, right. It lays out the members, one after the other. Now, the thing in objective C is that you can have a base class derived from it and then change the code in one framework to the other, and the base class changes, has new members and whatever, and the derived classes don't break, right. And the objective, in C++ you can't really do that. If you derive, then the base class's layout changes. You have new sizes.
Now, here I'm deriving from NSObject.
NSObject is effectively part of the ABI of the platform. So, we know it's not going to change, right. So, we have this layout of the class, and when we're implementing the class, right, so implement this initWithName method, I know exactly the layout of everything in my class, right. So, the compiler, as of Xcode 11, can go in and say, I know where the offsets are. I can hardcode them.
Well, okay, what does that actually mean? So, I look at this initWithName method.
It'll look something like this, right. So, I have self.name = name. Now, the setter, to generate a code for that looks about something like this before Xcode 11, right. So, it synthesizes a lookup into a table to know the offset of the name property or the name IVAR rather.
Now, that's a small amount of code, but what we do as of Xcode 11 is something like this instead, right. So, we just hardcode the offset when you're implementing the method that's a direct derivative of an S object.
That makes sense. We know it's not going to change because we're implementing the thing that you just wrote. And it seems pretty trivial; it's just one instruction out of three, right. But it turns out to be roughly a 2 percent saving in applications.
It's pretty s sweet. The next size optimization I'm going to tell you about is improved debuggability for C++ types. Now, you're going to go, wait, that's not size. It is. Just let me get to it. So, I say I write some code.
Fairly straightforward code here. It's a command line application, right, and what I'm doing is I'm taking the parameters from a command line as strings, transforming them as integers, putting them into a std::vector and then printing them out one after the other, right. Really straightforward demo application.
Now, what important here is I'm using the standard library's types. Specifically I'm using vector push back, and I want to go in and put a breakpoint here.
Now, this used to not to work really well before Xcode 11. The reason was that we were controlling the visibility of lib C++'s methods just like push back by force inlining them into your code, right. Now, generally that works pretty well. The problem is push back is giant, and then the optimizer goes to town, moves around, deletes some code, and the debugger, when you tell it break on push back, you're not trying to step into push back, you just want to put a breakpoint on that line. The debugger doesn't really know where push back is because it's been splat all over the place, right. So, what we do as of Xcode 11 is that we don't force inlining. We let the inliner decide when inlining should happen instead, right. So, what used to happen in this specific example, before Xcode 11 is the breakpoint that you put there would actually break on the second loop, right, because the pushbacks are just everywhere now. So, as of Xcode 11, we don't force inlining, and this is what the debugging session would look like. So, say I run lldb, I run my program, and I say put a breakpoint on line 12.
Right. Pretty straightforward.
Bugger goes in, says yep, breakpoint, got it.
Now, I hit run. This is what happens now. All right. So, I break on line 12. Cool. It just works, right. It's kind of a boring demo because it does what you expect. It used not to. So, the cool thing now is I'm talking about code size optimizations, right. Well, this, because we don't force inline really big things, if you use the stl a lot in your code, you create quite a big amount of code bloat. And so what we've measured is on big applications release mode up to a 7 percent code size reduction when you do this. Now, again, this is in release mode, right. So, that's a pretty good amount of code savings and better debugging. That's nice. The last code size saving I want to tell you about is C++ static destructor suppression.
And again, let me walk through an example to show you what I actually mean here.
So, say I write some really generic C++ code. Most applications end up having a logger, something like this, right. And when you do logging, you don't want to pass a logger around throughout your application, so you have just a global variable called logger. Fairly straightforward stuff. Now in C++ when you have a global like this, it'll have a destructor that'll run at the end of the application's lifetime.
Right. And you'll notice, the logger contains a buffer that's a std::vector of string, so that's what the destructor is going to do. It's going to destroy that vector of strings, right. Straightforward thing so far. Now, I go into my application, and it's a game. Right. So I add this code here. It's just a game. So, again, I just have one application and one game, and so I just have a global here for my game.
Totally sensible code, right.
Now, the problem is if I go in and I add some logging code to the struct of the game, well, you'll notice, I have the logger that's global. I have the game that's global. That might not work out so well. The reason is in C++, between different translation units, the order in which the destructors are called is not guaranteed, right. In a lot of cases, you'll destroy the logger before you destroy the game. What's going to happen, this is going to crash, not good, right. So, that's kind of a headache right here. And then dig a bit more into how C++ works, and this is my mental view of what that is, right. So, you start adding thread local storage. You start adding threads. There's like the graph for C++ destructor ordering is really complicated, and it's complicated enough that even like compiler people who are supposed to know how that works, we have no idea. I had to fix a bug a few months ago in Clang where Clang would in really rare cases crash upon termination when trying to clean itself up, right. That's kind of embarrassing, but it's just to show like it's not like a trivial thing to get destruction order correct. All right. And let's go a bit further. On iOS, this is what the lifecycle in an application looks like. Right. There's not really a logical time for the application to shut down. There's times when it goes to foreground, it goes to background, and it goes away, but like destruction, like the application shutting down is not really a thing that makes sense with that type of lifecycle. What you end up having is you implement callbacks, something like this, right, and then you're told you're going to go like in the background, you're going to come back, something like that. Destructors don't really run in a logical place.
So, if we go back to the code we had earlier, right. It's an application. It has a logger. This is what we wrote, well there's not really a logical time for this logger to flush its buffer, right. But the, like in the destructor. What you really want to do is say well, if you're going to go in the background, please flush the buffer first, right. So you don't have any cleanup to do in the destructor. It's kind of silly to have a destructor. There's a bunch of code that gets generated for nothing, right. So, we added, as of Xcode 11 is an attribute that allows you to say, hey, don't destroy this thing, right. It's global. It doesn't need to have a destructor.
Of course, you're still going to flush manually when a callback happens, and you can go into your entire application in Xcode and use the setting to do that for the entire application. It seems pretty trivial, but it gives you, depending on how much C++ you use in your code, maybe like a 1 percent code size reduction, right, which is pretty nice. So, let's move on from code size reduction and talk about diagnostics instead, right. So, I'm going to tell you about five diagnostics that are all on by default in Xcode 11.
The first one is call-to-pure-virtual function from a constructor or a destructor. What does that mean? So let's write some good object-oriented code starting with a table. All right. So, I have this table here, and I go and I want to have a pure virtual function to illustrate what I'm talking about. So, I'm going to go and write this galahad function that's pure virtual, and I'm going to have a destructor for the table, and when the table gets destroyed, I'm going to say, galahad, please go and find the grip, right. It makes a lot of sense. So, I do that, and I get a warning.
The reason you get this new warning as of Xcode 11 is because calling a pure virtual function from a constructor or a destructor really doesn't make sense. There's nothing to call. Because the table is the base class, and the most derived classes have already been destroyed in this case, right. So there's no implementation of this galahad function to call anymore.
So, how would you fix this? Well, you could go, and in the derived class that implements galahad, its destructor could call find galahad and return a grail or something like that. So that makes some sense.
All right. Let's move onto another diagnostic.
Memset with transposed arguments.
So, say I have this struct called inbox, right, and I have a bunch of emails in it. I come back from vacation, and I'll get to inbox 0. What do I do? Well, I just memset the whole inbox to 0. Now, I wrote this code. Who can spot the bug? Right. I've transposed the argument to memset. Like, I make that mistake semi-frequently because I have no idea what the order of argument to memset is. Whether the value I'm trying to set destruct to is the first argument or whether it's the second one, right. Whether it's the size that I'm trying to set it to. And so we now detect that as Xcode 11, and we tell you about it. How do you fix it? It's pretty simple; you just flip the arguments around, right.
Now, one thing you might want to consider here is instead of using memset, which is hard to get, and even like look at that code, it's still not obvious that it's correct, right. What you might want to do instead is use something like std::fill. In some cases it makes sense. You rewrite the code to look something like that, and now it's much harder to get wrong, and it's easier to get what it's doing. Right. So, that's kind of neat.
The third warning I want to tell you about, return of std move. So, move is a bit complicated in C++, but there's a bunch of diagnostics over time that help you really use it the proper way, right. So, again, let's write an object-oriented code to understand what I'm talking about here. So, say I have three structs, lion, goat, and snake, and love object orientation, I'm going to compose them into a chimara, right.
So, I go in and assign bellerophon and I want to go slay the chimara, and then return with proof that I've slain it. Well, I now have a diagnostic that tells me, hey, you know what? Like you're returning a chimara, but you're just, like the return type that you're returning is actually just a goat, right. So, I'm going to take that vector, copy it over, because it doesn't make sense to slice out the vector out of the chimara and put it into a goat instead. Right. So, what you're doing here, the code you wrote is basically what you've been trained to do related to std::move. You're trained to rely on copy elision, right. And most times, you don't need to put a std::move when you do return. In this case you do because it does a copy, right. The language says returning, just slicing out a part of the class is kind of weird anyways. Move shouldn't be implicit, right. So the warning tells that you probably don't want to do this, right. So, first way to fix it, you go in, you call std::move.
Right. Now, that will move the vector into the goat, right. So it's much more efficient to do the move this way.
So, another thing you might want to do is well instead of returning just the goat and having like people trust you that that's actually a chimara, well you can just return a chimara. That makes sense, and here you get copy elision, right. If you were to add std::move, the compiler would tell you that you're pessimizing things by adding the move.
Another thing you might want to do because you're not sure if you're going to get a chimara or not is you might want to return a std::optional with chimara, which again does the right thing. It's not slicing things out of the class and so the language says, yes, this gets implicit copy elision. All right. Another diagnostic I want to tell you about, size-of-pointer-div.
What does this look like? Well, say I write this code.
Totally fine code. No problem right now, right. So, what I'm saying is I have this array.
Take the size of the array divided by the 0th element, and that gives you the number of elements inside the array. That's really standard code for C style code, right, a really common idiom. So, the problem here is if I refactor this code and I do something like this. So, I pass the array as a parameter instead.
Well, what happens here is that the C rules say that the array now decays to a pointer. And the new diagnostic tells you, hey, this probably doesn't do what you want, right. This won't return you the number of elements in the array.
So, that's a problem. We catch it. How would you actually fix it? Well, you could have written the code a bit differently. So, instead of using that idiom, right, you could have gone in and used something like std::size, which means that instead of refactoring the code wrongly, you would have caught that problem when you tried to refactor it, right. So std::size just does the right thing here. So that's a kind of a neat warning that catches errors. The last diagnostic I want to tell you about is defaulted-function-delete. So, again, say I write this beautiful code here, all right. So, I have this struct aberration. It has some floating eyestalks, some eyes and mouth, and I'm like I want to default aberration. Please give me a default aberration. Well, the compiler comes in now and tells you, hey I don't know what a default aberration is. Why not? Well, I have a float reference, and I can't synthesize the fault constructor for that reference.
It's a type that I can't default create. Right. So there's a bunch of other ways, not just references in C++, to create things that can't be default constructed, and the compiler now tells you about it. If you ask for a default constructor, and I can't give you a default constructor, it'll tell you.
So, how would you fix this? Well, one way is to just create the constructor yourself. As you pass in the eyestalks, that creates a reference automatically. That's kind of neat. But personally, I think like beauty is in the eye of the beholder, but maybe this aberration should be coded differently and not have a float reference, and maybe you should just do this instead, right. That makes a lot of sense, right. So, now I can default create to the aberration.
All right. So, that was the diagnostics I wanted to the tell you about. Now, I'm going to hand it off to Devin, who's going to tell you about New Static Analyzer Checks.
Up until this point in the talk, the warnings that we've told you about have come from the compiler when you build.
But we have other tools that can help you find bugs.
One of them is the static analyzer.
The analyzer finds deep bugs in your code, and it can do it without even running your app.
This makes it great at testing and catching those hard-to-reproduce bugs that you never even thought to write tests for.
And it can even show you the sequence of steps along which the bug manifests.
This makes it easy to understand the problem and fix it.
Today, I'm going to tell you about three new C++ checks that we've added.
A check for use after move bugs, a check for dangling C string pointers with C++ std::string and to check for reference counting bugs in the new DriverKit and in IOKit.
Let's start with use after move.
In C++, moves allow you to avoid unwanted copies. And here's an example of where you might want to do that. Let's suppose that I have written a novel, and for those of you who know me, I can be quite verbose. And so, I don't want to pay the performance cost of copying the entire text of the novel when I hand it off to my publisher.
So, I'll use a move.
This moves from the source variable rather than copying it. And what's great about this is that it allows me to enforce a unique ownership semantics where there's no confusion about who has the latest version of the novel, me or my publisher.
But I do need to be careful here, and that's because moves leave the source in an unspecified state. Let's see how this can go wrong. Suppose I were to add a call to spell check my novel after I published it.
This can have unexpected results or even crash depending on the implementation of the book type.
Fortunately, the static analyzer can now catch this bug.
To fix this, what I should do is reorder the code.
It really does make sense to spell check the novel before I publish it.
Let's move on to dangling pointers from std::string. Those of you who have mixed C++ and C strings know that this can be really tricky, and here's an example.
I've created this generateGreeting function. It takes in a C string name and returns a C string greeting.
And in the implementation of this function, I've chosen to use a C++ std::string because it's easier to manipulate.
I declare a std::string local variable, initialize it to hello, append the passed in name and then, because the function returns a C string, I call the c str method on the C++ string, and that's where things start to go wrong.
And the key thing to note here is that c str returns an inner pointer to a buffer inside of the std::string.
And this buffer is deallocated when the std::string goes out of scope.
What this means is that I'm returning a pointer to memory that will be deallocated, and then when I use that memory, the program could crash.
The static analyzer can now catch this bug.
So how should I fix this? Well, we recommend matching the lifetimes of your C++ and C strings.
Here, I have changed the generateGreeting function to return a std::string, and then I store the result into a local variable.
This means that when I call the c str method that local variable stays in scope for as long as I need to use the C string.
In essence, what I have done here is change the scope of the std::string to last as long as I need it.
And I will note that it's often easier to stay within the C++ world as long as possible and only get the C string out at exactly the moment where I need it.
So, let's move on to the third and final check I'm going to tell you about, for Reference Counting bugs in DriverKit and IOKit.
These driver frameworks use Manual Retain/Release for their memory management.
And for those of you who are familiar with CoreFoundation or with Objective-C without Automated Reference Counting, it's very similar.
Manual Retain/Release gives you a lot of control over memory management, but it does come with some additional responsibilities.
You need to be careful to not over release memory, because if you do so, it could be deallocated, and then when you use it, your program could crash. Similarly, you should not under-release your memory because the memory could leak.
Let me give you an example of a leak.
Here, I've written some code that allocates a new array of devices.
It then fills in those devices and sets them up.
And the key thing to note here is that OSArray::withCapacity, it allocates a new array and returns it retained.
What this means is that the array will leak if it's not released.
The analyzer can now catch this bug.
So, how should I fix this? All I need to do is make sure to release the array when I'm done with it.
Now, the memory management rules are all based around naming conventions, and these are very similar in spirit to CoreFoundation and Objective-C under Manual Retain/Release.
But I do want to point out one key difference for IOKit and DriverKit, and that is that the default convention is to return retained. Or, as we sometimes call it, at +1.
What this means is that clients must call release on the result of a method that they call.
Otherwise, the object could leak.
An important exception to this rule is that getters return unretained, or as we call it, at +0.
Clients should not release the result of a getter. Now, you might write code that differs from this convention, and here's an example of some code that I wrote.
This method finds the first device in an array, and it has the default convention. It should return retained.
But if we look at the implementation, it returns the result of a getter, and getters return unretained.
So, there's a mismatch here.
Fortunately, the analyzer can tell us about this.
So, how should I fix this? Well, I have three different options.
The first is to change the behavior to follow the convention.
Here, the convention is that the method should return retained, so I could retain the result before returning it.
Another possibility is to rename the method.
And if I look at this findFirstDevice method, it looks a lot like a getter.
So, I could just rename it to getFirstDevice, and that would follow the guidelines.
But you might have a method that has the behavior that you want, and it has the perfect name. You don't want to change it, and that's okay.
What you should do in this case is add an annotation to tell both readers of your code and the analyzer that you're not following the convention on purpose.
In this case, I can add the DRIVERKIT RETURNS NOT RETAINED annotation to express my intent.
So, if you have an IOKit driver or you're writing a new DriverKit driver, I highly encourage you to run the analyzer on your code.
To do that, all you need to do is go to Xcode's product menu and choose analyze.
And you can even have Xcode run the analyzer every time you hit build by going to your target's build settings and enabling Analyze During Build.
This will help you catch your bugs before you even commit them.
All right, so we told you about a lot today.
We showed you how LLVM Bitcode enabled a seamless 64-bit transition for watchOS where your 32-bit apps worked on day one with Series 4 watches.
We showed you how to reduce code size with new compiler optimizations and language features and how to run the static analyzer on your code.
For more information, please check out our session website, and we really look forward to talking with you in the labs. Thank you.
[ Applause ]
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.