Skip to content

Commit 226c7c8

Browse files
authored
docs: convert FailableInitializers.rst to Markdown (swiftlang#62417)
Resolves partially swiftlang#49997.
1 parent 76d337c commit 226c7c8

File tree

3 files changed

+213
-205
lines changed

3 files changed

+213
-205
lines changed

docs/FailableInitializers.md

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Failable initializers
2+
3+
## Table of Contents
4+
5+
- [Lexicon](#lexicon)
6+
- [Convenience initializers](#convenience-initializers)
7+
- [Designated initializers](#designated-initializers)
8+
- [Description of the problem](#description-of-the-problem)
9+
- [Possible solutions](#possible-solutions)
10+
- [Proposed solution -- pure Swift case](#proposed-solution----pure-swift-case)
11+
- [Proposed solution -- Objective-C case](#proposed-solution----objective-c-case)
12+
- [Implementation](#implementation)
13+
14+
15+
A **failable initializer** can return early with an error, without
16+
having initialized a new object. Examples can include initializers which
17+
validate input arguments, or attempt to acquire a limited resource.
18+
19+
There are two types of failable initializers:
20+
21+
- An initializer can be declared as having an optional return type, in
22+
which case it can signal failure by returning nil.
23+
- An initializer can be declared as throwing, in which case it can
24+
signal failure by throwing an error.
25+
26+
## Lexicon
27+
28+
Some terminology used below:
29+
30+
- **deallocating** refers to freeing the memory of an object without
31+
running any destructors.
32+
- **releasing** refers to giving up a reference, which will result in
33+
running the destructor and deallocation of the object if this was
34+
the last reference.
35+
- A **destructor** is a Swift-generated entry point which call the
36+
user-defined deinitializer, then releases all stored properties.
37+
- A **deinitializer** is an optional user-defined entry point in a
38+
Swift class which handles any necessary cleanup beyond releasing
39+
stored properties.
40+
- A **slice** of an object is the set of stored properties defined in
41+
one particular class forming the superclass chain of the instance.
42+
43+
44+
## Convenience initializers
45+
46+
Failing convenience initializers are the easy case, and are fully
47+
supported now. The failure can occur either before or after the
48+
`self.init()` delegation, and is handled as follows:
49+
50+
1. A failure prior to the `self.init()` delegation is handled by
51+
deallocating the completely-uninitialized self value.
52+
2. A failure after the `self.init()` delegation is handled by releasing
53+
the fully-initialized self.value.
54+
55+
## Designated initializers
56+
57+
Failing designated initializers are more difficult, and are the subject
58+
of this proposal.
59+
60+
Similarly to convenience initializers, designated initializers can fail
61+
either before or after the `super.init()` delegation (or, for a root class
62+
initializer, the first location where all stored properties become
63+
initialized).
64+
65+
When failing after the `super.init()` delegation, we already have a
66+
fully-initialized self value, so releasing the self value is sufficient.
67+
The user-defined deinitializer, if any, is run in this case.
68+
69+
A failure prior to the `super.init()` delegation on the other hand will
70+
leave us with a partially-initialized self value that must be
71+
deallocated. We have to deinitialize any stored properties of self that
72+
we initialized, but we do not invoke the user-defined deinitializer
73+
method.
74+
75+
## Description of the problem
76+
77+
To illustrate, say we are constructing an instance of a class $C$, and let
78+
$superclasses(C)$ be the sequence of superclasses, starting from $C$ and
79+
ending at a root class $C_n$:
80+
81+
```math
82+
superclasses(C) = {C, C_1, C_2, ..., C_n}
83+
```
84+
85+
Suppose our failure occurs in the designated initializer for class $C_k$.
86+
At this point, the self value looks like this:
87+
88+
1. All stored properties in ${C, ..., C_(k-1)}$ have been
89+
initialized.
90+
2. Zero or more stored properties in $C_k$ have been initialized.
91+
3. The rest of the object ${C_(k+1), ..., C_n}$ is completely
92+
uninitialized.
93+
94+
In order to fail out of the constructor without leaking memory, we have
95+
to destroy the initialized stored properties only without calling any
96+
Swift deinit methods, then deallocate the object itself.
97+
98+
There is a further complication once we take Objective-C
99+
interoperability into account. Objective-C classes can override `-alloc`,
100+
to get the object from a memory pool, for example. Also, they can
101+
override `-retain` and `-release` to implement their own reference counting.
102+
This means that if our class has `@objc` ancestry, we have to release it
103+
with `-release` even if it is partially initialized -- since this will
104+
result in Swift destructors being called, they have to know to skip the
105+
uninitialized parts of the object.
106+
107+
There is an issue we need to sort out, tracked by rdar://18720947.
108+
Basically, if we haven't done the `super.init()`, is it safe to call
109+
`-release`. The rest of this proposal assumes the answer is "yes".
110+
111+
## Possible solutions
112+
113+
One approach is to think of the `super.init()` delegation as having a
114+
tri-state return value, instead of two-state:
115+
116+
1. First failure case -- object is fully initialized
117+
2. Second failure case -- object is partially initialized
118+
3. Success
119+
120+
This is problematic because now the ownership conventions in the
121+
initializer signature do not really describe the initializer's effect on
122+
reference counts; we now that this special return value for the second
123+
failure case, where the self value looks like it should have been
124+
consumed but it wasn't.
125+
126+
It is also difficult to encode this tri-state return for throwing
127+
initializers. One can imagine changing the try_apply and throw SIL
128+
instructions to support returning a pair `(Error, AnyObject)` instead of a
129+
single `Error`. But this would ripple changes throughout various SIL
130+
analyses, and require IRGen to encode the pair return value in an
131+
efficient way.
132+
133+
## Proposed solution -- pure Swift case
134+
135+
A simpler approach seems to be to introduce a new `partialDeinit` entry
136+
point, referenced through a special kind of `SILDeclRef`. This entry point
137+
is dispatched through the vtable and invoked using a standard
138+
`class_method` sequence in SIL.
139+
140+
This entry point's job is to conditionally deinitialize stored
141+
properties of the self value, without invoking the user-defined
142+
deinitializer.
143+
144+
When a designated initializer for class $C_k$ fails prior to performing
145+
the `super.init()` delegation, we emit the following code sequence:
146+
147+
1. First, de-initialize any stored properties this initializer may
148+
have initialized.
149+
2. Second, invoke `partialDeinit(self, M)`, where `M` is the static
150+
metatype of $C_k$.
151+
152+
The `partialDeinit` entry point is implemented as follows:
153+
154+
1. If the static self type of the entry point is not equal to `M`,
155+
first delegate to the superclass's `partialDeinit` entry point, then
156+
deinitialize all stored properties in $C_k$.
157+
2. If the static self type is equal to `M`, we have finished
158+
deinitializing the object, and we can now call a runtime function
159+
to deallocate it.
160+
161+
Note that we delegate to the superclass `partialDeinit` entry point before
162+
doing our own deinitialization, to ensure that stored properties are
163+
deinitialized in the reverse order in which they were initialized. This
164+
might not matter.
165+
166+
Note that if even if a class does not have any failing initializers of
167+
its own, it might delegate to a failing initializer in its superclass,
168+
using `super.init!` or `try!`. It might be easiest to emit a
169+
`partialDeinit` entry point for all classes, except those without any
170+
stored properties.
171+
172+
## Proposed solution -- Objective-C case
173+
174+
As noted above, if the class has `@objc` ancestry, the interoperability
175+
story becomes more complicated. In order to undo any custom logic
176+
implemented in an Objective-C override of `-alloc` or `-retain`, we have
177+
to free the partially-initialized object using `-release`.
178+
179+
To ensure we don't double-free any Swift stored properties, we will add
180+
a new hidden stored property to each class that directly defines failing
181+
initializers. The bit is set if this slice of the instance has been
182+
initialized.
183+
184+
Note that unlike `partialDeinit`, if a class does not have failing
185+
initializers, it does not need this bit, even if its initializer
186+
delegates to a failing initializer in a superclass.
187+
188+
If the bit is clear, the destructor will skip the slice and not call the
189+
user-defined `deinit` method, or delegate further up the chain. Note
190+
that since newly-allocated Objective-C objects are zeroed out, the
191+
initial state of this bit indicates the slice is not initialized.
192+
193+
The constructor will set the bit before delegating to `super.init()`.
194+
195+
If a destructor fails before delegating to `super.init()`, it will call
196+
the `partialDeinit` entry point as before, but then, release the instance
197+
instead of deallocating it.
198+
199+
A possible optimization would be not generate the bit if all stored
200+
properties are POD, or retainable pointers. In the latter case, all zero
201+
bits is a valid representation (all the `swift_retain`/`release entry`
202+
points in the runtime check for null pointers, at least for now).
203+
However, we do not have to do this optimization right away.
204+
205+
## Implementation
206+
207+
The bulk of this feature would be driven from DI. Right now, DI only
208+
implements failing designated initializers in their full generality for
209+
structs -- we already have logic for tracking which stored properties
210+
have been initialized, but the rest of the support for the `partialDeinit`
211+
entry point, as well as the Objective-C concerns needs to be fleshed
212+
out.

0 commit comments

Comments
 (0)