jsmn | 一個資源佔用極小,解析速度最快的json解析器

嵌入式開源項目精選專欄

本專欄由Mculover666創建,主要內容爲尋找嵌入式領域內的優質開源項目,一是幫助開發者使用開源項目實現更多的功能,二是通過這些開源項目,學習大佬的代碼及背後的實現思想,提升自己的代碼水平,和其它專欄相比,本專欄的優勢在於:

不會單純的介紹分享項目,還會包含作者親自實踐的過程分享,甚至還會有對它背後的設計思想解讀

目前本專欄包含的開源項目有:

如果您自己編寫或者發現的開源項目不錯,歡迎留言或者私信投稿到本專欄,分享獲得雙倍的快樂!

1. jsmn

本期給大家帶來的開源項目是 jsmn,一個資源佔用極小的json解析器,號稱世界上最快,作者zserge,目前收穫 2.1K 個 star,遵循 MIT 開源許可協議。

項目地址:https://github.com/zserge/jsmn

2. 移植jsmn

2.1. 移植思路

開源項目在移植過程中主要參考項目的readme文檔,一般只需兩步:

  • ① 添加源碼到裸機工程中;
  • ② 實現需要的接口;

2.2. 準備裸機工程

本文中我使用的是小熊派IoT開發套件,主控芯片爲STM32L431RCT6:

移植之前需要準備一份裸機工程,我使用STM32CubeMX生成,需要初始化以下配置:

  • 配置一個串口用於發送數據;
  • printf重定向

具體過程可以參考:

2.3. 添加jsmn到工程中

① 複製jsmn源碼到工程中:

② 將 jsmn.h 文件添加到keil中(沒有實質作用,方便編輯):

③ 添加jsmn頭文件路徑:

3. 使用jsmn解析json數據

3.1. 準備工作

① 包含jsmn頭文件
使用時包含頭文件:

/* USER CODE BEGIN Includes */
#include "jsmn.h"
#include <stdio.h>	//用於printf打印

/* USER CODE END Includes */

② 設置一段原始json數據
在main.c中設置原始的json數據,用於後續解析:

/* USER CODE BEGIN PV */
static const char *JSON_STRING =
    "{\"user\": \"johndoe\", \"admin\": false, \"uid\": 1000,\n"
    "\"groups\": [\"users\", \"wheel\", \"audio\", \"video\"]}";
/* USER CODE END PV */

③ 開闢一塊存放token的數組(token池)
jsmn中,每個數據段解析出來之後是一個token,關於token的詳細解釋,請參考下文第4.1小節。

/* USER CODE BEGIN PV */

jsmntok_t t[128];

/* USER CODE END PV */

④ 編寫在原始JSON數據中的字符串比較函數:

static int jsoneq(const char *json, jsmntok_t *tok, const char *s) {
  if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start &&
      strncmp(json + tok->start, s, tok->end - tok->start) == 0) {
    return 0;
  }
  return -1;
}

3.2. 創建並初始化解析器

在main函數的開始創建解析器:

/* USER CODE BEGIN 1 */
	int r;
	int i;
	
	jsmn_parser p;//jsmn解析器

/* USER CODE END 1 */

在隨後外設初始化完成之後的代碼中初始化解析器:

/* USER CODE BEGIN 2 */
 
	jsmn_init(&p);

/* USER CODE END 2 */

3.3. 解析數據,獲取token

r = jsmn_parse(&p, JSON_STRING, strlen(JSON_STRING), t,sizeof(t) / sizeof(t[0]));

  if (r < 0) {
    printf("Failed to parse JSON: %d\n", r);
    return 1;
  }
  
  /* Assume the top-level element is an object */
  if (r < 1 || t[0].type != JSMN_OBJECT) {
    printf("Object expected\n");
    return 1;
  }

3.4. 逐個解析token

/* Loop over all keys of the root object */
 for (i = 1; i < r; i++) 
 {
    if (jsoneq(JSON_STRING, &t[i], "user") == 0)
    {
      	/* We may use strndup() to fetch string value */
      	printf("- user: %.*s\n", t[i + 1].end - t[i + 1].start,
             JSON_STRING + t[i + 1].start);
      	i++;
    }
    else if (jsoneq(JSON_STRING, &t[i], "admin") == 0) 
    {
      	/* We may additionally check if the value is either "true" or "false" */
      	printf("- Admin: %.*s\n", t[i + 1].end - t[i + 1].start,
             JSON_STRING + t[i + 1].start);
      	i++;
    }
    else if (jsoneq(JSON_STRING, &t[i], "uid") == 0) 
    {
      	/* We may want to do strtol() here to get numeric value */
      	printf("- UID: %.*s\n", t[i + 1].end - t[i + 1].start,
             JSON_STRING + t[i + 1].start);
      	i++;
    }
    else if (jsoneq(JSON_STRING, &t[i], "groups") == 0) 
    {
      	int j;
      	printf("- Groups:\n");
      	if (t[i + 1].type != JSMN_ARRAY) 
      	{
        	continue; /* We expect groups to be an array of strings */
      	}
      	for (j = 0; j < t[i + 1].size; j++) 
      	{
        	jsmntok_t *g = &t[i + j + 2];
        	printf("  * %.*s\n", g->end - g->start, JSON_STRING + g->start);
      	}
     	 i += t[i + 1].size + 1;
    }
    else
    {
      	printf("Unexpected key: %.*s\n", t[i].end - t[i].start,
             JSON_STRING + t[i].start);
    }
  }

3.5. 解析結果

編譯、下載到開發板,使用串口助手進行測試:

3.6. 內存對比

4. jsmn設計思想解讀

4.1. jsmn對json數據項的抽象

jsmn對json數據中的每一個數據段都會抽象爲一個結構體,稱之爲token,此結構體非常簡潔:

/**
 * JSON token description.
 * type		type (object, array, string etc.)
 * start	start position in JSON data string
 * end		end position in JSON data string
 */
typedef struct jsmntok {
  jsmntype_t type;
  int start;
  int end;
  int size;
#ifdef JSMN_PARENT_LINKS
  int parent;
#endif
} jsmntok_t;

在本實驗中未開啓JSMN_PARENT_LINKS,所以此結構體佔用16Byte大小

從結構體中的數據成員可以看出,jsmn並不對數據進行任何操作,僅僅記錄:

  • 數據項的類型
  • 數據項數據段在原始json數據中的起始位置
  • 數據項數據段在原始json數據中的結束位置

其中,數據項的類型支持4種:

/**
 * JSON type identifier. Basic types are:
 * 	o Object
 * 	o Array
 * 	o String
 * 	o Other primitive: number, boolean (true/false) or null
 */
typedef enum {
  JSMN_UNDEFINED = 0,
  JSMN_OBJECT = 1,
  JSMN_ARRAY = 2,
  JSMN_STRING = 3,
  JSMN_PRIMITIVE = 4
} jsmntype_t;

4.2. jsmn如何解析出每個token

上述說到jsmn將每一個json數據段都抽象爲一個token,那麼jsmn是如何對整段json數據進行解析,得到每一個數據項的token呢?

jsmn解析器也是非常簡潔的一個結構體:

/**
 * JSON parser. Contains an array of token blocks available. Also stores
 * the string being parsed now and current position in that string.
 */
typedef struct jsmn_parser {
  unsigned int pos;     /* offset in the JSON string */
  unsigned int toknext; /* next token to allocate */
  int toksuper;         /* superior token node, e.g. parent object or array */
} jsmn_parser;

jsmn解析就是將json數據逐個字符進行解析,用pos數據成員來記錄解析器當前的位置,當尋找到特殊字符時,就去之前我們定義的token數組(t)中申請一個空的token成員,將該token在數組中的位置記錄在數據成員toknext中。

源碼在下面的函數中,代碼過多,暫且先不放:

JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
                        jsmntok_t *tokens, const unsigned int num_tokens);

下面用一個實例來看看token是怎麼分配的。

縮短json原始數據:

static const char *JSON_STRING =
    "{\"name\":\"mculover666\",\"admin\":false,\"uid\":1000}";

在解析之後將每個token打印出來:

printf("[type][start][end][size]\n");
for(i = 0;i < r; i++)
{
	printf("[%4d][%5d][%3d][%4d]\n", t[i].type, t[i].start, t[i].end, t[i].size);
}

結果如下:

5. 項目工程源碼獲取和問題交流

目前我將jsmn源碼、我移植到小熊派STM32L431RCT6開發板的工程源碼上傳到了QQ羣裏(包含好幾份HAL庫,QQ相對速度快點),可以在QQ羣裏下載,有問題也可以在羣裏交流,當然也歡迎大家分享出來自己移植的工程到QQ羣裏

放上QQ羣二維碼:

接收更多精彩文章及資源推送,歡迎訂閱我的微信公衆號:『mculover666』。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章