printfを自作してみる

printf() といえば言わずと知れたC言語のフォーマット型の
文字列出力関数です。

Cを初めて学んだその日から常にお世話になる関数ですが、
一体 printf() の中ではどんな処理をしているのか勉強するため
実際に作ってみます。

ソースコードは こちら からダウンロードできます。

目標

本家printf() のめぼしい機能を大体実現する。

出力変換指定子

printfの出力変換指定は以下の通りです。

%[flags][width][precision][modifier]type

〜〜今回対応した指定子〜〜

flags

出力時の形式をフラグ式に決定します。

  • ・・・ 出力データより width が大きい倍左詰めで出力する
  • ・・・ 正の数値の先頭に + を表示する
    [空白 ] ・・・ (空白) + と同様の効果
    # ・・・ 型を明示して数値を出力(例 16進数なら頭に 0x がつく)
    , ・・・ 整数で3桁ごとにカンマで区切る

width

(数字) ・・・ 出力全体の桁数を指定する。 先頭に0をつけると余白を0で埋める。

  • ・・・ 引数で渡された値を width として使用する

precision

(数字) ・・・ 数値の出力の桁数を指定する。 先頭に0をつけると余白を0で埋める。

  • ・・・ 引数で渡された値を width として使用する

modifier

hh・・・ 引数が char型であると明示
h ・・・ 引数が short型であると明示
l ・・・ 引数がlong型であると明示
ll ・・・ 引数がlong long型であると明示
j ・・・ 引数がintmax型に収まると明示
z ・・・ 引数がsize_t型に収まると明示
t ・・・ 引数が ptrdiff_t型に収まると明示

type

d,i ・・・ 符号付き10進数
u ・・・ 符号なし10進数
x ・・・ 符号なし16進数 a-f 表記
X ・・・ 符号なし16進数 A-F表記
o ・・・ 符号なし8進数
s ・・・ ヌル終端文字列ポインタを用いて文字列出力
c ・・・ int値を文字として出力
p ・・・ ポインタ値を出力

%% ・・・ % を表示

double/float 関連以外はだいたい網羅されています。

実装

まずはサブ関数をいくつか作ります。
今回は標準関数をできるだけ使わない方針でいきたいので
strchr() と同等な mystrchr を用意します。

char * mystrchr(const char *s, int c)
{
    char ch = (char) c;
    while (*s) {
        if (*s == ch)
            return (char *) s;
        s++;
    }
    return NULL;
}

続いて

以下のような列挙体を定義しておきます。
型のサイズを大きい順に並べてあります。

typedef enum INTEGER_t {
    LL =  2,    //Long Long
    L  =  1,    //Long
    I  =  0,    //Int
    S  = -1,    //Short
    C  = -2     //Char
} INTEGER;

この型サイズを指定して可変個引数のリストから
指定した型のデータをとってくる関数を用意しておきます。
signed/unsignedの2種類を用意しておきます。

static long long get_signed(va_list ap, INTEGER type)
{
    INTEGER t; t = type;
    if(t >=  LL) t = LL;
    if(t <=   C) t = C;

        switch(t)
        {
          case LL: return va_arg(ap, long long);         break;
          case  L: return va_arg(ap, long);              break;
          case  I: return va_arg(ap, int);               break;
          case  S: return (short) va_arg(ap, unsigned ); break;
          case  C: return (signed char) va_arg(ap, unsigned );  break;
        }

        return (signed char) va_arg(ap, unsigned );
}

static unsigned long long get_unsigned(va_list ap, INTEGER type)
{
    INTEGER t; t = type;
    if(t >=  LL) t = LL;
    if(t <=   C) t = C;

        switch(t)
        {
          case LL: return va_arg(ap, unsigned long long);         break;
          case  L: return va_arg(ap, unsigned long);              break;
          case  I: return va_arg(ap, unsigned int);               break;
          case  S: return (unsigned short) va_arg(ap, unsigned ); break;
          case  C: return (unsigned char) va_arg(ap, unsigned );  break;
        }

        return (unsigned char) va_arg(ap, unsigned );
}

最後に 整数型のデータを出力する
put_integer() を作ります。

static void put_integer(void (*__putc)(int), unsigned long long n, int radix, int length, char sign, int flags)
{
    static char *symbols_s = "0123456789abcdef";
    static char *symbols_c = "0123456789ABCDEF";
    char buf[80];
    int i = 0;
    int pad = ' ';
    char *symbols = symbols_s;

    if(flags & CAPITAL_LETTER) symbols = symbols_c;
    do {
        buf[i++] = symbols[n % radix];
        if( (flags & THOUSAND_GROUP) && (i%4)==3) buf[i++] = ',';
    } while (n /= radix);

    length -= i;

    if (!(flags & LEFT_JUSTIFIED)) {

        if(flags & ZERO_PADDING) pad = '0';
        while (length > 0) { length--; buf[i++] = pad;}
    }

    if (sign && radix == 10) buf[i++] = sign;
    if (flags & ALTERNATIVE)
    {
        if (radix == 8) buf[i++] = '0';
        else if (radix == 16)
        {
           buf[i++] = 'x';
           buf[i++] = '0';
        }
    }

    while (      i > 0 ) { __putc(buf[--i]);      }
    while ( length > 0 ) { length--; __putc(pad); }

}

引数は
1: 1文字出力関数putc への関数ポインタ
2: 表示したい数値(正)
3: 基数
4: 表示する長さ
5: 符号(+/-)
6: フラグ
です。
フラグは

#define    ZERO_PADDING         (1<<1)
#define    ALTERNATIVE          (1<<2)
#define    THOUSAND_GROUP       (1<<3)
#define    CAPITAL_LETTER       (1<<4)
#define    WITH_SIGN_CHAR       (1<<5)
#define    LEFT_JUSTIFIED       (1<<6)

こんなかんじです。

put_integer() では
まず最初の do-whileで itoa() 関数のように与えられた数値を文字列に変換して
バッファに保存します。この時、低い位から調べていくため、
バッファ内の文字列は前後が入れ替わって入ります。

例) 0x523a do-while部終了時

buf[0] = a
buf[1] = 3
buf[2] = 2
buf[3] = 5 (i=3)

となります。ちなみに簡易的な対応で THOUSAND_GROUP のフラグが有効な
場合3文字おきにカンマが挿入されるようにしてあります。

次に左寄せのフラグがない場合で、
表示桁数の指定がある場合にパッディングが必要であれば
挿入します。

do-whileで増加させた i は現在の文字数に等しいので
(length - i) が必要なパディングの文字数です。
ZERO_PADDINGのフラグでパディングにスペースを使うか
0 を使うかを切り替えています。

ここまでで数値の配置が完了したので
最後に符号等の処理です。
10進数で符号の指定がある場合は 符号を追加
8/16進数で ALTERNATIVEのフラグが立っている場合は
先頭に 0 / 0x を追加します。

例 "%05x" , 0x523a

buf[0] = a
buf[1] = 3
buf[2] = 2
buf[3] = 5
buf[4] = 0
buf[5] = x
buf[6] = 0 (i=6)

これで buf[] が綺麗に整列したので i を小さくしながら一文字ずつ
表示すれば画面に
 0x0523a
と表示されます。

LEFT_JUSTIFIED のフラグが立っている場合はまだ
length を消化していないので、表示幅に残りがあれば表示します。

これで必要なパーツは揃いました。

void myvprintf(void (__putc)(int), const char fmt, va_list ap)

サブ関数ができたので本丸も実装します。
基本的に 素直に行きます。

myvprintf が myprintfの本体です。
まずは

        while (*fmt && *fmt != '%') __putc(*fmt++);
        if (*fmt == '\0') { va_end(ap); break; }
        fmt++;

文字列の末端 or % が来るまでは素直に一文字ずつ表示していきます。
末端(\0) であれば終了してループを抜けます。
% が来たら fmt++; で%を読み飛ばして オプションの解析に入ります。

まずは flags を読み取ります。

        while (mystrchr("'-+ #0", *fmt)==*) {
            switch (*fmt++) {
                  case '\'': flags |= THOUSAND_GROUP;             break;
                  case  '-': flags |= LEFT_JUSTIFIED;             break;
                  case  '+': flags |= WITH_SIGN_CHAR; sign = '+'; break;
                  case  '#': flags |= ALTERNATIVE;                break;
                  case  '0': flags |= ZERO_PADDING;               break;
                  case  ' ': flags |= WITH_SIGN_CHAR; sign = ' '; break;
            }
        }

先頭の文字に flags関連の文字があれば switchでフラグを立てていきます。

続いて widthとprecisionです。
引数の数値(*)で指定された時は 引数と見に行き、直接数値が指定されていれば
その数値を length と precision にそれぞれ保存します。

        if(*fmt == '*'){ length = va_arg(ap,int); fmt++; }
        else {  while( _isnumc(*fmt) ) length = (length*10)+_ctoi(*fmt++);  }

        if (*fmt == '.')
        {
            fmt++;
            if (*fmt == '*'){ fmt++; precision = va_arg(ap, int);}
            else { while (_isnumc(*fmt) ) precision = precision * 10 + _ctoi(*fmt++); }
        }

ここで2つマクロを使用しています。

#define _isnumc(x) ( (x) >= '0' && (x) <= '9' )
#define _ctoi(x)   ( (x) -  '0' )

isnumc は x が文字コードで '0'〜'9' の間であるかを調べるもので
ctoi は x - '0' で文字コードと数値を変換しています。

ASCIIコードの数値は
'0' = 0x30
'1' = 0x31
'2' = 0x32
'3' = 0x33
・・・
'9' = 0x39

と連番になっているので (文字コード - '0' ) で数値と変換できます。

一文字読み取ってはもう一周するので
もし2桁以上が指定されていた場合は 以前の数値を10倍 します。

例 "%12x"

一周目
length = ctoa('1') = 1

二周目
length = 1 x 10 + ctoa('2') = 12

これで長さが取得できました。
コンマを挟んで precisionも同様に長さを取得します。

続いて
●modifier
は flags と同じようにできます。
 

        while (mystrchr("hljzt", *fmt)) {
            switch (*fmt++) {
                  case 'h': int_type--;  break;
                  case 'l': int_type++;  break;
                  case 'j': /*intmax   : long      */
                  case 'z': /*size     : long      */
                  case 't': /*ptrdiff  : long      */
                            int_type=L;  break;
            }
        }

ここでちょっとしたテクニックを使っています。
l,h は2つ並べてより大きな(小さな)型を指すので
enumで宣言した INTEGER int_type を加算することで
型の大きさを行き来するようにしてまとめてあります。

例) %ll
int_type = 0 = I(アイ) ・・・初期化

l(エル) , l(エル) → int_type++, int_type++

int_type = 2 = LL

これで特殊なオプションの処理が終わりました。
最後は普通に x や dといった type が指定されているはずなので

    switch (*fmt) {

            case 'd':
            case 'i':
                i = get_signed(ap, int_type);
                if (i < 0) { i = -i; sign = '-'; }
                put_integer(__putc, i, 10, length, sign, flags);
                break;

            case 'u':
                ui = get_unsigned(ap, int_type);
                put_integer(__putc,  ui, 10, length, sign, flags);
                break;

            case 'o':
 ・・・以下略

符号などに注意してそれぞれのタイプを表示してやればOKです。

注意が必要なのは ポインタ(%p) です。
32bit/64bit 環境で必要な桁数が違います。
32bitでは 8桁、64bitでは16桁が必要ですが、
コンパイル時にいちいち指定するのは面倒なのでちょっと
テクニックを使います。

Unix系のOSでは データモデルとして
32bitでは ILP32
64bitでは LP64
を採用しています。
このデータモデルだと
int などは同じ大きさですが

ILP32 long=32bit sizeof(long)=4
LP64 long=64bit sizeof(long)=8

が違います。
これを用いることで

            case 'p':
                length = sizeof(long) * 2;
                int_type = L;
                sign = 0;
                flags = ZERO_PADDING | ALTERNATIVE;

こうするだけで32bit/64bit どちらでも自動で最適な桁数になります。
(x86系以外では使えない場合もあるので注意)

以上で実装完了です。
実際にテストしてみます。
myvprintf() は 一文字出力関数への関数ポインタをとるので
putc(c, stdout) をラップした myputc を用意して
myprintf() を完成させます。

test.c

#include 
#include 

#include "myvprintf.h"

void myputc(int c)
{
    putc(c, stdout);
}

void myprintf(const char *fmt, ...)
{
        va_list ap;
        va_start(ap, fmt);
        myvprintf(myputc,fmt, ap);
        va_end(ap);
}

void main()
{
    char *msg = "hello world!";

    myprintf("%s\n", msg);
    myprintf("%'d\n", 1000000);
    myprintf("%07x\n", 21050);
    myprintf("%#x\n", 255);
    myprintf("%#X\n", 255);
}

このとおり、ちゃんと動作しているようです。
この myvfprintf() は 一文字出力関数だけ用意すれば
動くので

コピーするだけで色々な環境に組み込めます。

普段お世話になっている printf ですが、自分で実装するとなると
大変ですね。。。
double/fload も処理しようとすると大変なことになりそうです。。。

ソースコードは こちら からダウンロードできます。

コメントを追加する