안전하지 않은 블록에서 수행할 수 있는 두 번째 유형의 작업은 안전하지 않은 함수를 호출하는 것입니다.
안전하지 않은 함수와 메서드는 일반 함수나 메서드와 똑같아 보이지만, 정의 앞부분에 unsafe가 추가됩니다.
이 컨텍스트에서 unsafe 키워드는 이 함수를 호출할 때 지켜야 할 요구사항이 있음을 나타내는데, 이는 러스트가 이러한 요구사항을 충족했다고 보장할 수 없기 때문입니다.
안전하지 않은 함수를 unsafe 블록 내에서 호출한다는 것은 이 함수의 문서를 읽었으며 함수의 계약서를 준수할 책임이 있음을 의미합니다.
아래는 본문에서 아무 일도 하지 않는 dangerous라는 이름의 안전하지 않은
함수입니다.
unsafe fn dangerous() {} unsafe { dangerous(); }
dangerous 함수는 반드시 분리된 unsafe 블록 내에서 호출되어야 합니다.
unsafe 블록 없이 dangerous를 호출하려고 시도하면 다음과 같은 에러가 발생합니다.
$ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)error[E0133]: call to unsafe function is unsafe and requires unsafe function or block --> src/main.rs:4:5 |4 | dangerous(); | ^^^^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behaviorFor more information about this error, try `rustc --explain E0133`.error: could not compile `unsafe-example` due to previous error
unsafe 블록을 사용하는 것은 해당 함수의 설명서를 읽었고, 해당
함수를 올바르게 사용하는 방법을 이해했으며, 해당 함수의 계약서를
이행하고 있음을 확인한다고 러스트에게 단언하는 꼴입니다.
안전하지 않은 함수의 본문은 사실상 unsafe 블록이므로, 안전하지 않은 함수
내에서 안전하지 않은 연산을 수행하기 위해 또 unsafe 블록을 추가할
필요는 없습니다.
그런 다음 매개변수로 주어진 인덱스가 슬라이스의 길이보다 작거나 같은지 확인하는 것으로 슬라이스 내에 있음을 단언합니다.
이 단언문은 슬라이스를 분할하기 위해 길이보다 큰 인덱스를 전달하면 해당 인덱스를 사용하기 전에 함수가 패닉을 일으키리란 것을 의미합니다.
그다음 두 개의 가변 슬라이스를 튜플 안에 넣어 반환합니다.
하나는 원본 슬라이스의 시작부터 mid 인덱스까지의 슬라이스이고, 다른 하나는 mid 인덱스부터 원본 슬라이스의 끝까지의 슬라이스입니다.
예제 18-5의 코드를 컴파일 시도하면 아래와 같은 에러가 발생합니다.
$ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)error[E0499]: cannot borrow `*values` as mutable more than once at a time --> src/main.rs:6:31 |1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { | - let's call the lifetime of this reference `'1`...6 | (&mut values[..mid], &mut values[mid..]) | --------------------------^^^^^^-------- | | | | | | | second mutable borrow occurs here | | first mutable borrow occurs here | returning this value requires that `*values` is borrowed for `'1`For more information about this error, try `rustc --explain E0499`.error: could not compile `unsafe-example` due to previous error
러스트의 대여 검사기는 슬라이스의 서로 다른 부분을 빌린다는 것을 이해할 수 없습니다; 러스트는 그저 동일한 슬라이스를 두 번 빌린다는 것만 알고 있습니다.
슬라이스의 서로 다른 부분을 빌리는 것은 두 슬라이스가 겹치지 않기 때문에 기본적으로 괜찮지만, 러스트는 이를 알아차릴 만큼 똑똑하지 못합니다.
우리는 코드가 괜찮다는 것을 알지만 러스트는 그렇지 않다면, 안전하지 않은 코드를 이용할 시간입니다.
예제 18-6은 split_at_mut의 구현체를 작동시키기 위해 unsafe 블록,
원시 포인터, 그리고 안전하지 않은 함수 호출을 사용하는 방법을 보여줍니다.
예제 18-6: split_at_mut 함수의 구현체에서
안전하지 않은 코드 사용하기
use std::slice;fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) }}
3장의 ‘슬라이스 타입’ 절에서 슬라이스란 데이터를 가리키는 포인터와 슬라이스의 길이인 것을 기억하세요.
len 메서드를 사용하여 슬라이스의 길이를 얻고 as_mut_ptr 메서드를 사용하여 슬라이스의 원시 포인터를 얻었습니다.
이번 경우에는 i32 값에 대한 가변 슬라이스이므로, as_mut_ptr는 *mut i32 타입의 원시 포인터를 반환하며, 이 포인터는 ptr 변수에 저장됩니다.
mid 인덱스가 슬라이스 내에 있다는 단언은 유지합니다.
그다음 안전하지 않은 코드에 도달합니다.
slice::from_raw_parts_mut 함수는 원시 포인터와 길이를 받아 슬라이스를 생성합니다.
이 함수를 사용하여 ptr에서 시작하고 mid 길이의 아이템을 가진 슬라이스를 만듭니다.
그런 다음 ptr에서 mid를 인수로 add 메서드를 호출하여 mid에서 시작하는 원시 포인터를 가져오고, 이 포인터와 mid 이후의 나머지 아이템 개수를 길이로 사용하여 슬라이스를 생성합니다.
slice::from_raw_parts_mut 함수는 원시 포인터를 얻어와서 이 포인터의 유효성을 신뢰해야 하기 때문에 안전하지 않습니다.
원시 포인터에 대한 add 메서드도 오프셋 위치가 유효한 포인터임을 신뢰해야 하기 때문에 안전하지 않습니다.
따라서 slice::from_raw_parts_mut와 add를 호출할 수 있도록 주위에 unsafe 블록을 넣어야 했습니다.
코드를 살펴보고 mid가 len보다 작거나 같아야 한다는 단언문을 추가하면 unsafe 블록 내에서 사용되는 모든 원시 포인터가 슬라이스 내의 데이터에 대한 유효한 포인터가 될 것임을 알 수 있습니다.
이는 unsafe에 대한 받아들일 만하고 적절한 사용입니다.
결과인 split_at_mut 함수를 unsafe로 표시할 필요는 없으며, 안전한 러스트에서 이 함수를 호출할 수 있다는 점에 유의하세요.
이 함수는 접근할 수 있는 데이터에서 유효한 포인터만 생성하기 때문에, unsafe 코드를 안전한 방식으로 사용하는 함수의 구현을 통해 안전하지 않은 코드에 대한 안전한 추상화를 만든 것이 되었습니다.
반면 예제 18-7의 slice::from_raw_parts_mut 사용은 슬라이스가 사용될 때 크래시가 발생하기 쉽습니다.
이 코드는 임의의 메모리 위치를 가져와서 10,000개의 아이템을 가진 슬라이스를 생성합니다.
예제 18-7: 임의의 메모리 위치로부터 슬라이스
생성하기
use std::slice; let address = 0x01234usize; let r = address as *mut i32; let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
이 임의 위치에 있는 메모리를 소유하지 않고, 이 코드가 생성하는 슬라이스가 유효한 i32 값들을 포함하고 있는지에 대한 보장이 없습니다.
values를 마치 유효한 슬라이스인 것처럼 사용하려고 하면 정의되지 않은 동작이 발생합니다.
또한 extern을 사용하여 다른 언어에서 러스트 함수를 호출할 수 있는
인터페이스를 만들 수도 있습니다. 전체 extern 블록을 생성하는 대신,
extern 키워드를 추가하고 관련 함수에 대한 fn 키워드 바로 앞에
사용할 ABI를 지정합니다. 또한 #[no_mangle] 어노테이션을 추가하여
러스트 컴파일러가 이 함수의 이름을 맹글링하지 않도록 지시해야 합니다.
맹글링(mangling) 이란 우리가 함수에 부여한 이름을 컴파일러가 컴파일
과정의 다른 부분에서 사용할 수 있도록 더 많은 정보를 포함하지만 사람이
읽기엔 불편한 다른 이름으로 변경하는 것을 말합니다. 모든 프로그래밍 언어
컴파일러는 이름을 조금씩 다르게 변경하므로, 다른 언어에서 러스트 함수의 이름을
불리도록 하려면 러스트 컴파일러의 이름 맹글링 기능을 비활성화해야 합니다.
다음 예제에서는 call_from_c 함수를 공유 라이브러리로 컴파일하고 C에서
링크한 후, C 코드에서 함수에 접근할 수 있도록 합니다.
#[no_mangle]pub extern <b>C</b> fn call_from_c() { println!(<b>Just called a Rust function from C!</b>);}