orphan: |
---|
Pointer arguments are a fact of life in C and Cocoa, and there's no way we're going to realistically annotate or wrap every API safely. However, there are plenty of well-behaved APIs that use pointer arguments in well-behaved ways that map naturally to Swift argument conventions, and we should interact with those APIs in a natural, Swift-ish way. To do so, I propose adding language and library facilities that enable the following uses of pointer arguments:
- Const pointer arguments
const int *
, including const pointers to ObjC classesNSFoo * const *
, can be used as "in" array arguments, asinout
scalar arguments, or asUnsafeMutablePointer
arguments. - Non-const pointer arguments to C types,
int *
, can be used asinout
array or scalar arguments, or asUnsafeMutablePointer
arguments. - Non-const pointer arguments to ObjC class types,
NSFoo **
, can be used asinout
scalar arguments or passednil
. (They cannot be used as array arguments or asUnsafeMutablePointer
arguments.) const void *
andvoid *
pointers can be used in the same ways as pointers to any C type (but not ObjC types).
This model intentionally doesn't try to provide a mapping to every possible use case for pointers in C. It also intentionally avoids addressing special cases we could potentially provide higher-level support for. Some particular issues this proposal specifically does not address:
- Pointer return values
- Special handling of
char*
and/orconst char*
arguments - Special handling of Core Foundation types
- Special handling of
NSError**
arguments - Precise lifetime of values (beyond the minimal lifetime extension to the duration of a call)
- Overriding of ObjC methods that take pointer arguments with subclass methods that take non-pointer arguments
Const pointer arguments are frequently used in both C and Objective-C to take
an array of arguments effectively by value. To support this use case, we should
support passing a Swift Array
value to a const pointer argument. An
example from Core Foundation is CGColorCreate
, which takes a
CGFloat
array of color space-specific components:
let rgb = CGColorSpaceCreateCalibratedRGB() let cyan = CGColorCreate(rgb, [0, 1, 1])
We are willing to assume that the API is well-behaved and does not mutate the pointed-to array, so we can safely pass an interior pointer to the array storage without worrying about mutation. We only guarantee the lifetime of the array for the duration of the call, so it could potentially be promoted to a stack allocation.
Const pointer arguments are also used in many cases where a value is unmodified, but its identity is important. Somewhat more rarely, const pointer arguments are used as pure "in" value arguments with no regard for identity; this is particularly prevalent on platforms like Win32 where there has historically been no standard ABI for passing structs by value, but pure "in" pointer parameters are rare on our platforms. The potential consequences of disregarding value identity with C APIs are too high to allow scalar arguments to implicitly be used as pointer arguments:
// From C: void foo(const pthread_mutex_t *); import Foo let mutex = pthread_mutex_create() // This would pass a different temporary buffer on each call--not what you // want for a mutex! foo(mutex) foo(mutex) foo(mutex)
Although const pointers should never be used for actual mutation, we propose
that only inout
scalar arguments be accepted for const pointer parameters.
Although our semantics normally do not guarantee value identity, inout
parameters that refer to stored variables or stored properties of C-derived
types are in practice never subjected to implicit writebacks except in limited
circumstances such as capture of inout
references in closures that could be
diagnosed. Requiring inout
also prevents the use of rvalues or let
bindings that never have well-defined addresses as pointer arguments. This
more clearly communicates the intent for the callee to receive the same
variable on every call:
// From C: void foo(const pthread_mutex_t *); import Foo var mutex = pthread_mutex_create() foo(&mutex) foo(&mutex) foo(&mutex)
If using an rvalue as a pointer argument is desired, it can easily be wrapped in an array. This communicates that the value is being copied into the temporary array, so it's more obvious that identity would not be maintained:
// an immutable scalar we might want to pass into a "const double*". let grayLevel = 0.5 let monochrome = CGColorSpaceCreateGrayscale() // error, can't pass Double into second argument. let c1 = CGColorCreate(monochrome, grayval) // error, can't take the address of a 'let' (would be ok for a 'var') let c2 = CGColorCreate(monochrome, &grayval) // OK, we're explicitly forming an array let c3 = CGColorCreate(monochrome, [grayval])
Non-const arguments of C type can be used as "out" or "inout" parameters,
either of scalars or of arrays, and so should accept inout
parameters of
array or scalar type. Although a C API may expect a pure "out" parameter and
not require initialization of its arguments, it is safer to assume the argument
is inout
and always require initialization:
var s, c: Double // error, 's' and 'c' aren't initialized sincos(0.5, &s, &c) var s1 = 0.0, c1 = 0.0 // OK sincos(0.5, &s1, &c1)
For array parameters, the exact point of mutation inside the callee cannot be known, so a copy-on-write array buffer must be eagerly uniqued prior to the address of the array being taken:
func loadFloatsFromData(_ data: NSData) { var a: [Float] = [0.0, 0.0, 0.0, 0.0] var b = a // Should only mutate 'b' without affecting 'a', so its backing store // must be uniqued data.getBytes(&b, sizeof(Float.self) * b.count) }
ARC semantics treat an NSFoo**
type as a pointer to an __autoreleasing
NSFoo*
. Although in theory these interfaces could receive arrays of object
pointers in Objective-C, that use case doesn't come up in Cocoa, and we can't
reliably bridge such APIs into Swift. We only need to bridge ObjC mutable pointer
types to accept a scalar inout
object reference or nil
.
This proposal does not address the handling of return values, which should still
be imported into Swift as UnsafeMutablePointer
values.
The necessary conversions can be represented entirely in the standard library with the help of some new language features, inout address conversion, inout writeback conversion, and interior pointer conversion, described below. There are three categories of argument behavior needed, and thus three new types. These types should have no user-accessible operations of their own other than their implicit conversions. The necessary types are as follows:
CConstPointer<T>
is the imported representation of aconst T *
argument. It is implicitly convertible frominout T
by inout address conversion and fromArray<T>
by immutable interior pointer conversion. It is also implicitly convertible to and fromUnsafeMutablePointer<T>
by normal conversion.CMutablePointer<T>
is the imported representation of aT *
argument for a POD C typeT
. It is implicitly convertible frominout T
by inout address conversion and frominout Array<T>
by mutating interior pointer conversion. It is also implicitly convertible to and fromUnsafeMutablePointer<T>
by normal conversion.ObjCInOut<T>
is the imported representation of aT **
argument for an ObjC class typeT
. It is implicitly convertible frominout T
by inout writeback conversion and is implicitly convertible fromnil
. It cannot be converted from an array or toUnsafeMutablePointer
.
To support the necessary semantics for argument passing, some new conversion forms need to be supported by the language with special-cased lifetime behavior.
To be able to pass a pointer to array data as an argument, we need to be able
to guarantee the lifetime of the array buffer for the duration of the call.
If mutation can potentially occur through the pointer, then copy-on-write
buffers must also be uniqued prior to taking the address. A new form of
conversion, @unsafe_interior_pointer_conversion
, can be applied to an
instance method of a type, to allow that type to return both a converted
pointer and an owning reference that guarantees the validity of the pointer.
Such methods can be either mutating
or non-mutating; only non-mutating
conversions are considered for non- inout
parameters, and only mutating
conversions are considered for inout
parameters:
extension Array { @unsafe_interior_pointer_conversion func convertToConstPointer() -> (CConstPointer<T>, ArrayBuffer<T>) { return (CConstPointer(self.base), self.owner) } @unsafe_interior_pointer_conversion mutating func convertToMutablePointer() -> (CMutablePointer<T>, ArrayBuffer<T>) { // Make the backing buffer unique before handing out a mutable pointer. self.makeUnique() return (CMutablePointer(self.base), self.owner) } }
@unsafe_interior_pointer_conversion
conversions are only considered in
argument contexts. If such a conversion is found, the first element of the
return tuple is used as the argument, and a strong reference to the second
element is held for the duration of the callee that receives the converted
argument.
To pass an inout
as a pointer argument, we need to be able to lock an
address for the inout
for the duration of the call, which is not normally
possible. This functionality only needs to be available to the standard library,
so can be expressed in terms of builtins. A type can conform to the
_BuiltinInOutAddressConvertible
protocol to be convertible from an
inout reference. The protocol is defined as follows:
protocol _BuiltinInOutAddressConvertible { /// The type from which inout conversions are allowed to the conforming /// type. typealias InOutType /// Create a value of the conforming type using the address of an inout /// argument. class func _convertFromInOutAddress(_ p: Builtin.RawPointer) -> Self }
An example of a conformance for CMutablePointer
:
struct CMutablePointer<T>: _BuiltinInOutAddressConvertible { let ptr: Builtin.RawPointer typealias InOutType = T @_transparent static func _convertFromInOutAddress(_ p: Builtin.RawPointer) -> CMutablePointer { return CMutablePointer(p) } } func foo(_ p: CMutablePointer<Int>) { } var i = 0 foo(&i)
The lifetime of the variable, stored property owning object, or writeback buffer backing the inout is guaranteed for the lifetime of the callee that receives the converted parameter, as if the callee had received the inout parameter directly.
Inout address conversion alone is not enough for ObjCInOut
to work as
intended, because the change to the __autoreleasing
convention for the
pointed-to object reference requires a writeback temporary. The
_BuiltinInOutWritebackConvertible
protocol allows for an additional
writeback to be introduced before and after the address of the inout
is
taken:
protocol _BuiltinInOutWritebackConvertible { /// The original type from which inout conversions are allowed to the /// conforming type. typealias InOutType /// The type of the temporary writeback whose address is used to construct /// the converted value. typealias WritebackType /// Get the initial value the writeback temporary should have on entry to /// the call. class func _createWriteback(inout InOutType) -> WritebackType /// Create a value of the conforming type using the address of the writeback /// temporary. class func _convertFromWritebackAddress(_ p: Builtin.RawPointer) -> Self /// Write the writeback temporary back to the original value. class func _commitWriteback(inout InOutType, WritebackType) }
An example of a conformance for ObjCInOut
:
struct ObjCInOut<T: class>: _BuiltinInOutWritebackConvertible { let ptr: Builtin.RawPointer typealias InOutType = T! typealias WritebackType = Builtin.RawPointer @_transparent static func _createWriteback(ref: inout T!) -> Builtin.RawPointer { // The initial object reference is passed into the callee effectively // __unsafe_unretained, so pass it as a RawPointer. return unsafeBitCast(ref, Builtin.RawPointer.self) } @_transparent static func _commitWriteback(ref: inout T!, value: Builtin.RawPointer) { // The reference is autoreleased on return from the caller, so retain it // by loading it back as a T?. ref = unsafeBitCast(value, T!.self) } @_transparent static func _convertFromWritebackAddress(_ value: Builtin.RawPointer) { return ObjCInOut(value) } }
The lifetime of the writeback is guaranteed for the lifetime of the callee that receives the converted parameter, as if the callee had received the writeback temporary as a mutable logical property of the original inout parameter.