読者です 読者をやめる 読者になる 読者になる

YAMAGUCHI::weblog

土足で窓から失礼いたします。今日からあなたの息子になります。 当年とって92歳、下町の発明王、エジソンです。

C/APIで文字列操作をして色々ハマったこと

はじめに

こんにちは、Python界の情弱です。最近C/APIを使ってモジュールを書くブームがきているわけですが、まあPythonで文字列を扱うのが大学3年の嫌々やった演習以来ということで全然出来ない。というわけでハマったところをメモしました。特にメモリ周りはC/APIならではのポイントも有りました。

危険なコード

次のコードが色々と危険をはらんでいるのでキチンと対応する必要があると教わった。

buffer = realloc(buffer, old_size * 2);

reallocに関する注意

失敗する可能性

reallocは失敗するとNULLになってしまい、その時realloc前にmallocされた領域は解放されないままになってしまうので、freeする術がなくなってしまう。必ず戻り値をチェックすること。まして上記のようなコードにしてしまうと、もとのアドレス触れなくなってしまうので絶対ダメ。

Integer Overflow

id:moriyoshi が指摘してくれた。64ビットだとポインタサイズが余裕でメモリサイズの上限を超える*1ので大丈夫だけど、32ビット(4*10^9)だともしかすると超えちゃう。
integer overflowするとbuffferがもとのサイズよりも小さくなってしまうのでmemcpyでバッファオーバーフローする。議論としてはじゃあバッファオーバーフローしないようにリサイズする場合どのくらい確保するのがいいの?ってのがあるけど、1.5倍にするか2倍にするかという議論もあるらしい。

1.5倍で取っていくならこんな感じだと。(by id:moriyoshi)2倍が取ればその分メモリ確保する回数が減って、リサイズの際のコピーのコストも下がるけど、その分メモリ消費量も増えるっていうことで、トレードオフ。(by id:nishiohirokazu)

const size_t new_size = old_size + (old_size >> 1);
if (new_size < old_size)
    return ERROR;
void *new_buffer = realloc(buffer, new_size);
if (!new_buffer)
    return ERROR;
C/API使うならPythonのarena使わないと損 (by id:mopemope)

C書くときにメモリ管理といったらmalloc/freeを大量に書くことになる*2と思いますが、PythonでC/API書くならPyMem_Malloc/PyMem_Freeを使うべきである、という話。Pythonでのヒープはある程度のサイズをボコっと取ってくれるのですが、そのときにarenaという単位で取得する方が吉みたい。

typedef struct {
    char* buf;
    size_t size;
    siez_t len;
} buffer;

buffer*
new_buffer(size_t len) 
{
    buffer* buf;
    buf = (buffer*)PyMem_Malloc(sizeof(buffer));
    if (buf == NULL) {
        PyErr_NoMemory();
        return NULL;
    }

    buf->buf = (char*)PyMem_Malloc(sizeof(char) * len * 2);
    if (buf->buf == NULL) {
        PyErr_NoMemory();
        PyMem_Free(buf);
        return NULL;
    }

    buf->size = size;
    buf->len = 0;

    return buf;
}

int
realloc_buffer(buffer* buf, size_t new_size)
{
    buffer* new_buf;
    if (buf == NULL) {
        PyErr_SetString(PyExc_MemoryError, "argument 'buf' is NULL");
        return 1;
    }

    new_buf = (char*)PyMem_Realloc(buf->buf, sizeof(char) * new_size);
    if (new_buf == NULL) {
        PyErr_Format(PyExc_MemoryError, "realloc of %p failed", buf->buf);
        PyMem_Free(buf->buf);
        buf->buf = NULL;
        buf->size = buf->len = 0;
        PyMem_Free(buf);
        return 1;
    }

    buf->buf = new_buf;
    buf->size = new_size;
    return 0;
}
PyArg_ParseTupleでは#付きのフォーマットがいい

ここにある通りなんですが、Cで文字列操作していると終端文字が気になりますね。Python側から関数の引数で文字列を受け取るときはPyArg_ParseTupleを使うわけですが、ドキュメントを見てみると次のように載っていました。

Python の文字列または Unicode オブジェクトを、キャラクタ文字列を指す C のポインタに変換します。 変換先の文字列自体の記憶領域を提供する必要はありません; キャラクタ型ポインタ変数のアドレスを渡すと、すでに存在している 文字列へのポインタをその変数に記録します。C 文字列は NUL で終端されています。

というわけでPyArg_ParseTupleで取ってくると文字列の場合受け取ったchar*は必ず終端文字は'\0'になっているとのこと。
また上のリンクを見るとPyArg_ParseTupleへ渡すフォーマット文字列で文字列を受け取る場合普通に英字だけのもの(s, u, z, et)とその後ろに#がついたものがあって、後者の場合長さも同時に取れるのでいちいち自分でstrlen()とかやらずにすんで楽ですね。

*1:1.8*10^19

*2:あるいはそれのラッパを使う