blob: a42dc4c871fca00adc07673de24ceb41a0c5f6ec [file] [log] [blame]
:orphan:
This proposal describes the work required to address
rdar://problem/18216578.
Some terminology used below:
- **deallocating** refers to freeing the memory of an object without running
any destructors.
- **releasing** refers to giving up a reference, which will result in running
the destructor and deallocation of the object if this was the last
reference.
- A **destructor** is a Swift-generated entry point which call the user-defined
deinitializer, then releases all stored properties.
- A **deinitializer** is an optional user-defined entry point in a Swift class
which handles any necessary cleanup beyond releasing stored properties.
- A **slice** of an object is the set of stored properties defined in one
particular class forming the superclass chain of the instance.
Failable initializers
=====================
A **failable initializer** can return early with an error, without having
initialized a new object. Examples can include initializers which validate
input arguments, or attempt to acquire a limited resource.
There are two types of failable initializers:
- An initializer can be declared as having an optional return type, in
which case it can signal failure by returning nil.
- An initializer can be declared as throwing, in which case it can signal
failure by throwing an error.
Convenience initializers
------------------------
Failing convenience initializers are the easy case, and are fully supported
now. The failure can occur either before or after the self.init()
delegation, and is handled as follows:
#. A failure prior to the self.init() delegation is handled by deallocating
the completely-uninitialized self value.
#. A failure after the self.init() delegation is handled by releasing the
fully-initialized self.value.
Designated initializers
-----------------------
Failing designated initializers are more difficult, and are the subject of this
proposal.
Similarly to convenience initializers, designated initializers can fail either
before or after the super.init() delegation (or, for a root class initializer,
the first location where all stored properties become initialized).
When failing after the super.init() delegation, we already have a
fully-initialized self value, so releasing the self value is sufficient. The
user-defined deinitializer, if any, is run in this case.
A failure prior to the super.init() delegation on the other hand will leave us
with a partially-initialized self value that must be deallocated. We have to
deinitialize any stored properties of self that we initialized, but we do
not invoke the user-defined deinitializer method.
Description of the problem
--------------------------
To illustrate, say we are constructing an instance of a class C, and let
superclasses(C) be the sequence of superclasses, starting from C and ending
at a root class C_n:
::
superclasses(C) = {C, C_1, C_2, ..., C_n}
Suppose our failure occurs in the designated initializer for class C_k. At this
point, the self value looks like this:
#. All stored properties in ``{C, ..., C_(k-1)}`` have been initialized.
#. Zero or more stored properties in ``C_k`` have been initialized.
#. The rest of the object ``{C_(k+1), ..., C_n}`` is completely uninitialized.
In order to fail out of the constructor without leaking memory, we have to
destroy the initialized stored properties only without calling any Swift
deinit methods, then deallocate the object itself.
There is a further complication once we take Objective-C interoperability into
account. Objective-C classes can override -alloc, to get the object from a
memory pool, for example. Also, they can override -retain and -release to
implement their own reference counting. This means that if our class has @objc
ancestry, we have to release it with -release even if it is partially
initialized -- since this will result in Swift destructors being called, they
have to know to skip the uninitialized parts of the object.
There is an issue we need to sort out, tracked by rdar://18720947. Basically,
if we haven't done the ``super.init()``, is it safe to call ``-release``. The
rest of this proposal assumes the answer is "yes".
Possible solutions
------------------
One approach is to think of the super.init() delegation as having a tri-state
return value, instead of two-state:
#. First failure case -- object is fully initialized
#. Second failure case -- object is partially initialized
#. Success
This is problematic because now the ownership conventions in the initializer
signature do not really describe the initializer's effect on reference counts;
we now that this special return value for the second failure case, where the
self value looks like it should have been consumed but it wasn't.
It is also difficult to encode this tri-state return for throwing initializers.
One can imagine changing the try_apply and throw SIL instructions to support
returning a pair (Error, AnyObject) instead of a single Error. But
this would ripple changes throughout various SIL analyses, and require IRGen
to encode the pair return value in an efficient way.
Proposed solution -- pure Swift case
------------------------------------
A simpler approach seems to be to introduce a new partialDeinit entry point,
referenced through a special kind of SILDeclRef. This entry point is dispatched
through the vtable and invoked using a standard class_method sequence in SIL.
This entry point's job is to conditionally deinitialize stored properties
of the self value, without invoking the user-defined deinitializer.
When a designated initializer for class C_k fails prior to performing the
super.init() delegation, we emit the following code sequence:
#. First, de-initialize any stored properties this initializer may have
initialized.
#. Second, invoke ``partialDeinit(self, M)``, where M is the static
metatype of ``C_k``.
The partialDeinit entry point is implemented as follows:
#. If the static self type of the entry point is not equal to M, first
delegate to the superclass's partialDeinit entry point, then
deinitialize all stored properties in ``C_k``.
#. If the static self type is equal to M, we have finished deinitializing
the object, and we can now call a runtime function to deallocate it.
Note that we delegate to the superclass partialDeinit entry point before
doing our own deinitialization, to ensure that stored properties are
deinitialized in the reverse order in which they were initialized. This
might not matter.
Note that if even if a class does not have any failing initializers of its
own, it might delegate to a failing initializer in its superclass, using
``super.init!`` or ``try!``. It might be easiest to emit a partialDeinit
entry point for all classes, except those without any stored properties.
Proposed solution -- Objective-C case
-------------------------------------
As noted above, if the class has ``@objc`` ancestry, the interoperability
story becomes more complicated. In order to undo any custom logic implemented
in an Objective-C override of ``-alloc`` or ``-retain``, we have to free the
partially-initialized object using ``-release``.
To ensure we don't double-free any Swift stored properties, we will add
a new hidden stored property to each class that directly defines failing
initializers. The bit is set if this slice of the instance has been
initialized.
Note that unlike partialDeinit, if a class does not have failing initializers,
it does not need this bit, even if its initializer delegates to a failing
initializer in a superclass.
If the bit is clear, the destructor will skip the slice and not call the
user-defined ``deinit`` method, or delegate further up the chain. Note that
since newly-allocated Objective-C objects are zeroed out, the initial state
of this bit indicates the slice is not initialized.
The constructor will set the bit before delegating to ``super.init()``.
If a destructor fails before delegating to ``super.init()``, it will call
the partialDeinit entry point as before, but then, release the instance
instead of deallocating it.
A possible optimization would be not generate the bit if all stored
properties are POD, or retainable pointers. In the latter case, all zero bits
is a valid representation (all the swift_retain/release entry points in the
runtime check for null pointers, at least for now). However, we do not have
to do this optimization right away.
Implementation
--------------
The bulk of this feature would be driven from DI. Right now, DI only implements
failing designated initializers in their full generality for structs -- we
already have logic for tracking which stored properties have been initialized,
but the rest of the support for the partialDeinit entry point, as well as the
Objective-C concerns needs to be fleshed out.