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::newRegex::find에 비해 시간이 오래 걸리므로 패턴을 한 번만 new로 생성하신 뒤 여러 번 find를 하는 용도로 사용하는 것이 바람직합니다.

현재 정규식 기능은 매칭된 문자열 범위 탐색만 가능하지만, BOJ 런타임에 존재하는 libpcre.so를 통해 고급 정규식 기능을 구현 중에 있습니다.

오류 정정과 내용 제보는 언제든 환영입니다. 읽어주셔서 감사합니다.