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 ...
以上です。