표준 라이브러리는 타입, 프로토콜, 함수, 프로퍼티 등 다양한 구조를 제공
➡️ 이 중 몇몇은 Unsafe
로 표시됨
대부분 작업은 실행 전 입력을 검증하기 때문에 심각한 코딩 오류를 안정적으로 포착함
ex) Optional 강제 언래핑 - nil 값이면 런타임 오류가 발생하고 실행을 중지
- Safe = 가능한 모든 입력(요구 사항을 충족하지 않는 입력을 포함)에 대한 동작을 설명할 수 있다
- Unsafe = 일부 입력에서 정의되지 않는 동작을 보인다
let value: String? = nli
print(value.unsafelyUnwrapped) // ?!
Unsafe 인터페이스 사용을 통해 성취할 수 있는 것
- C 또는 Objective-C와의 상호 운용성 제공
- 런타임 성능 또는 프로그램 실행에 대한 세밀한 제어를 제공
unsafelyUnwrapped 경우 두 번째에 속함
safe code != no crashes
안전한 코드의 목표가 크래시를 없애는 것이 아님
실제로는 반대로 제약 조건이 벗어난 입력 시 안전한 API는 런타임 오류를 발생시켜 실행을 중지
-> 오류로 인해 생성된 crash report로 문제가 발생한 상황을 보고 디버깅하고 수정할 수 있음
Swift는
C 프로그래밍의 포인터와 거의 동일한 추상화인 강력한 unsafe 포인터 타입을 제공
Swift는 플랫 메모리 모델을 사용
메모리를 개별적으로 주소 지정 가능한 8비트 바이트의 선형 주소 공간으로 취급
런타임 시 주소 공간은 앱의 실행 상태를 반영하는 데이터로 드물게 채워짐
ex) 앱의 바이너리, 라이브러리와 프레임워크, 스택, 일부 함수의 매개변수 또는 로컬 및 임시 변수 등
실행이 계속되며 새 개체가 할당되고 스택이 변경되며 이전 항목이 파괴됨
Swift와 런타임은 이를 계속 추적하기 때문에 수동으로 메모리를 관리할 필요 없음
unsafe 포인터는 메모리를 효과적으로 관리하는 모든 하위 수준 작업을 제공하는 대신 위험을 감수
예를 들어 정수 값에 대한 스토리지를 동적으로 할당하면 스토리지 위치가 생성되고 이에 대한 직접적인 포인터가 제공됨
➡️ Xcode는 이러한 메모리 문제 파악을 위해 런타임 디버깅 도구인 Address Sanitizer를 제공
포인터가 그렇게 위험한데 왜 사용해?
=> C나 Objective-C와 같은 안전하지 않은 언어와의 상호 운용성 때문
C의 포인터들은 Swift의 unsafe pointer로 매핑됨
allocate
메서드를 통해 정수 값을 유지할 동적 버퍼를 만들기작업을 제어할 수는 있지만 모든 단계는 근본적으로 안전하지 않음
-> 적절한 시간에 수동으로 할당 해제하지 않으면 메모리 누수가 발생
이 중 하나라도 잘못되면 정의되지 않은 동작이 발생
잘 동작하지만 버퍼가 시작 주소로만 표시된다는 문제가 있음
버퍼를 (시작 주소, 길이) 쌍으로 모델링하여 코드의 명확성을 향상시키 수 있다
=> 버퍼의 경계를 쉽게 사용할 수 있으므로 범위를 벗어난 액세스를 쉽게 확인할 수 있음
⬆️ 이것이 표준 라이브러리가 네 가지 안전하지 않은 버퍼 포인터 타입을 제공하는 이유
- UnsafeBufferPointer<Element>
- UnsafeMutableBufferPointer<Element>
- UnsafeRawBufferPointer
- UnsafeMutableRawBufferPointer
개별 값에 대한 포인터가 아니라 메모리 영역으로 작업해야 할 때 유용
최적화되지 않은 디버그 빌드에서 이러한 버퍼 포인터는 첨자 작업을 통해 범위를 벗어난 액세스를 확인하여 안전성을 제공
Swift의 표준 연속 컬렉션은 편리한 unsafe 메서드를 통해 기본 스토리지 버퍼에 대한 임시 직접 접근을 제공하기 위해 버퍼 포인터를 사용
또한 개별 Swift 값에 대한 임시 포인터를 얻을 수 있으며, C 함수에 전달할 수 있음
이러한 방법으로 코드를 단순하고 unsafe한 작업을 작은 코드 섹션으로 격리할 수 있다
C 함수에 대한 포인터를 전달하는 필요성이 자주 발생해 Swift는 이에 대한 특수 구문을 제공
withUnsafeBufferPointer
를 생성함Swift에서 지원하는 변환 목록
inout
참조를 전달하여 변경 가능한 포인터를 얻을 수 있음inout
참조를 사용할 수 있음이 기능을 사용하면 복잡한 C 인터페이스도 호출 가능
예시 - 실행 중인 시스템에 대한 하위 수준 정보를 쿼리 또는 업데이트하는 Darwin 모듈에서 제공하는 C 함수
다음 6개의 매개변수가 제공
암시적 포인터 변환을 사용하면 모국어와 복잡성이 거의 비슷한 코드가 생성됨
실행 중인 프로세서 아키텍처의 캐시 라인 크기를 검색하는 함수를 만드는 예시
=> sysctl 문서는 하드위어 섹션의 CACHELINE
식별자에서 이 정보를 사용할 수 있음을 알려줌
var query = [CTL_HW, HW_CACHELINE]
=> 이 ID를 sysctl에 전달하기 위해 암시적 배열-포인터 변환 및 해당 개수에 대한 명시적 정수 변환을 사용
let r = sysctl(&query, CUnsignedInt(query.count), ...)
=> 검색하려는 정보가 C 정수 값이므로 로컬 정수 변수를 생성하고 inout-to-pointer
변환과 함께 세 번째 인수에 대한 임시 포인터를 생성
=> 이 함수는 캐시 라인의 크기를 이 포인터에서 시작하여 버퍼에 복사하고 원래의 0 값을 다른 정수로 덮어씀
var result: CInt = 0
let r = sysctl(&query, CUnsignedInt(query.count), &result...)
=> &resultSize
- 해당 정수 유형의 MemoryLayout에서 가져올 수 있는 버퍼의 크기에 대한 포인터
var resultSize = MemoryLayout<CInt>.size
let r = sysctl(&query, CUnsignedInt(query.count), &result, &resultSize..)
=> 현재 값을 검색하기만 하고 설정하지 않기 때문에 버퍼에 nil을 제공하고, 크기를 0으로 설정
let r = sysctl(&query, CUnsignedInt(query.count), &result, &resultSize, nil, 0)
=> 해당 코드가 실패하지 않음을 가정하지만 제공한 인수 중 실수한 경우 이 가정을 확인
precondition(r == 0, "Cannot query cache line size")
=> 호출이 C 정수 값에 있는 만큼의 바이트를 설정하기를 기대
precondition(query.count == MemoryLayout<CInt>.size)
=> 마지막으로 C 정수를 Swift Int로 변환하고 결과를 반환
return Int(result)
이렇게 명시적 클로저 기반 호출로 확장할 수도 있음 (취향차이)
어떻게 표현하든 생성된 포인터 값은 일시적이며 함수가 반환될 때 무효화된다는 것을 인식!
Swift 코드에서는 포인터를 덜 전달하는게 좋으므로 클로저 기반 API 사용을 선호
-> MutablePointer에 임시 포인터를 전달하면 초기화 호출에서 해당 값을 이스케이프
-> 결과 포인터 값에 접근은 의도되지 않은 동작
-> 기본 메모리 위치가 더 이상 존재하지 않거나 재사용되었을 수 있음
-> Swift 5.3 컴파일러는 이러한 경우를 감지해 경고를 생성
(또 다른 개선 사항)
Swift 표준 라이브러리가 기본 초기화되지 않은 저장소에 데이터를 직접 복사하여 Array 또는 String 값을 생성할 수 있는 새로운 초기화 제공
데이터를 준비하기 위해서만 임시 버퍼를 할당할 필요가 없음
"Error retrieving kern.version"
- 보고된 오류가 있는지 확인precondition
으로 호출 성공했는지 확인이 새로운 String 이니셜라이저를 사용하면 여기서 수동 메모리 관리가 필요하지 않음 👍
-> Swift String 인스턴스의 저장소가 될 버퍼에 직접 접근 가능
-> 수동으로 메모리 할당/해제 필요 없음
표준 라이브러리의 Unsafe API를 사용하여 까다로운 상호 운용성도 해결할 수 있다
Unsafe API를 효과적으로 사용하려면 기대치를 인식하고 항상 이를 충족하도록 주의해야 함
그렇지 않으면 정의되지 않은 동작을 얻게 될 것 (최소화해서 사용하면 이를 수행하기 쉽다)
가능할 때마다 더 안전한 대안 선택하기
둘 이상의 요소를 포함하는 메모리 영역 작업 시 포인터 보다는 UnsafeBufferPointer를 사용해 경례를 추적하는 것이 좋다
Xcode는 Address Sanitizer
를 포함하여 Unsafe API를 사용과 관련된 문제를 디버깅하는 데 도움을 주는 도구를 제공