Rust から ImageMagick 呼び出してみる

Rustがかなりかっこいいのですこし触っています。

Rust はまるで、めちゃめちゃ易しくなったc++かのようです。メモリリークやその他の危険が扱いやすくなったし、シンタックスも今風だ。関数型言語由来のパターンマッチやOptional/Eitherのエラー処理などすげーセンス良い機能が搭載された一方、メモリのようすが透明でなんでもできてどこでもリンクできるパワーはc++並みです。

Rustは、c/c++みたく、明示的に指定しないかぎりすべての変数はスタックに実体があるので、参照やヒープを使うときにこそ記号めいたものを書くという世界観です。これは、僕が普段さわっている、オブジェクトが基本ヒープの参照で、ランタイムがGCしてくれるってな世界観とはまったく逆なので、その辺はやはりすこし難解にかんじます。でも慣れると楽しいです。

Rustはサーバ用途としても強そうだし、ネイティブなプラットフォームでつかうライブラリ用途とかにもよさそうです。

FFI

今回は、RustからImageMagickのCのAPIを呼び出す行為をしてみました。

Rustには、Cのライブラリを呼び出す方法が提供されています。

Rustには入門向けのドキュメントがあって、とってもおもしろいです。

https://doc.rust-lang.org/book/

そしてこれを日本語に訳されている方々がおり、大変ありがたいです。m( )m

FFIについてはここに説明がありました。

https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/ffi.html

Rust から Cのライブラリをリンクするには、rustc コンパイラ-l とか -L とかcコンパイラのノリのオプションをそのまま渡すこともできますが、通常rustプロジェクトのビルドにはCargoを使うことになると思います。

CargoはRustの依存ライブラリ管理&ビルドツールで、標準で添付されています。Cargoは使いやすいし、クロスコンパイルも簡単みたいです。こういうツールが標準であると敷居が下がって嬉しい。

MagickWand とリンクする

ImageMagick には C から標準的な画像処理を簡単にできるようにしたMagickWandというライブラリが提供されています。 これをrust から叩いてみようと思います。

まず、cargo の設定ファイルに、依存ライブラリを追加します。

[package]
name = "sample-rust"
version = "0.1.0"
authors = ["hadashiA <tanuki@example.com>"]
build = "build.rs"

[dependencies]
libc = "0.2.0"

[build-dependencies]
pkg-config = "0.3.0"
  • pkg-config は、その環境にあるcライブラリのパスを探すツール、pkg-configをRustから利用できるモジュールです。
  • libc は、cの型をRustで定義したものが入っているモジュールです。

次に、ビルドスクリプトをちょっとだけ追加します。ビルドする環境にあるcのライブラリの場所は、動的に探す必要があるためです。

プロジェクトルートに、build.rs をつくり、以下のようにします。

extern crate pkg_config;

fn main() {
    pkg_config::probe_library("MagickWand").unwrap();
}

これだけ書いておけば、CargoはMagickWandをみつけてくれます。

このコードがなにをしているかというと、標準出力にビルドオプションを吐くという動作をしています。 僕の環境では上記のコードは以下の文字を吐きました。

cargo:rustc-link-search=native=/usr/local/Cellar/imagemagick/6.9.3-0_1/lib
cargo:rustc-link-lib=MagickWand-6.Q16
cargo:rustc-link-lib=MagickCore-6.Q16

Cargo のドキュメントによると、build.rs の標準出力への書き込みによって、ビルドオプションをコントロールするという仕様のようです。 http://doc.crates.io/build-script.html

bindgen

これで c のライブラリはリンクできますが、Rust からCの関数を叩くには、cの関数宣言をRustの文法で書いてあげる必要があるみたいです。cの .h 拡張子のヘッダファイルみたいなノリです。というかむしろ、cのヘッダファイルからRustの関数定義を生成してくれる bindgen というツールがあります。これをつかってみようとおもいます。

$ cargo install bindgen

bindgen コマンドに、cコンパイラ的なノリのオプションを渡してあげると、指定したcのヘッダファイルをrustコードに変換したものが生成されます。

$ bindgen -I /System/Library/Frameworks/Kernel.framework/Headers -I /usr/local/include -I /usr/local/Cellar/imagemagick/6.9.3-0_1/include/ImageMagick-6/ /usr/local/Cellar/imagemagick/6.9.3-0_1/include/ImageMagick-6/wand/MagickWand.h -o magickwand.rs

Macで実行していたら、stdarg.h がない とか色々エラーがでてしまったんですが、適当にオプションに追加したりしたら動いたみたいです。

生成されたコードはそのままでも動いたのですが、なぜか9000行以上あったりわかりずらかったので、今回は勉強がてら、使いたいインターフェイスの定義だけあらためて手で書いてみました。

extern crate libc;

use libc::*;

pub struct MagickWand {}

pub type MagickBooleanType = c_uint;
pub static MagickFalse: c_uint = 0;
pub static MagickTrue: c_uint = 1;

#[repr(u32)]
pub enum ExceptionType {
    UndefinedException = 0,
    WarningException = 300,
    TypeWarning = 305,
    OptionWarning = 310,
    DelegateWarning = 315,
    MissingDelegateWarning = 320,
    CorruptImageWarning = 325,
    FileOpenWarning = 330,
    BlobWarning = 335,
    StreamWarning = 340,
    CacheWarning = 345,
    CoderWarning = 350,
    FilterWarning = 352,
    ModuleWarning = 355,
    DrawWarning = 360,
    ImageWarning = 365,
    WandWarning = 370,
    RandomWarning = 375,
    XServerWarning = 380,
    MonitorWarning = 385,
    RegistryWarning = 390,
    ConfigureWarning = 395,
    PolicyWarning = 399,
    ErrorException = 400,
    TypeError = 405,
    OptionError = 410,
    DelegateError = 415,
    MissingDelegateError = 420,
    CorruptImageError = 425,
    FileOpenError = 430,
    BlobError = 435,
    StreamError = 440,
    CacheError = 445,
    CoderError = 450,
    FilterError = 452,
    ModuleError = 455,
    DrawError = 460,
    ImageError = 465,
    WandError = 470,
    RandomError = 475,
    XServerError = 480,
    MonitorError = 485,
    RegistryError = 490,
    ConfigureError = 495,
    PolicyError = 499,
    FatalErrorException = 700,
    TypeFatalError = 705,
    OptionFatalError = 710,
    DelegateFatalError = 715,
    MissingDelegateFatalError = 720,
    CorruptImageFatalError = 725,
    FileOpenFatalError = 730,
    BlobFatalError = 735,
    StreamFatalError = 740,
    CacheFatalError = 745,
    CoderFatalError = 750,
    FilterFatalError = 752,
    ModuleFatalError = 755,
    DrawFatalError = 760,
    ImageFatalError = 765,
    WandFatalError = 770,
    RandomFatalError = 775,
    XServerFatalError = 780,
    MonitorFatalError = 785,
    RegistryFatalError = 790,
    ConfigureFatalError = 795,
    PolicyFatalError = 799,
}

#[repr(u32)]
pub enum FilterType {
    UndefinedFilter = 0,
    PointFilter = 1,
    BoxFilter = 2,
    TriangleFilter = 3,
    HermiteFilter = 4,
    HanningFilter = 5,
    HammingFilter = 6,
    BlackmanFilter = 7,
    GaussianFilter = 8,
    QuadraticFilter = 9,
    CubicFilter = 10,
    CatromFilter = 11,
    MitchellFilter = 12,
    JincFilter = 13,
    SincFilter = 14,
    SincFastFilter = 15,
    KaiserFilter = 16,
    WelshFilter = 17,
    ParzenFilter = 18,
    BohmanFilter = 19,
    BartlettFilter = 20,
    LagrangeFilter = 21,
    LanczosFilter = 22,
    LanczosSharpFilter = 23,
    Lanczos2Filter = 24,
    Lanczos2SharpFilter = 25,
    RobidouxFilter = 26,
    RobidouxSharpFilter = 27,
    CosineFilter = 28,
    SplineFilter = 29,
    LanczosRadiusFilter = 30,
    SentinelFilter = 31,
}

extern {
    pub fn MagickWandGenesis();

    pub fn NewMagickWand() -> *mut MagickWand;
    pub fn DestroyMagickWand(wand: *mut MagickWand) -> *mut MagickWand;
    
    // Iteraor

    pub fn MagickGetIteratorIndex(wand: *mut MagickWand) -> ssize_t;
    pub fn MagickSetIteratorIndex(wand: *mut MagickWand, i: ssize_t) -> MagickBooleanType;

    pub fn MagickGetException(wand: *const MagickWand,
                              severity: *mut ExceptionType) -> *mut c_char;
    pub fn MagickReadImageBlob(wand: *mut MagickWand,
                               blob: *const c_void,
                               size: size_t) -> MagickBooleanType;
    pub fn MagickResetIterator(wand: *mut MagickWand);
    pub fn MagickNextImage(wand: *mut MagickWand) -> MagickBooleanType;
    pub fn MagickResizeImage(wand: *mut MagickWand,
                             columns: size_t,
                             rows: size_t,
                             filter: FilterType,
                             arg5: c_double);
    pub fn MagickSetImageFormat(wand: *mut MagickWand, format: *const c_char);
    pub fn MagickGetImageBlob(wand: *mut MagickWand, size: *mut size_t) -> *mut c_uchar;
}

こんなかんじで、実装のない関数宣言を c のヘッダからrust に翻訳したものをつくってあげます。 これの関数宣言を magickwand.rs として保存すると、 mod magickwand; で読み込むことができました。

extern crate libc;

mod magickwand;

use std::fs::File;
use std::io::{Read, Write};
use std::ffi::CString;
use libc::{size_t, c_void};
use magickwand::*;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 3 {
        println!("Usage: {} SRC DST", &args[0]);
        return
    }
    let src = &args[1];
    let dst = &args[2];
    
    let mut blob: Vec<u8> = Vec::new();
    let bytes = File::open(src)
        .and_then(|mut file| file.read_to_end(&mut blob))
        .unwrap();
        
    unsafe {
        // MagickWandGenesis();
        let wand = NewMagickWand();
        let status = MagickReadImageBlob(wand,
                                                     blob.as_ptr() as *const c_void,
                                                     bytes as size_t);
        if status == MagickFalse {
            let mut severity = ExceptionType::UndefinedException;
            let description = MagickGetException(wand, &mut severity);
            let description = CString::from_raw(description);
            println!("{}", description.to_str().unwrap());
            std::process::exit(0);
        }

        MagickSetIteratorIndex(wand, 0);
        MagickResizeImage(wand, 100, 100, FilterType::LanczosFilter, 1.0);
        MagickSetImageFormat(wand, CString::new("PNG").unwrap().as_ptr());

        let mut size: size_t = 0;
        let ptr = MagickGetImageBlob(wand, &mut size);
        let blob = Vec::from_raw_parts(ptr, size, size);

        File::create(dst)
            .and_then(|mut file| file.write_all(blob.as_slice()))
            .unwrap();
    }
}

ただcの関数を呼んでるだけでの、rustらしさのないコードになってしまいましたが、これでコンパイルとおりました。

$ cargo run src.png dest.png

実行してみたところ、MacikWandの活躍によって画像がリサイズされます。

RAII

しかし、上記のように生のc関数をそのまま呼ぶのは、rustの世界ではけっこうつかいずらいです。

unsafeブロックで囲まないといけないし、MacickWandが管理しているメモリをつかいおわったら手で解放する必要があるので、いまいちrustの恩恵に預かれないし、あと見栄えもよくないです。

そこでMagickWandをもうすこしrustでラップしてみます。

rustの値は明示的に指定しない限りスタックに置かれていて、メモリ解放するタイミングが明確なので、c++で頻出のRAIIイディオムでリソース管理することができます。

値型のコンストラクでリソースを取得し、デストラクタで華麗にリソース解放するあれです。

pub struct Wand {
    ptr: *mut magickwand::MagickWand,
}

impl Wand {
    pub fn new() -> Wand {
        let ptr = unsafe {
            magickwand::NewMagickWand()
        };
        Wand { ptr: ptr }
    }
}

impl Drop for Wand {
    fn drop(&mut self) {
        unsafe {
            magickwand::DestroyMagickWand(self.ptr);
        }
    }
}

このようにDropトレイトを実装しておくと、dropメソッドに書いた処理が、変数解放時に呼ばれます。

// たとえば、、
{
  let wand = Wand::new();
} // このスコープを抜けるときに DestroyMagickWand() が呼ばれる