Rust의 기본 라이브러리에서 제공하지 않지만, BOJ 환경에서 추가로 사용할 수 있는 기능을 소개합니다.
Rust에는 extern
키워드를 사용한 FFI (Foreign Function Interface) 기능이 존재합니다.
이를 통해서 시스템에 존재하는 공유 라이브러리를 불러와 그 안의 함수(혹은 변수)를 가져와 사용할 수 있습니다.
#[link(name = "my_library")]
extern "C" {
fn some_function() -> bool;
}
위 코드는 rustc로 컴파일 시에 -lmy_library
옵션을 링커에게 전달하고, 시스템에 libmy_library.so
가 있다면 컴파일이 성공합니다.
또한 런타임에 libmy_library.so
에서 some_function()
함수를 불러오게 됩니다.
아래 몇 가지 예시를 보시죠.
1. PRNG(Pseudo-Random Number Generator)
C 표준 라이브러리에는 time()
, rand()
, srand()
가 존재하여 PRNG를 사용할 수 있지만,
Rust의 std에는 PRNG와 관련한 기능이 없습니다. (rand
crate를 대신 이용)
extern
을 이용하여 쉽게 PRNG를 만들 수 있습니다.
mod random {
pub fn init() {
unsafe { srand(time(std::ptr::null()) as u32) };
}
pub fn gen() -> i32 {
unsafe { rand() }
}
#[link(name = "c")]
extern "C" {
fn time(timer: *const u64) -> u64;
fn srand(seed: u32);
fn rand() -> i32;
}
}
random::init();
let my_favorite = random::gen();
사실 정확히 말해서 Rust의 std에는 랜덤 관련 함수가 있습니다.
fn rand64() -> u64 {
let mut ret = 0;
unsafe { std::arch::x86_64::_rdrand64_step(&mut ret) };
ret
}
let my_favorite = rand64();
이는 x86-64(과 x86) 아키텍처의 rdrand 플래그가 있는 CPU에서 사용 가능한 함수로, 16비트, 32비트, 64비트 하드웨어 랜덤 값을 생성할 수 있습니다.
속도가 많이 빠르진 않으므로, _rdrand_step
으로 시드를 초기화한 rand
나 다른 PRNG를 이용하시는 것이 좋습니다.
여담으로, 랜덤 값을 구간 $[0, N)$으로 바꿀 때 보통 rand() % N
을 사용하는데, 이보다 빠른 방법이 있습니다.
fn uniform_map(rand: u32, n: u32) -> u32 {
((rand as u64 * n as u64) >> 32) as u32
}
rand()와 같은 정도의 공평성을 가지면서 나머지 연산을 피해 속도가 더 빠릅니다.
2. 기타 수학 함수
Rust의 f64
는 많은 기능
을 제공하지만,
C 라이브러리 수학 함수 (libm)에만 존재하는 기능도 있습니다.
수학 함수들의 목록은 여기에서 보실 수 있으며,
대표적으로 lgamma
의 사용법을 보여드리겠습니다.
#[link(name = "m")]
extern "C" {
fn lgamma(x: f64) -> f64;
}
let log_factorial_2000 = unsafe { lgamma(2001) };
다른 수학 함수들도 같은 방법으로 extern
하여 사용하실 수 있습니다.
3. 정규식 매칭
libc에는 정규식 문자열 매칭을 위한 헤더 regex.h
가 존재합니다.
그리고 우리는 libc를 불러올 수 있으므로, 정규식 문자열 매칭을 Rust에서도 사용할 수 있습니다.
위 헤더를 이용하여 간단히 래퍼를 짜 보았습니다. (https://snippets.kiwiyou.dev/regex 에도 코드가 있습니다.)
struct Regexp([u64; 8]);
impl Regexp {
fn new(pattern: &str, case_insensitive: bool) -> Self {
let flag = if case_insensitive { 3 } else { 1 };
let mut tmp = pattern.as_bytes().to_vec();
tmp.push(0);
let mut v = [0; 8];
unsafe { regcomp(v.as_mut_ptr(), tmp.as_ptr(), flag) };
Self(v)
}
fn find(&self, text: &str) -> Option<(usize, usize)> {
let mut text = text.as_bytes().to_vec();
text.push(0);
let mut m = [0; 2];
let result = unsafe { regexec(self.0.as_ptr(), text.as_ptr(), 1, m.as_mut_ptr(), 0) };
if result == 0 {
Some((m[0] as usize, m[1] as usize))
} else {
None
}
}
}
#[link(name = "c")]
extern "C" {
fn regcomp(preg: *mut u64, pattern: *const u8, flags: i32);
fn regexec(preg: *const u64, s: *const u8, n_expr: usize, m: *mut i32, flags: i32) -> i32;
}
사용 방법은 아래와 같습니다.
let pattern = "[A-Za-z]+";
let mut regex = Regex::new(pattern, false);
let text = "0123ThisIsMatching_NotMatching27156";
assert_eq!(regex.find(text), Some((4, 18)));
Regex::new
는 Regex::find
에 비해 시간이 오래 걸리므로 패턴을 한 번만 new
로 생성하신 뒤
여러 번 find
를 하는 용도로 사용하는 것이 바람직합니다.
현재 정규식 기능은 매칭된 문자열 범위 탐색만 가능하지만,
BOJ 런타임에 존재하는 libpcre.so
를 통해 고급 정규식 기능을 구현 중에 있습니다.
오류 정정과 내용 제보는 언제든 환영입니다. 읽어주셔서 감사합니다.