cJSON庫源碼分析


本文采用以下協議進行授權: 自由轉載-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,轉載請注明作者及出處。    

        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命令行參數如下:

1

indent - bad - bli 0 - ce - kr - nsob -- space - after - if -- space - after - while -- space - after - for -- use - tabs - i8

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#include <stdio.h>

#include <stdlib.h>

    

#include "cJSON.h"

    

int main ( int argc , char * argv [ ] )

{

     cJSON * root , * fmt ;

            

     root = cJSON_CreateObject ( ) ;

     cJSON_AddStringToObject ( root , "name" , "Jack (\"Bee\") Nimble" ) ;

     fmt = cJSON_CreateObject ( ) ;

     cJSON_AddItemToObject ( root , "format" , fmt ) ;

     cJSON_AddStringToObject ( fmt , "type" , "rect" ) ;

     cJSON_AddNumberToObject ( fmt , "width" , 1920 ) ;

     cJSON_AddFalseToObject ( fmt , "interlace" ) ;

    

     char * result = cJSON_Print ( root ) ;

     puts ( result ) ;

            

     free ( result ) ;

     cJSON_Delete ( root ) ;

    

     return EXIT_SUCCESS ;

}

        編譯運行后 ( 編譯時注意要鏈接數學庫,參數行要加 -lm) ,運行結果如下:

1
2
3
4
5
6
7
8
{

     "name" : "Jack (\"Bee\") Nimble" ,

     "format" : {

         "type" : "rect" ,

         "width" : 1920 ,

         "interlace" : false

     }
}

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

1
2
3
4
5
6
7
8
9
10
11
12

typedef struct cJSON {

     struct cJSON * next , * prev ;

     struct cJSON * child ;

    
     int type ;
    

     char * valuestring ;

     int valueint ;

     double valuedouble ;

    

     char * string ;

} cJSON ;

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

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

1
2
3
4
5
6
7
8

/* cJSON Types: */

#define cJSON_False  0

#define cJSON_True   1

#define cJSON_NULL   2

#define cJSON_Number 3

#define cJSON_String 4

#define cJSON_Array  5

#define cJSON_Object 6

        很顯然通過檢測這里的type字段,就很容易知道該節點的類型以及其實際存儲數據的字段了。其它的字段是什么意思呢?cJSON.h文件里的注釋說的很明白了,valueint,valuedouble以及valuestring保存的是相應的值,string存放的是本字段的名字。

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

        調試過程中,我們發現 cJSON_AddStringToObject() 等其實是宏定義,本質上調用的都是 cJSON_AddItemToObject() 函數,在cJSON.h文件中可以看到如下定義:

1
2
3
4
5
6

#define cJSON_AddNullToObject(object,name)      cJSON_AddItemToObject(object, name, cJSON_CreateNull())

#define cJSON_AddTrueToObject(object,name)      cJSON_AddItemToObject(object, name, cJSON_CreateTrue())

#define cJSON_AddFalseToObject(object,name)     cJSON_AddItemToObject(object, name, cJSON_CreateFalse())

#define cJSON_AddBoolToObject(object,name,b)    cJSON_AddItemToObject(object, name, cJSON_CreateBool(b))

#define cJSON_AddNumberToObject(object,name,n)  cJSON_AddItemToObject(object, name, cJSON_CreateNumber(n))

#define cJSON_AddStringToObject(object,name,s)  cJSON_AddItemToObject(object, name, cJSON_CreateString(s))

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


         構造的Json字符串最終在內存中形成的結構如下圖所示:

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

         我們最后調用 cJSON_Print() 函數生成這個結構所對應的字符串。生成說起來容易,遍歷起整個結構並進行字符串格式控制卻比較繁瑣。這里相關的代碼還有遞歸清理這個內存結構的函數不再贅述,有興趣的同學請自行研究。

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


         昨天簡單的分析了一下cJSON對Json格式的構造過程,今天仔細讀了讀README文件,發現README其實說的已經很詳細了。重復造輪子就重復造輪子吧,今天我們再一起分析解析的過程。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

#include <stdio.h>

#include <stdlib.h>

#include "cJSON.h"

int main ( int argc , char * argv [ ] )

{

char * text = "{\"name\": \"Jack (\\\"Bee\\\") Nimble\", "

                         "\"format\": {\"type\": \"rect\", "

                         "\"width\": 1920, \"interlace\": false}}" ;

cJSON * root = cJSON_Parse ( text ) ;

if ( ! root ) {

printf ( "Error before: [%s]\n" , cJSON_GetErrorPtr ( ) ) ;

return EXIT_FAILURE ;

}

char * out = cJSON_Print ( root ) ;

printf ( "text:\n%s\n\n" , out ) ;

free ( out ) ;

char * name = cJSON_GetObjectItem ( root , "name" ) -> valuestring ;

printf ( "name : %s\n" , name ) ;

cJSON * format = cJSON_GetObjectItem ( root , "format" ) ;

int width = cJSON_GetObjectItem ( format , "width" ) -> valueint ;

printf ( "width : %d\n" , width ) ;

cJSON_Delete ( root ) ;

return EXIT_SUCCESS ;

}

         程序運行輸出:

1
2
3
4
5
6
7
8
9
10
11
12
text :
{

"name" : "Jack (\"Bee\") Nimble" ,

"format" : {
"type" : "rect" ,
"width" : 1920 ,

"interlace" : false

}
}

name : Jack ( "Bee" ) Nimble

width : 1920

         從這段代碼中可以看到,解析過程就 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() 等函數進行解析,然后返回結束的位置。

         函數調用的關系如下圖:


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

        這樣cJSON庫的整個構建和解析過程的主干內容就總結出來了,剩下的邊邊角角可以在這個主線分析結束之后再繼續下去,比如Json格式化,解析出來的內存結構復制,從這個內存結構解析出字符串以及這個內存結構的遞歸刪除等等留給大家自己進行吧。

         P.S. cJSON_InitHooks() 這個函數不過是cJSON允許用戶使用其它的內存申請和釋放函數罷了(默認是malloc和free),另外啰嗦一下,這個接口也可以用來檢測內存泄露。只要實現malloc和free的包裝函數,在其中統計和打印內存申請釋放操作就可以了。

若無特別聲明,本站文章皆為原創,轉載時煩請注明: 轉載自 淺墨的博客


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM