🗺️

【rust】ファイル名を渡してmmapした後にメモリ読み書きのAPIを提供するcrateを作ってみた。+ ManuallyDropの話

2023/10/09に公開

mmapを使う事で、ファイルとメモリがマッピングされ、メモリへの書き込みと同時にファイルが自動的に更新されます。

何らかの処理の実行結果をファイルに保存して永続化する、という事がこれを使えば手軽に実現できます。
ロールプレイングゲームでいうオートセーブ的なやつです。

メモリ容量が限られた環境で容量が大きいファイルを扱う場合、ファイルを部分的に読み書きする、という工夫も必要になると思いますが、それもこのmmapを使うと、実際にアクセスがある場合(該当メモリアドレスへの参照がある場合)に初めて実際にメモリ上にデータがロードされたりと、OSがよきに計らって管理してくれるようで、file操作用のAPIを使うよりも高速になる場合もあるようです。

rustでmmapを使用するためのcrateとしては、memmapmemmap2
等がありました。

これらは、mmapを使用するための最低限の低レベルAPIを提供してくれるライブラリです。

そのため、メモリの読み書きや、ファイルそのものの管理については自力で処理を書く必要があります。

そこで、その辺りの処理をもっと簡単に使えるようにしたものを作りました。
https://crates.io/crates/file_mmap
(内部的にはmemmap2クレートを使わせてもらっています)

使い方

use file_mmap::FileMmap;

let mut fm=FileMmap::new('ファイル名');

//書き込み
fm.append(b"hogehoge".to_vec());

//読み込み
let bytes=unsafe{ fm.bytes(0,8) };

※bytes()は、境界チェックをしてないのでunsafeになっています。

このライブラリを開発するにあたり、ちょっと厄介だったのが、ファイルサイズを拡張する際に、ページサイズを超えるとマッピングされたメモリアドレスがリセットされる、という事でした。

append()でデータを追記する際に、ファイルサイズを自動的に増加させる、という処理を入れているのですが、この時に4096バイト境界を超えると、memmapが参照するメモリアドレスが変わる、という事が発生します。(windowsでしか試していないので他のOSでどうなるかは未検証)

そこで、その境界を超える場合は一旦mmapを破棄し、ファイルサイズを変更してから再度mmapしよう、という事になったのですが、rustでは基本的にはlifetimeを抜けた変数は自動的に破棄される(lifetimeというだけあって当たり前なのですが...)ので、明示的に変数を破棄する標準的な記法は提供されていません。
そこで、ManuallyDropというstructを使用します。
file_mmapのソースを一部抜粋します。

static PAGE_SIZE: Lazy<usize> = Lazy::new(|| sysconf::page::pagesize());

pub struct FileMmap {
    file: fs::File,
    mmap: ManuallyDrop<MmapRaw>,
}
impl FileMmap {
    pub fn new<P: AsRef<Path>>(path: P) -> io::Result<Self> {
        let file = fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&path)?;
        let mmap = ManuallyDrop::new(MmapRaw::map_raw(&file)?);
        Ok(FileMmap { file, mmap })
    }
    
    pub fn set_len(&mut self, len: u64) -> io::Result<()> {
        let current_len = self.file.metadata()?.len();
        if current_len > len
            || current_len == 0
            || ((current_len as usize - 1) / *PAGE_SIZE != len as usize / *PAGE_SIZE)
        {
            unsafe { ManuallyDrop::drop(&mut self.mmap) };
            self.file.set_len(len)?;
            self.mmap = ManuallyDrop::new(MmapRaw::map_raw(&self.file)?);
            Ok(())
        } else {
            self.file.set_len(len)
        }
    }
}

new()では、ManuallyDropでラップするかたちで、MmapRawを作成しています。

set_len()では、mmapが示すメモリアドレスが変更になるようなケースで、

unsafe { ManuallyDrop::drop(&mut self.mmap) }; //一旦self.mmapをdropし
self.file.set_len(len)?; //ファイルサイズ変更
self.mmap = ManuallyDrop::new(MmapRaw::map_raw(&self.file)?); //再度MmapRawを作成

という流れになっています。

ManuallyDropでdropしなかった場合、
ファイルをmmapが参照しているため、
self.file.set_len(len)
が正常動作しません。
そのため、先にdropする必要がありました。

Discussion