天天看點

cJSON庫源碼分析

cjson是一個超輕巧,攜帶友善,單檔案,簡單的可以作為ansi-c标準的json格式解析庫。

那什麼是json格式?這裡照搬度娘百科的說法:

json(javascript object notation) 是一種輕量級的資料交換格式。它基于javascript(standard ecma-262 3rd edition – december 1999)的一個子集。json采用完全獨立于語言的文本格式,但是也使用了類似于c語言家族的習慣(包括c, c++, c#, java, javascript, perl, python等)。這些特性使json成為理想的資料交換語言。易于人閱讀和編寫,同時也易于機器解析和生成。

更加詳細的解釋和示例請檢視 http://www.json.org/ 首頁。

其實簡單說,json就是一種資訊交換格式,而cjson其實就是對json格式的字元串進行建構和解析的一個c語言函數庫。

可以在這個位址下載下傳到cjson的源代碼: http://sourceforge.net/projects/cjson/

__macosx目錄是提供給mac os的源碼,我的機器運作的是fedora 18,是以選擇另外一個目錄即可。

簡單的閱讀下readme檔案,先學習cjson庫的使用方法。若是連庫都還不會使用,分析源碼就無從談起了。通過簡單的了解,我們得知cjson庫實際上隻有cjson.c和cjson.h兩個檔案組成,絕對輕量級。

不過,代碼風格貌似有點非主流,先用indent格式化一下代碼吧。我個人喜歡k&r風格的代碼,使用的indent指令行參數如下:

格式化之後,代碼結構看起來清晰多了。

那麼,從何處下手來分析呢?打開代碼檔案逐行閱讀麼?當然不是了,有main函數的程式大都是從main函數開始分析,那麼沒有main函數的純函數庫呢?那就自己寫main函數呗。

cjson作為json格式的解析庫,其主要功能無非就是建構和解析json格式了,我們先寫一個建構json格式字元串的程式,盡可能的使其用到的類型多一點(事實上readme檔案裡提供了不錯的示例代碼,我們直接借鑒一下吧)。代碼如下:

編譯運作後(編譯時注意要連結數學庫,參數行要加 -lm),運作結果如下:

打開cjson.h這個頭檔案,我們可以看到每一個節點,實際上都是由cjson這個結構體來描述的:

結合這個結構體和上面相關api的調用,其實我們大概可以猜測出cjson對于json格式的描述和處理的方法了:

每一個cjson結構都描述了一項”鍵-值”對的資料,其中next和prev指針顯然是指向同級前後的cjson結構,而child指針自然是指向孩子節點的cjson結構。type類型顯然是為了區分值的類型而設定的,在cjson.h檔案一開始就定義了這些類型的值:

很顯然通過檢測這裡的type字段,就很容易知道該節點的類型以及其實際存儲資料的字段了。其它的字段是什麼意思呢?cjson.h檔案裡的注釋說的很明白了,valueint,valuedouble以及valuestring儲存的是相應的值,string存放的是本字段的名字。

接下來分析程式的執行過程,編譯參數加上-g,使用gdb調試程式,畫出整個構造過程的函數調用圖。具體的調試過程就不細說了,我撿一些關鍵點說說:

調試過程中,我們發現cjson_addstringtoobject()等其實是宏定義,本質上調用的都是cjson_additemtoobject()函數,在cjson.h檔案中可以看到如下定義:

另外cjson_createnull()等函數都是調用cjson_new_item()函數申請到初始化為0的空間構造相關的節點資訊。構造過程中的函數調用圖如下:

cJSON庫源碼分析

構造的json字元串最終在記憶體中形成的結構如下圖所示:

cJSON庫源碼分析

構造過程相對來說比較簡單,數組類型這裡沒有涉及到,但是分析起來也很簡單。

我們最後調用cjson_print()函數生成這個結構所對應的字元串。生成說起來容易,周遊起整個結構并進行字元串格式控制卻比較繁瑣。這裡相關的代碼還有遞歸清理這個記憶體結構的函數不再贅述,有興趣的同學請自行研究。

構造的過程我們就說到這裡,明天我們研究下解析的過程。

========

昨天簡單的分析了一下cjson對json格式的構造過程,今天仔細讀了讀readme檔案,發現readme其實說的已經很詳細了。重複造輪子就重複造輪子吧,今天我們再一起分析解析的過程。

繼續用之前構造的json格式來進行解析,之前分析構造函數的時候,我們隻是簡單的分析了幾個cjson結構的構造過程,并沒有涉及到各種類型的數組等構造。因為我覺得了解了一般的構造過程,更複雜的類型自己再簡單看看源碼,畫畫圖就很容易了解。

學習一個事物一定要先抓住主線,先掌握一個事物最常用的那50%,其他的邊邊角角完全可以留給實踐去零敲碎打(孟岩語)。

閑話打住,先上一段解析使用的代碼:

程式運作輸出:

從這段代碼中可以看到,解析過程就cjson_parse()一個接口,調用成功傳回cjson結構體的指針,錯誤傳回null,此時調用cjson_geterrorptr()可以得要錯誤原因的描述字元串。檢視cjson_geterrorptr()的源碼可以得知,其實錯誤資訊就儲存在全局字元串指針ep裡。關鍵就是對cjson_parse()過程的分析了,我們帶參數-g重新編譯代碼并下斷點開始調試跟蹤。

首先cjson_parse()調用cjson_new_item()申請一個新的cjson節點,然後使用函數對輸入字元串進行解析(中間使用了skip()函數來跳過空格和換行符等字元)。

parse_value()函數對輸入字元串進行比對和解析,檢測輸入資料的類型并調用parse_string()、parse_number()、parse_array()、parse_object()等函數進行解析,然後傳回結束的位置。

函數調用的關系如下圖:

cJSON庫源碼分析

這些函數之間互相調用,傳遞待解析的字元串直到結束或者遇見錯誤便傳回,最後會建構出一個和之前結構一樣的json記憶體結構來,解析的過程就完成了。檢索過程很簡單cjson_getobjectitem()函數負責進行某個對象的自成員的名字比對和指針的傳回。不過要注意這裡采用了cjson_strcasecmp()這個無視大小寫的字元串比較函數,因為json格式的鍵值對的名稱不區分大小寫。

這樣cjson庫的整個建構和解析過程的主幹内容就總結出來了,剩下的邊邊角角可以在這個主線分析結束之後再繼續下去,比如json格式化,解析出來的記憶體結構複制,從這個記憶體結構解析出字元串以及這個記憶體結構的遞歸删除等等留給大家自己進行吧。

p.s. cjson_inithooks()這個函數不過是cjson允許使用者使用其它的記憶體申請和釋放函數罷了(預設是malloc和free),另外啰嗦一下,這個接口也可以用來檢測記憶體洩露。隻要實作malloc和free的包裝函數,在其中統計和列印記憶體申請釋放操作就可以了。