はじめに
BSDやLinuxのglibcにはftsという関数群があります。これはディレクトリ内の再帰的なファイル列挙を高速に行う関数で、ls
やfind
などの実装に使われています。
Rustでこのようなファイル列挙を行う場合、std::fs::readdir
で実装するか、既存のwalkdir
(これも内部的にはreaddir
を使っているようです)を使うことになりますが、ftsが速いらしいということで、FFIライブラリを作ってそれを使ったwalkdir
相当の実装を行いました。
ちなみにglibcに依存するのでおそらくBSD(OSX含む)とLinux限定です。Windows対応が必要なら素直にwalkdir
を使いましょう。
リンク
使い方
[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
の場合、シンボリックリンクを辿り、かつリンク先が別デバイスであっても辿る設定になります。conf2
はmetadata
を取得しないオプションで、その分高速になります。
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 readdir
がreaddir
を再帰で呼ぶだけの適当な実装、test walkdir
がwalkdir
を使ったものになります。また、_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_name
はchar
の要素数1の配列と定義されていますが、実際にはそのあとも文字列は続いています。つまりstruct
の定義から実体がはみ出している状態です。(glibc自体の実装を見ると、struct
のalloc
時に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 ) };