WWDC Quick Look 💓 By SwiftGGTeam
Unsafe Swift

Unsafe Swift

Watch original video

Highlight

Swift marks interfaces that may enter undefined behavior after violating preconditions asUnsafe, developers can use buffer pointer,withUnsafe*Closures and diagnostics in Swift 5.3 limit pointer lifetimes to a smaller range.

Core Content

Swift safety does not mean never crashing. Common forced unpacking encountersnilA clear fatal runtime error will be triggered, and the crash report can tell you where the constraints were broken.unsafelyUnwrappedIt is also required that the value cannot benil, but under optimized construction it will trust the caller and read the value directly. Once the preconditions are not met, the result may be a crash, garbage values, or state pollution that is harder to track.

Pointers magnify the risk. Swift’s runtime generally manages the location of objects, stack variables, and dynamic memory, so developers don’t need to manually track addresses. With an unsafe pointer, the pointer is just an address; it doesn’t know if the memory is still alive, or if that memory is now the same Swift value. The freed dangling pointer still looks like a valid pointer, and writing to it may destroy other state of the application.

C and Objective-C interop are the main scenarios where pointers must be touched. The C API often represents arrays, strings, and single values ​​as pointer plus count, and Swift needs to temporarily expose its own values ​​into these forms. Apple recommends prioritizing arrays, strings,withUnsafeBufferPointerwithUnsafeMutablePointerThis type of closure API generates temporary pointers and pushes unsafe code into a function call or a closure.

The second half of Session gives a more practical judgment: when you can use implicit conversion to call the C API, do not allocate and release memory yourself; when you need to display the life cycle, use the closure-based API to write the pointer validity period in the code structure; only construct it directlyArrayorStringThe new one is used only when the underlying storageunsafeUninitializedCapacityInitializer and completes all bounds checks before returning.

Detailed Content

safe operation will give a certain result

(00:52) Ordinary force unpacking can also be a fatal error, but Swift will turn the error into a well-defined fatal runtime error. This difference is understoodUnsafeThe starting point of the prefix.

let value: Int? = nil

print(value!) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

Key points:

  • valueyesnil, does not meet the preconditions for forced unpacking. -value!Still a safe operation because it has explicit behavior for invalid input.
  • The runtime will stop execution and report the error location, and developers can fix the code based on the crash information.

01:58unsafelyUnwrappedChecks in optimized builds are omitted. It is only suitable for use when the caller can already prove that the value is non-null.

let value: String? = "Hello"

print(value.unsafelyUnwrapped) // Hello

Key points:

  • This code works becausevaluedoes contain the string. -unsafelyUnwrappedHand the non-null proof to the caller.
  • transcript clearly says that it is a performance scenario and should only be placed on code paths where cost impact is confirmed after measurement.

(02:25) If the precondition is not true, the unsafe operation may read a non-existent value. The focus here is not on the crash form, but on the fact that the behavior has been undefined.

let value: String? = nil

print(value.unsafelyUnwrapped) // ?!

Key points:

  • nilNone to readStringvalue.
  • Under optimized construction, this read is not guaranteed to trigger the same error.
  • The same piece of code may behave differently under different runs and different optimization conditions.

Manually allocated pointers do not manage life cycle

07:37UnsafeMutablePointerCan directly allocate memory, initialize values, and readpointee, but it does not prevent further access after release.

let ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
ptr.initialize(to: 42)
print(ptr.pointee) // 42
ptr.deallocate()
ptr.pointee = 23 // UNDEFINED BEHAVIOR

Key points:

  • allocate(capacity:)Give a section that can be storedIntof dynamic memory. -initialize(to:)Start the life cycle of the value in this memory. -deallocate()After releasing the storage,ptrThe variable itself still holds the original address.
  • keep writingptr.pointeeAccessing expired memory may pollute other objects or reuse the value of that address later.

buffer pointer puts the starting address and length together

(12:33) When only one starting address is passed, the length will appear repeatedly in the code. Swift provides a buffer pointer type that handles the address and number of elements as an object.

UnsafeBufferPointer<Element>
UnsafeMutableBufferPointer<Element>

UnsafeRawBufferPointer
UnsafeMutableRawBufferPointer

Key points:

  • typed buffer pointer is suitable for contiguous memory where the element type is already known.
  • raw buffer pointer is suitable for processing external data by bytes.
  • Under the debug build, the subscript of the buffer pointer will be bounded.
  • They still don’t verify that the underlying memory is of the correct type and lifecycle, they just reduce missed length errors.

Use closure to generate temporary pointer

(13:48) When passing a Swift array to a C function, give priority to exposing the temporary buffer pointer in the closure. This way the memory is still managed by the array, and the pointer is only valid within the closure.

let values: [CInt] = [0, 2, 4, 6]

values.withUnsafeBufferPointer { buffer in
  print_integers(buffer.baseAddress!, buffer.count)
}

Key points:

  • valuesMaintain normal Swift array semantics. -withUnsafeBufferPointerProvide underlying contiguous storage in closures. -buffer.baseAddress!Is the starting pointer for C functions. -buffer.countIt comes from the same buffer as the pointer, reducing the chance of wrong writing.

(14:25) This scenario is so common that Swift can also generate equivalent temporary pointers for C function calls.

let values: [CInt] = [0, 2, 4, 6]

print_integers(values, values.count)

Key points:

  • Array values ​​can be passed directly to C parameters expecting unsafe pointers.
  • Compiler generates temporary pointer conversion.
  • The pointer is only valid during the function call, the C function cannot save it for later access.

Complex C APIs can use implicit conversions to isolate unsafe regions

16:32sysctlThere are six parameters, including input buffer, output buffer, length pointer and optional new value buffer. Swift can use arrays to pointers,inoutConversion to pointer allows the unsafe part to be concentrated in one call.

import Darwin

func cachelineSize() -> Int {
    var query = [CTL_HW, HW_CACHELINE]
    var result: CInt = 0
    var resultSize = MemoryLayout<CInt>.size
    let r = sysctl(&query, CUnsignedInt(query.count), &result, &resultSize, nil, 0)
    precondition(r == 0, "Cannot query cache line size")
    precondition(resultSize == MemoryLayout<CInt>.size)
    return Int(result)
}

print(cachelineSize()) // 64

Key points:

  • queryIs the integer identifier buffer required by the C API. -&queryTriggers a temporary conversion of an array to a pointer. -&resultand&resultSizeProvides a writable output location for a C function.
  • twopreconditionWrite the assumptions mentioned in session as a runtime check: the call is successful, and the number of bytes returned is equal toCIntsize.

(19:19) In pure Swift code, the closure-based API more clearly marks the pointer validity period. insert temporary pointerUnsafeMutablePointerInitializers can produce dangling pointers, and Swift 5.3 will warn about this detectable condition.

var value = 42
withUnsafeMutablePointer(to: &value) { p in
  p.pointee += 1
}
print(value)  // 43

var value2 = 42
let p = UnsafeMutablePointer(&value2) // BROKEN -- dangling pointer!
p.pointee += 1
print(value2)

Key points:

  • In the first paragraph,pThe validity period is wrapped by a closure.
  • The second paragraph saves the value of the temporary pointer top, the underlying pointer has expired after the initializer call ends.
  • Follow-up visitsp.pointeeis undefined behavior.
  • transcript recommends favoring closures in pure Swift because lifetimes are easier to pronounce.

New initializer reduces temporary buffer

(20:02) Newly added to the Swift standard libraryString.init(unsafeUninitializedCapacity:initializingUTF8With:)Allows C functions to write directly to the buffer that will be the string storage. This eliminates the need to manually allocate temporary memory.

import Darwin

func kernelVersion() -> String {
    var query = [CTL_KERN, KERN_VERSION]
    var length = 0
    let r = sysctl(&query, 2, nil, &length, nil, 0)
    precondition(r == 0, "Error retrieving kern.version")
    return String(unsafeUninitializedCapacity: length) { buffer in
        var length = buffer.count
        let r = sysctl(&query, 2, buffer.baseAddress, &length, nil, 0)
        precondition(r == 0, "Error retrieving kern.version")
        precondition(length > 0 && length <= buffer.count)
        precondition(buffer[length - 1] == 0)
        return length - 1
    }
}

print(kernelVersion())
// Darwin Kernel Version 19.5.0: Thu Apr 30 18:25:59 PDT 2020; root:xnu-6153.121.1~7/RELEASE_X86_64

Key points:

  • firstsysctlusenilOutput buffer query required length. -StringThe initializer provides an uninitialized UTF-8 buffer.
  • second timesysctlCopy the version string directly into this buffer.
  • threepreconditionCheck that the call is successful, that the write length is legal, and that the end is a NUL byte of the C string.
  • returnlength - 1NUL bytes are discarded, leaving the string containing only valid UTF-8 content.

Core Takeaways

  • What to do: Write a secure C API wrapper. Why it’s worth doing: session description Swift can combine arrays, strings andinoutThe value is temporarily converted to a C pointer. How ​​to start: First wrap each C function into a Swift function and call it in the wrapping layerwithUnsafeBufferPointerOr pass in an array value and usepreconditionCheck return code and output length.

  • What to do: Make a binary array reading tool. Why it’s worth doing: The buffer pointer puts the starting address and length together and is suitable for processing file headers, network packets or device data. How ​​to start: UseArray.withUnsafeBytesGet the raw buffer, check the byte count before reading, and group the offset and target type into a few parsing functions.

  • What to do: Audit dangling pointer risks in the project. Why it’s worth doing: Swift 5.3 can already warn about some temporary pointer escape scenarios, and the broken example of session gives a clear search target. How ​​to get started: SearchUnsafeMutablePointer(&UnsafePointer(&and unsafe pointer saved to properties, change them towithUnsafePointerorwithUnsafeMutablePointerclosure.

  • What: Read the C string output using a new string initializer. Why it’s worth doing:String.init(unsafeUninitializedCapacity:initializingUTF8With:)You can let C functions write directly to string storage, avoiding temporary buffers. How ​​to start: First call the C API to query the length, then write the buffer in the initializer closure, and return the number of UTF-8 bytes after removing the NUL end.

Comments

GitHub Issues · utterances