メモリマップド I/O レジスタにアクセスする Python モジュール を C で記述


## はじめに

CPU が I/O デバイスのレジスタをアクセスする際は、物理メモリ空間にマッピングされたアドレスを介して行われるのが一般的です。
また、Linux のような仮想アドレスをサポートしている OS では、I/O デバイスのレジスタがマッピングされた物理メモリ空間を仮想アドレスにマッピングしてからアクセスします。

Python で、仮想アドレスにマッピングされた I/O デバイスレジスタをアクセスする方法として、以下の記事で Numpy を使う例を紹介しました。

  * [『Python と Numpy で UIO を制御』@Qiita](https://qiita.com/ikwzm/items/d7559858d099c86c350c)

Numpy を使う方法は手軽で良いのですが、筆者は Numpy 本来の目的と異なる使い方をしているのではないかという疑念を持っています。もともと Numpy は数値演算を行うためのもので I/O レジスタのアクセスを目的としていません。現在の Numpy では「たまたま」I/O レジスタのアクセスにも使えるというだけです。これが将来にわたって使える保証はありません。

このまま Numpy に頼りつづけるのは少々不安なので、Numpy を使わずに済むように、低レベルな I/O アクセスを行うための C で記述した Python モジュールを作ることにしました。すでに同様のものがあるかと思いますが、自分の勉強の目的もあって、あえて車輪の再発明をしています。

この記事では、この Python モジュールの説明をします。

また、この記事で紹介する Python のパッケージは 以下の URL で公開しています。

  * https://github.com/ikwzm/python-mmapio


## ソースコードの説明

python-mmapio は以下のようなディレクトリの構成になっています。Python パッケージの名前は mmapio です。

 * `setup.py`
 * `mmapio/`
   - `__init__.py`
   - `mmapio.c`
   - `uio.py`

この章では、これらのソースコードを解説します。

### `mmapio/mmapio.c`

mmapio パッケージの mmapio モジュールのソースコードです。

まずはヘッダーにライセンスを表記しています。

```C:mmapio/mmapio.c
/*********************************************************************************
 *
 *       Copyright (C) 2025 Ichiro Kawazome
 *       All rights reserved.
 * 
 *       Redistribution and use in source and binary forms, with or without
 *       modification, are permitted provided that the following conditions
 *       are met:
 * 
 *         1. Redistributions of source code must retain the above copyright
 *            notice, this list of conditions and the following disclaimer.
 * 
 *         2. Redistributions in binary form must reproduce the above copyright
 *            notice, this list of conditions and the following disclaimer in
 *            the documentation and/or other materials provided with the
 *            distribution.
 * 
 *       THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *       "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *       LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *       A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
 *       OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *       SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *       LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *       DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 *       THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 *       (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *       OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 ********************************************************************************/
```

インクルードするファイルを指定します。python のモジュールを C で記述す際は、Python development headers に含まれている Python.h をインクルードします。


```C:mmapio/mmapio.c
#include <Python.h>
#include <sys/mman.h>
#include <unistd.h>
```

モジュール名とオブジェクト名(クラス名)を定義します。mmapio.c ではこれらの名前をコンパイル時に外部から別名で上書きできるようにしています。

```C:mmapio/mmapio.c
#ifndef MODULE_NAME
#define MODULE_NAME         mmapio
#endif
#ifndef OBJECT_NAME
#define OBJECT_NAME         MemoryMappedIO
#endif
```

モジュールのバージョンや、著作者、ライセンス、簡単な説明を定義します。

```C:mmapio/mmapio.c
#define MODULE_VERSION      "0.0.3"
#define MODULE_AUTHOR       "Ichiro Kawazome"
#define MODULE_AUTHOR_EMAIL "ichiro_k@ca2-so-net.ne.jp"
#define MODULE_LICENSE      "BSD 2-Clause"
#define MODULE_DESCRIPTION  "Memory Mapped I/O Module"
```

モジュール名とオブジェクト名を文字列に変換したものを定義します。

```C:mmapio/mmapio.c
#define TO_STR(x)           #x
#define NAME_TO_STR(x)      TO_STR(x)
#define MODULE_NAME_STRING  NAME_TO_STR(MODULE_NAME)
#define OBJECT_NAME_STRING  NAME_TO_STR(OBJECT_NAME)
```

Python オブジェクトが保持する mmapio_object 構造体を定義します。具体的には、CPU からみたアドレス領域の先頭アドレスと大きさを保持します。

```C:mmapio/mmapio.c
typedef struct {
    PyObject_HEAD
    void*        addr;
    Py_ssize_t   size;
} mmapio_object;
```

I/O アクセスの際に引数として与えられる offset 値の妥当性をチェックする関数を定義します。
具体的には、offset 値が、割り当てられたアドレス領域の範囲内にあるかどうかと、アクセスする際の単位(8bit、16bit、32bit、64bit)のアライメントにあっているかをチェックします。
また、アクセスする際の単位ごとに個別に用意するため、プリプロセッサ演算子とマクロを使って定義しています。


```C:mmapio/mmapio.c
#define DEFINE_MMAPIO_CHECK_OFFSET_METHOD(type)                                   \
static int mmapio_check_offset_ ## type(mmapio_object* self, PY_LONG_LONG offset) \
{                                                                                 \
    if ((offset < 0) || (offset >= self->size)) {                                 \
        PyErr_SetString(PyExc_ValueError, "Offset exceeds mapped region");        \
        return -1;                                                                \
    }                                                                             \
    if ((offset % sizeof(type)) != 0) {                                           \
        PyErr_SetString(PyExc_ValueError, "Offset violates " #type " alignment"); \
        return -1;                                                                \
    }                                                                             \
    return 0;                                                                     \
}

DEFINE_MMAPIO_CHECK_OFFSET_METHOD(uint8_t)
DEFINE_MMAPIO_CHECK_OFFSET_METHOD(uint16_t)
DEFINE_MMAPIO_CHECK_OFFSET_METHOD(uint32_t)
DEFINE_MMAPIO_CHECK_OFFSET_METHOD(uint64_t)
```

I/O アクセスを行うメソッドを定義します。
メソッドはアクセスする際の単位(8bit、16bit、32bit、64bit)ごとに個別に用意するため、プリプロセッサ演算子とマクロを使って定義しています。
ここでは次のメソッドを16個のメソッドを定義します。

  * byte(8bit)
    - read_byte(offset: int) -> int
    - write_byte(offset: int, data: int) -> None
  * half(16bit)
    - read_half(offset: int) -> int
    - write_half(offset: int, data: int) -> None
  * word(32bit)
    - read_word(offset: int) -> int
    - write_word(offset: int, data: int) -> None
  * quad(64bit)
    - read_quad(offset: int) -> int
    - write_quad(offset: int, data: int) -> None
  * uint8
    - read_uint8(offset: int) -> int
    - write_uint8(offset: int, data: int) -> None
  * uint16
    - read_uint16(offset: int) -> int
    - write_uint16(offset: int, data: int) -> None
  * uint32
    - read_uint32(offset: int) -> int
    - write_uint32(offset: int, data: int) -> None
  * uint64
    - read_uint64(offset: int) -> int
    - write_uint64(offset: int, data: int) -> None


```C:mmapio/mmapio.c
#define DEFINE_MMAPIO_RW_METHOD(name,type)                                 \
static PyObject* mmapio_read_ ## name(mmapio_object* self, PyObject* args) \
{                                                                          \
    PY_LONG_LONG  offset;                                                  \
    type          data;                                                    \
    if (!PyArg_ParseTuple(args, "L", &offset)) {                           \
        return NULL;                                                       \
    }                                                                      \
    if (mmapio_check_offset_ ## type(self, offset) != 0) {                 \
        return NULL;                                                       \
    }                                                                      \
    data = *(type *)(self->addr + offset);                                 \
    return PyLong_FromUnsignedLongLong((unsigned PY_LONG_LONG)(data));     \
}                                                                          \
static PyObject* mmapio_write_ ## name(mmapio_object* self, PyObject* args)\
{                                                                          \
    PY_LONG_LONG           offset;                                         \
    unsigned PY_LONG_LONG  data;                                           \
    unsigned PY_LONG_LONG  mask = (unsigned PY_LONG_LONG)(~(type)0);       \
    if (!PyArg_ParseTuple(args, "LK", &offset, &data)) {                   \
        return NULL;                                                       \
    }                                                                      \
    if (mmapio_check_offset_ ## type(self, offset) != 0) {                 \
        return NULL;                                                       \
    }                                                                      \
    *(type *)(self->addr + offset) = (type)(data & mask);                  \
    Py_RETURN_NONE;                                                        \
}                                                                          \

DEFINE_MMAPIO_RW_METHOD(byte  ,uint8_t )
DEFINE_MMAPIO_RW_METHOD(half  ,uint16_t)
DEFINE_MMAPIO_RW_METHOD(word  ,uint32_t)
DEFINE_MMAPIO_RW_METHOD(quad  ,uint64_t)
DEFINE_MMAPIO_RW_METHOD(uint8 ,uint8_t )
DEFINE_MMAPIO_RW_METHOD(uint16,uint16_t)
DEFINE_MMAPIO_RW_METHOD(uint32,uint32_t)
DEFINE_MMAPIO_RW_METHOD(uint64,uint64_t)

static PyMethodDef mmapio_methods[] = {
    {"read_byte"   ,(PyCFunction)mmapio_read_byte   , METH_VARARGS, "Read Byte from Memrory Mapped I/O" },
    {"read_half"   ,(PyCFunction)mmapio_read_half   , METH_VARARGS, "Read 16bit from Memrory Mapped I/O"},
    {"read_word"   ,(PyCFunction)mmapio_read_word   , METH_VARARGS, "Read 32bit from Memrory Mapped I/O"},
    {"read_quad"   ,(PyCFunction)mmapio_read_quad   , METH_VARARGS, "Read 64bit from Memrory Mapped I/O"},
    {"write_byte"  ,(PyCFunction)mmapio_write_byte  , METH_VARARGS, "Write Byte to Memrory Mapped I/O"  },
    {"write_half"  ,(PyCFunction)mmapio_write_half  , METH_VARARGS, "Write 16bit to Memrory Mapped I/O" },
    {"write_word"  ,(PyCFunction)mmapio_write_word  , METH_VARARGS, "Write 32bit to Memrory Mapped I/O" },
    {"write_quad"  ,(PyCFunction)mmapio_write_quad  , METH_VARARGS, "Write 64bit to Memrory Mapped I/O" },
    {"read_uint8"  ,(PyCFunction)mmapio_read_uint8  , METH_VARARGS, "Read Byte from Memrory Mapped I/O" },
    {"read_uint16" ,(PyCFunction)mmapio_read_uint16 , METH_VARARGS, "Read 16bit from Memrory Mapped I/O"},
    {"read_uint32" ,(PyCFunction)mmapio_read_uint32 , METH_VARARGS, "Read 32bit from Memrory Mapped I/O"},
    {"read_uint64" ,(PyCFunction)mmapio_read_uint64 , METH_VARARGS, "Read 64bit from Memrory Mapped I/O"},
    {"write_uint8" ,(PyCFunction)mmapio_write_uint8 , METH_VARARGS, "Write Byte to Memrory Mapped I/O"  },
    {"write_uint16",(PyCFunction)mmapio_write_uint16, METH_VARARGS, "Write 16bit to Memrory Mapped I/O" },
    {"write_uint32",(PyCFunction)mmapio_write_uint32, METH_VARARGS, "Write 32bit to Memrory Mapped I/O" },
    {"write_uint64",(PyCFunction)mmapio_write_uint64, METH_VARARGS, "Write 64bit to Memrory Mapped I/O" },
    {NULL}
};
```

新しい mmapio オブジェクトを生成する関数を定義します。

```C:mmapio/mmapio.c
static PyObject*
mmapio_object_new(PyTypeObject* type, PyObject* args, PyObject* kwdict)
{
    mmapio_object* self;
    self = (mmapio_object*)type->tp_alloc(type, 0);
    if (self != NULL) {
        self->addr = NULL;
        self->size   = 0;
    }
    return (PyObject*)self;
}
```

mmapio オブジェクトを初期化する関数を定義します。
初期化の際には引数として、buffer、offset、length が与えられます。buffer は、Python のバッファオブジェクト、offset はバッファオブジェクトからのオフセット値、length はオフセット値からの大きさです。
与えられた引数の妥当性をチェックして、mmapio_object_new() で確保した mmapio_object 構造体の addr フィールド と size フィールドに値をセットします。


```C:mmapio/mmapio.c
static int
mmapio_object_init(mmapio_object* self, PyObject* args, PyObject* kwdict)
{
    PyObject*    buf;
    Py_ssize_t   offset = 0;
    Py_ssize_t   length = 0;
    Py_buffer    view;
    void*        buf_ptr;
    Py_ssize_t   buf_size;
    static char* kwlist[] = {"buffer", "offset", "length", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwdict, "O|nn", kwlist, &buf, &offset, &length))  {
        return -1;
    }

    if (Py_TYPE(buf)->tp_as_buffer &&
        Py_TYPE(buf)->tp_as_buffer->bf_releasebuffer) {
        buf = PyMemoryView_FromObject(buf);
        if (buf == NULL) {
            return -1;
        }
    } else {
        Py_INCREF(buf);
    }

    if (PyObject_GetBuffer(buf, &view, PyBUF_SIMPLE) < 0) {
        Py_DECREF(buf);
        return -1;
    }
    buf_ptr  = (void*)view.buf;
    buf_size = view.len;
    PyBuffer_Release(&view);

    if ((offset < 0) || (offset > buf_size)) {
        PyErr_Format(PyExc_ValueError,
                     "offset must be non-negative and no greater than buffer " \
                     "length (%lld)", buf_size);
        Py_DECREF(buf);
        return -1;
    }
    if (length == 0) {
        length = buf_size - offset;
    }
    if (offset+length > buf_size) {
        PyErr_Format(PyExc_ValueError,
                     "offset+length must be no greater than buffer " \
                     "length (%lld)", buf_size);
        Py_DECREF(buf);
        return -1;
    }
    self->addr = buf_ptr  + offset;
    self->size = length;

    Py_DECREF(buf);
    return 0;
}
```

mmapio オブジェクトをメモリから削除する関数を定義します。

```C:mmapio/mmapio.c
static void
mmapio_object_dealloc(mmapio_object* self)
{
    Py_TYPE(self)->tp_free((PyObject*)self);
}
```

mmapio オブジェクトを定義します。

.tp_name はモジュールを呼び出すときの名前です。ここでは "モジュール名.オブジェクト名" になります。    
.tp_basicsize は mmapio_object 構造体 の大きさを指定します。    
.tp_doc には help() で表示される内容を指定します。   
.tp_methods にはメソッドを定義した mmapio_methods テーブルを指定します。   
.tp_new、.tp_init、tp_dealloc には各々前述した mmapio_object_new()、mmapio_object_init()、mmapio_object_dealloc() を指定します。


```C:mmapio/mmapio.c
static PyTypeObject mmapio_type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name      = MODULE_NAME_STRING "." OBJECT_NAME_STRING,
    .tp_basicsize = sizeof(mmapio_object),
    .tp_dealloc   = (destructor)mmapio_object_dealloc,
    .tp_flags     = Py_TPFLAGS_DEFAULT,
    .tp_doc       = "Memory Mapped I/O Object \n\n"
                    OBJECT_NAME_STRING "(buffer, offset, length) \n"
                    "    Create a new " OBJECT_NAME_STRING " instance \n"
                    "    buffer: buffer object \n"
                    "    offset: map offset (default=0)\n"
                    "    length: map size (default=buffer.length-offset)",
    .tp_methods   = mmapio_methods,
    .tp_init      = (initproc)mmapio_object_init,
    .tp_new       = mmapio_object_new,
};
```

mmapio モジュールを定義します。

```C:mmapio/mmapio.c
static struct PyModuleDef mmapio_module = {
    PyModuleDef_HEAD_INIT,
    MODULE_NAME_STRING,
    MODULE_DESCRIPTION "\n"
    "License: " MODULE_LICENSE "\n"
    "Author:  " MODULE_AUTHOR  "\n"
    "Version: " MODULE_VERSION,
    -1,
    NULL,
};
```

最後に、このモジュールの初期化関数を定義します。
この初期化関数は、動的ライブラリとしてコンパイルされたこのモジュールを Python が import した際に最初に呼ばれる関数です。
この関数で、mmapio_module で定義した mmapio モジュールを Python のモジュールとして登録し、mmapio_type で定義した mmapio オブジェクトを Python のオブジェクトとして mmapio モジュールに追加します。
なお、モジュール名をコンパイル時に別名で上書きできるようにするため、プリプロセッサ演算子とマクロを使って関数名を定義しています。


```C:mmapio/mmapio.c
#define PYINIT_FUNC_NAME(x) PyInit_ ## x
#define PYINIT_FUNC(x) PyMODINIT_FUNC PYINIT_FUNC_NAME(x)(void) 
PYINIT_FUNC(MODULE_NAME) {
    PyObject* m;

    if (PyType_Ready(&mmapio_type) < 0) {
        return NULL;
    }

    m = PyModule_Create(&mmapio_module);
    if (m == NULL) {
        return NULL;
    }

    Py_INCREF(&mmapio_type);
    if (PyModule_AddObject(m, OBJECT_NAME_STRING, (PyObject*)&mmapio_type) < 0) {
        Py_DECREF(&mmapio_type);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}
```

### `mmapio/__init__.py`

mmapio パッケージの初期化ファイルです。
このファイルでは、mmapio パッケージのバージョンやライセンスなどを記述しています。また、mmapio モジュールで定義されている MemoryMappedIO オブジェクトを mmapio パッケージから直接 import できるようにしています。また、同様に uio モジュールで定義されている Uio クラスも mmapio パッケージから直接 import できるようにしています。


```python:mmapio/__init__.py
__version__     = "0.0.4"
__author__      = "Ichiro Kawazome"
__copyright__   = "Copyright (c) 2025 Ichiro Kawazome"
__license__     = "BSD 2-Clause"
__email__       = "ichiro_k@ca2-so-net.ne.jp"
__description__ = "Memory Mapped I/O Package"

from .mmapio import MemoryMappedIO
from .uio    import Uio

```

### `mmapio/uio.py`

`mmapio/uio.py` に関しては次の記事で説明します。

  * [『Python と mmapio モジュール で UIO を制御』@Qiita](https://qiita.com/ikwzm/items/01a3c7d58c340db57b2c)

### `setup.py`

mmapio パッケージのセットアップファイルです。

パッケージ名に mmapio を指定します。

```python:setup.py
from setuptools import setup, find_packages, Extension
from os import path
import re

package_name = "mmapio"
```

パッケージのバージョンやライセンスなどを mmapio のパッケージ初期化ファイル(`mmapio/__init__.py`)から読んで変数に設定します。

```python:setup.py
root_dir = path.abspath(path.dirname(__file__))
with open(path.join(root_dir, package_name, "__init__.py")) as f:
    source       = f.read()
    version      = re.search(r'__version__\s*=\s*[\'\"](.+?)[\'\"]'     , source).group(1)
    license      = re.search(r'__license__\s*=\s*[\'\"](.+?)[\'\"]'     , source).group(1)
    author       = re.search(r'__author__\s*=\s*[\'\"](.+?)[\'\"]'      , source).group(1)
    author_email = re.search(r'__email__\s*=\s*[\'\"](.+?)[\'\"]'       , source).group(1)
    description  = re.search(r'__description__\s*=\s*[\'\"](.+?)[\'\"]' , source).group(1)
          
assert version
assert license
assert author
assert author_email
assert description
```

C で記述された mmapio モジュールをビルドするための情報を定義します。

```python:setup.py
mmapio_name   = 'mmapio'
mmapio_module = Extension(
    f'{package_name}.{mmapio_name}',
    sources = [f'{package_name}/mmapio.c'],
    define_macros = [('MODULE_NAME', mmapio_name)]
)
```

最後にパッケージのメタ情報を定義します。

```python:setup.py
setup(
    name=package_name,
    version=version,
    description=description,
    long_description="Memory Mapped I/O Package",
    author=author,
    author_email=author_email,
    license=license,
    ext_modules=[mmapio_module],
    packages=find_packages()
)
```

## ビルド

```console
shell$ python3 setup.py build
running build
running build_py
copying mmapio/__init__.py -> build/lib.linux-x86_64-3.8/mmapio
running build_ext
building 'mmapio.mmapio' extension
x86_64-linux-gnu-gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -fPIC -DMODULE_NAME=mmapio -I/usr/include/python3.8 -c mmapio/mmapio.c -o build/temp.linux-x86_64-3.8/mmapio/mmapio.o
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -Wl,-Bsymbolic-functions -Wl,-z,relro -g -fwrapv -O2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 build/temp.linux-x86_64-3.8/mmapio/mmapio.o -o build/lib.linux-x86_64-3.8/mmapio/mmapio.cpython-38-x86_64-linux-gnu.so
```

## インストール

```console
shell$ sudo python3 setup.py install
```

## 使用例

```python:sample.py
import os
import mmap
from mmapio import MemoryMappedIO

# /dev/uio0 をオープン
device_file = os.open('/dev/uio0', os.O_RDWR | os.O_SYNC)

# /dev/uio0 の メモリマップされたバッファオブジェクトを生成
device_mmap = mmap.mmap(device_file, 0x1000, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, 0)

# MemoryMappedIO オブジェクトを生成
#    MemoryMappedIO(buffer, offset, length) 
#      buffer(obj): マップするバッファオブジェクト(必須)
#      offset(int): マップするバッファオブジェクトの開始位置(デフォルトは0)
#      length(int): マップの大きさ(デフォルトはバッファオブジェクトのサイズ-offset)
regs = MemoryMappedIO(buffer=device_mmap, offset=0x0000, length=0x1000)

# オフセット 0x60 の値をワード(32bit)単位で読む
print("0x%08X" % regs.read_word(0x00))

# オフセット 0x64 にワード(32bit)単位で書く
regs.write_word(0x64,0xDEADBEAF)

# オフセット 0x64 の値をワード(32bit)単位で読む
print("0x%08X" % regs.read_word(0x64))
```

## 参考

  * [『Python と mmapio モジュール で UIO を制御』@Qiita](https://qiita.com/ikwzm/items/01a3c7d58c340db57b2c)
  * [『Python と Numpy で UIO を制御』@Qiita](https://qiita.com/ikwzm/items/d7559858d099c86c350c)
  * [『Python の mmap.read メソッドを使って arm64 の非キャッシュ領域をアクセスするときの挙動について』@Qiita](https://qiita.com/ikwzm/items/e048f94d9aef4c12e39c)