-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathhow-availability-works-internally-in-swift.html
400 lines (349 loc) · 23.3 KB
/
how-availability-works-internally-in-swift.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="We use API availability checks all the time, but have you wondered how the Swift compiler handles this? In this article, we'll take a deep dive on how the #availability condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized.">
<meta name="title" content="How Swift API Availability Works Internally">
<meta name="url" content="https://swiftrocks.com/how-availability-works-internally-in-swift">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="How Swift API Availability Works Internally"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta property="og:description" content="We use API availability checks all the time, but have you wondered how the Swift compiler handles this? In this article, we'll take a deep dive on how the #availability condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/how-availability-works-internally-in-swift"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb.jpg?4"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="How Swift API Availability Works Internally"/>
<meta name="twitter:description" content="We use API availability checks all the time, but have you wondered how the Swift compiler handles this? In this article, we'll take a deep dive on how the #availability condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized."/>
<meta name="twitter:site" content="@rockbruno_"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="images/favicon/iconsmall2.png" sizes="32x32" />
<link rel="apple-touch-icon" href="images/favicon/iconsmall2.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism4.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style48.css">
<link rel="stylesheet" type="text/css" href="css/sponsor4.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/how-availability-works-internally-in-swift"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb.jpg"
],
"datePublished": "2020-09-29T14:00:00+02:00",
"dateModified": "2020-09-29T14:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb.jpg"
}
},
"headline": "How Swift API Availability Works Internally",
"abstract": "We use API availability checks all the time, but have you wondered how the Swift compiler handles this? In this article, we'll take a deep dive on how the #availability condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2light.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=How Swift API Availability Works Internally-->
<!--WRITEIT_POST_HTML_NAME=how-availability-works-internally-in-swift-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITEIT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=We use API availability checks all the time, but have you wondered how the Swift compiler handles this? In this article, we'll take a deep dive on how the #availability condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized.-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-09-29T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2020-09-29T14:00:00+02:00-->
<title>How Swift API Availability Works Internally</title>
<div class="blog-post">
<div class="post-title-index">
<h1>How Swift API Availability Works Internally</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 29 Sep 2020
</div>
</div>
<p>We use API availability checks all the time to provide fallbacks for users running older iOS versions, but have you wondered <b>how the Swift compiler handles this?</b> In this article, we'll take a deep dive on how the <code>#availability</code> condition works, how the Swift compiler is able to know if a specific symbol is available for usage and what the code you wrote looks like after being optimized.</p>
<div class="sponsor-article-ad-auto hidden"></div>
<p>I have recently authored an <a href="https://github.com/apple/swift-evolution/pull/1184">evolution proposal</a> to add a new <code>#unavailable</code> attribute to Swift, and while I haven't needed to do any meaningful work on Swift's availability system to implement it, this gave me the opportunity to learn a little more about things work deep down. Here's a deep dive on Swift's availability system!</p>
<h2>Why are <code>#available</code> checks necessary?</h2>
<p>While the reason <i>why</i> API availability checks are needed might be obvious, let's take a practical look at it for educational purposes before seeing how it works deep down.</p>
<p>Every <code>UIKit</code> or <code>Foundation</code> code you use comes from the iOS SDKs in your machine. While users of new iOS versions can still use your app normally if you don't update your app to support the new version, you'll only be able to use the version's new features if you submit a version that links with the relevant SDK. Currently these SDKs are shipped within Xcode, so you can always expect newer iOS versions to accompany a new Xcode version as well. You can always find an Xcode's provided SDKs in the description of the version.</p>
<p><code>Xcode 12 includes Swift 5.3 and SDKs for iOS 14, iPadOS 14, tvOS 14, watchOS 7 and maccOS Catalina.</code></p>
<p>However, even though your app now links to the correct SDK and uses its features, you don't know if the users of your app have the latest iOS version installed! If you were allowed to ship your app without compatibility checks, an app using newer iOS features would crash when used in old versions because the SDK in that user's device will not have those symbols. Thus, unless you explicitly set your app's minimum deployment target to be the latest available iOS version, you must use Swift's <code>#available</code> condition to provide a suitable fallback for older versions.</p>
<pre><code>if #available(iOS 14.0, *) {
SomeiOS14NewType()
} else {
SomeOlderType()
}</code></pre>
<p>Here, <code>(iOS 14.0, *)</code> means "If this is an iOS device, return <code>true</code> only if it contains the iOS 14 SDK. Always return <code>true</code> if this is a different platform (*)."</p>
<p>You can only use platforms that are hardcoded into Swift (iOS, OSX, tvOS and watchOS), but you can use any version you wish. The Swift compiler funnily uses absurd version numbers to prevent things from running:</p>
<pre><code>if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *) {
expectTrue(isP(CFBitVector.makeImmutable(from: [10, 20])))
expectTrue(isP(CFMutableBitVector.makeMutable(from: [10, 20])))
}</code></pre>
<p>You can also lock types to work only when used in a very distant future, although that might not be really useful unless you're trying to predict future features, I guess.</p>
<pre><code>@available(iOS, introduced: 999)
final class HologramCreator {}
HologramCreator()
// 'HologramCreator' is only available in iOS 999 or newer</code></pre>
<h2>How Availability works in the Swift Compiler</h2>
<p>In the compiler, the availability of symbols is treated during the type-checking phase.</p>
<div class="post-image margin-top-40 margin-bottom-40">
<img src="https://i.imgur.com/xbVOfbq.png" alt="" style="background-color: #fff;">
</div>
<p>If you haven't read my previous articles or need a refresher, the type-checking phase is when the compiler wants to determine if what you coded semantically correct. At this stage, the compiler has a basic <b>Abstract Syntax Tree</b> of your code that it knows to be correct in terms of structure, but it needs to know if what you're doing is actually possible. For example, does the type you're referencing actually contains the method you're calling? Are the return types correct?</p>
<h3>Type Refinement Contexts for AST nodes</h3>
<p>Naturally, checking if a symbol you're calling is available for usage falls into that phase. The compiler does it by building <b>Type Refinement Contexts</b>, which is a special structure that can hold any relevant additional information that a scope should have. Currently, this is only used for the very symbol availability we're interested in.</p>
<p>The process starts when the compiler wants to type-check a statement that contains an availability check. Let's take this one as an example:</p>
<pre><code>if #available(iOS 14.0, *) {
} else {
}</code></pre>
<p>At this phase, the purpose of the compiler is to search for any availability conditions and build a proper refinement context if needed. For every condition, the compiler will extract the currently-being-built platform from the condition and attempt to build a <i>range</i> of available version numbers. In this case, the range will simply be <code>minimumTarget...iOS 14</code>, while the else branch will keep whatever its parent refinement context was unless your condition is checking for some <i>lower</i> than the current context (which would reduce it instead). The main refinement context allows you to freely use anything up to your app's minimum deployment target, which is why you don't need to bother with these checks when you have a sufficiently high deployment target.</p>
<pre><code>if #available(iOS 14.0, *) {
// Symbol Availability: minimumTarget...iOS 14
} else {
// Symbol Availability: 0...minimumTarget (The default)
}</code></pre>
<p>The fact that it works with ranges makes it possible for it to locate potentially useless checks. If a condition's range is completely contained by the current context, the compiler will ignore it and display a warning.</p>
<pre><code>if #available(iOS 14.0, *), #available(iOS 13.0, *) {
// (iOS 13.0) Unnecessary check for 'iOS'; enclosing scope ensures guard will always be true
// Symbol Availability: minimumTarget...iOS 14
} else {
// Symbol Availability: 0...minimumTarget (The default)
}</code></pre>
<p>When the refinement contexts for each scope are determined, the compiler will associate it with the current statement's AST node and use it for any future availability checks. The refinement contexts are built like trees (where a context has a pointer to its parent), but they are used as a stack. As the compiler traverses your code, these refinement contexts will get pushed and popped as needed.</p>
<pre><code>if #available(iOS 9.0, *) {
// Symbol Availability: minimumTarget...iOS 9
if #available(iOS 13.0, *) {
// Symbol Availability: minimumTarget...iOS 13
} else {
// Symbol Availability: minimumTarget...iOS 9
}
} else {
// Symbol Availability: 0...minimumTarget (The default)
}</code></pre>
<p>While the outer else scope will make no changes to availability, the inner else scope will keep the increased iOS 9 availability as that was the current refinement context at the time. Here's a visual example of how this works in practice:</p>
<pre><code>// Refinement Context Stack: [MinimumTarget]
if #available(iOS 9.0, *) {
// Push: iOS 9 ([MinimumTarget, iOS 9])
if #available(iOS 13.0, *) {
// Push: iOS 13 ([MinimumTarget, iOS 9, iOS 13])
} else {
// Pop: iOS 13 ([MinimumTarget, iOS 9])
}
} else {
// Pop: iOS 9 ([MinimumTarget])
}</code></pre>
<p>Everything shown here also applies for <code>guard</code> statements, except in that case the positive availability changes are applied in what's left of the current scope.</p>
<pre><code>guard #available(iOS 14, *) else {
// Symbol Availability: 0...minimumTarget (The default)
return
}
// Symbol Availability: minimumTarget...iOS 14</code></pre>
<p>This process involving refinement contexts is precisely why you can't use availability conditions outside of statements like this:</p>
<pre><code>let isAvailable: Bool = #available(iOS 13.0, *)
if isAvailable {
// ?????
}
// error: #available may only be used as condition of an 'if', 'guard' or 'while' statement</code></pre>
<p>Even though doing something like this might make sense at first glace, what should be the symbol availability of that <code>if</code> statement? Something like this would be incredibly hard to process as now every boolean you create must also have its own refinement context, and would lead to many situations where the compiler wouldn't be able to process something that a human being can visualize as being possible.</p>
<h2>Determining if a symbol is available</h2>
<p>With the type refinement contexts created, the compiler can check if something is available by matching its current availability status with the top context in the refinement stack. The availability of the declaration in question is determined by the presence of the <code>@availability</code> attribute in its type. If there's none, the type will always be available.</p>
<pre><code>Optional<AvailabilityContext> AnnotatedRange = annotatedAvailableRange(D, Ctx);
if (AnnotatedRange.hasValue()) {
return AnnotatedRange.getValue();
}
// Treat unannotated declarations as always available.
return AvailabilityContext::alwaysAvailable();</code></pre>
<p>To check whether or not this specific declaration is available, the compiler retrieves the current refinement context of the declaration and checks if it's contained by the declaration's own availability range. This is where an app's minimum deployment target comes into play: If there's no refinement context (because no availability conditions have been seen yet), the compiler will build one that has it as the maximum available version. Finally, if this availability check returns false, the compiler will emit an error and suggest a fix-it that includes adding an availability condition.</p>
<pre><code>// Code sligthly changed for readability purposes
bool TypeChecker::isDeclAvailable(const Decl *D,
const DeclContext *referenceDC) {
ASTContext &Context = referenceDC->getASTContext();
AvailabilityContext declAvailability{
AvailabilityInference::availableRange(D, Context)};
AvailabilityContext currentAvailability =
overApproximateAvailabilityAtLocation(referenceDC);
return currentAvailability.isContainedIn(declAvailability);
}</code></pre>
<p>Additionally, if you're building something outside of Xcode, the minimum deployment target will be the current version of whatever it is that you're doing. For example, when running <code>.swift</code> scripts, the "minimum deployment target" will be your macOS's version.</p>
<pre><code>/// Returns the minimum platform version to which code will be deployed.
///
/// This is only implemented on certain OSs. If no target has been
/// configured, returns v0.0.0.
llvm::VersionTuple getMinPlatformVersion() const {
unsigned major = 0, minor = 0, revision = 0;
if (Target.isMacOSX()) {
Target.getMacOSXVersion(major, minor, revision);
} else if (Target.isiOS()) {
Target.getiOSVersion(major, minor, revision);
} else if (Target.isWatchOS()) {
Target.getOSVersion(major, minor, revision);
}
return llvm::VersionTuple(major, minor, revision);
}</code></pre>
<h2>Translating <code>#available</code> to a boolean</h2>
<p>Finally, after determining that our code is structurally and semantically correct, the compiler can wrap up by swapping the availability conditions with actual booleans. Currently, this works by replacing your statement with a call to <code>_stdlib_isOSVersionAtLeast</code> that receives the version range calculated and stored in each refinement context and returns a boolean if the current device is running the desired version.</p>
<pre><code>// Before
if #available(iOS 14.0, *) {
}
// After
if _stdlib_isOSVersionAtLeast(14, 0, 0)
}</code></pre>
<p>As you would expect, <code>_stdlib_isOSVersionAtLeast</code> works by determining the current OS version and checking if it matches the provided value. Here's how the compiler tries to determine the current iOS version:</p>
<pre><code>static os_system_version_s getOSVersion() {
auto lookup =
(int(*)(struct os_system_version_s * _Nonnull))
dlsym(RTLD_DEFAULT, "os_system_version_get_current_version");
struct os_system_version_s vers = { 0, 0, 0 };
lookup(&vers);
return vers;
}</code></pre>
<div class="sponsor-article-ad-auto hidden"></div>
<p>If you'd like to see this happening in practice, you can ask the compiler to emit the Swift Intermediate Language for a certain code like this:</p>
<pre class="command-line language-bash" data-host="SwiftRocks"><code>swiftc -emit-sil myFile.swift</code></pre>
<p>After running it, you can see all availability conditions being replaced by the lower-level OS version check.</p>
<pre><code>// function_ref _stdlib_isOSVersionAtLeast(_:_:_:)
%5 = function_ref @$ss26_stdlib_isOSVersionAtLeastyBi1_Bw_BwBwtF</code></pre>
<h2>References and Good reads</h2>
<a href="https://github.com/apple/swift">The Swift Source Code</a>
<br>
<a href="https://github.com/apple/swift/blob/9af806e8fd93df3499b1811deae7729176879cb0/lib/Sema/TypeCheckAvailability.cpp#L400">TypeCheckAvailability.cpp</a>
<br>
<a href="https://github.com/apple/swift-evolution/pull/1184">SE-NNNN: Unavailability Condition</a>
<br>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism4.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>