Rustでプラットフォーム依存の処理を書く

#[cfg(...)] Attribute

Rustでは #[cfg(...)] アトリビュートを使うことによって、OSやCPUに応じた条件コンパイルを行うことができる。

例えば、次のように書くことでコンパイルターゲットのOSに応じたhello()をビルドし、呼び出すことができる。

#[cfg(target_os = "windows")]
fn hello() { println!("Hello, I'm Windows!"); }

#[cfg(target_os = "linux")]
fn hello() { println!("Hello, I'm Linux!"); }

#[cfg(target_os = "macos")]
fn hello() { println!("hello, I'm MacOS."); }

fn main() {
    hello();
}

モジュールを分ける

各複数プラットフォーム向けの処理が小さい時には、前述のように1つのファイルの中にベタ書きでも問題ないが、コードが膨らんできた場合は各プラットフォーム毎にファイルを分けた方がいいだろう。

そこで、次のようなモジュール構成を考てみる。 nameモジュールを用意し、OS依存部分はnameモジュールの中に隠蔽することを目的とする。

Cargo.toml
src/
├── lib.rs
└── name
    ├── linux.rs
    ├── macos.rs
    ├── mod.rs
    └── windows.rs

name/${os_dependent}.rs

OS依存の処理を name/以下の linux.rs, macos.rs, windows.rsにそれぞれ書いていく。 各モジュールはOSの名前をStringで返すだけのname()関数を実装する。

name/linux.rs

pub fn name() -> String {
    "Linux".to_owned()
}

name/macos.rs

pub fn name() -> String {
    "MacOS".to_owned()
}

name/windows.rs

pub fn name() -> String {
    "Windows".to_owned()
}

name/mod.rs

各OS依存の処理を他のモジュールから扱えるようにするために name/mod.rs を記述する。

name/mod.rs

#[cfg(target_os = "linux")]
pub mod linux;
#[cfg(target_os = "linux")]
pub use self::linux::*;

#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
pub use self::macos::*;

#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
pub use self::windows::*;

ここでのmod.rsの役割は、コンパイル対象とするモジュールの切り替えと、他のモジュールへの再エキスポートだ。

コンパイル対象とするモジュールの切り替え

これは前述のように #[cfg(target_os = "linux")] を使ってコンパイル対象のモジュールを指定する。

#[cfg(target_os = "linux")]
pub mod linux;

他のモジュールへの再エクスポート

pub useによって再エクスポートすることによって、他のモジュールからnameのOS依存モジュールを意識せずに使えるようにしている。

#[cfg(target_os = "linux")]
pub use self::linux::*;

この記述がないと、nameモジュールのOS依存処理を使う時は常にモジュール名を明確に指定しなくてはならない。 これでは以下のように他のモジュールが常にOS依存を気にしたコードを書かなくてはならず、非常によろしくない。

mod name;
let os_name = name::linux::name();

pub useすることによって、OS依存モジュール名を省略して使用することができるようになり、他のモジュールからOS依存を気にせずに使用できるようになる。

// mod.rsでpub use self::linux::*;
mod name;
let os_name = name::name();

lib.rs

最後に、今まで定義したnameモジュールを使ったlib.rsを次に記述する。

lib.rs

mod name;
pub fn greeting() {
    println!("Hello, I'm {}.", name::name());
}
// -> Hello, I'm ${YOUR_OS}.

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(target_os = "linux")]
    #[test]
    fn test_linux() {
        assert_eq!("Linux", name::name());
    }

    #[cfg(target_os = "macos")]
    #[test]
    fn test_linux() {
        assert_eq!("MacOS", name::name());
    }

    #[cfg(target_os = "windows")]
    #[test]
    fn test_linux() {
        assert_eq!("Windows", name::name());
    }
}

Linux上でテストを実行すると、name()は"Linux"の文字列を返してくれているのが確認できる。

$ cargo test
   Compiling hello v0.1.0 (/home/ryo/code/sandbox/rust/scraps/target_os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.75s
     Running target/debug/deps/hello-69d1a57a65d70c5b

running 1 test
test tests::test_linux ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests hello

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

参考