[Rust] unsafe

8 minute read

컴파일 타임에 강제되는 메모리 안전성을 해제하고 안전하지 않은 코드를 작성할 수 있다.

큰 힘에는 큰 책임이 따른다.

안전하지 않은 코드를 작성하기 위해 unsafe 키워드를 사용한다.

안전하지 않은 러스트는 아래 4가지 행동이 가능하다.

  • 로우 포인터 역참조하기
  • 안전하지 않은 함수나 메서드 호출하기
  • 가변 정적 변수 접근과 수정하기
  • 안전하지 않은 트레잇 구현하기

unsafe 내의 코드가 무저건 위험한 것은 아니다.

unsafe는 그 안의 코드가 올바른 방법으로 메모리에 접근하겠다는 것을 명시하는 것이다.

안전하지 않은 코드를 안전한 API로 추상화할 수도 있다.

로우 포인터 역참조하기

로우 포인터는 참조자 처럼 불변이나 가변 타입을 가진다.

불변은 *const T로 가변은 *mut T라고 쓴다.

여기서 애스터리스크 *는 역참조가 아니라 타입명의 일부다.

로우 포인터의 불변은 포인터가 역참조된 후에 직접 대입될 수 없음을 의미한다.

로우 포인터의 성질

  • 로우 포인터는 빌림 규칙을 무시할 수 있어서 포인터가 불변과 가변 성질을 다 갖거나 다수의 가변 포인터가 존재할 수 있다.
  • 로우 포인터가 가리키는 메모리가 유효한지 알 수 없다.
  • 널이 될 수 있다.
  • 메모리 정리가 자동으로 되지 않는다.

안정성을 포기하고 성능을 향상하거나 다른 언어나 하드웨어와 상호작용을 할 수 있다.

아래 코드는 참조자에서 불변, 가변 로우 포인터를 만든다.

let mut num = 5;

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

위 코드는 유효한 참조자에서 로우 포인터를 만들어서, 로우 포인터가 유효함을 알 수 있다.

반면, 다음과 같이 임의의 위치를 가리켜서 유효한지 알 수 없는 로우 포인터를 만들 수 있다.

let address = 0x012345usize;
let r = address as *const i32;

안전한 코드 내에서는 로우 포인터 생성은 가능해도, 로우 포인터를 역참조해서 해당 포인터가 가리키는 데이터는 읽을 수 없다.

다음 코드는 unsafe 블록 내에서 로우 포인터의 값을 사용한다.

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);
}

위에 처럼 동일한 메모리를 가리키는 로우 포인터를 여러개 만들 수 있기 때문에, 데이터 레이스를 유의해야 한다.

안전하지 않은 함수나 메소드 호출하기

안전하지 않은 함수는 앞에 unsafe가 붙어 있다.

이 함수들은 다음과 같이 항상 unsafe 블록 내에서 호출되어야 한다.

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

함수 본문는 unsafe 블록이므로, 본문 내에서 안전하지 않은 함수 사용 시 unsafe 블록을 추가할 필요 없다.

안전하지 않은 코드 상에 안전한 추상화 생성하기

함수가 unsafe 코드를 가지고 있어도 안전할 수 있다.

슬라이스를 쪼개서 둘로 만드는 split_at_mut 메서드는 다음과 같이 사용한다.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

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

러스트를 안전하게만 사용하면 이 메서드를 구현할 수 없다.

split_at_muti32 타입만 지원하는 함수로 구현해본다.

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

    assert!(mid <= len);

    (&mut slice[..mid],
     &mut slice[mid..])
}

컴파일 시 cannot borrow `*slice` as mutable more than once at a time 에러가 발생한다.

슬라이스의 서로 다른 부분을 빌리고 있지만, 러스트는 같은 슬라이스를 두 번 빌리고 있다고 생각한다.

unsafe 블록과 함수, 로우 포인터를 사용해서 함수를 동작하게 만들 수 있다.

use std::slice;

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.offset(mid as isize), len - mid))
    }
}

변수 ptr*mut i32 타입 로우 포인터를 갖는다.

slice::from_raw_parts_mut 함수는 ptr로 시작하는 mid 길이의 슬라이스를 생성한다.

assert!(mid <= len);unsafe 블록 내의 모든 로우 포인터들이 슬라이스 안의 데이터를 가리킴을 보장한다.

split_at_mut 함수는 안전하기에 unsafe 표시를 하지 않아도 된다.

extern 함수로 외부 코드 호출하기

extern 키워드로 Foreign Function Interface를 생성하고 사용할 수 있다.

다음 코드는 C 표준 라이브러리의 abs 함수를 가져올 수 있도록 설정한다.

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

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

"C" 부분은 해당 외부 함수의 ABI(application binary interface)를 정의해서 어셈블리 수준에서 어떻게 함수를 호출할 지 정한다.

"C" ABI는 C 언어의 ABI를 준수한다.

[참고] 다른 언어에서 러스트 함수 호출하기

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

#[no_mangle] 어노테이션으로 컴파일 최적화 과정에서 함수의 이름이 변경되는 것을 방지한다.

extern 키워드 뒤에 ABI를 명시하고 fn 키워드 뒤에 함수 시그내처를 작성한다.

다른 언어에 함수를 노출하기 위한 extern 사용 시 unsafe가 필요 없다.

가변 정적 변수 접근하고 수정하기

러스트에서 전역 변수를 사용할 수는 있다.

하지만, 전역 변수는 소유권 문제를 일으킬 뿐만 아니라, 다수의 스레드에서 동일한 전역 변수 접근 시 데이터 레이스 문제가 발생할 수 있다.

러스트는 전역 변수를 정적 변수라고 부른다.

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

상수와 다른 정적 변수의 특징

  • 변수 이름은 반드시 대문자 스네이크 케이스이다.
  • 타입이 반드시 명시돼야 한다.
  • 정적 변수의 참조자는 'static 라이프타임을 갖는다.
  • 값이 메모리 내의 고정된 주소값을 갖고 접근하는 값의 주소가 항상 동일하다. (상수는 사용될 때마다 복사됨)
  • 가변으로 선언할 수 있다.

다음 코드는 가변 정적 변수를 선언 후 접근하고 수정한다.

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

static mut 키워드로 가변 정적 변수를 선언한다.

unsafe 블록 내에서만 가변 정적 변수를 읽거나 쓸 수 있다.

멀티 스레드 환경에서는 가변 정적 변수보다 스레드 세이프한 스마트 포인터를 사용하는 편이 낫다.

안전하지 않은 트레잇 구현하기

trait 키워드 앞에 unsafe를 붙여서 안전하지 않은 트레잇을 만든다.

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

내부 타입들이 모두 SyncSend하다면 자동으로 SyncSend 트레잇이 구현된다.

SyncSend하지 않은 타입에 SyncSend 표시를 하려면 unsafe를 사용해야 한다.

Categories:

Updated: