Skip to content

Commit 7eab91d

Browse files
committed
[SE-0458] Don't require declarations with unsafe signatures to be @unsafe
This extra step of requiring @unsafe is mostly busywork. Rather, we infer @unsafe from unsafe types, and can later suppress false positives for actually-safe declarations by marking them @safe.
1 parent 8b38f17 commit 7eab91d

File tree

1 file changed

+44
-50
lines changed

1 file changed

+44
-50
lines changed

proposals/0458-strict-memory-safety.md

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,13 @@ The `UnsafeBufferPointer` type will be marked with `@unsafe` in the Standard lib
5959
public struct UnsafeBufferPointer<Element> { ... }
6060
```
6161

62-
This indicates that use of this type is not memory-safe. Any declaration that has `UnsafeBufferPointer` as part of its type is also unsafe, and would produce a warning under this strict safety mode, e.g.,
62+
This indicates that use of this type is not memory-safe. Any declaration that has `UnsafeBufferPointer` as part of its type is implicitly `@unsafe`.
6363

6464
```swift
65-
// warning: reference to unsafe generic struct 'UnsafePointer'
65+
// note: implicitly @unsafe due to the use of the unsafe type UnsafePointer
6666
func sumIntBuffer(_ address: UnsafePointer<Int>?, _ count: Int) -> Int { ... }
6767
```
6868

69-
This warning can be suppressed by marking the function as `@unsafe`:
70-
71-
```swift
72-
@unsafe
73-
func sumIntBuffer(_ address: UnsafePointer<Int>?, _ count: Int, _ start: Int) -> Int { ... }
74-
```
75-
7669
Users of this function that enable strict safety checking will see warnings when using it. For example:
7770

7871
```swift
@@ -94,14 +87,13 @@ To suppress these warnings, the expressions involving unsafe code must be marked
9487
extension Array<Int> {
9588
func sum() -> Int {
9689
withUnsafeBufferPointer { buffer in
97-
// warning: use of unsafe function 'sumIntBuffer' and unsafe property 'baseAddress'
9890
unsafe sumIntBuffer(buffer.baseAddress, buffer.count, 0)
9991
}
10092
}
10193
}
10294
```
10395

104-
The `unsafe` keyword here indicates the presence of unsafe code within that expression. As with `try` and `await`, it can cover multiple sources of unsafety within that expression: the call to `sumIntBuffer` is unsafe, as is the use of `buffer` and `buffer.baseAddress`, yet they are all covered by one `unsafe`.
96+
The `unsafe` keyword here indicates the presence of unsafe code within that expression. As with `try` and `await`, it can cover multiple sources of unsafety within that expression: the call to `sumIntBuffer` is unsafe, as is the use of `buffer` and `buffer.baseAddress`, yet they are all covered by one `unsafe`. It is up to the authors of an unsafe API to document the conditions under which it is safe to use that API, and the user of that API to ensure that those conditions are met within the `unsafe` expression.
10597

10698
Unlike `try`, `unsafe` doesn't propagate outward: we do *not* require that the `sum` function be marked `@unsafe` just because it has unsafe code in it. Similarly, the call to `withUnsafeBufferPointer` doesn't have to be marked as `unsafe` just because it has a closure that is unsafe. The programmer may choose to indicate that `sum` is unsafe, but the assumption is that unsafe behavior is properly encapsulated when using `unsafe` if the signature doesn't contain any unsafe types.
10799

@@ -123,7 +115,8 @@ The operation `UnsafeMutableBufferPointer.swapAt` swaps the values at the given
123115

124116
```swift
125117
extension UnsafeMutableBufferPointer {
126-
@unsafe public func swapAt(_ i: Index, _ j: Index) {
118+
/*implicitly @unsafe*/
119+
public func swapAt(_ i: Index, _ j: Index) {
127120
guard i != j else { return }
128121
precondition(i >= 0 && j >= 0)
129122
precondition(i < endIndex && j < endIndex)
@@ -141,7 +134,7 @@ The `swapAt` implementation uses a mix of safe and unsafe code. The code marked
141134
* Performing pointer arithmetic on `baseAddress`: Swift cannot reason about the lifetime of that underlying pointer, nor whether the resulting pointer is still within the bounds of the allocation.
142135
* Moving and initializing the actual elements. The elements need to already be initialized.
143136

144-
The code itself has preconditions to ensure that the provided indices aren't out of bounds before performing the pointer arithmetic. However, there are other safety properties that cannot be checked with preconditions: that the memory associated with the pointer has been properly initialized, has a lifetime that spans the whole call, and is not being used simultaneously by any other part of the code. These safety properties are something that must be established by the *caller* of `swapAt`. Therefore, `swapAt` is marked `@unsafe` because callers of it need to reason about these properties.
137+
The code itself has preconditions to ensure that the provided indices aren't out of bounds before performing the pointer arithmetic. However, there are other safety properties that cannot be checked with preconditions: that the memory associated with the pointer has been properly initialized, has a lifetime that spans the whole call, and is not being used simultaneously by any other part of the code. These safety properties are something that must be established by the *caller* of `swapAt`. Therefore, `swapAt` is considered `@unsafe` because callers of it need to reason about these properties. Because it's part of an unsafe type, it is implicitly `@unsafe`, although the author could choose to mark it as `@unsafe` explicitly.
145138

146139
### Incremental adoption
147140

@@ -150,17 +143,17 @@ The strict memory safety checking proposed here enforces a subset of Swift. Code
150143
* Strict concurrency checking, the focus of the Swift 6 language mode, required major changes to the type system, including the propagation of `Sendable` and the understanding of what code must be run on the main actor. These are global properties that don't permit local reasoning, or even local fixes, making the interoperability problem particularly hard. In contrast, strict safety checking has little or no effect on the type system, and unsafety can be encapsulated with `unsafe` expressions or ignored by a module that doesn't enable the checking.
151144
* [Embedded Swift](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md) is a subset of Swift that works without a runtime. Like the proposed strictly-safe subset, code written in Embedded Swift will also work as regular Swift. Embedded Swift and the strict safety checking proposed here are orthogonal and can be composed to (for example) ensure that firmware written in Swift has no runtime and provides the best memory-safety guarantees.
152145

153-
A Swift module that adopts strict safety checking can address all of the resulting diagnostics by applying the `@unsafe` attribute and `unsafe` expression in the appropriate places, without changing any other code. This application of attributes can be automated through Fix-Its, making it possible to enable the mode and silence all diagnostics automatically. It would then be left to the programmer to audit those places that have used `unsafe` to encapsulate unsafe behavior, to ensure that they are indeed safe. Note that the strict safety checking does not by itself make the code more memory-safety: rather, it identifies those constructs that aren't safe, encouraging the use of safe alternatives and making it easier to audit for unsafe behavior.
146+
A Swift module that adopts strict safety checking can address all of the resulting diagnostics by applying the `@unsafe` attribute and `unsafe` expression in the appropriate places, without changing any other code. This application of attributes can be automated through Fix-Its, making it possible to enable the mode and silence all diagnostics automatically. It would then be left to the programmer to audit those places that have used `unsafe` to encapsulate unsafe behavior, to ensure that they are indeed safe. Note that the strict safety checking does not by itself make the code more memory-safe: rather, it identifies those constructs that aren't safe, encouraging the use of safe alternatives and making it easier to audit for unsafe behavior.
154147

155148
The introduction of the `@unsafe` attribute on a declaration has no effect on clients compiled without strict safety enabled. For clients that have enabled strict safety, they will start diagnosing uses of the newly-`@unsafe` API. However, these diagnostics are warnings with their own diagnostic group, so a client can ensure that they do not prevent the client from building. Therefore, modules can adopt strict safety checking at their own pace (or not) and clients of those modules are never "stuck" having to make major changes in response.
156149

157150
## Detailed design
158151

159-
This section describes how the primary proposed constructs, the `@unsafe` attribute and `unsafe` expression, interact with the strict type checking mode, and enumerates the places in the language, standard library, and compiler that introduce non-memory-safe code.
152+
This section describes how the primary proposed constructs, the `@unsafe` attribute, `@safe` attribute, and `unsafe` expression, interact with the strict memory safety mode, and enumerates the places in the language, standard library, and compiler that introduce non-memory-safe code.
160153

161154
### `@unsafe` attribute
162155

163-
The `@unsafe` attribute can be applied to any declaration to indicate that use of that declaration can undermine memory safety. Any use of a declaration marked `@unsafe` will result in a warning. The closest analogue in the language today is `@available(*, deprecated)`, which has effectively no impact on the type system, yet any use of a deprecated declaration results in a warning.
156+
The `@unsafe` attribute can be applied to any declaration to indicate that use of that declaration can undermine memory safety. Any use of an `@unsafe` declaration that isn't acknowledged in the source code (e.g., via the `unsafe` expression) will result in a warning. The closest analogue in the language today is `@available(*, deprecated)`, which has effectively no impact on the type system, yet any use of a deprecated declaration results in a warning.
164157

165158
When a type is marked `@unsafe`, a declaration that uses that type in its interface is implicitly `@unsafe`. For example, consider a program containing three separate modules:
166159

@@ -170,7 +163,9 @@ When a type is marked `@unsafe`, a declaration that uses that type in its interf
170163
public struct DataWrapper {
171164
var buffer: UnsafeBufferPointer<UInt8>
172165

173-
public func checksum() -> Int32 { ... }
166+
public func checksum() -> Int32 {
167+
...
168+
}
174169
}
175170

176171
// Module B
@@ -191,12 +186,37 @@ extension MyType {
191186
}
192187
```
193188

194-
Module `A` defines a type, `DataWrapper`, that is `@unsafe`. It can be compiled with or without strict safety checking enabled, and is fine either way.
189+
Module `A` defines a type, `DataWrapper` that stores an `UnsafeBufferPointer`, and is marked `@unsafe`. It can be compiled with or without strict safety checking enabled. If compiled with
195190

196-
Module `B` uses the `DataWrapper` type. If compiled without strict safety checking, there will be no diagnostics about memory safety. If compiled with strict safety checking, there will be a diagnostic about `wrapper` using an `@unsafe` type (`DataWrapper`) in its interface. This diagnostic can be ignored, but ideally the `wrapper` property will be marked as `@unsafe` (silencing the warning).
191+
Module `B` uses the `DataWrapper` type. If compiled without strict safety checking, there will be no diagnostics about memory safety. The `wrapper` property is implicitly `@unsafe`, although the author may mark it as `@unsafe` explicitly if they choose to. If compiled with strict safety checking, the code will produce a warning because the storage of `MyType` involves an unsafe type (`DataWrapper`). This warning can be suppressed by either marking `MyType` as `@unsafe` (propagating unsafety) or `@safe` (`MyType` is safe to use). If left alone, it will be assumed to be safe.
197192

198193
If module `C` enables strict memory safety, the use of `MyType` is considered safe (since it was not marked `@unsafe` and doesn't involve unsafe types in its interface). However, the access to `wrapper` will result in a diagnostic, because the type of `wrapper` involves an `@unsafe` type. This diagnostic will occur whether or not `wrapper` has been explicitly marked `@unsafe`.
199194

195+
### `unsafe` expression
196+
197+
Any time there is executable code that makes use of unsafe constructs, the compiler will produce a diagnostic that indicates the use of those unsafe constructs unless it is within an `unsafe` expression. As noted in the previous section, use of the `wrapper` property (which is implicitly `@unsafe` due to the use of unsafe types) without an enclosing `unsafe` will produce a warning::
198+
199+
```swift
200+
extension MyType {
201+
public func checksum() -> Int32 {}
202+
// warning: use of property `wrapper` with unsafe type `DataWrapper`
203+
return wrapper.checksum()
204+
}
205+
}
206+
```
207+
208+
To suppress the warning, introduce an `unsafe` prior to the expression involving the unsafe code:
209+
210+
```swift
211+
extension MyType {
212+
public func checksum() -> Int32 {}
213+
return unsafe wrapper.checksum()
214+
}
215+
}
216+
```
217+
218+
The `unsafe` expression is much like `try` and `await`, in that it acknowledges that unsafe constructs (`wrapper`) are used within the subexpression but otherwise does not change the type. Unlike `try` and `await`, which require the enclosing context to handle throwing or be asynchronous, respectively, the `unsafe` expression does not imply any requirements about the enclosing block: it is purely a marker to indicate the presence of unsafe code, silencing a diagnostic.
219+
200220
There are a few exemptions to the rule that any unsafe constructs within the signature require the declaration to be `@unsafe`:
201221

202222
* Local variables involving unsafe types do not need to be marked with `@unsafe`. For example, the local variable `base` will have unsafe type `UnsafePointer?`, but does not require `@unsafe` because every *use* of this local variable will need to be marked using the `unsafe` expression described in the next section.
@@ -218,7 +238,7 @@ There are a few exemptions to the rule that any unsafe constructs within the sig
218238

219239
### `@safe` attribute
220240

221-
Like the `@unsafe` attribute, the `@safe` attribute is used on declarations whose signatures involve unsafe types. However, the `@safe` attribute means that the declaration is considered safe to use even though its signature includes unsafe types. For example, marking `UnsafeBufferPointer` as `@unsafe` means that all operations involving an unsafe buffer pointer are implicitly considered `@unsafe`. The `@safe` attribute can be used to say that those particular operations are actually safe. For example, any operation involving buffer indices or count are safe, because they don't touch the memory itself. This can be indicated by marking these APIs `@safe`:
241+
The `@safe` attribute is used on declarations whose signatures involve unsafe types but are, nonetheless, safe to use. For example, marking `UnsafeBufferPointer` as `@unsafe` means that all operations involving an unsafe buffer pointer are implicitly considered `@unsafe`. The `@safe` attribute can be used to say that those certain operations are actually safe. For example, any operation involving buffer indices or count are safe, because they don't touch the memory itself. This can be indicated by marking these APIs `@safe`:
222242

223243
```swift
224244
extension UnsafeBufferPointer {
@@ -264,35 +284,6 @@ extension Array<Int> {
264284
}
265285
```
266286

267-
### `unsafe` expression
268-
269-
When a declaration is marked `@unsafe`, it is free to use any other unsafe types as part of its interface. Any time there is executable code that makes use of unsafe constructs, that code must be within an `unsafe` expression or it will receive a diagnostic about uses of unsafe code. In the example from the previous section, `wrapper` can be marked as `@unsafe` to suppress diagnostics by explicitly propagating unsafety to their clients:
270-
271-
```swift
272-
// Module B
273-
import A
274-
275-
public struct MyType {
276-
@unsafe public var wrapper: DataWrapper
277-
}
278-
```
279-
280-
However, the use of the `wrapper` property in module `C` will produce a diagnostic unless it is part of an `unsafe` expression, like this:
281-
282-
```swift
283-
// Module C
284-
import A
285-
import B
286-
287-
extension MyType {
288-
public func checksum() -> Int32 {}
289-
return unsafe wrapper.checksum()
290-
}
291-
}
292-
```
293-
294-
The `unsafe` expression is much like `try` and `await`, in that it acknowledges that unsafe constructs (`wrapper`) are used within the subexpression but otherwise does not change the type. Unlike `try` and `await`, which require the enclosing context to handle throwing or be asynchronous, respectively, the `unsafe` expression does not imply any requirements about the enclosing block: it is purely a marker to indicate the presence of unsafe code, silencing a diagnostic.
295-
296287
### Unsafe language constructs
297288

298289
The following language constructs are always considered to be unsafe:
@@ -740,7 +731,10 @@ We could introduce an optional `message` argument to the `@unsafe` attribute, wh
740731

741732
## Revision history
742733

743-
* **Revision 2 (following first review)**
734+
* **Revision 3 (following second review eextension)**
735+
* Do not require declarations with unsafe types in their signature to be marked `@unsafe`; it is implied. They may be marked `@safe` to indicate that they are actually safe.
736+
737+
* **Revision 2 (following first review extension)**
744738
* Specified that variables of unsafe type passed in to uses of `@safe` declarations (e.g., calls, property accesses) are not diagnosed as themselves being unsafe. This makes means that expressions like `unsafeBufferePointer.count` will be considered safe.
745739
* Require types whose storage involves an unsafe type or conformance to be marked as `@safe` or `@unsafe`, much like other declarations that have unsafe types or conformances in their signature.
746740
* Add an Alternatives Considered section on prohibiting unsafe conformances and overrides.

0 commit comments

Comments
 (0)