Friday, March 4, 2011

where are the functions defined?

0.  Token Concatenation

在 C 代码中,宏定义中可以进行"文本"层面上的变换,比如在 systemd 的链表实现 (类似 Linux kernel 的链表) 中有这样的用法,
#define LIST_FIELDS(t,name)                                             \
        t *name##_next, *name##_prev
"##" 能完成所谓的 Token Concatenation, 具体解释看 gnu.org 的手册, 和维基百科,简单的说,## 能把前后两个 token 连为一个。上边定义的 LIST_FIELDS 可以这样使用,
struct Job {
        LIST_FIELDS(Job, transaction);
        LIST_FIELDS(Job, run_queue);
}
编译的时候就能扩展为
struct Job {
        Job *transaction_next, *transaction_prev;
        Job *run_queue_next, *run_queue_prev;
}
这样确实很聪明,也很省事。但是如果你不熟悉这种用法,初读代码会一头雾水,因为遇到下边这样的代码时,根本找不到这些 _next 和 _prev 变量的定义,无论是用 cscope 还是 grep。
assert(!j->transaction_next);
assert(!j->transaction_prev);

1. 没有定义的 to_string 和 from_string 函数

最近看 systemd 代码,遇到了一个叫 log_target_from_string 的函数。某个头文件有它的声明,
LogTarget log_target_from_string(const char *s);
但是却找不到它的定义。最后发现是这样的,
/* src/log.c */

static const char *const log_target_table[] = {
        [LOG_TARGET_CONSOLE] = "console",
        [LOG_TARGET_SYSLOG] = "syslog",
        [LOG_TARGET_KMSG] = "kmsg",
        [LOG_TARGET_SYSLOG_OR_KMSG] = "syslog-or-kmsg",
        [LOG_TARGET_NULL] = "null",
        [LOG_TARGET_AUTO] = "auto"
};

DEFINE_STRING_TABLE_LOOKUP(log_target, LogTarget);

/* src/util.h */

#define DEFINE_STRING_TABLE_LOOKUP(name,type)                           \
        const char *name##_to_string(type i) {                          \
                if (i < 0 || i >= (type) ELEMENTSOF(name##_table))      \
                        return NULL;                                    \
                return name##_table[i];                                 \
        }                                                               \
        type name##_from_string(const char *s) {                        \
                type i;                                                 \
                unsigned u = 0;                                         \
                assert(s);                                              \
                for (i = 0; i < (type)ELEMENTSOF(name##_table); i++)    \
                        if (name##_table[i] &&                          \
                            streq(name##_table[i], s))                  \
                                return i;                               \
                if (safe_atou(s, &u) >= 0 &&                            \
                    u < ELEMENTSOF(name##_table))                       \
                        return (type) u;                                \
                return (type) -1;                                       \
        }                                                               \
        struct __useless_struct_to_allow_trailing_semicolon__
也就是说有一个通用的宏 DEFINE_STRING_TABLE_LOOKUP,它提供了 to_string 和 from_string 这两个函数的“模板”。

systemd 需要进行很多的 enum 数值和对应的字符串之间的转换,这些对应关系都是定义在一个个的字符串数组中,比如上面的 log_target_table。转换操作其实很简单,无非就是在数组中进行查找,但是代码树里有很多这样的数组,也有很多的 enum 类型。如果要给每个数组都定义自己的 to_string 和 from_string 函数,那肯定是一大坨重复代码。

使用 token concatenation 显然是一个很聪明的办法,达到的效果跟 C++ 的模板机制是类似的,能够大幅度地减少代码的重复。DRY! (C vs. C++... 不敢妄下评论,you be the judge)。

(定义一个通用的查找函数也许是另一种办法)。