C & Rust で mmap やってみた

# 概要

本記事では、**`mmap`** を使ってメモリ操作の基本を実践してみます。

C と Rust でそれぞれ実装を行い、特にCではポインタ操作にも注目しながら進めます。

今回の実装内容は以下のとおりです

* **文字を書き込んでみる**
* **文字列を書き込んでみる**
* **ファイルをメモリにマッピングしてみる**

C では `sys/mman.h`、Rust では `memmap2` クレートを使用します。

この記事のコードを動かすだけで、**メモリの確保・書き込み・読み込み・解放** といった一通りの操作が体験できます。

ファイルに関しては、ファイルの内容をそのままメモリに展開する **マッピング** を行い、値の読み書きを行います。

難しいことは抜きにして、まずは手を動かして感覚で覚えていきましょう。

# mmapとは

プロセスが実行されると、OS から **仮想アドレス空間** が動的に割り当てられます。

仮想アドレス空間では、メモリの確保と解放が繰り返されることで、空き領域が断片化し、大きな連続したメモリ領域を確保できなくなることがあります。

`mmap` は、仮想アドレス空間に **連続したメモリ領域を<font color="red">明示的に</font>確保** するためのシステムコールです。

確保した領域には、**ファイルの内容をマッピングしたり、任意の値を書き込んだり** することができます。

たとえば、ファイルの読み込みや大きな値の書き込みなどでは、大きな連続領域が必要になります。
`mmap` を使えば、その必要なサイズのメモリ領域を確実に確保することができます。

詳しくは wiki を参照してください。

https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%9E%E3%83%83%E3%83%97%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB

# リポジトリ

記事中のコードは下記のリポジトリで公開しています

https://github.com/BlackTanuTanuNinjas/tutorial_mmap

追記: 公開設定に変更し忘れてました。すみません。

# 環境

本記事の内容は、Mac および Debian 上で動作確認を行っています。  
各環境の主なバージョン情報は以下のとおりです。

```zsh
# Mac
$ clang --version
Apple clang version 16.0.0 (clang-1600.0.26.6)
Target: arm64-apple-darwin24.4.0

# Debian
$ gcc --version
gcc (Debian 12.2.0-14) 12.2.0

$ rustup --version
rustup 1.28.1 (f9edccde0 2025-03-05)
```

# Cで実装

C言語で、概要に挙げた3つの実装を行います。

どの実装でも、基本的な流れは共通です。

1. **mmap で匿名メモリ領域を確保する**
2. **メモリに値を書き込む**
3. **書き込んだ内容を読み込む**
4. **確保したメモリ領域を解放する**

また、各実装に共通して使用する関数は以下の3つです

* **`getpagesize()`**
    * システムのページサイズを取得します
    * 記事中では `mmap()` で確保するメモリ領域のサイズに指定する
* **`mmap()`**
    * 仮想アドレス空間上に、匿名メモリ領域を確保する
    * `<sys/mman.h>` をincludeする必要がある
* **`munmap()`**
    * `mmap()` で確保したメモリ領域を解放する

以下は、すべての実装で共通して登場する基本的なコードの形です。

```c
int main()
{
    // 確保したいメモリ領域のサイズ(ページサイズ)を取得
    size_t size = getpagesize();

    // 読み書き可能な匿名メモリ領域を確保
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

    // 確保できていない場合は終了する
    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }

    // ここで値の書き込みを行う

    // メモリ領域を解放
    munmap(ptr, size);

    return 0;
}
```

## 文字を書き込んでみる

以下は、1バイトの文字を匿名メモリ領域に書き込む例です。

```c
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main()
{

    size_t size = getpagesize();
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }

    ((char *)ptr)[0] = 'A';
    printf("%c\n", ((char *)ptr)[0]);

    munmap(ptr, size);

    return 0;
}
```

## 文字列を書き込んでみる

以下は、文字列リテラルを匿名メモリ領域に書き込む例です。 
`memcpy` を使って、仮想アドレス空間上に存在する文字列リテラルの内容を、匿名メモリ領域にコピーしています。

```c
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

int main()
{
    size_t size = getpagesize();

    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        return 1;
    }

    const char *message = "Hello, World!";

    // 終端の '\0' も含めてコピーするため +1 する
    memcpy(ptr, message, strlen(message) + 1);

    printf("%s\n", (char *)ptr);

    munmap(ptr, size);

    return 0;
}
```

## ファイルをメモリにマッピングしてみる

以下は、ファイルの内容をメモリ領域にマッピングする例です。

特に注目したいのは、`mmap` を使って **値を書き込むための匿名メモリを確保する** のではなく、
<font color="red">**ファイルの内容をマッピングする目的でメモリ領域を確保している**</font> という点です。

```c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    // ファイルを開く、ファイルディスクリプタ(ファイル識別子)を取得
    int fd = open("test.txt", O_RDONLY);

    // ファイルが開けていない場合は終了する
    if (fd == -1)
    {
        perror("file");
        return 1;
    }


    struct stat sb;

    // ファイル情報(サイズなど)を取得する
    if (fstat(fd, &sb) == -1)
    {
        perror("fstat");
        return 1;
    }

    // ファイルのサイズを取得する(mmapのサイズ指定に使用する)
    size_t size = sb.st_size;

    // ファイルの内容を仮想アドレス空間上にマッピングする
    void *ptr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);

    if (ptr == MAP_FAILED)
    {
        perror("mmap");
        close(fd);
        return 1;
    }

    // メモリの内容を標準出力に出力する
    write(STDOUT_FILENO, ptr, size);

    munmap(ptr, size);

    close(fd);

    return 0;
}
```

ちなみに、`MAP_ANON` を指定してファイルを紐づけずに確保したメモリ領域のことを <font color="red">**匿名メモリ領域**</font> と呼びます。

# Rustで実装

次に Rust で、概要に挙げた3つの実装を行います。

ここでは `mmap`を使用するために、 **`memmap2`** というクレートを使用します。

https://github.com/razrfalcon/memmap2-rs

基本的な処理の流れは C とほぼ同じですが、下記の点で異なります。

* **メモリの解放は Rust のスコープとライフタイムで自動的に管理される**

また、記事中で使用する主な関数は以下の2つです。

* **`MmapMut::map_anon()`**
    * 仮想アドレス空間上に、匿名メモリ領域を確保する
* **`Mmap::map()`**
    * ファイルの内容を仮想アドレス空間にマッピングする(unsafeが必要)

以下は、すべての実装で共通して登場する基本的なコードの形です。

```rust
fn main() {
    // 書き込みたいサイズのメモリ領域を確保(ここでは 4096 バイト)
    let mut mmap = memmap2::MmapMut::map_anon(4096).expect("mmap failed");

    // ここで値の書き込みを行う
}
```

## 文字を書き込んでみる

以下は、1バイトの文字を匿名メモリ領域に書き込む例です。

```rust
use memmap2::MmapMut;

fn main() {
    let mut mmap = MmapMut::map_anon(4096).expect("mmap");

    mmap[0] = b'A';
    println!("{}", mmap[0] as char);
}
```

## 文字列を書き込んでみる

以下は、文字列を匿名メモリ領域に書き込む例です。

```rust
use memmap2::MmapMut;
use std::str;

fn main() {
    let mut mmap = MmapMut::map_anon(4096).expect("mmap");

    let message = b"Hello, World!";

    mmap[..message.len()].copy_from_slice(message);

    let text = str::from_utf8(&mmap[..message.len()]).expect("utf8");

    println!("{}", text);
}

```

## ファイル内容を書き込んでみる

以下は、ファイルの内容を仮想アドレス空間にマッピングする例です。

```rust
use std::fs::File;

use memmap2::Mmap;

fn main() {
    let file = File::open("test.txt").expect("file");

    // ファイルの内容を仮想アドレス空間上にマッピングする
    let mmap = unsafe { Mmap::map(&file).expect("mmap") };
    
    let text = str::from_utf8(&mmap).expect("utf8");

    println!("{}", text);
}
```

# まとめ

本記事では mmap を通じてメモリの確保・書き込み・解放といったメモリの操作を実践形式で学びました。

実際に手を動かすことで、これまで抽象的だったメモリ操作の仕組みを、具体的に理解できたと感じています。

普段はWebアプリケーションやモバイル開発が中心ですが、メモリに直接触れることで、新しい視点を得ることができました。

これからも、こうした基礎を少しずつ積み上げて、着実に力をつけていきたいと思います。