Real Macro Metaprogramming
on C

Shiro Kawai

Scheme Arts, LLC

C言語

システムプログラミングでは依然として主流

本物のマクロで抽象化限界突破

(!cppの偽マクロ)

題材

FileLOC
ls.h38
extern.h21
ls.c526
cmp.c73
print.c319
util.c164
1141

しかし…

3つの問題点

問題点1 - 冗長なコード

cmp.c

int namecmp(const FTSENT *a, const FTSENT *b)
{
    return (strcoll(a->fts_name, b->fts_name));
}

int revnamecmp(const FTSENT *a, const FTSENT *b)
{
    return (strcoll(b->fts_name, a->fts_name));
}

問題点1 - 冗長なコード (cont'd)

util.c

問題点2 - 非本質的な情報

int i;
   :
for (i = 0; i < (int)clen; i++)
    putchar((unsigned char)s[i]);

for (int i = [0..clen]) { putchar(...) } とか書けたらなあ

FTSENT *p;
   :
for (p = dp->list; p; p = p->fts_link) {
   :
}

foreach (FTSENT *p in dp->list) { ... } とか書けたらなあ

問題点3 - 情報の分散

ls.h

extern int f_accesstime; /* use time of last access */

ls.c (decl)

int f_accesstime; /* use time of last access */

ls.c (main)

case 'c':
    f_statustime = 1;
    f_accesstime = 0;
case 'u':
    f_accesstime = 1;
    f_statustime = 0;

匠の方針

リフォームのポイント:構文なんて飾りです

CiSE (C in S-Expression)

int
main(int argc, char *argv[])
{
    static char dot[] = ".", *dotav[] = {dot, NULL};
    struct winsize win;
    int ch, fts_options, notused;
    char *p;

    (void)setlocale(LC_ALL, "");
    if (isatty(STDOUT_FILENO)) {
        termwidth = 80;
        if ((p = getenv("COLUMNS")) != NULL && *p != '\0')
            termwidth = atoi(p);
        else if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &win) != -1 &&
                 win.ws_col > 0)
            termwidth = win.ws_col;
        f_nonprint = 1;

CiSE (C in S-Expression)

(define-cfn main (argc::int argv::char**) ::int
  (setlocale LC_ALL "")
  (cond [(isatty STDOUT_FILENO)
         (= termwidth 80)
         (let* ([p::char* (getenv "COLUMNS")]
                [win::(struct winsize)])
           (cond [(and p (!= (* p) #\null)) (= termwidth (atoi p))]
                 [(and (!= (ioctl STDOUT_FILENO TIOCGWINSZ (& win)) -1)
                       (> (ref win ws_col) 0))
                  (= termwidth (ref win ws_col))])
           (= f_nonprint 1))]

CiSE (C in S-Expression) (cont'd)

マクロ

構文木置換

(define-cise-stmt (when test . body)
  `(if ,test (begin ,@body)))

  :

(when (is_foo x) (do_this) (do_that))
   ↓
(if (is_foo x) (begin (do_this) (do_that)))

;; if (is_foo(x)) { do_this(); do_that(); }

マクロ (cont'd)

パターンの抽出/隠蔽

(dotimes [i (strlen s)] (printf "%02x" (aref s i)))
   ↓
(let* ([i::int 0] [cise__213::int (strlen s)])
  (for [() (< i cise__213) (inc! i)]
    (printf "%02x" (aref s i))))

;; {
;;    int i = 0; int cise__213 = strlen(s);
;;    for (; i < cise__213; i++) {
;;      printf("%02x", s[i]);
;;    }
;; }

マクロ (cont'd)

グローバルなコンパイル時計算

(define-enum FooMode (MODE_X MODE_Y))
  :
(gen-printer FooMode)

enum FooMode { MODE_X, MODE_Y };
  :
void FooMode_print(enum FooMode m)
{
  switch (m) {
    case MODE_X: puts("MODE_X"); break;
    case MODE_Y: puts("MODE_Y"); break;
  }
}

改造1 - 繰り返しパターンの除去

cmp.sc

(define-cmpfn namecmp
  (return (strcoll (-> a fts_name) (-> b fts_name))))
(define-cmpfn-stat modcmp  st_mtime)
(define-cmpfn-stat acccmp  st_atime)
(define-cmpfn-stat statcmp st_ctime)
(define-cmpfn-stat sizecmp st_size)

改造1 - 繰り返しパターンの除去 (cont'd)

FTSENT *p;
   :
for (p = dp->list; p; p = p->fts_link) {
   ...
}

(do-ftsent [p (-> dp list)] ...)

改造2 - 本質の括り出し

util.sc

(define-cfn prn_normal (s::(const char*)) ::int
  (let* ([n::int 0])
    (make-printer
     (putchar (cast (unsigned char) (* s)))       ; ilseq (clen == -1)
     (+= n (printf "%s" s))                       ; incomplete (clen == -2)
     (default-print)                              ; nonprintable
     (begin (default-print) (+= n (wcwidth wc)))) ; printable
    (return n)))

元のC関数: 29LOC

改造2 - 本質の括り出し (cont'd)

util.sc

(define-cfn prn_octal (s::(const char*)) ::int
  (let* ([len::int 0]
         [esc::(.array (static const char) (())) "\\\\\"\"\aa\bb\ff\nn\rr\tt\vv"])
    (make-printer
     (octal-print 1)                    ; ilseq (clen == -1)
     (octal-print (strlen s))           ; incomplete (clen == -2)
     (esc-print)                        ; nonprint
     (if (and (!= wc (cast wchar_t #\"))  ;print
              (!= wc (cast wchar_t #\\)))
       (begin (default-print) (+= len (wcwidth wc)))
       (esc-print)))
    (return len)))

元のC関数: 50LOC

改造3 - DSL

(define-flag int f_accesstime "u" "cU")  ; use time of last access
(define-flag int f_statustime "c" "uU")  ; use time of last mode change
(define-flag int f_longform   "l" "1Cxm"); long listing format
(define-flag int f_nonprint   "q" "Bbw") ; show unprintables as ?

比較

BeforeAfter
cmp.c7315
ls.c526277
print.c319183
util.c16473
1082548

CAUTION

!!注意!!

CAUTION (cont'd)

``Macro Club has two rules, plus one exception''

  1. マクロは書くな
  2. それがパターンをカプセル化する唯一の方法ならば、マクロを書け
  3. 同等の関数に比べて、呼び出し側が楽になるならば、マクロを書いても構わない

Stuart Halloway ("Programming Clojure")

匠のメッセージ

プログラマであるあなたは
万能の神
言語に使われるのではなく
言語を使い
言語と戯れよう