|
| 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