blob: 791f41f8edce857d6176a84e0497b4a2807e390c [file] [log] [blame]
:orphan:
.. _CallingConvention:
The Swift Calling Convention
****************************
.. contents::
This whitepaper discusses the Swift calling convention, at least as we
want it to be.
It's a basic assumption in this paper that Swift shouldn't make an
implicit promise to exactly match the default platform calling
convention. That is, if a C or Objective-C programmer manages to derive the
address of a Swift function, we don't have to promise that an obvious
translation of the type of that function will be correctly callable
from C. For example, this wouldn't be guaranteed to work::
// In Swift:
func foo(_ x: Int, y: Double) -> MyClass { ... }
// In Objective-C:
extern id _TF4main3fooFTSiSd_CS_7MyClass(intptr_t x, double y);
We do sometimes need to be able to match C conventions, both to use
them and to generate implementations of them, but that level of
compatibility should be opt-in and site-specific. If Swift would
benefit from internally using a better convention than C/Objective-C uses,
and switching to that convention doesn't damage the dynamic abilities
of our target platforms (debugging, dtrace, stack traces, unwinding,
etc.), there should be nothing preventing us from doing so. (If we
did want to guarantee compatibility on this level, this paper would be
a lot shorter!)
Function call rules in high-level languages have three major
components, each operating on a different abstraction level:
* the high-level semantics of the call (pass-by-reference
vs. pass-by-value),
* the ownership and validity conventions about argument and result
values ("+0" vs. "+1", etc.), and
* the "physical" representation conventions of how values are actually
communicated between functions (in registers, on the stack, etc.).
We'll tackle each of these in turn, then conclude with a detailed
discussion of function signature lowering.
High-level semantic conventions
===============================
The major division in argument passing conventions between languages
is between pass-by-reference and pass-by-value languages. It's a
distinction that only really makes sense in languages with the concept
of an l-value, but Swift does, so it's pertinent.
In general, the terms "pass-by-X" and "call-by-X" are used
interchangeably. It's unfortunate, because these conventions are
argument specific, and functions can be passed multiple arguments
that are each handled in a different way. As such, we'll prefer
"pass-by-X" for consistency and to emphasize that these conventions
are argument-specific.
Pass-by-reference
-----------------
In pass-by-reference (also called pass-by-name or pass-by-address), if
`A` is an l-value expression, `foo(A)` is passed some sort of opaque
reference through which the original l-value can be modified. If `A`
is not an l-value, the language may prohibit this, or (if
pass-by-reference is the default convention) it may pass a temporary
variable containing the result of `A`.
Don't confuse pass-by-reference with the concept of a *reference
type*. A reference type is a type whose value is a reference to a
different object; for example, a pointer type in C, or a class type in
Java or Swift. A variable of reference type can be passed by value
(copying the reference itself) or by reference (passing the variable
itself, allowing it to be changed to refer to a different object).
Note that references in C++ are a generalization of pass-by-reference,
not really a reference type; in C++, a variable of reference type
behaves completely unlike any other variable in the language.
Also, don't confuse pass-by-reference with the physical convention of
passing an argument value indirectly. In pass-by-reference, what's
logically being passed is a reference to a tangible, user-accessible
object; changes to the original object will be visible in the
reference, and changes to the reference will be reflected in the
original object. In an indirect physical convention, the argument is
still logically an independent value, no longer associated with the
original object (if there was one).
If every object in the language is stored in addressable memory,
pass-by-reference can be easily implemented by simply passing the
address of the object. If an l-value can have more structure than
just a single, independently-addressable object, more information may
be required from the caller. For example, an array argument in
FORTRAN can be a row or column vector from a matrix, and so arrays are
generally passed as both an address and a stride. C and C++ do have
unaddressable l-values because of bitfields, but they forbid passing
bitfields by reference (in C++) or taking their address (in either
language), which greatly simplifies pointer and reference types in
those languages.
FORTRAN is the last remaining example of a language that defaults to
pass-by-reference. Early FORTRAN implementations famously passed
constants by passing the address of mutable global memory initialized
to the constant; if the callee modified its parameter (illegal under
the standard, but...), it literally changed the constant for future
uses. FORTRAN now allows procedures to explicitly take arguments by
value and explicitly declare that arguments must be l-values.
However, many languages do allow parameters to be explicitly marked as
pass-by-reference. As mentioned for C++, sometimes only certain kinds
of l-values are allowed.
Swift allows parameters to be marked as pass-by-reference with
`inout`. Arbitrary l-values can be passed. The Swift convention is
to always pass an address; if the parameter is not addressable, it
must be materialized into a temporary and then written back. See the
accessors proposal for more details about the high-level semantics of
`inout` arguments.
Pass-by-value
-------------
In pass-by-value, if `A` is an l-value expression, `foo(A)` copies the
current value there. Any modifications `foo` makes to its parameter
are made to this copy, not to the original l-value.
Most modern languages are pass-by-value, with specific functions able
to opt in to pass-by-reference semantics. This is exactly what Swift
does.
There's not much room for variation in the high-level semantics of
passing arguments by value; all the variation is in the ownership and
physical conventions.
Ownership transfer conventions
==============================
Arguments and results that require cleanup, like an Objective-C object
reference or a non-POD C++ object, raise two questions about
responsibility: who is responsible for cleaning it up, and when?
These questions arise even when the cleanup is explicit in code. C's
`strdup` function returns newly-allocated memory which the caller is
responsible for freeing, but `strtok` does not. Objective-C has
standard naming conventions that describe which functions return
objects that the caller is responsible for releasing, and outside of
ARC these must be followed manually. Of course, conventions designed
to be implemented by programmers are often designed around the
simplicity of that implementation, rather than necessarily being more
efficient.
Pass-by-reference arguments
---------------------------
Pass-by-reference arguments generally don't involve a *transfer* of
ownership. It's assumed that the caller will ensure that the referent
is valid at the time of the call, and that the callee will ensure that
the referent is still valid at the time of return.
FORTRAN does actually allow parameters to be tagged as out-parameters,
where the caller doesn't guarantee the validity of the argument before
the call. Objective-C has something similar, where an indirect method
argument can be marked `out`; ARC takes advantage of this with
autoreleasing parameters to avoid a copy into the writeback temporary.
Neither of these are something we semantically care about supporting
in Swift.
There is one other theoretically interesting convention question here:
the argument has to be valid before the call and after the call, but
does it have to valid during the call? Swift's answer to this is
generally "yes". Swift does have `inout` aliasing rules that allow a
certain amount of optimization, but the compiler is forbidden from
exploiting these rules in any way that could cause memory corruption
(at least in the absence of race conditions). So Swift has to ensure
that an `inout` argument is valid whenever it does something
(including calling an opaque function) that could potentially access
the original l-value.
If Swift allowed local variables to be captured through `inout`
parameters, and therefore needed to pass an implicit owner parameter
along with an address, this owner parameter would behave like a
pass-by-value argument and could use any of the conventions listed
below. However, the optimal convention for this is obvious: it should
be `guaranteed`, since captures are very unlikely and callers are
almost always expected to use the value of an `inout` variable
afterwards.
Pass-by-value arguments
-----------------------
All conventions for this have performance trade-offs.
We're only going to discuss *static* conventions, where the transfer
is picked at compile time. It's possible to have a *dynamic*
convention, where the caller passes a flag indicating whether it's
okay to directly take responsibility for the value, and the callee can
(conceptually) return a flag indicating whether it actually did take
responsibility for it. If copying is extremely expensive, that can be
worthwhile; otherwise, the code cost may overwhelm any other benefits.
This discussion will ignore one particular impact of these conventions
on code size. If a function has many callers, conventions that
require more code in the caller are worse, all else aside. If a
single call site has many possible targets, conventions that require
more code in the callee are worse, all else aside. It's not really
reasonable to decide this in advance for unknown code; we could maybe
make rules about code calling system APIs, except that system APIs are
by definition locked down, and we can't change them. It's a
reasonable thing to consider changing with PGO, though.
Responsibility
~~~~~~~~~~~~~~
A common refrain in this performance analysis will be whether a
function has responsibility for a value. A function has to get a
value from *somewhere*:
* A caller is usually responsible for the return values it receives:
the callee generated the value and the caller is responsible for
destroying it. Any other convention has to rely on heavily
restricting what kind of value can be returned. (If you're thinking
about Objective-C autoreleased results, just accept this for now;
we'll talk about that later.)
* A function isn't necessarily responsible for a value it loads from
memory. Ignoring race conditions, the function may be able to
immediately use the value without taking any specific action to keep
it valid.
* A callee may or may not be responsible for a value passed as a
parameter, depending on the convention it was passed with.
* A function might come from a source that doesn't necessarily make
the function responsible, but if the function takes an action which
invalidates the source before using the value, the function has to
take action to keep the value valid. At that point, the function
has responsibility for the value despite its original source.
For example, a function `foo()` might load a reference `r` from a
global variable `x`, call an unknown function `bar()`, and then use
`r` in some way. If `bar()` can't possibly overwrite `x`, `foo()`
doesn't have to do anything to keep `r` alive across the call;
otherwise it does (e.g. by retaining it in a refcounted
environment). This is a situation where humans are often much
smarter than compilers. Of course, it's also a situation where
humans are sometimes insufficiently conservative.
A function may also require responsibility for a value as part of its
operation:
* Since a variable is always responsible for the current value it
stores, a function which stores a value into memory must first gain
responsibility for that value.
* A callee normally transfers responsibility for its return value to
its caller; therefore it must gain responsibility for its return
value before returning it.
* A caller may need to gain responsibility for a value before passing
it as an argument, depending on the parameter's ownership-transfer
convention.
Known conventions
~~~~~~~~~~~~~~~~~
There are three static parameter conventions for ownership worth
considering here:
* The caller may transfer responsibility for the value to the callee.
In SIL, we call this an **owned** parameter.
This is optimal if the caller has responsibility for the value and
doesn't need it after the call. This is an extremely common
situation; for example, it comes up whenever a call result is
immediately used as an argument. By giving the callee responsibility
for the value, this convention allows the callee to use the value at
a later point without taking any extra action to keep it alive.
The flip side is that this convention requires a lot of extra work
when a single value is used multiple times in the caller. For
example, a value passed in every iteration of a loop will need to be
copied/retained/whatever each time.
* The caller may provide the value without any responsibility on
either side. In SIL, we call this an **unowned** parameter. The
value is guaranteed to be valid at the moment of the call, and in
the absence of race conditions, that guarantee can be assumed to
continue unless the callee does something that might invalidate it.
As discussed above, humans are often much smarter than computers
about knowing when that's possible.
This is optimal if the caller can acquire the value without
responsibility and the callee doesn't require responsibility of it.
In very simple code --- e.g., loading values from an array and
passing them to a comparator function which just reads a few fields
from each and returns --- this can be extremely efficient.
Unfortunately, this convention is completely undermined if either
side has to do anything that forces it to take action to keep the
value alive. Also, if that happens on the caller side, the
convention can keep values alive longer than is necessary. It's
very easy for both sides of the convention to end up doing extra
work because of this.
* The caller may assert responsibility for the value. In SIL, we call
this a **guaranteed** parameter. The callee can rely on the value
staying valid for the duration of the call.
This is optimal if the caller needs to use the value after the call
and either has responsibility for it or has a guarantee like this
for it. Therefore, this convention is particularly nice when a
value is likely to be forwarded by value a great deal.
However, this convention does generally keep values alive longer
than is necessary, since the outermost function which passed it as
an argument will generally be forced to hold a reference for the
duration. By the same mechanism, in refcounted systems, this
convention tends to cause values to have multiple retains active at
once; for example, if a copy-on-write array is created in one
function, passed to another, stored in a mutable variable, and then
modified, the callee will see a reference count of 2 and be forced
to do a structural copy. This can occur even if the caller
literally constructed the array for the sole and immediate purpose
of passing it to the callee.
Analysis
~~~~~~~~
Objective-C generally uses the unowned convention for object-pointer
parameters. It is possible to mark a parameter as being consumed,
which is basically the owned convention. As a special case, in ARC we
assume that callers are responsible for keeping `self` values alive
(including in blocks), which is effectively the `guaranteed`
convention.
`unowned` causes a lot of problems without really solving any, in my
experience looking at ARC-generated code and optimizer output. A
human can take advantage of it, but the compiler is so frequently
blocked. There are many common idioms (like chains of functions that
just add default arguments at each step) have really awful performance
because the compiler is adding retains and releases at every single
level. It's just not a good convention to adopt by default. However,
we might want to consider allowing specific function parameters to opt
into it; sort comparators are a particularly interesting candidate
for this. `unowned` is very similar to C++'s `const &` for things
like that.
`guaranteed` is good for some things, but it causes a lot of silly
code bloat when values are really only used in one place, which is
quite common. The liveness / refcounting issues are also pretty
problematic. But there is one example that's very nice for
`guaranteed`: `self`. It's quite common for clients of a type to call
multiple methods on a single value, or for methods to dispatch to
multiple other methods, which are exactly the situations where
`guaranteed` excels. And it's relatively uncommon (but not
unimaginable) for a non-mutating method on a copy-on-write struct to
suddenly store `self` aside and start mutating that copy.
`owned` is a good default for other parameters. It has some minor
performance disadvantages (unnecessary retains if you have an
unoptimizable call in a loop) and some minor code size benefits (in
common straight-line code), but frankly, both of those points pale in
importance to the ability to transfer copy-on-write structures around
without spuriously increasing reference counts. It doesn't take too
many unnecessary structural copies before any amount of
reference-counting traffic (especially the Swift-native
reference-counting used in copy-on-write structures) is basically
irrelevant in comparison.
Result values
-------------
There's no major semantic split in result conventions like that
between pass-by-reference and pass-by-value. In most languages, a
function has to return a value (or nothing). There are languages like
C++ where functions can return references, but that's inherently
limited, because the reference has to refer to something that exists
outside the function. If Swift ever adds a similar language
mechanism, it'll have to be memory-safe and extremely opaque, and
it'll be easy to just think of that as a kind of weird value result.
So we'll just consider value results here.
Value results raise some of the same ownership-transfer questions as
value arguments. There's one major limitation: just like a
by-reference result, an actual `unowned` convention is inherently
limited, because something else other than the result value must be
keeping it valid. So that's off the table for Swift.
What Objective-C does is something more dynamic. Most APIs in
Objective-C give you a very ephemeral guarantee about the validity of
the result: it's valid now, but you shouldn't count on it being valid
indefinitely later. This might be because the result is actually
owned by some other object somewhere, or it might be because the
result has been placed in the autorelease pool, a thread-local data
structure which will (when explicitly drained by something up the call
chain) eventually release that's been put into it. This autorelease
pool can be a major source of spurious memory growth, and in classic
manual reference-counting it was important to drain it fairly
frequently. ARC's response to this convention was to add an
optimization which attempts to prevent things from ending up in the
autorelease pool; the net effect of this optimization is that ARC ends
up with an owned reference regardless of whether the value was
autoreleased. So in effect, from ARC's perspective, these APIs still
return an owned reference, mediated through some extra runtime calls
to undo the damage of the convention.
So there's really no compelling alternative to an owned return
convention as the default in Swift.
Physical conventions
====================
The lowest abstraction level for a calling convention is the actual
"physical" rules for the call:
* where the caller should place argument values in registers and
memory before the call,
* how the callee should pass back the return values in registers
and/or memory after the call, and
* what invariants hold about registers and memory over the call.
In theory, all of these could be changed in the Swift ABI. In
practice, it's best to avoid changes to the invariant rules, because
those rules could complicate Swift-to-C interoperation:
* Assuming a higher stack alignment would require dynamic realignment
whenever Swift code is called from C.
* Assuming a different set of callee-saved registers would require
additional saves and restores when either Swift code calls C or is
called from C, depending on the exact change. That would then
inhibit some kinds of tail call.
So we will limit ourselves to considering the rules for allocating
parameters and results to registers. Our platform C ABIs are usually
quite good at this, and it's fair to ask why Swift shouldn't just use
C's rules. There are three general answers:
* Platform C ABIs are specified in terms of the C type system, and the
Swift type system allows things to be expressed which don't have
direct analogues in C (for example, enums with payloads).
* The layout of structures in Swift does not necessarily match their
layout in C, which means that the C rules don't necessarily cover
all the cases in Swift.
* Swift places a larger emphasis on first-class structs than C does.
C ABIs often fail to allocate even small structs to registers, or
use inefficient registers for them, and we would like to be somewhat
more aggressive than that.
Accordingly, the Swift ABI is defined largely in terms of lowering: a
Swift function signature is translated to a C function signature with
all the aggregate arguments and results eliminated (possibly by
deciding to pass them indirectly). This lowering will be described in
detail in the final section of this whitepaper.
However, there are some specific circumstances where we'd like to
deviate from the platform ABI:
Aggregate results
-----------------
As mentioned above, Swift puts a lot of focus on first-class value
types. As part of this, it's very valuable to be able to return
common value types fully in registers instead of indirectly. The
magic number here is three: it's very common for copy-on-write value
types to want about three pointers' worth of data, because that's just
enough for some sort of owner pointer plus a begin/end pair.
Unfortunately, many common C ABIs fall slightly short of that. Even
those ABIs that do allow small structs to be returned in registers
tend to only allow two pointers' worth. So in general, Swift would
benefit from a very slightly-tweaked calling convention that allocates
one or two more registers to the result.
Implicit parameters
-------------------
There are several language features in Swift which require implicit
parameters:
Closures
~~~~~~~~
Swift's function types are "thick" by default, meaning that a function
value carries an optional context object which is implicitly passed to
the function when it is called. This context object is
reference-counted, and it should be passed `guaranteed` for
straightforward reasons:
* It's not uncommon for closures to be called many times, in which
case an `owned` convention would be unnecessarily expensive.
* While it's easy to imagine a closure which would want to take
responsibility for its captured values, giving it responsibility for
a retain of the context object doesn't generally allow that. The
closure would only be able to take ownership of the captured values
if it had responsibility for a *unique* reference to the context.
So the closure would have to be written to do different things based
on the uniqueness of the reference, and it would have to be able to
tear down and deallocate the context object after stealing values
from it. The optimization just isn't worth it.
* It's usually straightforward for the caller to guarantee the
validity of the context reference; worst case, a single extra
Swift-native retain/release is pretty cheap. Meanwhile, not having
that guarantee would force many closure functions to retain their
contexts, since many closures do multiple things with values from
the context object. So `unowned` would not be a good convention.
Many functions don't actually need a context, however; they are
naturally "thin". It would be best if it were possible to construct a
thick function directly from a thin function without having to
introduce a thunk just to move parameters around the missing context
parameter. In the worst case, a thunk would actually require the
allocation of a context object just to store the original function
pointer; but that's only necessary when converting from a completely
opaque function value. When the source function is known statically,
which is far more likely, the thunk can just be a global function
which immediately calls the target with the correctly shuffled
arguments. Still, it'd be better to be able to avoid creating such
thunks entirely.
In order to reliably avoid creating thunks, it must be possible for
code invoking an opaque thick function to pass the context pointer in
a way that can be safely and implicitly ignored if the function
happens to actually be thin. There are two ways to achieve this:
* The context can be passed as the final parameter. In most C calling
conventions, extra arguments can be safely ignored; this is because
most C calling conventions support variadic arguments, and such
conventions inherently can't rely on the callee knowing the extent
of the arguments.
However, this is sub-optimal because the context is often used
repeatedly in a closure, especially at the beginning, and putting it
at the end of the argument list makes it more likely to be passed on
the stack.
* The context can be passed in a register outside of the normal
argument sequence. Some ABIs actually even reserve a register for
this purpose; for example, on x86-64 it's `%r10`. Neither of the
ARM ABIs do, however.
Having an out-of-band register would be the best solution.
(Surprisingly, the ownership transfer convention for the context
doesn't actually matter here. You might think that an `owned`
convention would be prohibited, since the callee would fail to release
the context and would therefore leak it. However, a thin function
should always have a `nil` context, so this would be harmless.)
Either solution works acceptably with curried partial application,
since the inner parameters can be left in place while transforming the
context into the outer parameters. However, an `owned` convention
would either prevent the uncurrying forwarder from tail-calling the
main function or force all the arguments to be spilled. Neither is
really acceptable; one more argument against an `owned` convention.
(This is another example where `guaranteed` works quite nicely, since
the guarantees are straightforward to extend to the main function.)
`self`
~~~~~~
Methods (both static and instance) require a `self` parameter. In all
of these cases, it's reasonable to expect that `self` will used
frequently, so it's best to pass it in a register. Also, many methods
call other methods on the same object, so it's also best if the
register storing `self` is stable across different method signatures.
In static methods on value types, `self` doesn't require any dynamic
information: there's only one value of the metatype, and there's
usually no point in passing it.
In static methods on class types, `self` is a reference to the class
metadata, a single pointer. This is necessary because it could
actually be the class object of a subclass.
In instance methods on class types, `self` is a reference to the
instance, again a single pointer.
In mutating instance methods on value types, `self` is the address of
an object.
In non-mutating instance methods on value types, `self` is a value; it
may require multiple registers, or none, or it may need to be passed
indirectly.
All of these cases except mutating instance methods on value types can
be partially applied to create a function closure whose type is the
formal type of the method. That is, if class `A` has a method
declared `func foo(_ x: Int) -> Double`, then `A.foo` yields a function
of type `(Int) -> Double`. Assuming that we continue to feel that
this is a useful language feature, it's worth considered how we could
support it efficiently. The expenses associated with a partial
application are (1) the allocation of a context object and (2) needing
to introduce a thunk to forward to the original function. All else
aside, we can avoid the allocation if the representation of `self` is
compatible with the representation of a context object reference; this
is essentially true only if `self` is a class instance using Swift
reference counting. Avoiding the thunk is possible only if we
successfully avoided the allocation (since otherwise a thunk is
required in order to extract the correct `self` value from the
allocated context object) and `self` is passed in exactly the same
manner as a closure context would be.
It's unclear whether making this more efficient would really be
worthwhile on its own, but if we do support an out-of-band context
parameter, taking advantage of it for methods is essentially trivial.
Error handling
--------------
The calling convention implications of Swift's error handling design
aren't yet settled. It may involve extra parameters; it may involve
extra return values. Considerations:
* Callers will generally need to immediately check for an error.
Being able to quickly check a register would be extremely
convenient.
* If the error is returned as a component of the result value, it
shouldn't be physically combined with the normal result. If the
normal result is returned in registers, it would be unfortunate to
have to do complicated logic to test for error. If the normal
result is returned indirectly, contorting the indirect result with
the error would likely prevent the caller from evaluating the call
in-place.
* It would be very convenient to be able to trivially turn a function
which can't produce an error into a function which can. This is an
operation that we expect higher-order code to have do frequently, if
it isn't completely inlined away. For example::
// foo() expects its argument to follow the conventions of a
// function that's capable of throwing.
func foo(_ fn: () throws -> ()) throwsIf(fn)
// Here we're passing foo() a function that can't throw; this is
// allowed by the subtyping rules of the language. We'd like to be
// able to do this without having to introduce a thunk that maps
// between the conventions.
func bar(_ fn: () -> ()) {
foo(fn)
}
We'll consider two ways to satisfy this.
The first is to pass a pointer argument that doesn't interfere with
the normal argument sequence. The caller would initialize the memory
to a zero value. If the callee is a throwing function, it would be
expected to write the error value into this argument; otherwise, it
would naturally ignore it. Of course, the caller then has to load
from memory to see whether there's an error. This would also either
consume yet another register not in the normal argument sequence or
have to be placed at the end of the argument list, making it more
likely to be passed on the stack.
The second is basically the same idea, but using a register that's
otherwise callee-save. The caller would initialize the register to a
zero value. A throwing function would write the error into it; a
non-throwing function would consider it callee-save and naturally
preserve it. It would then be extremely easy to check it for an
error. Of course, this would take away a callee-save register in the
caller when calling throwing functions. Also, if the caller itself
isn't throwing, it would have to save and restore that register.
Both solutions would allow tail calls, and the zero store could be
eliminated for direct calls to known functions that can throw. The
second is the clearly superior solution, but definitely requires more
work in the backend.
Default argument generators
---------------------------
By default, Swift is resilient about default arguments and treats them
as essentially one part of the implementation of the function. This
means that, in general, a caller using a default argument must call a
function to emit the argument, instead of simply inlining that
emission directly into the call.
These default argument generation functions are unlike any other
because they have very precise information about how their result will
be used: it will be placed into a specific position in specific
argument list. The only reason the caller would ever want to do
anything else with the result is if it needs to spill the value before
emitting the call.
Therefore, in principle, it would be really nice if it were possible
to tell these functions to return in a very specific way, e.g. to
return two values in the second and third argument registers, or to
return a value at a specific location relative to the stack pointer
(although this might be excessively constraining; it would be
reasonable to simply opt into an indirect return instead). The
function should also preserve earlier argument registers (although
this could be tricky if the default argument generator is in a generic
context and therefore needs to be passed type-argument information).
This enhancement is very easy to postpone because it doesn't affect
any basic language mechanics. The generators are always called
directly, and they're inherently attached to a declaration, so it's
quite easy to take any particular generator and compatibly enhance it
with a better convention.
ARM32
-----
Most of the platforms we support have pretty good C calling
conventions. The exceptions are i386 (for the iOS simulator) and
ARM32 (for iOS). We really, really don't care about i386, but iOS on
ARM32 is still an important platform. Switching to a better physical
calling convention (only for calls from Swift to Swift, of course)
would be a major improvement.
It would be great if this were as simple as flipping a switch, but
unfortunately the obvious convention to switch to (AAPCS-VFP) has a
slightly different set of callee-save registers: iOS treats `r9` as a
scratch register. So we'd really want a variant of AAPCS-VFP that did
the same. We'd also need to make sure that SJ/LJ exceptions weren't
disturbed by this calling convention; we aren't really *supporting*
exception propagation through Swift frames, but completely breaking
propagation would be unfortunate, and we may need to be able to
*catch* exceptions.
So this would also require some amount of additional support from the
backend.
Function signature lowering
===========================
Function signatures in Swift are lowered in two phases.
Semantic lowering
-----------------
The first phase is a high-level semantic lowering, which does a number
of things:
* It determines a high-level calling convention: specifically, whether
the function must match the C calling convention or the Swift
calling convention.
* It decides the types of the parameters:
* Functions exported for the purposes of C or Objective-C may need
to use bridged types rather than Swift's native types. For
example, a function that formally returns Swift's `String` type
may be bridged to return an `NSString` reference instead.
* Functions which are values, not simply immediately called, may
need their types lowered to follow to match a specific generic
abstraction pattern. This applies to functions that are
parameters or results of the outer function signature.
* It identifies specific arguments and results which *must* be passed
indirectly:
* Some types are inherently address-only:
* The address of a weak reference must be registered with the
runtime at all times; therefore, any `struct` with a weak field
must always be passed indirectly.
* An existential type (if not class-bounded) may contain an
inherently address-only value, or its layout may be sensitive to
its current address.
* A value type containing an inherently address-only type as a
field or case payload becomes itself inherently address-only.
* Some types must be treated as address-only because their layout is
not known statically:
* The layout of a resilient value type may change in a later
release; the type may even become inherently address-only by
adding a weak reference.
* In a generic context, the layout of a type may be dependent on a
type parameter. The type parameter might even be inherently
address-only at runtime.
* A value type containing a type whose layout isn't known
statically itself generally will not have a layout that can be
known statically.
* Other types must be passed or returned indirectly because the
function type uses an abstraction pattern that requires it. For
example, a generic `map` function expects a function that takes a
`T` and returns a `U`; the generic implementation of `map` will
expect these values to be passed indirectly because their layout
isn't statically known. Therefore, the signature of a function
intended to be passed as this argument must pass them indirectly,
even if they are actually known statically to be non-address-only
types like (e.g.) `Int` and `Float`.
* It expands tuples in the parameter and result types. This is done
at this level both because it is affected by abstraction patterns
and because different tuple elements may use different ownership
conventions. (This is most likely for imported APIs, where it's the
tuple elements that correspond to specific C or Objective-C parameters.)
This completely eliminates top-level tuple types from the function
signature except when they are a target of abstraction and thus are
passed indirectly. (A function with type `(Float, Int) -> Float`
can be abstracted as `(T) -> U`, where `T == (Float, Int)`.)
* It determines ownership conventions for all parameters and results.
After this phase, a function type consists of an abstract calling
convention, a list of parameters, and a list of results. A parameter
is a type, a flag for indirectness, and an ownership convention. A
result is a type, a flag for indirectness, and an ownership
convention. (Results need ownership conventions only for non-Swift
calling conventions.) Types will not be tuples unless they are
indirect.
Semantic lowering may also need to mark certain parameters and results
as special, for the purposes of the special-case physical treatments
of `self`, closure contexts, and error results.
Physical lowering
-----------------
The second phase of lowering translates a function type produced by
semantic lowering into a C function signature. If the function
involves a parameter or result with special physical treatment,
physical lowering initially ignores this value, then adds in the
special treatment as agreed upon with the backend.
General expansion algorithm
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Central to the operation of the physical-lowering algorithm is the
**generic expansion algorithm**. This algorithm turns any
non-address-only Swift type in a sequence of zero or more **legal
type**, where a legal type is either:
* an integer type, with a power-of-two size no larger than the maximum
integer size supported by C on the target,
* a floating-point type supported by the target, or
* a vector type supported by the target.
Obviously, this is target-specific. The target also specifies a
maximum voluntary integer size. The legal type sequence only contains
vector types or integer types larger than the maximum voluntary size
when the type was explicit in the input.
Pointers are represented as integers in the legal type sequence. We
assume there's never a reason to differentiate them in the ABI as long
as the effect of address spaces on pointer size is taken into account.
If that's not true, this algorithm should be adjusted.
The result of the algorithm also associates each legal type with an
offset. This information is sufficient to reconstruct an object in
memory from a series of values and vice-versa.
The algorithm proceeds in two steps.
Typed layouts
^^^^^^^^^^^^^
First, the type is recursively analyzed to produce a **typed layout**.
A typed layout associates ranges of bytes with either (1) a legal type
(whose storage size must match the size of the associated byte
range), (2) the special type **opaque**, or (3) the special type
**empty**. Adjacent ranges mapped to **opaque** or **empty** can be
combined.
For most of the types in Swift, this process is obvious: they either
correspond to an obvious legal type (e.g. thick metatypes are
pointer-sized integers), or to an obvious sequence of scalars
(e.g. class existentials are a sequence of pointer-sized integers).
Only a few cases remain:
* Integer types that are not legal types should be mapped as opaque.
* Vector types that are not legal types should be broken into smaller
vectors, if their size is an even multiple of a legal vector type,
or else broken into their components. (This rule may need some
tinkering.)
* Tuples and structs are mapped by merging the typed layouts of the
fields, as padded out to the extents of the aggregate with
empty-mapped ranges. Note that, if fields do not overlap, this is
equivalent to concatenating the typed layouts of the fields, in
address order, mapping internal padding to empty. Bit-fields should
map the bits they occupy to opaque.
For example, given the following struct type::
struct FlaggedPair {
var flag: Bool
var pair: (MyClass, Float)
}
If Swift performs naive, C-like layout of this structure, and this
is a 64-bit platform, typed layout is mapped as follows::
FlaggedPair.flag := [0: i1, ]
FlaggedPair.pair := [ 8-15: i64, 16-19: float]
FlaggedPair := [0: i1, 8-15: i64, 16-19: float]
If Swift instead allocates `flag` into the spare (little-endian) low
bits of `pair.0`, the typed layout map would be::
FlaggedPair.flag := [0: i1 ]
FlaggedPair.pair := [0-7: i64, 8-11: float]
FlaggedPair := [0-7: opaque, 8-11: float]
* Unions (imported from C) are mapped by merging the typed layouts of
the fields, as padded out to the extents of the aggregate with
empty-mapped ranges. This will often result in a fully-opaque
mapping.
* Enums are mapped by merging the typed layouts of the cases, as
padded out to the extents of the aggregate with empty-mapped ranges.
A case's typed layout consists of the typed layout of the case's
directly-stored payload (if any), merged with the typed layout for
its discriminator. We assume that checking for a discriminator
involves a series of comparisons of bits extracted from
non-overlapping ranges of the value; the typed layout of a
discriminator maps all these bits to opaque and the rest to empty.
For example, given the following enum type::
enum Sum {
case Yes(MyClass)
case No(Float)
case Maybe
}
If Swift, in its infinite wisdom, decided to lay this out
sequentially, and to use invalid pointer values the class to
indicate that the other cases are present, the layout would look as
follows::
Sum.Yes.payload := [0-7: i64 ]
Sum.Yes.discriminator := [0-7: opaque ]
Sum.Yes := [0-7: opaque ]
Sum.No.payload := [ 8-11: float]
Sum.No.discriminator := [0-7: opaque ]
Sum.No := [0-7: opaque, 8-11: float]
Sum.Maybe := [0-7: opaque ]
Sum := [0-7: opaque, 8-11: float]
If Swift instead chose to just use a discriminator byte, the layout
would look as follows::
Sum.Yes.payload := [0-7: i64 ]
Sum.Yes.discriminator := [ 8: opaque]
Sum.Yes := [0-7: i64, 8: opaque]
Sum.No.payload := [0-3: float ]
Sum.No.discriminator := [ 8: opaque]
Sum.No := [0-3: float, 8: opaque]
Sum.Maybe := [ 8: opaque]
Sum := [0-8: opaque ]
If Swift chose to use spare low (little-endian) bits in the class
pointer, and to offset the float to make this possible, the layout
would look as follows::
Sum.Yes.payload := [0-7: i64 ]
Sum.Yes.discriminator := [0: opaque ]
Sum.Yes := [0-7: opaque ]
Sum.No.payload := [ 4-7: float]
Sum.No.discriminator := [0: opaque ]
Sum.No := [0: opaque, 4-7: float]
Sum.Maybe := [0: opaque ]
Sum := [0-7: opaque ]
The merge algorithm for typed layouts is as follows. Consider two
typed layouts `L` and `R`. A range from `L` is said to *conflict*
with a range from `R` if they intersect and they are mapped as
different non-empty types. If two ranges conflict, and either range
is mapped to a vector, replace it with mapped ranges for the vector
elements. If two ranges conflict, and neither range is mapped to a
vector, map them both to opaque, combining them with adjacent opaque
ranges as necessary. If a range is mapped to a non-empty type, and
the bytes in the range are all mapped as empty in the other map, add
that range-mapping to the other map. `L` and `R` should now match
perfectly; this is the result of the merge. Note that this algorithm
is both associative and commutative.
Forming a legal type sequence
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Once the typed layout is constructed, it can be turned into a legal
type sequence.
Note that this transformation is sensitive to the offsets of ranges in
the complete type. It's possible that the simplifications described
here could be integrated directly into the construction of the typed
layout without changing the results, but that's not yet proven.
In all of these examples, the maximum voluntary integer size is 4
(`i32`) unless otherwise specified.
If any range is mapped as a non-empty, non-opaque type, but its start
offset is not a multiple of its natural alignment, remap it as opaque.
For these purposes, the natural alignment of an integer type is the
minimum of its size and the maximum voluntary integer size; the
natural alignment of any other type is its C ABI type. Combine
adjacent opaque ranges.
For example::
[1-2: i16, 4: i8, 6-7: i16] ==> [1-2: opaque, 4: i8, 6-7: i16]
If any range is mapped as an integer type that is not larger than the
maximum voluntary size, remap it as opaque. Combine adjacent opaque
ranges.
For example::
[1-2: opaque, 4: i8, 6-7: i16] ==> [1-2: opaque, 4: opaque, 6-7: opaque]
[0-3: i32, 4-11: i64, 12-13: i16] ==> [0-3: opaque, 4-11: i64, 12-13: opaque]
An *aligned storage unit* is an N-byte-aligned range of N bytes, where
N is a power of 2 no greater than the maximum voluntary integer size.
A *maximal* aligned storage unit has a size equal to the maximum
voluntary integer size.
Note that any remaining ranges mapped as integers must fully occupy
multiple maximal aligned storage units.
Split all opaque ranges at the boundaries of maximal aligned storage
units. From this point on, never combine adjacent opaque ranges
across these boundaries.
For example::
[1-6: opaque] ==> [1-3: opaque, 4-6: opaque]
Within each maximal aligned storage unit, find the smallest aligned
storage unit which contains all the opaque ranges. Replace the first
opaque range in the maximal aligned storage unit with a mapping from
that aligned storage unit to an integer of the aligned storage unit's
size. Remove any other opaque ranges in the maximal aligned storage
unit. Note that this can create overlapping ranges in some cases.
For the purposes of this calculation, the last maximal aligned
storage unit should be considered "full", as if the type had an
infinite amount of empty tail-padding.
For example::
[1-2: opaque] ==> [0-3: i32]
[0-1: opaque] ==> [0-1: i16]
[0: opaque, 2: opaque] ==> [0-3: i32]
[0-9: fp80, 10: opaque] ==> [0-9: fp80, 10: i8]
// If maximum voluntary size is 8 (i64):
[0-9: fp80, 11: opaque, 13: opaque] ==> [0-9: fp80, 8-15: i64]
(This assumes that `fp80` is a legal type for illustrative purposes.
It would probably be a better policy for the actual x86-64 target to
consider it illegal and treat it as opaque from the start, at least
when lowering for the Swift calling convention; for C, it is important
to produce an `fp80` mapping for ABI interoperation with C functions
that take or return `long double` by value.)
The final legal type sequence is the sequence of types for the
non-empty ranges in the map. The associated offset for each type is
the offset of the start of the corresponding range.
Only the final step can introduce overlapping ranges, and this is only
possible if there's a non-integer legal type which:
* has a natural alignment less than half of the size of the maximum
voluntary integer size or
* has a store size is not a multiple of half the size of the maximum
voluntary integer size.
On our supported platforms, these conditions are only true on x86-64,
and only of `long double`.
Deconstruction and Reconstruction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Given the address of an object and a legal type sequence for its type,
it's straightforward to load a valid sequence or store the sequence
back into memory. For the most part, it's sufficient to simply load
or store each value at its appropriate offset. There are two
subtleties:
* If the legal type sequence had any overlapping ranges, the integer
values should be stored first to prevent overwriting parts of the
other values they overlap.
* Care must be taken with the final values in the sequence; integer
values may extend slightly beyond the ordinary storage size of the
argument type. This is usually easy to compensate for.
The value sequence essentially has the same semantics that the value
in memory would have: any bits that aren't part of the actual
representation of the original type have a completely unspecified
value.
Forming a C function signature
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As mentioned before, in principle the process of physical lowering
turns a semantically-lowered Swift function type (in implementation
terms, a SILFunctionType) into a C function signature, which can then
be lowered according to the usual rules for the ABI. This is, in
fact, what we do when trying to match a C calling convention.
However, for the native Swift calling convention, because we actively
want to use more aggressive rules for results, we instead build an
LLVM function type directly. We first construct a direct result type
that we're certain the backend knows how to interpret according to our
more aggressive desired rules, and then we use the expansion algorithm
to construct a parameter sequence consisting solely of types with
obvious ABI lowering that the backend can reliably handle. This
bypasses the need to consult Clang for our own native calling
convention.
We have this generic expansion algorithm, but it's important to
understand that the physical lowering process does not just naively
use the results of this algorithm. The expansion algorithm will
happily expand an arbitrary structure; if that structure is very
large, the algorithm might turn it into hundreds of values. It would
be foolish to pass it as an argument that way; it would use up all the
argument registers and basically turn into a very inefficient memcpy,
and if the caller wanted it all in one place, they'd have to very
painstakingly reassemble. It's much better to pass large structures
indirectly. And with result values, we really just don't have a
choice; there's only so many registers you can use before you have to
give up and return indirectly. Therefore, even in the Swift native
convention, the expansion algorithm is basically used as a first pass.
A second pass then decides whether the expanded sequence is actually
reasonable to pass directly.
Recall that one aspect of the semantically-lowered Swift function type
is whether we should be matching the C calling convention or not. The
following algorithm here assumes that the importer and semantic
lowering have conspired in a very particular way to make that
possible. Specifically, we assume is that an imported C function
type, lowered semantically by Swift, will follow some simple
structural rules:
* If there was a by-value `struct` or `union` parameter or result in
the imported C type, it will correspond to a by-value direct
parameter or return type in Swift, and the Swift type will be a
nominal type whose declaration links back to the original C
declaration.
* Any other parameter or result will be transformed by the importer
and semantic lowering to a type that the generic expansion algorithm
will expand to a single legal type whose representation is
ABI-compatible with the original parameter. For example, an
imported pointer type will eventually expand to an integer of
pointer size.
* There will be at most one result in the lowered Swift type, and it
will be direct.
Given this, we go about lowering the function type as follows. Recall
that, when matching the C calling convention, we're building a C
function type; but that when matching the Swift native calling
convention, we're building an LLVM function type directly.
Results
^^^^^^^
The first step is to consider the results of the function.
There's a different set of rules here when we're matching the C
calling convention. If there's a single direct result type, and it's
a nominal type imported from Clang, then the result type of the C
function type is that imported Clang type. Otherwise, concatenate the
legal type sequences from the direct results. If this yields an empty
sequence, the result type is `void`. If it yields a single legal
type, the result type is the corresponding Clang type. No other could
actually have come from an imported C declaration, so we don't have
any real compatibility requirements; for the convenience of
interoperation, this is handled by constructing a new C struct which
contains the corresponding Clang types for the legal type sequence as
its fields.
Otherwise, we are matching the Swift calling convention. Concatenate
the legal type sequences from all the direct results. If
target-specific logic decides that this is an acceptable collection to
return directly, construct the appropriate IR result type to convince
the backend to handle it. Otherwise, use the `void` IR result type
and return the "direct" results indirectly by passing the address of a
tuple combining the original direct results (*not* the types from the
legal type sequence).
Finally, any indirect results from the semantically-lowered function
type are simply added as pointer parameters.
Parameters
^^^^^^^^^^
After all the results are collected, it's time to collect the
parameters. This is done one at the time, from left to right, adding
parameters to our physically-lowered type.
If semantic lowering has decided that we have to pass the parameter
indirectly, we simply add a pointer to the type. This covers both
mandatory-indirect pass-by-value parameters and pass-by-reference
parameters. The latter can arise even in C and Objective-C.
Otherwise, the rules are somewhat different if we're matching the C
calling convention. If the parameter is a nominal type imported from
Clang, then we just add the imported Clang type to the Clang function
type as a parameter. Otherwise, we derive the legal type sequence for
the parameter type. Again, we should only have compatibility
requirements if the legal type sequence has a single element, but for
the convenience of interoperation, we collect the corresponding Clang
types for all of the elements of the sequence.
Finally, if we're matching the Swift calling convention, derive the
legal type sequence. If the result appears to be a reasonably small
and efficient set of parameters, add their corresponding IR types to
the function type we're building; otherwise, ignore the legal type
sequence and pass the address of the original type indirectly.
Considerations for whether a legal type sequence is reasonable to pass
directly:
* There probably ought to be a maximum size. Unless it's a single
256-bit vector, it's hard to imagine wanting to pass more than, say,
32 bytes of data as individual values. The callee may decide that
it needs to reconstruct the value for some reason, and the larger
the type gets, the more expensive this is. It may also be
reasonable for this cap to be lower on 32-bit targets, but that
might be dealt with better by the next restriction.
* There should also be a cap on the number of values. A 32-byte limit
might be reasonable for passing 4 doubles. It's probably not
reasonable for passing 8 pointers. That many values will exhaust
all the parameter registers for just a single value. 4 is probably
a reasonable cap here.
* There's no reason to require the data to be homogeneous. If a
struct contains three floats and a pointer, why force it to be
passed in memory?
When all of the parameters have been processed in this manner,
the function type is complete.