65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ - 零动态分配 JSON 解析器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2024 年 5 月 9 日

CPOL

16分钟阅读

viewsIcon

10043

downloadIcon

197

本文讨论了在 C 语言中实现零分配 JSON 解析器,适用于内存资源有限的系统。

引言

本项目的目的是实现一个零分配的 JSON 解析器。使其非常适合嵌入式系统或内存要求严格的项目。

与其他声称零分配但实际上只实现“推入式解析器”或分词器(将分配责任留给调用者)的库不同,该库实际上实现了一个解析器,它将 JSON 文本重新解释为可迭代的结构,具有原生类型和解码数据。

该库通过将 JSON 数据解析到持有原始 JSON 文本的同一缓冲区中来避免堆分配,因此它实际上重用了相同的内存空间。这确实意味着原始数据会被销毁,所以如果您想保留原始文本数据,必须在将其传递给解析器之前复制该缓冲区。

很多时候,生成的结构比 JSON 文本更小,因此如果需要长时间保留解析后的信息,可以复制到一个较小的缓冲区(释放原始的“较大”缓冲区)。

用法

解析 JSON 对象

JsonResult oResult = Json_Parse("{\"foo\":\"bar\"}");
if (!oResult.Success)
{
    printf("Parsing failed : %s at %i\n", oResult.Error, oResult.Index);
}
else
{
    JsonProperty oProperty = Json_GetPropertyByName(oResult.RootObject, "foo");
    print("The object has property \"%s\" with value \"%s\"\n", oProperty.Name, oProperty.Value.StringValue)
}

解析 JSON 数组

JsonResult oResult = Json_Parse("[1,2,3]");
if (!oResult.Success)
{
    printf("Parsing failed : %s at %i\n", oResult.Error, oResult.Index);
}
else
{
    JsonElement oElement = Json_GetElementAtIndex(oResult.RootObject ,1);
    print("The array has value \"%s\" at index %i\n", oElement.Index, oElement.Value.DoubleValue)
}

解析 JSON 标量

尽管 JSON 规范不允许标量值作为根对象,但解析器仍然能够为它们生成值。

JsonResult oResult = Json_Parse("\"this is just a string\"");
if (!oResult.Success)
{
    printf("Parsing failed : %s at %i\n", oResult.Error, oResult.Index);
}
else
{
    print("the root is not an object but a string : \"%s\"\n", oResult.RootObject.StringValue)
}

文档

解析 API

元素 描述
enum JsonType 一个枚举器,用于限定 JsonObject 包含的数据类型,或者在发生解析或枚举错误时为 JsonTypeInvalid
struct JsonObject 一个结构,用于保存已解析的值,指示其类型,或者在发生解析或枚举错误时为 JsonTypeInvalid
struct JsonResult Json_Parse 函数返回的结构,在成功时包含解析状态、失败时的错误或根 JsonObject
struct JsonProperty 对象枚举函数返回的结构,包含属性名称及其值(作为 JsonObject
struct JsonElement 数组枚举函数返回的结构,其值作为 JsonObject,并具有元素的索引。
JsonResult Json_Parse(char* pJson) 将 JSON 文本解析为序列化的原生结构,重用同一缓冲区,这是一个破坏性操作,如果需要原始数据,则必须在调用此函数之前复制文本数据。
JsonObject Json_Load(char* pJson) 加载先前解析过的缓冲区,用于在解析后已将其持久化的场景。
JsonProperty Json_IterateProperties(JsonObject oJsonObject) 返回给定 JsonObject 的第一个属性,给定对象必须是 JsonTypeObject 类型
JsonProperty Json_NextProperty(JsonProperty oJsonProperty) 返回给定 JsonProperty 后面的属性,如果给定属性是最后一个,则返回的 JsonProperty 的属性将被清零,值类型将为 JsonTypeInvalid
JsonProperty Json_GetPropertyByName(JsonObject oJsonObject, char* pName) 迭代对象并检索给定 JsonObject 中具有给定名称的属性,如果没有找到此类属性,则返回的 JsonProperty 的属性将被清零,值类型将为 JsonTypeInvalid
int Json_GetPropertyCount(JsonObject oJsonObject) 返回给定 JsonObject 的属性数量,如果您希望将值复制到另一个结构并需要分配大小,则很有用
JsonElement Json_IterateElements(JsonObject oJsonArray) 返回给定 JsonObject 的第一个值,给定对象必须是 JsonTypeArray 类型
JsonElement Json_NextElement(JsonElement oJsonElement) 返回给定 JsonElement 后面的值,如果给定元素是最后一个,则返回的 JsonElement 的属性将被清零,值类型将为 JsonTypeInvalid
JsonElement Json_GetElementAtIndex(JsonObject oJsonArray, int nIndex) 迭代数组并在给定属性处检索给定 JsonObject 的值,如果索引超出范围,则 JsonElement 的属性将被清零,值类型将为 JsonTypeInvalid
int Json_GetElementCount(JsonObject oJsonArray) 返回给定 JsonObject 的值的数量

enum JsonType

枚举器用于反映 JSON 文本中找到的数据类型,其中包含一个特殊的 JsonTypeInvalid,以便解析或枚举函数能够返回失败。

enumeration JsonType
{
    JsonTypeInvalid //returned if the parsing or enumeration failed
    JsonTypeNull    //a literal "null" in the JSON
    JsonTypeBool    //a literal "true" or "false" in the JSON, the value will be parsed into JsonObject.BoolValue
    JsonTypeNumber  //an integer or decimal value in the JSON, the value will be parsed into JsonObject.DoubleValue
    JsonTypeString  //a double quote delimited string JSON, the value will be parsed into JsonObject.StringValue
    JsonTypeObject  //a {} delimited object JSON
    JsonTypeArray   //a [] delimited array JSON
}

structure JsonResult

一个结构,用于保存已解析的值,指示其类型,或者在发生解析或枚举错误时为 JsonTypeInvalid

如果 JsonResult.Success 等于 0,则 Error 元素将指向一个包含描述的字符串,Index 将指向解析失败的输入索引。如果 JsonResult.Success 是其他任何值,则 InitialSizeEndSize 将包含初始字符串大小和最终使用的总大小(InitialSize 之前的剩余字节将被清零),这将让您大致了解实现的“压缩”程度,并且 EndSize 可用于为长期持久化分配新缓冲区。RootObject 将包含已解析的值,在使用前应根据其类型检查对象,确保它不是 JsonTypeInvalid

解析 JSON 文本后,解析器会将剩余的缓冲区用 \0 填充。

struct JsonResult
{
    int Success             //0 in case of failure
    //if Success == 0
    char* Error             //a description in case of error, undefined otherwise
    int Index               //the index at which the parser failed, undefined otherwise
    //if Success != 0
    int InitialSize         //the size of the parsed json text
    int EndSize             //the size of buffer used to hold the parsed data ( will be <= than InitialSize )
    JsonObject RootObject   //the object that was parsed out of the JSON text
}

structure JsonObject

此结构表示一个 json 值或一个 json 结构,JSON 定义支持两种类型的结构。对象是无序的键值对集合,由 {} 分隔,或者数组是值的有序列表,由 [] 分隔。这些类型分别设置为 JsonTypeObjectJsonTypeArray。在这种情况下,JsonObject 没有值,应使用迭代方法。

对于其他类型,其他属性包含已解析的值,除了 JsonTypeNull(表示 JSON 中的文字 null)或 JsonTypeInvalid(用于表示失败)。

struct JsonObject
{
    char* Position      //used internally, by the iteration functions to locate a referenced object
    JsonType Type       //the type of the JSON value/structure that this object represents 
    char* StringValue   //a null terminated string in case of "JsonTypeString", undefined otherwise
    double DoubleValue  //a decimal numbers in case of "JsonTypeNumber", undefined otherwise
    char BoolValue      //0 or 1 case of "JsonTypeBool", undefined otherwise
}

structure JsonProperty

此结构由 Json_IteratePropertiesJson_GetPropertyByName 返回,还必须将其传递给 Json_NextProperty 以继续迭代。

struct JsonProperty
{
    char* Position      //used internally, by the iteration functions to locate a referenced object
    char* Name          //the null terminated string that holds the name of the property 
    JsonObject Value    //an object that holds that property value
}

structure JsonElement

此结构由 Json_IterateElementsJson_GetElementAtIndex 返回,还必须将其传递给 Json_NextElement 以继续迭代。

struct JsonElement
{
    char* Position      //used internally, by the iteration functions to locate a referenced object
    int Index           //index of the value in the parent array
    JsonObject Value    //the value found at the index
}

function Json_Parse

此函数接受一个可写且以 null 结尾的字符串,其中包含有效的 JSON 文本,并将文本解析到同一缓冲区内存空间。您必须注意,这是一个破坏性操作。

JsonResult 由此函数返回并仅供此函数使用,以便轻松捕获解析失败。

解析 JSON 文本后,解析器会将剩余的缓冲区用 \0 填充。

这是一个破坏性操作,如果需要原始数据,则必须在调用此函数之前进行复制。

用法
JsonResult oResult = Json_Parse("{\"foo\":\"bar\"}");
if(oResult.Success)
    //do something with oResult.RootObject

functions Json_IterateProperties & Json_NextProperty

这些函数可用于“探索”结构未知的 JSON 对象,方法是枚举其所有属性。它们的行为和结果相似,唯一的主要区别是 Json_IterateProperties 接收一个 JsonObject 并检索第一个 JsonProperty。而 Json_NextProperty 检索给定属性后面的下一个 JsonProperty

用法
JsonProperty oFirstProperty = Json_IterateProperties(oObject);
if(oFirstProperty.Value.Type != JsonTypeInvalid)
    //the object has at least 1 property 

JsonProperty oSecondProperty = Json_NextProperty(oFirstProperty);
if(oSecondProperty.Value.Type != JsonTypeInvalid)
    //the object has at least 2 properties
//lets output them all
for (JsonProperty oProperty = Json_IterateProperties(oObject); oProperty.Value.Type != JsonTypeInvalid; oProperty = Json_NextProperty(oProperty))
   print("The object has property named \"%s\" \n", oProperty.Name)

function Json_GetPropertyByName

如果 JSON 数据具有已知结构,您可以使用 Json_GetPropertyByName 直接访问属性值。请注意,如果属性不存在,将返回一个 JsonTypeInvalid 类型的对象,因此应验证返回的类型以防止错误。

请注意,此操作是 O(n)

用法
JsonProperty oUnsureProperty = Json_GetPropertyByName(oObject,"user_name");
if(oUnsureProperty.Value.Type != JsonTypeInvalid)
    //the property is present
//get values from a known structure disregarding validations
double nPrice = Json_GetPropertyByName(oObject,"product_price").Value.DoubleValue;
int bActive = Json_GetPropertyByName(oObject,"is_active").Value.BoolValue == 1;

function Json_GetPropertyCount

这只是一个辅助函数,返回 JsonObject 的属性计数。由于不能按索引获取属性,因此此函数没有太大用处。但如果您需要进行与对象“大小”成比例的分配,它可能会有帮助。

如果传入的 JsonObject 不是 JsonTypeObject 类型,则返回 -1。

请注意,此操作是 O(n)

用法
JsonObject oObject; //gotten from a previous parsing

int nTotalProperties = Json_GetPropertyCount(oObject);

functions Json_IterateElements & Json_NextElement

这些函数用于迭代 JSON 数组。它们的行为和结果相似,区别在于 Json_IterateElements 接收一个 JsonObject 并检索第一个 JsonElement。而 Json_NextElement 检索给定元素后面的下一个 JsonElement

用法
JsonElement oFirstElement = Json_IterateElements(oObject);
if(oFirstElement.Value.Type == JsonTypeInvalid)
    print("array is empty");
//lets output them all
for (JsonElement oElement = Json_IterateElements(oObject); oElement.Value.Type != JsonTypeInvalid; oElement = Json_NextElement(oElement))
    print("The array contains \"%s\" \n", oElement.Value.StringValue)

function Json_GetElementCount

这只是一个辅助函数,返回 JsonObject 数组中的元素数量。

请注意,此操作是 O(n)

用法
int nTotalElements = Json_GetElementCount(oObject);

function Json_GetElementAtIndex

数组中的元素可以通过其索引以随机顺序访问。给定的索引必须在有效范围内。要获取项目总数,请使用 Json_GetElementCount 函数。

请注意,此操作是 O(n)

用法

//if iteration of the full array is needed you should use "IterateElements" and "NextElement"
//as this takes O(n) + O(n log n)
int nTotalElements = Json_GetElementCount(oObject);
for(int i = 0 ; i < nTotalElements ; i++)
    Json_GetElementAtIndex(oObject,i).Value.DoubleValue;

示例

一个示例文件作为以下代码的参考。

{
    "name": "my house",
     "location":{
        "lat":123.123,
        "lon":0.9999,
     },
     "mixed_array":[
        true,
        false,
        123465,
        "hello",
        {
            "child_object":"inside"
        }
     ]
}

解析文件,尝试每一步验证以避免任何运行时错误。

#include "json.h"

//load from a file to a "big enough" buffer (this is lazy , bad code, DON'T COPY) 
char pBuffer[1024];
hFile = fopen("sample.json", "r");
fgets(pBuffer, 1024, hFile);

//this first example will show some defensive code and how to process the data with the correct validations to avoid runtime errors
JsonResult oResult = Json_Parse(pBuffer);
if (!oResult.Success)
{
    printf("Parsing failed : %s at %i\n", oResult.Error, oResult.Index);
}
else if(oResult.RootObject.Type == JsonTypeObject)
{
    //get the name property
    JsonProperty oName = Json_GetPropertyByName(oResult.RootObject, "name");
    if(oName.Value.Type == JsonTypeString)
        printf("the name of my object : %s \n", oName.Value.StringValue);

    //get the sub object of the "location" property 
    JsonProperty oLocation = Json_GetPropertyByName(oResult.RootObject, "location");
    if(oLocation.Value.Type == JsonTypeObject)
    {
        //iterate its properties
       JsonObject oCoordinates = oLocation.Value;
       for (JsonProperty oDimension = Json_IterateProperties(oCoordinates); oDimension.Value.Type != JsonTypeInvalid; oDimension = Json_NextProperty(oDimension))
       {
           if(oDimension.Value.Type == JsonTypeNumber)
                print("coordinate \"%s\" is %f \n", oDimension.Name,oDimension.Value.DoubleValue)
            else
                print("coordinate \"%s\" is not of type number\n", oDimension.Name)
       }
    }

    JsonProperty oMixed = Json_GetPropertyByName(oResult.RootObject, "mixed_array");
    if(oMixed.Value.Type == JsonTypeArray)
    {
        //iterate the items of the array
       JsonObject oTheArray = oMixed.Value;
       for (JsonProperty oEntry = Json_IterateElements(oTheArray); oEntry.Value.Type != JsonTypeInvalid; oEntry = Json_NextElement(oEntry))
       {
         switch (oEntry.Type)
            {
                case JsonTypeArray:
                    printf("an array with %i items\n", Json_GetElementCount(oEntry));
                    break;
                case JsonTypeBool:
                    printf("a boolean literal: %s \n", oEntry.BoolValue ? "true" : "false");
                    break;
                case JsonTypeNull:
                    printf("literally just a null");
                    break;
                case JsonTypeNumber:
                    printf("a number: %f\n", oEntry.DoubleValue);
                    break;
                case JsonTypeObject:
                    printf("an inner object with %i properties\n", Json_GetPropertyCount(oEntry));
                    break;
                case JsonTypeString:
                    printf("a string containing: %s\n", oEntry.StringValue);
                    break;
                default:
                case JsonTypeInvalid:
                    printf("but it worked on by machine!!!");
                    break;
            }
       }
    }
}

简短简单的代码示例,我希望一切都能如此简单,但老实说,这一个可能会因为缺少验证而让你头疼。

#include "json.h"

//If the previous example seems to have to much code... well...
//If you know the structure of the object and is guarantied that it is properly constructed 
//You can go straight for the prize (at you own risk)

JsonResult oObject = Json_Parse(pBuffer).RootObject;

printf("the name of my object : %s \n", Json_GetPropertyByName(oObject, "name").Value.StringValue);
JsonObject oCoordinates = Json_GetPropertyByName(oObject, "location").Value
print("coordinate \"lat\" is %f \n", Json_GetPropertyByName(oCoordinates, "lat").Value.DoubleValue)
print("coordinate \"lon\" is %f \n", Json_GetPropertyByName(oCoordinates, "lon").Value.DoubleValue)
JsonObject oArray = Json_GetPropertyByName(oObject, "mixed_array").Value
 
printf("a string containing: %s\n", Json_GetElementAtIndex(oObject, 3).Value.StringValue);
printf("a boolean literal: %s \n", Json_GetElementAtIndex(oObject, 1).Value.BoolValue ? "true" : "false");

最后是一些用于“探索”和打印未知 JSON 文本的代码。

void dump_object(int nLevel, JsonObject oJson)
{
    switch (oJson.Type)
    {
        case JsonTypeArray:
            printf("%*s array(%i)\n", nLevel * 4, "", Json_GetElementCount(oJson));
            for (JsonElement oElement = Json_IterateElements(oJson); oElement.Value.Type != JsonTypeInvalid; oElement = Json_NextElement(oElement))
            {
                printf("%*s element(%i)\n", (nLevel + 1) * 4, "", oElement.Index);
                dump_object(nLevel + 1, oElement.Value);

            }
            break;
        case JsonTypeBool:
            printf("%*s bool(%s)\n", nLevel * 4, "", oJson.BoolValue ? "true" : "false");
            break;
        case JsonTypeNull:
            printf("%*s null\n", nLevel * 4, "");
            break;
        case JsonTypeNumber:
            printf("%*s number(%f)\n", nLevel * 4, "", oJson.DoubleValue);
            break;
        case JsonTypeObject:
            printf("%*s object(%i)\n", nLevel * 4, "", Json_GetPropertyCount(oJson));
            for (JsonProperty oProperty = Json_IterateProperties(oJson); oProperty.Value.Type != JsonTypeInvalid; oProperty = Json_NextProperty(oProperty))
            {
                printf("%*s property(%s)\n", (nLevel + 1) * 4, "", oProperty.Name);
                dump_object(nLevel + 1, oProperty.Value);

            }
            break;
        case JsonTypeString:
            printf("%*s string(%s)\n", nLevel * 4, "", oJson.StringValue);
            break;
        default:
        case JsonTypeInvalid:
            printf("%*s invalid\n", nLevel * 4, "");
            break;
    }
}

//call the "dump" function that will print everything it finds 
JsonResult oResult = Json_Parse(pBuffer);
if (oResult.Success && oResult.RootObject.Type != JsonTypeInvalid)
    dumpObject(0, oResult.RootObject);

构建 API

由于解析 JSON 文本通常也包括创建它,因此该库还提供了写入 JSON 文本的函数。在这种情况下,动态内存分配是不可避免的,但缓冲区的分配和重新分配由库本身管理。最值得注意的是,在每一步中,缓冲区都包含有效的 JSON 文本。这意味着在每次交互时都会进行一些额外的内存复制。

构建函数接收指向指针的指针,因为它们重新分配缓冲区时,缓冲区地址可能会发生变化。

元素 描述
char* Json_CreateBuffer() 为构建 JSON 文本分配一个缓冲区,它可以在任何可以使用字符串的地方使用。但不能通过普通函数重新分配/释放。
void Json_ReleaseBuffer(char*) 释放先前分配的缓冲区的内存。
int Json_AddNull(char** pBuffer) 在当前 array 作用域的插入点写入一个文字 null 值。
int Json_AddBool(char** pBuffer, int bValue) 在当前 array 作用域的插入点添加一个文字 truefalse 值。
int Json_AddString(char** pBuffer, const char* sValue) 在当前 array 作用域的插入点添加一个字符串值。
int Json_AddNumber(char** pBuffer, double nValue) 在当前 array 作用域的插入点添加一个数字值。
int Json_AddArray(char** pBuffer) 在当前 array 的插入点打开一个数组作用域,并将插入点移动到新创建的数组的作用域内部
int Json_AddObject(char** pBuffer) 在当前 array 的插入点打开一个对象作用域,并将插入点移动到新创建的对象的作用域内部
int Json_AddPropertyNull(char** pBuffer, const char* sName) 在当前 object 作用域的插入点写入一个具有 null 值的属性。
int Json_AddPropertyBool(char** pBuffer, const char* sName, int bValue) 在当前 object 作用域的插入点写入一个具有 truefalse 值的属性。
int Json_AddPropertyString(char** pBuffer, const char* sName, const char* sValue) 在当前 object 作用域的插入点写入一个具有字符串值的属性。
int Json_AddPropertyNumber(char** pBuffer, const char* sName, double nValue) 在当前 object 作用域的插入点写入一个具有数字值的属性。
int Json_AddPropertyArray(char** pBuffer, const char* sName) 在当前 object 的插入点写入一个具有空数组值的属性,并将插入点移动到新创建的数组的作用域内部
int Json_AddPropertyObject(char** pBuffer, const char* sName) 在当前 object 的插入点写入一个具有空对象值的属性,并将插入点移动到新创建的对象的作用域内部
int Json_ExitScope(char** pBuffer) 将插入指针移回父作用域。
const char* Json_GetError(char* pBuffer) 在任何先前函数返回 0 的情况下返回错误消息。
char* Json_Indent(char*) 返回一个已分配的缓冲区,其中包含格式化后的 JSON 文本。(缓冲区必须使用 free() 释放
char* Json_Compress(char* pChar) 从 JSON 文本中删除不显著的空白字符(修改是就地进行的)。

示例

一个示例文件作为以下代码的参考。

char* pBuffer = Json_CreateBuffer();
//the root object will be an array (brackets are used only for indentation)
Json_AddArray(&pBuffer);
{
    Json_AddObject(&pBuffer);//this opens a new scope and moves the insertion point to that scope
    {
        //so now we are adding properties to the object
        Json_AddPropertyNull(&pBuffer, "null_property");
        Json_AddPropertyBool(&pBuffer, "bool_property", 1);
        Json_AddPropertyString(&pBuffer, "string_property", "foo\nbar");
        Json_AddPropertyNumber(&pBuffer, "number_property", -111.222);
        Json_AddPropertyObject(&pBuffer, "child_object");//opens a new scope for child object
        {
            Json_AddPropertyArray(&pBuffer, "empty_array");//opens a new scope for an array
            Json_ExitScope(&pBuffer);//exit the newly created array scope (leaving it empty)
        }
        Json_ExitScope(&pBuffer);//exit the child object scope
        Json_AddPropertyString(&pBuffer, "parent_scope", "after the child object");
    }
    Json_ExitScope(&pBuffer);//exits the object scope, meaning we return to the array scope
    
    //these are added to the array
    Json_AddNull(&pBuffer);
    Json_AddBool(&pBuffer, 0);
    Json_AddString(&pBuffer, "hello world");
    Json_AddNumber(&pBuffer, 321.0123);
    Json_AddNumber(&pBuffer, .1);
    Json_AddNumber(&pBuffer, -0.5);
}
//the text was printable at any time, but here is the end result
printf("%s", pBuffer);

char* pPretty = Json_Indent(pBuffer);
//and here it is formatted and indented
printf("%s", pBuffer);

//the formatted version must be released with "free" 
free(pPretty)
//construction buffer CANT be used with "free",  `Json_ReleaseBuffer` MUST be used
Json_ReleaseBuffer(pBuffer);

工作原理

作用域数据

要了解此技术如何工作,我们必须认识到 JSON 包含的额外信息占用了原生数据操作不需要的空间。以字符串 "foo" 为例。它占用 5 个字节,包含以下信息:

|0x01|0x02|0x03|0x04|0x05|
|   "|   f|   o|   o|   "|

要将其转换为原生的以 null 结尾的 c 字符串,我们只需在字符串最后一个 o 之后放置一个 null。但是,前面已经有一个字节带有 ",我们不需要它。因此,我们只需更改内存内容为:

|0x01|0x02|0x03|0x04|0x05|
|   "|   f|   o|   o|  \0|

现在,返回指向地址 0x02 的指针将指向一个有效的以 null 结尾的 c 字符串。由于我们重用了字节,因此字符串开头有一个额外的字节,其中还有一个无用的 "。我们可以用它来放置一个特定的字节值来“标记”字符串的开头。因此,内存中的实际数据将变为:

|0x01|0x02|0x03|0x04|0x05|
| str|   f|   o|   o|  \0|

当然,字符串可能需要反转义,但这有利于我们,因为它意味着它将占用更少的空间。这是字符串 "foo\nbar" 的另一个示例:

        |0x01|0x02|0x03|0x04|0x05|0x06|0x07|0x08|0x09|0x0A|0x0B|0x0C|0x0D|0x0E|0x0F|
original|   "|   f|   o|   o|   \|   n|   b|   a|   r|   "|    |    |    |    |    |
  parsed| str|   f|   o|   o|  \n|   b|   a|   r|  \0|    |    |    |    |    |    |

对于 JSON 对象也是如此,它以 {} 的形式提供了 2 个额外的字节,而数组以 [] 的形式提供了额外的字节。

对象始终由键值对组成,因此可以将其内容解析并迭代为对,从而无需任何分隔符并丢弃 :, 字符。

数组也是如此,直到我们到达数组末尾,每个连续的值都是数组的一个项。

因此,对象 {"foo":"bar"} 的示例可以这样解析:

        |0x01|0x02|0x03|0x04|0x05|0x06|0x07|0x08|0x09|0x0A|0x0B|0x0C|0x0D|0x0E|0x0F|
original|   {|   "|   f|   o|   o|   "|   :|   "|   b|   a|   r|   "|   }|    |    |
  parsed| obj| str|   f|   o|   o|  \0| str|   b|   a|   r|  \0| end|    |    |    |

而像 ["foo","bar"] 这样的数组将非常相似,唯一的区别在于类型“标记”以及我们如何迭代数据。

        |0x01|0x02|0x03|0x04|0x05|0x06|0x07|0x08|0x09|0x0A|0x0B|0x0C|0x0D|0x0E|0x0F|
original|   [|   "|   f|   o|   o|   "|   ,|   "|   b|   a|   r|   "|   ]|    |    |
  parsed| arr| str|   f|   o|   o|  \0| str|   b|   a|   r|  \0| end|    |    |    |

字面量

在理解了内存中的替换后,对于字面量 nulltruefalse,事情变得更加容易,因为它们提供了 4 个和 5 个字节,而我们只需要一个字节来放置一个“标记”,其字节值表示相应类型。所以这里是一个 JSON [true,false,null] 的简单示例:

        |0x01|0x02|0x03|0x04|0x05|0x06|0x07|0x08|0x09|0x0A|0x0B|0x0C|0x0D|0x0E|0x0F|0x10|0x11|
original|   [|   t|   r|   u|   e|   ,|   f|   a|   l|   s|   e|   ,|   n|   u|   l|   l|   ]|
  parsed| arr| tru| fal| nul| end|    |    |    |    |    |    |    |    |    |    |    |    |

数字

在这里我们遇到了一个障碍,与作用域数据不同,没有额外的字节供我们放置“标记”,而且与字面量不同,也没有固定的大小。

存在简单的情况,例如数字 1234,它占用 4 个字节,允许我们存储一个 32 位整数,但这仍然留下像 9 这样的情况,只有一个字节可用。如果我们用它来放置数字“标记”,我们将没有存储数据的空间。如果我们存储数据,我们就不知道如何在没有“标记”指示类型的情况下解释该数据。

但是,如果我们考虑“标记”字节及其值,我们可以看到到目前为止我们只定义了几个唯一值(实际上是 7 个),并且要存储 8 个不同的值,我们只需要 3 位。这给我们留下了“标记”字节的 5 位,我们永远不会使用它们,也许我们可以用这些位来存储实际数据。

这让我们走上了正确的道路。不仅如此,而且通过调整“标记”位,我们可以处理另一个问题……

以 JSON ["One","Two","Three"] 为例。如果我们有一个指向数组开头的指针,并且我们希望获取位置 1 的项(即第二个字符串)。我们需要“跳过”第一个项,但转到以 null 结尾的字符串末尾的唯一方法是读取所有字节直到遇到 \0。如果我们知道字符串的大小,我们实际上可以跳转到其末尾,然后获取下一个值。

因此,如果我们能够存储对象的大小,那么随机访问会更快。当然,JSON 允许任意大小的字符串、数组或对象。一个 32 位整数可以处理所有这些,但我们没有地方存放它,所以必须做出折衷。我们将把短字符串、数组或对象的大小存储在我们“标记”的可用位中,并为那些需要迭代才能找到实际大小的项设置一个不同的“标记”值。

这引导我们实现了我们“标记”值的最终版本,它们实际上是我们已解析值的标志和数据的编码。

8   7   6   5   4   3   2   1
-----------------------------   
6bit size [0-63]        0   1   JsonMarkerSmallString
6bit size [0-63]        1   0   JsonMarkerSmallObject
6bit size [0-63]        1   1   JsonMarkerSmallArray
5bit int [0-31]     1   0   0   JsonMarkerDecimal
4bit int [0-15] 1   0   0   0   JsonMarkerDigit
3bit [0-7]  1   0   0   0   0   JsonMarkerInt
1   0   0   0   0   0   0   0   JsonMarkerLargeString
1   0   1   0   0   0   0   0   JsonMarkerLargeObject
1   1   0   0   0   0   0   0   JsonMarkerLargeArray
1   1   1   0   0   0   0   0   JsonMarkerSequenceEnd
0   0   1   0   0   0   0   0   JsonMarkerNull
0   1   0   0   0   0   0   0   JsonMarkerTrue
0   1   1   0   0   0   0   0   JsonMarkerFalse
0   0   0   0   0   0   0   0   unused null

如果我们的“标记”的前 2 位中的任何一位被信号,我们就知道我们正在处理一个作用域,并且我们可以使用其他 6 位来保存从 0 到 63 的值,表示作用域的字节大小。这使我们能够轻松跳过短字符串,我相信这将是对象属性名称的 100% 的情况。以及其他可能批量出现的小对象,如 {"x":123,"y":321},也将很容易跳过。

如果前 2 位是 0,我们则检查下一位。如果被信号,则表示后面将跟着一个整数,但小数必须移动 n 位。n 存储在标记的下一 5 位中,允许我们最多有 15 位小数。如果您想知道是否有“空间”来存储这个“标记”,请记住,要在 JSON 中定义任何带有小数位的内容都需要放置一个 .,因此总会有一个额外的字节可用于小数标记。

如果第 3 位也是 0,我们则检查第 4 位。如果为 1,则这是单个数字的标记,接下来的 4 位允许我们拥有从 0 到 16 的值,这足以存储从 '0' 到 '9' 的值,这些是数字的最坏情况。

如果第 4 位也是 0 但第 5 位是 1,我们在剩余的 3 位中有一个枚举器,它告诉我们后面整数的大小。

  • 1 = 8 位有符号整数
  • 2 = 16 位有符号整数
  • 3 = 32 位有符号整数
  • 4 = 64 位有符号整数

并且该整数的字节由 JSON 文本中的数字保证,我们可以检查最坏情况来确保。

  • 9 1 字节 - 特殊情况,我们使用 JsonMarkerDigit
  • 99 2 字节 - 1 字节“标记”表示 8 位整数,1 字节存储 -128 到 127 的值
  • 999 3 字节 - 1 字节“标记”表示 16 位整数,2 字节存储 -32768 到 32767 的值
  • 9999 4 字节 - 1 字节“标记”表示 16 位整数,2 字节存储 -32768 到 32767 的值(节省 1 字节)
  • 99999 5 字节 - 1 字节“标记”表示 32 位整数,4 字节存储 -2147483648 到 2147483647 的值
  • 等等...

如果我们在此任何一个数字前加上一个 - 负号,我们将获得更多空间。

最后,我们标记的最后 3 位组合提供了 8 种组合,足以定义我们其他类型值所需的标记。

全零组合被特意留空,以便在字符串终止或传递的缓冲区方面不会出错。遇到的 0 始终表示错误。

最后的想法

这项技术和编码是我自己创建的,没有任何外部灵感。这并不意味着我认为它是独一无二或新颖的。外面有很多聪明人,我不会惊讶如果某些其他算法使用了类似的编码技术。无论如何,我认为这段代码对于内存要求非常严格的项目可能有用,它的内存占用空间非常小,但又不会过多地牺牲性能。

代码按原样提供,您可以随意使用。

祝您编码愉快。

© . All rights reserved.