#32 unsafe

Pt J·2020년 9월 22일
1

[完] Rust Programming

목록 보기
35/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

우리는 지금까지 Rust의 보편적인 기능들을 살펴보았다.
이번 시간부터 두 시간 동안에는 Rust의 고급 기능들을 살펴보도록 하겠다.

Unsafe Rust

컴파일 시간에 검증할 수 없는 안전성에 대해 실행 시간에 안전성이 확보된다면
그것은 컴파일러에게 검증을 맡길 수 없다.
바꿔 말하면
컴파일러 입장에서는 안전성을 보장할 수 없지만
개발자가 그것의 안전성을 보장할 수 있는 경우에는
개발자 본인이 안전성을 책임질테니 컴파일러는 걱정하지 말라고 하고 작성할 수 있다.
이런 식으로 사용되는 것을 우리는 Unsafe Rust라고 부른다.
여기서 unsafe라는 것은 컴파일러 입장에서 unsafe라는 것이다.

정적 분석을 통해서 우리는 많은 정보를 얻어내고 버그를 잡을 수 있지만
이것이 모든 버그를 다 잡아줄 수 있는 것은 아니다.
안전성을 확신할 수 없는 상태에서
안전하지 않은 것을 안전하다고 허용하는 것보다는
안전한 것을 안전하지 않다고 거부하는 것이 더 안전하기 때문에
Rust는 기본적으로 안전성을 확신할 수 없으면 거부한다.

그런데 이런 식으로는 활용도가 떨어질 수 있다.
정적 분석 상으로 안전한 것 외에는 만들 수 없게 된다.
Rust는 이런 제약을 해소하기 위해
개발자가 실행 시간에 안전이 확보된다고 확신한다면
그를 믿고 컴파일하도록 unsafe를 지원하는 것이다.

그리고 때로는 unsafe 코드의 사용이 필수불가결할 수도 있다.
특히 시스템 소프트웨어 영역으로 가면 더욱 그렇다.

Unsafe Superpowers

unsafe 코드는 unsafe 키워드가 붙은 코드블록 안에서 사용할 수 있다.
그리고 이 안에서만 허용되는, 원래라면 절대 허용되지 않는 기능들이 존재하는데
우리는 그것들을 unsafe superpowers라고 부른다.
unsafe superpowers로는 다음과 같은 것들이 있다.

  • 원시 포인터 역참조
    Dereference a raw pointer
  • unsafe 함수 또는 메서드 호출
    Call an unsafe function or method
  • 가변 정적 변수 접근 또는 수정
    Access or modify a mutable static variable
  • 안전하지 않은 트레이트 구현
    Implement an unsafe trait
  • 공용체의 필드 접근
    Access fields of unions

unsafe 코드블록은 기존에 있던 안전성 검사를 무시하는 것이 아니라
그것들은 검사하며 위와 같은, 컴파일 시간에 보장할 수 없는 것을 허용해주는 것임을 유의하자.
unsafe 코드를 사용하여 개발하다가 메모리 안전성에 관한 오류가 발생하면
우리는 그것이 unsafe 코드블록에서 발생하였음을 알 수 있다.
이는 버그를 보다 쉽게 발견할 수 있게 한다.
특히 unsafe 코드블록을 최대한 작게 유지할수록 그 효과가 크다.
이를 위해서는 안전한 코드와 그렇지 않은 코드를 잘 분리해내야 한다.

unsafe 코드를 안전한 API로 제공하면
그 API를 사용하는 쪽에서는 unsafe 키워드를 사용하지 않아도 된다.
따라서 unsafe 코드는 안전한 추상화 속에 작성한 뒤 API를 제공하는 것이 좋다.

원시 포인터 역참조

unsafe superpowers 중 하나인 원시 포인터 역참조에 대해 알아보자.
unsafe 코드에서는 원시 포인터라는 자료형을 제공한다.
그것은 가변성 유무에 따라 불변이라면 *const T, 가변이라면 *mut T와 같이 표기된다.
이 때, *도 자료형 이름의 일부라는 것을 유의하자.
여기서 가변성은 참조 대상이 변할 수 있는지가 아니라
역참조 후 값을 대입할 수 있는지 여부를 의미한다.

원시 포인터는 다음과 같은 특징이 있다.

  • 불변 참조와 가변 참조를 동시에 갖거나 여러 개의 가변 참조를 갖는 등 대여 규칙을 무시하는 게 혀용된다.
    Are allowed to ignore the borrowing rules by having both immutable and mutable pointers or multiple mutable pointers to the same location
  • 유효한 메모리를 가리키고 있다는 보장이 되지 않는다.
    Aren’t guaranteed to point to valid memory
  • null을 허용한다.
    Are allowed to be null
  • 메모리를 자동으로 해제해주는 기능이 구현되어 있지 않다
    Don’t implement any automatic cleanup

이렇게 안전성이 보장되던 부분을 희생함으로써 더 강력한 성능을 얻고자 하는 것이 unsafe다.

원시 포인터의 생성 자체는 unsafe 코드블록 밖에서도 이루어질 수 있지만
그것에 대한 역참조는 반드시 unsafe 코드블록 안에서 이루어져야 한다.

원시 포인터를 사용하는 간단한 예제를 살펴보자.

peter@hp-laptop:~/rust-practice$ mkdir chapter19
peter@hp-laptop:~/rust-practice$ cd chapter19
peter@hp-laptop:~/rust-practice/chapter19$ cargo new raw_pointer
     Created binary (application) `raw_pointer` package
peter@hp-laptop:~/rust-practice/chapter19$ cd raw_pointer/
peter@hp-laptop:~/rust-practice/chapter19/raw_pointer$ vi src/main.rs

src/main.rs

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}
peter@hp-laptop:~/rust-practice/chapter19/raw_pointer$ cargo run
   Compiling raw_pointer v0.1.0 (/home/peter/rust-practice/chapter19/raw_pointer)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/raw_pointer`
r1 is: 5
r2 is: 5
peter@hp-laptop:~/rust-practice/chapter19/raw_pointer$ 

일반 참조는 as 키워드를 통해 원시 포인터로 형변환 할 수 있다.
위 예제에서 우리는 역참조 연산자 *를 통해 각각의 원시 포인터를 역참조했다.
r1r2는 모두 num의 메모리 주소라는 같은 값을 가지고 있음을 유의하자.
여기서 가변 원시 포인터 r2를 통해 값을 변경할 경우
데이터 레이스가 발생할 수도 있다는 점에 유의하자.

사실 이렇게 역참조하는 것 자체는 위험하지 않다.
다만, 유효하지 않은 값을 참조하려고 하는 것과 같은 상황이 발생할 수 있다는 것이 문제다.
아무튼 위험성을 어느 정도 수반하고 있다는 건데,
그렇다면 이런 위험성을 가진 원시 포인터는 왜 필요한 걸까?
그것에 대해서는 잠시 후에 다른 superpower에 대해 알아보면서 이야기하도록 하겠다.

unsafe 함수 또는 메서드 호출

어떤 함수나 메서드 전체 범위에 unsafe 코드블록을 사용해야 할 경우
그 시그니처 앞에 unsafe를 붙여 함수나 메서드 전체를 unsafe 코드로 만들 수 있다.
이 때, 이러한 unsafe 함수 또는 메서드를 호출하기 위해서는
그것을 호출하는 코드를 unsafe 코드블록 안에 넣어야 한다.

unsafe 함수 또는 메서드를 사용할 땐
관련 문서를 읽고 사용 방법을 익힌 후 그것의 요구사항을 맞추고 unsafe 코드블록에 넣어야 한다.

안전한 추상화

함수나 메서드에서 부분적으로만 unsafe에 해당할 경우
그것 전체를 unsafe로 만들기보다는 unsafe 코드블록의 크기를 최소화하여
내부적으로는 unsafe 코드를 포함하는 안전한 추상화를 한 API로 만들 수 있다면
이렇게 하는 게 더 좋다.

unsafe 코드블록을 포함하는 안전한 함수를 작성해보자.
어떤 정수 벡터를 인자로 받아 그것을 둘로 나누어
가변 문자열 슬라이스로 반환하는 함수를 작성하겠다.
코드의 단순화를 위해 i32 자료형만으로 한정한다.
몇 번째 아이템을 기준으로 나눌지는 인자로 전달받으며, 그것이 전체 크기보다 클 경우 패닉을 띄운다.
분할된 앞부분과 뒷부분의 문자열 슬라이드를 Rust는 같은 문자열의 가변 참조로 본다는 점에서
이를 안전한 코드로 구현할 수 없다는 점을 떠올리며 다음 예제를 살펴보도록 하자.

peter@hp-laptop:~/rust-practice/chapter19/raw_pointer$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new safe_abstraction
     Created binary (application) `safe_abstraction` package
peter@hp-laptop:~/rust-practice/chapter19$ cd safe_abstraction/
peter@hp-laptop:~/rust-practice/chapter19/safe_abstraction$ vi src/main.rs

src/main.rs

use std::slice;

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let (a, b) = split_at_mut(&mut v, 3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

as_mut_ptr 메서드는 *mut T, 여기서는 *mut i32 원시 포인터를 반환한다.
unsafe 코드블록에 사용된 slice::from_raw_parts_mut 함수는
인자로 전달된 원시 포인터가 유효하다고 믿고 사용하는 unsafe 함수다.
add 메서드 또한 그것을 호출하는 녀석이 유효하다고 믿고 있다.
우리는 유효하지 않은 경우에 대한 오류처리를 assert 메서드로 이미 한 상태이므로
우리는 unsafe 코드를 안전하게 사용할 수 있고,
이걸 호출하는 입장에서는 안전한 코드처럼 사용할 수 있다.
split_at_mut 함수는 unsafe 코드를 포함하지만 unsafe 함수가 아니므로
안전한 추상화가 제공된다고 할 수 있다.

extern 함수 사용

unsafe 함수 또는 메서드뿐만 아니라
extern 함수를 호출할 때도 unsafe 코드블럭이 필요하다.
extern 키워드는 다른 언어로 작성된 코드를 호출할 때 사용된다.
다른 언어로 작성된 코드는 Rust의 자체적인 규칙과 검사가 적용되지 않기 때문에
안전성을 보장할 수 없어 unsafe 키워드가 붙어야 한다.

다음은 Rust에서 C언어의 abs 함수를 사용하는 예제다.
extern 키워드 옆에는 ABIapplication binary interface가 오며
C언어를 나타내는 ABI는 "C"다.
해당 코드블록 내에는 사용할 외부 함수의 시그니처가 Rust식으로 정의되어 있다.

peter@hp-laptop:~/rust-practice/chapter19/safe_abstraction$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new c_abs
     Created binary (application) `c_abs` package
peter@hp-laptop:~/rust-practice/chapter19$ cd c_abs/
peter@hp-laptop:~/rust-practice/chapter19/c_abs$ vi src/main.rs

src/main.rs

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

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
peter@hp-laptop:~/rust-practice/chapter19/c_abs$ cargo run
   Compiling c_abs v0.1.0 (/home/peter/rust-practice/chapter19/c_abs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/c_abs`
Absolute value of -3 according to C: 3
peter@hp-laptop:~/rust-practice/chapter19/c_abs$ 

여담: extern 키워드
extern 키워드는 다른 언어에서 구현한 코드를 Rust에서 사용할 때뿐만 아니라
반대의 경우에도 사용된다.
이 경우에는 함수를 선언하는 fn 키워드 앞에 extern 키워드와 ABI를 명시한다.
그리고 컴파일 과정에서 함수의 이름이 변경되지 않도록
#[no_mangle] 애노테이션을 추가한다.
다음과 같이 작성할 수 있으며, 이 경우에는 unsafe 키워드가 필요하지 않다.

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

가변 정적 변수 접근 또는 수정

많은 언어들에 전역 변수라는 녀석이 존재하는데 우리는 아직 이 녀석을 언급한 적 없다.
전역 변수를 사용할 경우 소유권 문제가 있을 수 있고 데이터 레이스가 발생할 수 있다.
그렇다고 해서 Rust가 전역 변수를 지원하지 않는 것은 아니다.
Rust의 전역 변수는 정적 변수로, static 키워드와 함께 선언된다.
정적 변수는 모두 대문자로 이루어진 식별자를 가지고 있으며 (snake case)
수명은 자동으로 'static으로 지정된다.

불변 정적 변수는 그 수명도 명확하고 변경되지 않기 때문에 안전하게 사용할 수 있다.
하지만 가변 정적 변수의 경우 말이 다르다.
이것은 읽는 것과 쓰는 것 모두 unsafe 코드에 해당한다.

따라서 다음 예제와 같이 unsafe 코드블록에서 사용해야 한다.

peter@hp-laptop:~/rust-practice/chapter19/c_abs$ cd ..
peter@hp-laptop:~/rust-practice/chapter19$ cargo new mutable_static
     Created binary (application) `mutable_static` package
peter@hp-laptop:~/rust-practice/chapter19$ cd mutable_static/
peter@hp-laptop:~/rust-practice/chapter19/mutable_static$ vi src/main.rs

src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_counter(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}
fn main() {
    add_to_counter(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}
peter@hp-laptop:~/rust-practice/chapter19/mutable_static$ cargo run
   Compiling mutable_static v0.1.0 (/home/peter/rust-practice/chapter19/mutable_static)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/mutable_static`
COUNTER: 3
peter@hp-laptop:~/rust-practice/chapter19/mutable_static$ 

보다 더 안전한 코드를 작성하기 위해서는
여기에 멀티 쓰레드 환경을 위해 동시성에 대한 처리도 포함해줄 수 있다.

안전하지 않은 트레이트 구현

어떤 트레이트가 가진 메서드 중 하나라도 unsafe 메서드가 존재한다면
이 트레이트는 unsafe trait 키워드로 선언되어야 하며
그것의 메서드는 unsafe impl 코드블록에 작성되어야 한다.
그리고 이 트레이트를 구현하는 모든 자료형은 unsafe로 취급된다.

공용체의 필드 접근

공용체Union는 구조체와 비슷하게 생겼지만
그것이 가진 필드 중 한 순간에 하나만 유효한 자료형이다.
구조체의 필드가 선형적으로 존재하는 것과 달리 공용체는 그 시작점이 동일하다.
Rust의 공용체는 그 자체로 사용되기 보다 주로 C언어의 공용체와의 인터페이스 용도로 사용된다.
Rust에서는 공용체 인스턴스에 현재 저장된 것이 어떤 필드로 해석되어야 하는지 알 수 없어
그것에 접근하는 것은 unsafe하다고 판단한다.
따라서 이것은 unsafe 키워드 없이 사용할 수 없다.
공용체에 대해 더 알고 싶다면 공식문서를 참고하자.

unsafe 코드를 사용하게 될 경우
그것의 안전성을 정말 보장할 수 있는지 유의하여 사용하도록 하자.

이 포스트의 내용은 공식문서의 19장 1절 Unsafe Rust에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글