boost.interprocessさん凄いです

この記事は C++ Advent Calendar 2010 の参加記事です。詳細は以下。
C++ Advent Calendar jp 2010 : ATND
今日はクリスマスですし、みなさんお忙しいと思いますし、それに僕はあんまり変態的な事を書けませんので…。*1
そんな中であまりTwitter上であまり話が出てこない(気がする)boost.interprocessのほんの一機能について備忘録的にまとめてみました。

Memory Mapped Files?

boost.interprocessは基本的に共有メモリの強力なラッパライブラリなのですが、その機能としてMemory Mapped Filesというものがあります。これはファイルをメモリとほぼ同じ感覚で操作するための機能です。ファイルを開いてそれに仮想的なアドレスを与え、あたかもメモリ上にあるデータにアクセスしてるかのような、まさにポインタと同じ操作ができます。*2
結構、普段はあんまり意義を見出せないような気がしないでもないこの機能なのですが、これは見方によっては結構すごいものなんです。ファイルをメモリのように扱える、ということはよく使われるファイルをいちいちメモリに自分でロードする必要がない、という事です。最近のパソコンはメモリが豊富なので50MBぐらいのファイルデータだったらメモリに展開しておいた方が処理は高速です。いちいちファイルを開いて操作したり、メモリにバッファ作って自前で展開するのも面倒ですしね。
最小構成のものもあるのですが、今回は私の眼を惹いたManaged Memory Segmentsを使ってみます。

Managed Memory Segments?今回はManaged Mapped Filesだけ

全体的にはメモリ管理のサポートなどが目的なようですが、この中にはMemory Mapped Filesのさらに強力なラッパであるManaged Mapped Filesというものがあります。これから説明しますが、Memory Mapped Filesの低機能な部分を統合したもの+オブジェクト生成支援(名前付きオブジェクト(専用コンテナ含む)生成)関数群、といった構成になっているのがManaged Mapped Filesです。

Memory Mapped Filesを使ってみる

Managedを使用しないで、低レベルなラッパ部分のみを使ってみます。マップしたいファイルのパスを渡して読み込み専用などで開いて、後は先頭のアドレスとそのサイズを使ってなにがしするだけです。今回はファイルをコピーしましょう。

#include <boost/interprocess/file_mapping.hpp>
#include <boost/interprocess/mapped_region.hpp>
#include <boost/filesystem/v2/convenience.hpp>
#include <fstream>
#include <string>
#include <iostream>

void create_buffile( std::string const& FilePath, std::string const& CopyName )
{
    std::filebuf fbuf;
    fbuf.open( CopyName, std::ios_base::in | std::ios_base::out 
                       | std::ios_base::trunc | std::ios_base::binary );
    fbuf.pubseekoff( boost::filesystem::file_size(FilePath) - 1, std::ios_base::beg );
    fbuf.sputc(0);
}

int main()
{
    namespace bip = boost::interprocess;
	
    const std::string FilePath( "test.ogg" );
    const std::string CopyName( "copy.ogg" );
    // ファイルがないとマッピングできないのでコピー先ファイルを先に作成
    create_buffile( FilePath, CopyName );

    try {
        // コピー先/元をマッピング
        bip::file_mapping fmsrc( FilePath.c_str(), bip::read_only );
        bip::file_mapping fmdst( CopyName.c_str(), bip::read_write );
        bip::mapped_region mapsrc( fmsrc, bip::read_only );
        bip::mapped_region mapdst( fmdst, bip::read_write );
        BOOST_ASSERT( mapsrc.get_size() == mapdst.get_size() );

        // ポインタと等価なのでmemcpyできる!
        memcpy( mapdst.get_address(), mapsrc.get_address(), mapsrc.get_size() );

    } catch( std::exception e ) {
        std::cout << e.what() << std::endl;
    }
}

ポインタと等価にアクセスできるのでmemcpyでコピー可能です。ここが強みと言える部分ですね。もちろん、ポインタと同じような危険性も持っているのですが、それを鑑みても有り余るほどの魅力があると思います。

Managedな部分を使ってみる

さて、上のような処理は基本的な部分で、Managedでも同じ事ができます。managed_mapped_fileというのがManagedなMemory Mapped Filesになるのですが*3、Managedを使用することの強みはオブジェクト生成支援関数群にあります。使った印象にしかすぎませんが、これはファイルをメモリプールと見立ててオブジェクトを作ったりすることができるようです。また、名前付きオブジェクトとして生成することで、ファイルの中から対応するオブジェクトを見つけてきたり、なかったら作ってしまったり、といった操作も可能です。用意されている専用のコンテナをファイル上に作ったりできます。

#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/range/algorithm/for_each.hpp>
#include <boost/range/algorithm/generate.hpp>
#include <string>
#include <iostream>

void manage_map_test();

int main()
{
    try {
        manage_map_test();
    } catch( std::exception& e ) {
        std::cerr << e.what() << std::endl;
    }
}

void manage_map_test()
{
    namespace bip = boost::interprocess;

    typedef bip::allocator<int,bip::managed_mapped_file::segment_manager> allocator_int_t;
    typedef bip::vector<int,allocator_int_t> int_vector_t;

    std::string const& filename( "tmp.dat" );
    boost::uint32_t const fileSize = 4096;

    {
        bip::file_mapping::remove(filename.c_str());
        bip::managed_mapped_file mfile(bip::create_only,filename.c_str(),fileSize);

        // 作成するためのproxyを取得
        auto construct_proxy = mfile.construct<int_vector_t>("MyVector");

        // オブジェクトのアロケータをproxyに渡して作成
        const allocator_int_t my_allocator(mfile.get_segment_manager());
        int_vector_t& mfile_vect = *(construct_proxy(my_allocator));
        BOOST_ASSERT(&mfile_vect == mfile.find<int_vector_t>("MyVector").first);

        // 適当に出力してflush
        mfile_vect.resize(10);
        boost::generate(mfile_vect,[](){ return 42; });
        mfile.flush();
    }
    // ファイルサイズの最適化もできる
    bip::managed_mapped_file::shrink_to_fit(filename.c_str());
    {
        bip::managed_mapped_file mfile(bip::open_only,filename.c_str());

        // 検索して、vectorの中身を表示してみる
        int_vector_t const& mfile_vect = *(mfile.find<int_vector_t>("MyVector").first);
        BOOST_ASSERT(&mfile_vect);
        boost::for_each(mfile_vect,[](int i){ std::cout << i << " "; });
    }
}

やってることはコメント通りなのですが、これで作成されたtmp.datファイルをバイナリエディタで開いてみると、上部にMyVectorという文字列が埋め込まれていて、42(バイナリだと確か2A)が10個連続して埋め込まれていると思います。なんというか、もうファイル=メモリみたいに使ってますね。メモリのようにデータを書き込んで、ファイルとして保存するような、そんなイメージ。
最初、私はconstructメソッドが作成してくれるのかなーと思っていたんですが、これは作成するためのproxyを取得するためのメソッドで、実際の作成操作はこのproxyのoperator()を呼んでアロケータを渡さないといけないようです。渡すアロケータもboost.interprocess固有のもので、マッピングオブジェクトからsegment_managerなるものを取得して初期化したものを渡す。アロケータはこのsegment_managerを通してファイル上にオブジェクトを作成する仕様のようです。
ちなみに、作成できるコンテナはboost/interprocess/containersに入っていて、vector以外にlist,map,stable_vector,flat_mapなどSTL+αが利用できるようです。
constructしたオブジェクトを削除する場合はdestroy*メソッドを使います。

    bip::managed_mapped_file mfile(bip::open_only,filename.c_str());
		
    // オブジェクトを指すポインタを用いて削除
    int_vector_t* mfile_vect = mfile.find<int_vector_t>("MyVector").first;
    BOOST_ASSERT(mfile_vect);
    mfile.destroy_ptr(mfile_vect);
    mfile_vect = mfile.find<int_vector_t>("MyVector").first;
    BOOST_ASSERT(!mfile_vect);

    ...

    // オブジェクトの名前を使って削除もできる
    mfile.destroy<int_vector_t>("MyVector");

他にも、ファイルに対してshrink_to_fitを行ったり、マッピングファイルのサイズを拡張することもできます。

    bip::managed_mapped_file::shrink_to_fit(filename.c_str());
    bip::managed_mapped_file::grow(filename.c_str(),4096);  // 4096Byte拡張

後、この間もmanaged_mapped_fileで既存のファイルを開けない、とかTwitterでつぶやいてた気がしますが、どうもmanaged_mapped_fileの場合何かしらのフォーマットがあるようです。読み込みで開けるのはこのmanaged_mapped_fileで作成、保存したファイルだけのようですね。もしそれ以外のファイルを開きたい場合はfile_mappingとmapped_regionを使わないといけないようです。

セーブデータ保存などやってみる

何かに利用できないかなーという具体例を考えてみました。といっても最近はゲームプログラミング(とオーディオライブラリ)ばっかりやっててその辺の事しか思い浮かびませんでしたが。そしてゲームプログラミングも微妙なレベルですが、例えばセーブデータを保存するのにうってつけな気がします。

// 何かしらのセーブデータ
struct save_data
{
    boost::date_time::ptime   Save;
    boost::date_time::ptime   PlayTime;
    boost::uint64_t           Score;
};

namespace bip = boost::interprocess;

typedef bip::allocator<save_data,bip::managed_mapped_file::segment_manager> save_allocator_t;
typedef bip::vector<save_data,save_allocator_t> save_vector_t;
std::string const SaveFileName( "save.dat" );
boost::uint32_t const SaveFileSize = 65536; // 後でshrink_to_fitすれば大丈夫

// データをすべてセーブ
void save( std::vector<save_data> const& dat )
{
    {
        bip::managed_mapped_file mfile(bip::create_only,SaveFileName.c_str(),SaveFileSize);
        save_allocator_t const save_alloc(mfile.get_segment_manager());
        save_vector_t& vec = *(mfile.construct<save_vector_t>("SaveData")(save_alloc));
        boost::copy(dat,std::back_inserter(vec));
    }
    bip::managed_mapped_file::shrink_to_fit(SaveFileName.c_str());
}

// ファイルから全てロード
void load( std::vector<save_data>& dat )
{
    bip::managed_mapped_file mfile(bip::open_only,SaveFileName.c_str());
    save_vector_t const& vec = *(mfile.find<save_vector_t>("SaveData").first);
    boost::copy(vec,std::back_inserter(dat));
}

(※2011/1/6 - save関数でmanaged_mapped_file::constructメソッドのテンプレート引数がアロケータになっていたので修正。)
恐らくこんな感じで。エラーチェックなどはすべて省いていますが、凄く簡単ですね。ファイルに保存してるのにアラインメントとかそこまで気にしなくても問題ないので…とは言っても気にしなきゃいけない部分もあるかもしれませんが。
他にもアーカイバみたいなのを作れるんじゃなかろうか、などと思ったのですが、長くなりそうなので(すでに長いので)割愛。

終わりに

boost.interprocessは想像以上に闇が奥が深くて楽しいです。boostのライブラリ全般そんな気がしてきましたが。というか「boost.interprocessさん凄いです」というタイトルなクセに一部しか紹介してない。むりですおおすぎてかききれませんごめんなさい。
プログラミング関連でよくよく、「○○(有名なライブラリとかAPIとか)の使い方覚えないと…」とか言ってる人が多々いるのですが、僕自身は使い方を覚えてる暇があったらドキュメント引っ張って利用できる力を身に着けた方が良いんじゃないかと思ってます。人間が覚えていられる情報量などたかが知れてますし。いや、そりゃ言語仕様だとか最低限覚えてないといけないような箇所は多々ありますが、ライブラリの使い方を覚えたところでそれいつも使うの?使わないところはないの?みたいな話で。
あ、論点ずれた。…とりあえず、みなさんもお正月はboostのドキュメントを引っ張って自分が使ったことないようなところを調べてみると面白い事がわかるんじゃないでしょうか。正月特番しか見るものないよみたいな方、オススメです。

*1:前日までネタを決めかねているレベル。

*2:実際はアクセスするたびにメモリにデータが読み込まれるようになっています。

*3:本当はbasic_managed_mapped_fileのtypedefですが、今回は割愛。