為C語言添加OO能力的嘗試從上世紀70年代到現在一直沒有停止過,除了大獲成的C++/Objective-C以外,還有很多其它的成功案例,比如GTK在libg中實現了一個對象系統,還有前幾年一個OOC,以及很多用宏實現的所謂輕量級OO系統。上周在網上發現了又一個自稱為OOC系統,我決定總結一下這方面的內容。 大部分面向對象系統可以分成兩類,一類是基於原型的設計,類似javascript;另一類是基於類模板的設計,比如C++/Java。當然,這不是絕對化,近幾年,在很多動態語言實現中,有很多混搭的實現,例如Dart。因為有C++的例子,基於類模板的對象系統可能對C語言程序員更自然一些,我們以此為例。這個系統要改成一個基於原型的系統也非常簡單。 對象系統中最核心的概念當然是對象。對象在C語言中沒有直接的對應成份(沒有內置於語言),我們可以選擇這麼幾種來表示對象,一是無類型的指針,二是結構體指針,三是表示為int的ID。從本質上來看,這些並沒有區別,無非是語法上簡潔和復雜。我們選擇一個結構指針類型struct object*來表示對象. 對象之間的消息傳遞,對於C/C++等命令式語言(相對於函數式語言)來說,都對應於一個函數調用。像C++語言一樣,我們可以定義一個虛表;也可以像Objective-C一樣定義一個消息轉發鏈。從實際效果上來說,都有以下過程: struct object* a; member_function* pf = find_function(a, the-function-id); pf(a, other-param); 以上偽代碼合並成一行調用,我們定義一個向對象a發送為function_id的消息,使用如下語法: interface(a)->function_id(a, other-param); 這裡引入了一個概念interface(接口),接口是一組消息的集合,對象可以接受的消息由它實現的接口定義,發送消息即變成取得相應接口,並調用接口上的函數。這個設計綜合了虛表和消息轉發鏈的設計,比較接近於COM中的接口概念。接口以類似虛表的形式定義: struct XXX_interface { struct object* kclass; int (*XXX_function)(struct object* this_object, other-param); … }; 接口實際上暗示了我們的實現是對象->接口表->接口->類(運行時信息)。借用下圖,左側藍色為對象,這兩個對象是屬於同一個類,它有一個成員_vtab指向右側黃色的一個虛表或者說是接口表,接口表有一個成員_class指向相應的類數據。接口表和類數據注冊到類型系統中,而對象由用戶分配內存。 image 有了接口概念我們可以實現接口繼承,但實現繼承需要另一個機制,我們不打算像C++選擇多重繼承,而是直接選擇更為直接的Mixup(混入)方式。我們可以通過在類型構造時直接調用mixup,傳入類型對象和mixup結構。 這裡我們會遇到為C語言添加OO支持最大的困難,我們沒有辦法在編譯期添加特性,比如構造類/生成指針表/混入實現,我們只能選擇在運行時添加一個class_init,類型初始化函數。這個問題帶來了兩個不足之處,一是有很多記簿的工作需要程序員完成;二是無法實現靜態對象。每個類型需要這樣一個初始化過程: struct klass XXXClass = { }; void XXX_init() { declare(&XXXClasss, &baseClass); XXXClasss.init = XXX_init; … struct XXX_interface i* = implement(&XXXClasss, &XXX_interface) i->function_id = some_implement_function; … mixup(&XXXClass, &XXX_mixup); … register(&XXXClass); } 這裡用到一個struct klass結構,它的作用是記錄對象的相關信息,主要內容如下: struct klass { int id; char* name; size_t object_size; struct klass* parent; size_t itable_size; struct itable* itables; void (* init) ( Class this ); /* class initializer */ void (* ctor) (Object self, const void * params ); /* constructor */ void (* dtor) (Object self, Vtable vtab); /* destructor */ int (* copy) (Object self, const Object from); /* copy constructor */ }; 當Declare這個對象時,系統開始記錄它的id/name並計算object_size。後面一系列代碼用於初始化基本的函數指針和接口表指針。最後Register這個類到系統中,用於動態類型查找。每個類這些記簿式的代碼非常類似。 前面用到的interface(a)這個函數就是通過遍歷itables來找到對應的接口虛表,接口表有反向指針指回類說明,因此可以通過一個接口來查詢其它接口。 在main函數的開始部分,需要對整個對象系統手動初始化,這可以說是一段非常不人道的代碼: int main() { object_system_init(); XXX_init(); XXX2_init(); … } 雖然我們可以聲明一個數組來完成對各個init函數的自動調用,但這個聲明過程依然非常不人道。要得到對程序員比較友好的過程,我們需要通過一個額外的源代碼分析過程,自動生成上面class_init函數,以及system_init過程。 分配一個對象,事實上只需要三步,一是找到對應的類型,二是分配空間,三是設置虛表指針。第一步,可以直接使用全局的靜態struct kclass對象,也可通過查找函數find_class("class name")來完成。第二步這步分配空間,可以由用戶完成,只需要下一步調用object_new_at(user_space, class)。如果使用系統分配空間即可由object_new一次完成二、三兩步。 //用戶分配 struct klass XXXClasss; void* ptr = malloc(XXXClass.object_size); object_new_at(ptr, &XXXClass); //系統分配 struct object* ptr = object_new(&XXXClass); 在對象初始化過程中,object_new會調用構造函數,也就是kclass中的init函數,相應的destructor/copy等函數也會在對應的object_destroy/object_copy過程中調用。 以上基本構造了一個簡單的對象系統核心,我們如果再補充一些錯誤處理、內存管理以及多線程處理,一個小型而完整的對象系統就構造出來了,但它最大問題還是語法復雜度比較高。雖然我們可以使用宏來優化語法,但效果不如人意,同時還帶來了理解上的困難。 在一些簡單的應用中,並不需要這樣一個復雜而完整的對象系統,我們更簡單的抽象甚至更好一點。一個對象可以表示如下,vtable可以指向一個函數如f(void* data); struct object { void* vtable; void* data; }; 構造和析構函數都專用函數XXX_new和XXX_destroy即可。