mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
This is bullet (5) of the proposed solution in SE-0112, and the last major piece to be implemented.
205 lines
8.7 KiB
ReStructuredText
205 lines
8.7 KiB
ReStructuredText
: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.
|