The introduction

Continue to learn Swift documents, from the previous chapter: automatic reference counting, we learned Swift automatic reference counting content, mainly iOS through ARC automatic memory management to achieve memory allocation, use reference counting to determine whether objects need to be destroyed; There are also situations that generate circular references and solutions to circular references. Now, let’s learn about Swift memory security. Due to the long space, here is a section to record, next, let’s begin!

Memory safety

By default, Swift prevents unsafe behavior from occurring in your code. For example, Swift ensures that variables are initialized before use, memory is not accessed after release, and checks array indexes for out-of-bounds errors.

Swift also ensures that multiple accesses to the same memory area do not conflict by requiring code that modifies a location in memory to have exclusive access to that memory. Because Swift automatically manages memory, most of the time you don’t even have to think about accessing it. However, it is important to know where conflicts can occur so that you can avoid writing code that conflicts with memory access. If your code does contain conflicts, you will get a compile-time or run-time error.

1 Learn about memory access conflicts

Memory is accessed in your code when you perform operations such as setting the value of a variable or passing parameters to a function. For example, the following code contains both read and write access:

// A write access to the memory where one is stored. var one = 1 // A read access from the memory where one is stored. print("We're number \(one)!" )Copy the code

Memory access conflicts can occur when different parts of code try to access the same location in memory at the same time. Multiple simultaneous accesses to a location in memory can produce unpredictable or inconsistent behavior. In Swift, there are methods to modify values that span several lines of code, so that you can try to access values during the modification process.

You can find similar problems by thinking about how to update your budget on paper. Updating the budget requires two steps: first add the name and price of the item, and then change the total to reflect the current item in the list. Before and after the update, you can read any information from the budget and get the correct answer, as shown in the figure below.

When you add items to the budget, the budget is in a temporary, invalid state because the totals have not been updated to reflect the new additions. Reading the total amount while adding items gives you the wrong information.

This example also illustrates one of the challenges that can be encountered when fixing memory access conflicts: sometimes there are multiple ways to fix a conflict, but different answers are produced, and it is not always obvious which answer is the right one. In this case, depending on whether you want the original total or the updated total, $5 or $320 might be the right answer. Before fixing conflicting access, you must determine the purpose of the access.

Pay attention to

If you write concurrent or multithreaded code, conflicting memory access can be a common problem. However, the conflicting access discussed here can occur on a single thread and does not involve concurrent or multithreaded code. If there is conflicting access to memory in one thread, Swift guarantees errors at compile time or run time. For multithreaded code, use thread cleaners to help detect conflicting access between threads.

1.1 Memory Access Features

In the context of conflicting access, three characteristics of memory access need to be considered: whether the access is read or written, the duration of the access, and the location in the memory being accessed. In particular, a conflict occurs if you have two accesses that satisfy all of the following conditions:

  • At least one of them is write access.
  • They access the same location in memory.
  • Their durations overlap.

The difference between read and write access is usually obvious: write access changes location in memory, but read access does not. A location in memory refers to what is being accessed, such as a variable, constant, or attribute. The duration of memory access can be instantaneous or long-term.

Access is immediate if it is impossible for other code to run after the access begins but before the access ends. In essence, two instant accesses cannot occur at the same time. Most memory accesses are instantaneous. For example, all read and write access in the following code list is instantaneous:

func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
Copy the code

However, there are several ways to access memory, called long-term access, across the execution of other code. The difference between instant access and long access is that other code may be executed after long access begins but before it ends, which is called overlap. Long-term access can overlap with other long-term and immediate access.

Overlapped access occurs primarily in code that takes in-out arguments in functions and methods, or in mutable methods of structures. Specific types of Swift code that use long-term access are discussed in the following sections.

1.2 Access In-out Parameters Conflict

The function has long-term write access to all of its input and output parameters. Write access to in-out parameters begins after all non-in-out parameters are evaluated and continues for the entire duration of the function call. If there are multiple in-out parameters, write access starts in the same order as the parameters appear.

One consequence of this long write access is that you cannot access the original variable passed as in-out, even if scope rules and access controls allow it to access the original variable, leading to conflicts. Such as:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize
Copy the code

In the code above, stepSize is a global variable that is normally accessible from increment (:). However, read access to stepSize overlaps with write access to number. As shown in the figure below, number and stepSize both point to the same location in memory. Read and write access refers to the same memory, and they overlap, causing conflicts.

One way to resolve this conflict is to explicitly copy stepSize:

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
Copy the code

When creating a copyOfStepSize before calling increment (:), it is clear that the value of copyOfStepSize will increase by the current step. Read access ends before write access begins, so there is no conflict.

Another consequence of long write access to in-out parameters is that passing a single variable as an argument to multiple in-out parameters of the same function creates conflicts. Such as:

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore
Copy the code

The balance (: 🙂 function above modifies its two arguments to evenly distribute the total value between them. Calling it with playerScore and playerTwoScore as arguments doesn’t cause a conflict there are two write accesses that overlap in time, but they access different places in memory. In contrast, passing playerOnScore as the value of two parameters creates a conflict because it attempts to perform two write accesses simultaneously to the same location in memory.

Pay attention to

Because operators are functions, they also have long-term access to their in-out parameters. For example, if balance (:) was an operator function named <^>, writing playerOneScore<^>playerOneScore would result in the same conflict as balance (&playerOneScore, &playerScore).

Access self conflict in the method

A mutable method on a structure has write access to self during a method call. For example, consider a game where each player has one health that decreases when taking damage, and energy that decreases when using a special ability.

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}
Copy the code

In the restoreHealth () method above, write access to self starts at the beginning of the method and continues until the method returns. In this case, there is no other code in restoreHealth () that can overlap to access the properties of the Player instance. The shareHealth (with 🙂 method below takes another Player instance as an in-out parameter, creating the possibility of overlapping access.

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // OK
Copy the code

In the above example, calling the shareHealth (with 🙂 method lets Oscar’s player share the health with Maria’s player without causing a conflict. There is write access to Oscar during the method call because Oscar is the value of self in the mutable method, and write access to Maria for the same duration because Maria is passed as an in-out parameter. As shown in the figure below, they access different locations in memory. Even though the two write accesses overlap in time, they do not conflict.

However, if you pass Oscar as an argument to shareHealth (with:), a conflict occurs:

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
Copy the code

The mutable method needs write access to self for the duration of the method, and the in-out parameter needs write access to the teammate for the same duration. In this method, both self and teammate reference the same location in memory, as shown in the figure below. Two write accesses refer to the same memory, and they overlap, resulting in conflicts.

3 Access attribute conflict

Types such as structs, tuples, and enumerations are made up of constituent values, such as properties of a structure or elements of a tuple. Because these are value types, changing any part of the value changes the entire value, meaning that read or write access to one of the attributes requires read or write access to the entire value. For example, overlapping write access to tuple elements can cause conflicts:

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
Copy the code

In the above example, calling balance (:) on the elements of the tuple causes a conflict because of overlapping write access to playerInformation. Both playerInformation and playerInformation.health and playerInformation.energy are passed as in-out parameters, which means that balance (: 🙂 requires write access to them during function calls. In both cases, write access to a tuple element requires write access to the entire tuple. This means that there are two write access to playerInformation with overlapping durations, resulting in conflicts.

The following code shows the same error for override access to properties of structures stored in global variables.

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error
Copy the code

In practice, most access to structure attributes can be safely overlapped. For example, if the variable Holly in the above example is changed to a local variable instead of a global variable, the compiler can prove that overlapping access to the structure’s stored properties is safe:

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}
Copy the code

In the above example, Oscar’s health and energy are passed to balance (: 🙂 as two in-out parameters. The compiler can prove that memory security is preserved because the two stored properties do not interact in any way.

Restrictions on overlapping access to structure attributes are not always necessary to protect memory security. Memory security is an ideal guarantee, but exclusive access is a more stringent requirement than memory security, which means that some code preserves memory security even if it violates exclusive access to memory. Swift allows memory-safe code if the compiler can prove that non-exclusive access to memory is still secure. In particular, overlapping access to structure attributes can be proved to be secure if the following conditions are met:

  • You access only the storage properties of the instance, not the computed or class properties.
  • Structs are values of local variables, not global variables.
  • This structure is either not caught by any closures or is only caught by unmasked closures.

If the compiler cannot prove that access is secure, it will not allow access.

Swift – Memory Safety