asprintf

2012.10.17 (2012.11.05更新)

1. はじめに

asprintf(3) という関数があります。Ubuntu 12.04 の man によると、そのプロトタイプおよび機能は以下のとおりです。

SYNOPSIS
      #define _GNU_SOURCE
      #include <stdio.h>

      int asprintf(char **strp, const char *fmt, ...);

DESCRIPTION
      The  functions asprintf() and vasprintf() are analogs of sprintf(3) and
      vsprintf(3), except that they allocate a string large  enough  to  hold
      the output including the terminating null byte, and return a pointer to
      it via the first argument.  This pointer should be passed to free(3) to
      release the allocated storage when it is no longer needed.

注目すべきポイントとして以下の点があります。

  • 出力を書き込むバッファは、asprintf(3) 自体が用意します。
  • 必要なバッファの長さを、事前に asprintf(3) に教える必要はありません。
  • バッファオーバフローが発生しないよう、asprintf(3)がバッファの長さをよろしく調整してくれます。

必要なバッファの長さを事前に知る事が困難な場合、プログラマにとっては便利に使える関数です。

ただしこの関数は GNU 拡張であり、どの環境でも使えるとは限りません。glibc を使用している各種 Linux ディストリビューション、および *BSD では利用可能です。

2. undefined reference to `asprintf’

先日社内某所で、”あるプログラムをある OS 上でコンパイルしようとしたところ、asprintf(3) が無くてコンパイルできなかった。” という話を聞きました。

このような場合、asprintf(3) を他の関数で置き換える必要がありますが、この置き換えはそれほど単純な話ではありません。

素朴に考えると、以下のような実装が出来れば良いことになります。

int my_asprintf(char **strp, const char *fmt, ...)
{
    unsigned int buffer_len = XXX;
    char *buffer;
    int rtn_code;
    va_list ap;

    buffer = malloc(buffer_len);
    if (buffer) {
        va_start(ap, fmt);
        rtn_code = vsnprintf(buffer, buffer_len, fmt, ap);
        va_end(ap);
        *strp = buffer;
        return rtn_code;
    }
    else {
        ....
    }
 }

buffer_len はどうしましょうか・・・。短すぎると出力が尻切れトンボになってしまい、長すぎると単なるメモリの無駄遣いです。この実装はどうやらスジが悪いようです。

glibc や *BSD ではどうやってこの問題に対応しているのでしょうか?

ちょっと調べてみましょう。

3. OpenBSD の場合

まずは OpenBSD の状況を見てみましょう。大抵の場合/usr/src に展開済みだと思いますので、そちらを参照します。目指すソースファイルは /usr/src/lib/libc/stdio/asprintf.c です。

27 int
28 asprintf(char **str, const char *fmt, ...)
29 {
30         int ret;
31         va_list ap;
32         FILE f;
33         struct __sfileext fext;
34         unsigned char *_base;
35
36         _FILEEXT_SETUP(&f, &fext);
37         f._file = -1;
38         f._flags = __SWR | __SSTR | __SALC;
39         f._bf._base = f._p = (unsigned char *)malloc(128);
40         if (f._bf._base == NULL)
41                 goto err;
42         f._bf._size = f._w = 127;               /* Leave room for the NUL */
43         va_start(ap, fmt);
44         ret = __vfprintf(&f, fmt, ap);
45         va_end(ap);
46         if (ret == -1)
47                 goto err;
48         *f._p = '\0';
49         _base = realloc(f._bf._base, ret + 1);
50         if (_base == NULL)
51                 goto err;
52         *str = (char *)_base;
53         return (ret);
54
55 err:
56         if (f._bf._base) {
57                 free(f._bf._base);
58                 f._bf._base = NULL;
59         }
60         *str = NULL;
61         errno = ENOMEM;
62         return (-1);
63 }

バッファは、いったん128byteで確保しているようです。38行目で設定しているフラグ __SSTR と __SALC は、/usr/src/include/stdio.h で定義されています。

156 #define __SSTR  0x0200          /* this is an sprintf/snprintf string */

161 #define __SALC  0x4000          /* allocate string space dynamically */

バッファの動的拡張は、/usr/src/lib/libc/stdio/fvwrite.c の __sfvwrite 関数で実現されています。__sfvwrite はいろいろな出力関数から使用される、よろず出力関数といった立ち位置にいるようです。

 41 /*
 42  * Write some memory regions.  Return zero on success, EOF on error.
 43  *
 44  * This routine is large and unsightly, but most of the ugliness due
 45  * to the three different kinds of output buffering is handled here.
 46  */
 47 int
 48 __sfvwrite(FILE *fp, struct __suio *uio)
 49 {

104                         GETIOV(;);
105                         if ((fp->_flags & (__SALC | __SSTR)) ==
106                             (__SALC | __SSTR) && fp->_w < len) {
107                                 size_t blen = fp->_p - fp->_bf._base;
108                                 unsigned char *_base;
109                                 int _size;
110
111                                 /* Allocate space exponentially. */
112                                 _size = fp->_bf._size;
113                                 do {
114                                         _size = (_size << 1) + 1;
115                                 } while (_size < blen + len);
116                                 _base = realloc(fp->_bf._base, _size + 1);
117                                 if (_base == NULL)
118                                         goto err;
119                                 fp->_w += _size - fp->_bf._size;
120                                 fp->_bf._base = _base;
121                                 fp->_bf._size = _size;
122                                 fp->_p = _base + blen;
123                         }

106行目の len には何の値が入っているのでしょう?

答えは、104行目の GETIOV(;); にあります。

68         iov = uio->uio_iov;
69         p = iov->iov_base;
70         len = iov->iov_len;
71         iov++;
72 #define GETIOV(extra_work) \
73         while (len == 0) { \
74                 extra_work; \
75                 p = iov->iov_base; \
76                 len = iov->iov_len; \
77                 iov++; \
78         }

__sfvwrite には struct __suio * 型のオブジェクトが引数として渡されます。このオブジェクトの uio_iov というメンバには、出力すべき部分文字列が I/O vector 形式で設定されているので、次に出力すべき部分文字列の長さを調べつつ、バッファが足りなかったら拡張するという流れになります。

printf フォーマット文字列から、出力すべき部分文字列への変換は、/usr/src/lib/libc/stdio/vfprintf.c の __vfprintf 関数で実現されています。

__vfprintf は、以下に示す PRINT マクロ等を利用して、変換文字列(‘%’) 毎に、出力すべき部分文字列を I/O vector として構築しています。

355 #define PRINT(ptr, len) do { \
356         iovp->iov_base = (ptr); \
357         iovp->iov_len = (len); \
358         uio.uio_resid += (len); \
359         iovp++; \
360         if (++uio.uio_iovcnt >= NIOV) { \
361                 if (__sprint(fp, &uio)) \
362                         goto error; \
363                 iovp = iov; \
364         } \
365 } while (0)

4. glibc の場合

次に glibc の状況です。大抵の場合 glibc のソースツリーは手元に無いと思いますので、本家から落してきましょう。

$ git clone git://sourceware.org/git/glibc.git

目指すファイルは、stdio-common/asprintf.c です。

22 #define vasprintf(s, f, a) _IO_vasprintf (s, f, a)
23 #undef __asprintf

28 int
29 ___asprintf (char **string_ptr, const char *format, ...)
30 {
31   va_list arg;
32   int done;
33
34   va_start (arg, format);
35   done = vasprintf (string_ptr, format, arg);
36   va_end (arg);
37
38   return done;
39 }
40 ldbl_hidden_def (___asprintf, __asprintf)
41
42 ldbl_strong_alias (___asprintf, __asprintf)
43 ldbl_weak_alias (___asprintf, asprintf)

ちょっと複雑ですが、_IO_vasprintf が呼ばれるようです。_IO_vasprintf は、libio/vasprintf.c に定義されています。

34 int
35 _IO_vasprintf (result_ptr, format, args)
36      char **result_ptr;
37      const char *format;
38      _IO_va_list args;
39 {
40   /* Initial size of the buffer to be used.  Will be doubled each time an
41      overflow occurs.  */
42   const _IO_size_t init_string_size = 100;
43   char *string;
44   _IO_strfile sf;
45   int ret;
46   _IO_size_t needed;
47   _IO_size_t allocated;
48   /* No need to clear the memory here (unlike for open_memstream) since
49      we know we will never seek on the stream.  */
50   string = (char *) malloc (init_string_size);
51   if (string == NULL)
52     return -1;
53 #ifdef _IO_MTSAFE_IO
54   sf._sbf._f._lock = NULL;
55 #endif
56   _IO_no_init (&sf._sbf._f, _IO_USER_LOCK, -1, NULL, NULL);
57   _IO_JUMPS (&sf._sbf) = &_IO_str_jumps;
58   _IO_str_init_static_internal (&sf, string, init_string_size, string);
59   sf._sbf._f._flags &= ~_IO_USER_BUF;
60   sf._s._allocate_buffer = (_IO_alloc_type) malloc;
61   sf._s._free_buffer = (_IO_free_type) free;
62   ret = _IO_vfprintf (&sf._sbf._f, format, args);
63   if (ret < 0)
64     {
65       free (sf._sbf._f._IO_buf_base);
66       return ret;
67     }

57行目では、_IO_JUMPS というマクロを使用して、_IO_str_jumps というジャンプテーブルらしきものを設定しています。_IO_str_jumps は、libio/strops.c で定義されています。

348 const struct _IO_jump_t _IO_str_jumps =
349 {
350   JUMP_INIT_DUMMY,
351   JUMP_INIT(finish, _IO_str_finish),
352   JUMP_INIT(overflow, _IO_str_overflow),
353   JUMP_INIT(underflow, _IO_str_underflow),

60行目では、sf._s._allocate_buffer に malloc が設定されています。これが使用されているところを検索してみると、libio/strops.c の _IO_str_overflow にそれらしきコード片があります。

 90 int
 91 _IO_str_overflow (fp, c)
 92      _IO_FILE *fp;
 93      int c;
 94 {

105   pos = fp->_IO_write_ptr - fp->_IO_write_base;
106   if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
107     {
108       if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
109         return EOF;
110       else
111         {
112           char *new_buf;
113           char *old_buf = fp->_IO_buf_base;
114           size_t old_blen = _IO_blen (fp);
115           _IO_size_t new_size = 2 * old_blen + 100;
116           if (new_size < old_blen)
117             return EOF;
118           new_buf
119             = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
120           if (new_buf == NULL)
121             {
122               /*          __ferror(fp) = 1; */
123               return EOF;
124             }
125           if (old_buf)
126             {
127               memcpy (new_buf, old_buf, old_blen);
128               (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
129               /* Make sure _IO_setb won't try to delete _IO_buf_base. */
130               fp->_IO_buf_base = NULL;
131             }
132           memset (new_buf + old_blen, '\0', new_size - old_blen);

_IO_vasprintf からは IO_vfprintf が呼ばれます。そこから先は良く分らなかったので想像ですが、更にその先のどこかで変換文字列(%)毎に出力文字列の長さが計算され、バッファの空きスペースと比較が行われます。

バッファの空きスペースが足りないと、ジャンプテーブル経由で _IO_str_overflow が実行され、バッファの動的拡張が行われるのではないかと思います。

5. まとめ

asprintf(3) がどのように実現されているのか、OpenBSD と glibc のソースを調べてみました。分った事をまとめます。

  • OpenBSD の実装では、変換文字列(%)の解釈・変換と同時に、必要に応じてバッファの動的拡張が行われる。
  • Linux の実装の詳細は不明だが、File 入出力の亜種として実装されている。(_IO_strfile) おそらく他の File 入出力コードとの共通化が図られている。(_IO_JUMPS マクロ)
  • sprintf(3) 等を使用して、asprintf(3) を正しく実装するのは困難である。
  • glibc のコードはゴチャゴチャして読み難い。インデントが気持ち悪い。プリプロセッサ使いすぎ。etc ...

以上です。

著者プロフィール

hkoba

組み込み系 (Linux/NetBSD/VxWorks) からネットワークプログラミングまで幅広く担当。普段は OpenBSD で生活しています。社内での愛称はローズ・S・コバヤシ。「ローズさん」と呼ばれることが多いです。USB ドライバ(ホスト側・デバイス側両方)や X Window System あたりは結構やりました。※ この項目はインタビューをもとに、編集担当が作成しました。

記事一覧Index