在 C 裡面動態帶進來一個 void*
,根本不知道他是什麼型別
要嘛一路 cast
,要嘛寫一堆 if/else
判斷,邏輯又醜又慢
筆者直接把「值的世界」統一起來:用一個 mongory_value
包住所有型別,配上型別標籤(type tag)與一組 function pointer(比較、字串化),matcher 就能用一致語意在 C 裡輕鬆說話
mongory_type
)comp
、to_str
),避免外部灑滿 switch(type)
pool->alloc
origin
指向原始世界(Ruby/Go)的 VALUE/handle,便於 bridge 做淺包裝typedef enum mongory_type {
MG_T_NULL,
MG_T_BOOL,
MG_T_INT,
MG_T_DOUBLE,
MG_T_STRING,
MG_T_ARRAY,
MG_T_TABLE,
MG_T_REGEX,
MG_T_PTR,
MG_T_UNSUPPORTED
} mongory_type;
typedef struct mongory_value mongory_value;
typedef int (*mongory_value_compare_func)(mongory_value *self, mongory_value *other);
typedef char *(*mongory_value_to_str_func)(mongory_value *v, mongory_memory_pool *temp_pool); // 字串化使用的暫時性 memory pool
struct mongory_value {
mongory_memory_pool *pool; // 生命週期
mongory_type type; // 型別標籤
mongory_value_compare_func comp; // 比較函式
mongory_value_to_str_func to_str; // 字串化
union {
bool b;
int64_t i;
double d;
char *s;
struct mongory_array *a;
struct mongory_table *t;
void *regex;
void *ptr;
void *u; // unsupported
} data;
void *origin; // 原始世界的指標/handle(bridge 用)
};
這個 mongory_value
的關鍵是:外界不需要知道他哪種型別,只要呼叫 v->comp(v, other)
或 v->to_str(v, pool)
,就能獲得一致行為
以 int
/double
/string
為例,包裝時就把函式指標與資料欄位設好:
mongory_value *mongory_value_wrap_i(mongory_memory_pool *pool, int64_t i) {
mongory_value *v = pool->alloc(pool, sizeof(mongory_value));
v->pool = pool;
v->type = MG_T_INT;
v->data.i = i;
v->comp = mongory_value_int_compare; // 型別專屬 compare
v->to_str = mongory_value_int_to_str; // 型別專屬 to_str
return v;
}
mongory_value *mongory_value_wrap_d(mongory_memory_pool *pool, double d);
mongory_value *mongory_value_wrap_s(mongory_memory_pool *pool, const char *s);
mongory_value *mongory_value_wrap_a(mongory_memory_pool *pool, struct mongory_array *a);
mongory_value *mongory_value_wrap_t(mongory_memory_pool *pool, struct mongory_table *t);
包裝發生在「邊界」:
comp
)各型別之間的比較在 wrap 階段就被決定
例如:
static inline int mongory_value_int_compare(mongory_value *a, mongory_value *b) {
if (b->type == MG_T_INT) return (a->data.i > b->data.i) - (a->data.i < b->data.i);
if (b->type == MG_T_DOUBLE){
double ai = (double)a->data.i, bd = b->data.d;
return (ai > bd) - (ai < bd);
// 統一 api,大於就回傳 1,小於就回傳 -1,相等則回傳 0
}
// 其他型別:視需求決定(通常當作不相等)
return mongory_value_compare_fail // 另立一個常數代表比對失敗,交給外層處理
}
string
/array
/table
也都有各自的 compare 策略:
to_str
)為了方便 debug 與 explain,筆者實作了一個簡單的 string_buffer
,專責動態串接字串並與 pool 整合to_str
就能把任何 mongory_value
變成 char*
,輸出給 trace/explain:
typedef struct mongory_string_buffer {
mongory_memory_pool *pool;
char *buffer;
size_t size;
size_t capacity;
} mongory_string_buffer;
// 省略 grow 與 append 細節
static char *mongory_value_to_str(mongory_value *v, mongory_memory_pool *pool) {
mongory_string_buffer *sb = mongory_string_buffer_new(pool);
switch (v->type) {
case MG_T_INT: mongory_string_buffer_appendf(sb, "%lld", (long long)v->data.i); break;
case MG_T_DOUBLE: mongory_string_buffer_appendf(sb, "%g", v->data.d); break;
case MG_T_STRING: mongory_string_buffer_appendf(sb, "\"%s\"", v->data.s); break;
// ... 其他型別
}
return sb->buffer; // buffer 記憶體受 pool 管理
}
matcher 只關心「值能比較」與「可以呈現字串」
例如 $gt
只需要 a->comp(a, b) == 1
,完全不在乎 a/b 的內部結構
這讓 matcher 的邏輯非常簡潔,也讓新增型別時不用去動到 matcher
mongory_value
與內部字串、緩衝都走 pool->alloc
to_str
使用的緩衝在另一個 temp pool;一個 explain/trace 階段可以放在一個臨時 pool,最後 reset
一鍵回收有了 value wrapper,matcher 就能專注在語意,dispatch 交給函式指標
寫 C 的手感變得像在寫高階語言,幾乎不必煩惱型別錯配,舒服
寫 C 的每一步,要思考的一直都是:如何讓你的下一步更舒服。
Day 12 會把容器打開:Array 與 Table 的設計,為什麼 API 要長得像 Ruby,還有排序與遍歷的細節