LoginSignup
9

More than 5 years have passed since last update.

RustのFFIを使ってみた(libc ftsによるwalkdir実装)

Last updated at Posted at 2016-03-17

はじめに

BSDやLinuxのglibcにはftsという関数群があります。これはディレクトリ内の再帰的なファイル列挙を高速に行う関数で、lsfindなどの実装に使われています。
Rustでこのようなファイル列挙を行う場合、std::fs::readdirで実装するか、既存のwalkdir(これも内部的にはreaddirを使っているようです)を使うことになりますが、ftsが速いらしいということで、FFIライブラリを作ってそれを使ったwalkdir相当の実装を行いました。
ちなみにglibcに依存するのでおそらくBSD(OSX含む)とLinux限定です。Windows対応が必要なら素直にwalkdirを使いましょう。

リンク

使い方

Cargo.toml
[dependencies]
fts = "*"

walkdirと同様にイテレータ実装しているのでforで回せます。

use std::path::Path;
use fts::walkdir::{WalkDir, WalkDirConf}

let path = Path::new( "." );
for p in WalkDir::new( WalkDirConf::new( path ) ) {
    println!( "{:?}", p.unwrap() );
}

フィルタも出来ます。

use std::path::Path;
use fts::walkdir::{WalkDir, WalkDirConf}

let path = Path::new( "." );
let iter = WalkDir::new( WalkDirConf::new( path ) ).into_iter().filter_map( |x| x.ok() );
for p in iter.filter( |x| x.file_type().is_file() ) {
    println!( "{:?}", p );
}

WalkDirConfはBuilderになっていて、以下のように設定できます。以下のconf1の場合、シンボリックリンクを辿り、かつリンク先が別デバイスであっても辿る設定になります。conf2metadataを取得しないオプションで、その分高速になります。

se std::path::Path;
use fts::walkdir::{WalkDir, WalkDirConf}

let path = Path::new( "." );
let conf1 = WalkDirConf::new( path ).follow_symlink().cross_device();
let conf2 = WalkDirConf::new( path ).no_metadata();

for p in WalkDir::new( conf1 ) {
    println!( "{:?}", p.unwrap() );
}
for p in WalkDir::new( conf2 ) {
    println!( "{:?}", p.unwrap() );
}

ベンチマーク

cargo benchの結果です。test fts_walkdirが今回実装したもの、test readdirreaddirを再帰で呼ぶだけの適当な実装、test walkdirwalkdirを使ったものになります。また、_metadataと付いているものは各ディレクトリエントリに対し、DirEntry::metadata()を呼んでいるバージョンになります。
walkdir比でだいたい2倍くらい速くなっています。

test fts_walkdir          ... bench: 315,114,126 ns/iter (+/- 8,478,709)
test fts_walkdir_metadata ... bench: 480,089,245 ns/iter (+/- 11,478,335)
test readdir              ... bench: 575,856,224 ns/iter (+/- 15,021,486)
test readdir_metadata     ... bench: 790,838,218 ns/iter (+/- 12,780,010)
test walkdir              ... bench: 688,884,058 ns/iter (+/- 8,023,838)
test walkdir_metadata     ... bench: 904,379,691 ns/iter (+/- 10,212,776)

解説

FFI関連の実装詳細を多少解説します。
基本はここに書いてあるので、それ以外の細かいところを。

値型

intなど基本的なもの以外でもlibcクレートに大抵あるのでここで調べるとよいと思います。ino_tとかdev_tもあります。vold*c_voidです。

とはいえ、まれにlibcの型定義でうまくいかないことがあります。例えばOSXにおけるino_tはlibc的には64bitなのですが、ftsが期待するino_tは32bitのようでした。(このあたり詳しい事情は分かりませんし、そもそもOSX環境が手元にないので、Travis-CIのOSXビルドではそうだった、というだけですが…)

2016/3/19追記

OSXのino_tですが、以下のようにlink_nameを指定することで、libc定義通り64bitのino_tを得ることが出来ました。
(rustのlibstd実装見て気づきました。よく見るとAppleのドキュメントにも$INODE64付きのシンボル名で64bit版になると書いてあります)

#[cfg_attr(target_os = "macos", link_name = "fts_read$INODE64")]
pub fn fts_read( ftsp: *mut FTS ) -> *const FTSENT;

nullポインタ

nullポインタそのものはstd::ptr::null()で得られます。例えばfts_openは引数に「nullポインタで終端された文字列ポインタ配列へのポインタ」を渡すので以下のようになります。また、nullポインタチェックはis_null()が使えます。

let path_ptr = std::ffi::CString::new( "." ).unwrap().as_ptr();
let path_ptrs = vec![path_ptr, std::ptr::null()];
let fts = fts_open( path_ptrs.as_ptr(), 0, None );
assert!( !fts.is_null() );

定数定義

Cで#defineされているような定数についてです。

ビットフラグの場合はbitflagsが使えます。ちゃんと型指定できて、ビット演算も普通に出来ます。Cに渡すときはbits()で整数型に変換して渡します。下の例だとフラグの型はFlagsですが、定義した値はFlags::Aとはならず単にAになってしまうのでmodでくるんでoption::Aになるようにしています。

pub mod option {
    bitflags! {
        pub flags Flags: u32 {
            const A = 0x0001,
            const B = 0x0002,
            const C = 0x0004,
        }
    }
}

let opt = option::A | option::B;
assert_eq!( opt.bits(), 0x3 );

ビットフラグでない場合、基本的にはenumで良いのですが、Cからの戻り値をenumに変換するのが面倒です。enum_primitiveを使えば各種from_を自動実装してくれます。

enum_from_primitive! {
    #[derive(Clone,Debug,PartialEq)]
    pub enum Info {
        A = 0,
        B = 1,
        C = 2,
    }
}

assert_rq!( Info::A, Info::from_isize( 0 ) );
assert_rq!( Info::B, Info::from_isize( 1 ) );

errno

エラーがいわゆるerrnoで返ってくる場合、自分でデコードする必要はなくて、std::io::Error::from_raw_os_error()で変換出来ます。

let errno = 13;
let err = std::io::Error::from_raw_os_error( errno );
assert_eq!( err.kind(), std::io::ErrorKind::PermissionDenied );

transmute

std::mem::transmute()で無理矢理キャストしたい場合があります。
今回は2ヶ所で使っていて、1つ目はmetadataです。Cから返ってくるlibc::statとRustで使いたいstd::fs::MetadataはUnixではおそらく同一のメモリレイアウトなのでtransmuteで変換しています。この辺りドキュメントに記載されているわけではないので、プラットホームやバージョンで変わる可能性はあります。
(とはいえMetadataExt::as_raw_stat()libc::statポインタが返ってくる以上、メモリレイアウトが異なるというのも考えにくいです。本当はfrom_raw_stat()があってくれればいいのですが)

let stat = ...
let metadata = std::mem::transmute::<*const libc::stat, *const std::fs::Metadata>( stat );

2つ目はC側が少し不思議なことになっていて、fts_open()の戻り値である構造体定義は以下のようになっています。

typedef struct _ftsent {
    ....
    char fts_name[1];
} FTSENT;

ここで最後の要素のfts_namecharの要素数1の配列と定義されていますが、実際にはそのあとも文字列は続いています。つまりstructの定義から実体がはみ出している状態です。(glibc自体の実装を見ると、structalloc時にMAX_PATH_LENだけ余計にメモリ確保しているようです)
この状態を適切に表せる型があればいいのですが、多分なさそうなのでRust側も要素数1の配列として宣言して、アクセスするときにtransmuteでキャストしています。

#[repr(C)]
#[derive(Debug)]
pub struct FTSENT {
    ...
    pub fts_name   : [c_char;1]   ,
}

let ent: *const FTSENT = ...
let ptr  = unsafe { mem::transmute::<&[c_char;1], *mut u8>( &(*ent).fts_name ) };

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9