개발자

'unsafe'를 활용한 러스트 프로그래밍 예제

Serdar Yegulalp | InfoWorld 2024.07.16
러스트의 인기가 높아지는 데는 빠른 속도, 가비지 수집이 필요 없는 메모리 안전성, 최고 수준의 툴 등 많은 이유가 있다. 또한 숙련된 프로그래머라면 속도와 직접적인 저수준 메모리 조작을 위해 안전 기능을 전부는 아니더라도 일부 선택적으로 해제할 수 있다.
 
"안전하지 않은" 러스트는 unsafe 키워드로 표시된 블록에 포함된 러스트 코드를 가리키는 용어다. unsafe 블록 내에서는 러스트의 안전 규칙 중 일부(전부는 아님)를 구부릴 수 있다(부러뜨리는 정도는 안 됨).
 
ⓒ Getty Images Bank
 
C 또는 다른 저수준 시스템 언어를 사용하다 러스트로 왔다면 저수준 조작을 위해 익숙한 패턴을 사용하고 싶을 때마다 unsafe를 사용하고 싶은 생각이 들 수 있다. 실제로 러스트에는 unsafe 코드를 통하지 않으면 할 수 없는 일이 몇 가지 있다. 그러나 많은 경우 필요한 기능이 러스트에 이미 있으므로 굳이 unsafe를 사용할 필요가 없다.
 
이 기사에서는 러스트에서 unsafe의 실제 용도, 할 수 있는 일과 없는 일, 현명하게 사용하는 방법을 알아본다.
 

'unsafe' 러스트로 할 수 있는 일

러스트에서 unsafe 키워드를 사용하면 코드 블록 또는 함수를 지정해서 러스트 언어의 특정 기능 하위 집합을 활성화할 수 있다. 러스트의 unsafe 블록 내에서 액세스할 수 있는 기능을 살펴보자.

원시 포인터
러스트의 원시 포인터는 가변 또는 불변 값을 참조할 수 있으며, 러스트의 참조보다는 C의 포인터 개념에 더 가깝다. 원시 포인터를 사용하면 작업 차용 방법에 적용되는 일부 규칙을 무시할 수 있다. 
 
  • 원시 포인터는 null 값이 될 수 있다.
  • 여러 개의 원시 가변 포인터가 메모리의 같은 공간을 가리킬 수 있다.
  • 또한 불변 포인터와 가변 포인터 모두 같은 메모리를 참조하는 데 사용할 수 있다.
  • 원시 포인터가 유효한 메모리 영역을 가리킨다고 보장할 필요가 없다.
 
원시 포인터는 하드웨어에 직접 액세스하거나(예를 들어 디바이스 드라이버), 다른 언어로 작성된 애플리케이션과 메모리의 원시 영역을 통해 통신해야 하는 경우 유용하다. 


외부 함수 호출
unsafe의 또 다른 일반적인 용도는 외부 함수 인터페이스, 즉 FFI를 통한 호출이다. 이러한 호출에서 받는 것이 러스트의 규칙을 따른다는 보장은 없으며 이러한 규칙에 부합하지 않는 것을 제공해야 할 가능성도 있다(예를 들어 원시 포인터).
 
러스트 설명서에서 발췌한 다음 예제를 보자.
 

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

extern "C" 블록을 통해 노출된 함수에 대한 모든 호출은 unsafe로 래핑해서, 이 함수로 보내는 것과 이를 통해 받은 것에 대해 적절한 책임을 져야 한다.

가변 정적 변수 변경하기
러스트에서 전역 또는 정적 변수는 고정 메모리 주소를 점유하므로 mutable로 설정할 수 있다. 다만 가변 정적 변수는 unsafe 블록 내에서만 수정할 수 있다.
 
가변 정적 변수를 변경하기 위해 unsafe가 필요한 가장 큰 이유는 데이터 경합이다. 동일한 가변 정적 변수가 여러 스레드에서 수정되도록 허용할 경우 예상치 못한 결과가 발생할 수 있다. 따라서 unsafe를 사용해서 변경할 수 있지만 그에 따르는 모든 데이터 경합 문제에 대한 책임은 러스트가 아닌 unsafe를 사용한 본인에게 있다. 일반적으로 러스트는 데이터 경합을 완전히 방지할 수는 없지만 unsafe 블록에서는 이 부분에 대해 두 배로 주의를 기울여야 한다.

unsafe 메서드와 트레잇 만들기
메서드(함수)는 unsafe fn <function_name>() 선언을 통해 unsafe로 설정할 수 있다. 이를 사용하면 해당 메서드에 대한 모든 호출도 unsafe 블록 내에서 수행되도록 보장할 수 있다.
 
예를 들어 인수로 원시 포인터가 필요한 함수가 있다면, 애초에 호출자가 호출이 수행되는 방식을 잘 살펴보도록 하는 것이 좋다. 함수 호출 경계에서 시작하고 끝나는 안전장치는 없다.
 
또한 트레잇과 해당 구현도 비슷한 구문을 사용해 unsafe로 선언할 수 있다. 트레잇의 경우 unsafe trait <trait_name>, 구현의 경우 unsafe impl <trait_name> 구문을 사용하면 된다.
 
다만 unsafe 메서드와 달리 unsafe 트레잇 구현은 unsafe 블록 내에서만 호출되어야 할 필요는 없다. 안전에 대한 부담은 구현을 호출하는 사람이 아니라 그 구현을 만드는 사람이 짊어진다.

유니온
러스트의 유니온(union)은 C의 유니온과 본질적으로 동일하다. 즉, 내용에 대해 가능한 유형 정의가 여러 개 있는 구조체다. 이와 같은 느슨한 동작은 C에서는 허용되지만 정확성과 안전성에 대한 더 엄격한 약속이 있는 러스트에서는 기본적으로 허용되지 않는다.
 
그러나 C 유니온에 매핑되는 러스트 구조를 만들어야 하는 경우가 간혹 있다. 예를 들어 유니온을 다루기 위해 C 라이브러리를 호출하는 경우가 그렇다. 이를 위해서는 unsafe를 사용해서 한 번에 하나의 특정 필드 정의에 액세스해야 한다.
 
다음은 종합 러스트(Comprehensive Rust) 가이드에 나온 예제다.
 

#[repr(C)]
union MyUnion {
    i: u8,
    b: bool,
}

fn main() {
    let u = MyUnion { i: 42 };
    println!("int: {}", unsafe { u.i });
    println!("bool: {}", unsafe { u.b }); // Undefined behavior!
}

유니온에 대한 각 액세스에서 unsafe를 사용해야 한다. 또한 한 번에 하나의 유니온 필드에만 액세스하려는 경우에도 차용 검사기는 유니온의 모든 필드를 차용할 것을 요구한다.
 
참고로 유니온에 대한 쓰기에는 이와 같은 제약이 없다. 러스트 시각에서는 추적이 필요한 항목에 쓰고 있는 것이 아니기 때문이다. 같은 이유로, let 문을 사용해서 유니온의 내용을 정의할 때 unsafe를 사용할 필요가 없다.
 

러스트 'unsafe'로 할 수 없는 일

위에서 다룬 4개의 주 용도 외에 unsafe가 달리 유용한 상황은 없다.
 
가장 큰 한계는 unsafe를 사용해서 차용 검사기를 피해갈 수 없다는 것이다. 차용은 다른 모든 부분과 마찬가지로 unsafe의 값에도 여전히 강제된다. 러스트에서 절대 불변의 원칙 중 하나는 차용과 참조의 작동 방식이며, unsafe 역시 이 둘을 바꿀 수는 없다.
 
좋은 예는 앞에서 설명했듯이 유니온에서 차용이 여전히 강제되는 방식에서 볼 수 있다. 이 주제에 관한 더 자세한 내용은 스티브 클랩닉의 블로그를 참조하라. unsafe는 러스트의 상위 집합이라고 생각하는 것이 좋다. 즉, 기존 기능을 그대로 둔 채 몇 가지 새로운 기능을 추가해준다.
 

unsafe 코드 모범 사례

unsafe는 다른 언어 기능과 마찬가지로 맞는 용도와 제약이 있으므로 신중하게, 주의를 기울여 사용해야 한다. 몇 가지 중요한 내용을 간추리면 다음과 같다. 

'unsafe'로 래핑하는 코드는 최대한 적게
unsafe 블록은 작을수록 좋다. 많은 경우 두 줄이 넘어가는 unsafe 블록은 만들 필요가 없다. unsafe가 필요한 코드가 실제로 얼만큼인지, 이 코드에 대해 경계와 인터페이스를 어떻게 강제할 것인지에 대해 생각해야 한다. 안전하지 않은 코드에는 안전한 인터페이스를 두는 것이 최선이다.
 
종종 인터페이스 자체가 안전하지 않아야 하는 경우가 있다. 앞서 언급했듯이 전체 함수를 예를 들어 unsafe fn <function_name>()과 같이 unsafe로 선언할 수 있다. 함수에 대한 인수에 unsafe가 필요하다면 해당 함수에 대한 모든 호출 역시 unsafe여야 한다.
 
그러나 전체적으로는 개별 unsafe 블록부터 시작하고 필요한 경우에만 함수로 승격하는 것이 최선이다.

정의되지 않은 동작에 유의
정의되지 않은 동작은 러스트에 존재하므로 unsafe에도 존재한다. 예를 들어 러스트의 기본 안전장치는 데이터 경합 또는 초기화되지 않은 메모리에서 읽기에 대한 보호 기능을 제공하지 않는다. unsafe 블록에서 정의되지 않은 동작이 발생할 수 있다는 점에 특별히 주의를 기울여야 한다.
 
피해야 할 정의되지 않은 동작 목록은 러스트 참조에 나와 있다. 상당히 많지만 이것도 전부는 아니다.

'unsafe'가 필요한 이유를 문서화
코드의 주석은 무엇을 하는 코드인지가 아니라 왜 하는지를 설명해야 한다는 말이 있다. 이는 unsafe 블록에 절대적으로 적용된다.
 
가능한 모든 경우 특정 시점에 unsafe가 왜 필요한지 명확히 문서화해야 한다. 그러면 다른 사람들이 unsafe를 사용한 근거를 알 수 있고, 경우에 따라서는 앞으로 unsafe가 필요하지 않도록 코드를 다시 쓸 방법도 떠올릴 수 있다.

러스토노미콘(Rustonomicon) 읽기
러스트의 문서는 최고 수준이다. 여기에는 "안전하지 않은 러스트 프로그램을 작성할 때 알아야 하는 모든 세부 사항"을 철저하게 문서화한, 책 한 권 분량의 문서인 러스토노미콘도 포함된다. 이 문서는 "안전하지 않은" 러스트와 "일반" 러스트가 상호작용하는 방법을 상세하게 다루므로 러스트에 익숙해진 다음에 보는 것이 좋다.
 
C 쪽에서 건너오는 사람이라면 위험한 방법으로 러스트 배우기(Learn Rust The Dangerous Way)라는 문서도 유용하다. 이 문서 모음은 러스트의 unsafe 기능을 사용하기 위한 모범 사례를 포함해 저수준 C 프로그래밍에 익숙한 사람이 이해할 수 있는 방식으로 러스트를 설명한다.
editor@itworld.co.kr 
Sponsored

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.