cJSON源码解析之cJSON_Print函数

发布于:2024-07-03 ⋅ 阅读:(14) ⋅ 点赞:(0)


前言

在处理JSON数据时,我们经常需要将内存中的JSON对象转换为字符串,以便于存储或传输。在C语言的cJSON库中,这个任务由cJSON_Print函数完成。cJSON_Print函数接收一个cJSON对象作为参数,返回一个新分配的字符串,该字符串包含了JSON对象的文本表示。在这篇文章中,我们将深入探讨cJSON_Print函数的内部实现。


cJSON_Print是干什么的

这个函数的作用就是把一个cjson的结构体变成我们看得懂的字符串,仅此而已

cJSON_Print源码解析

cJSON_Print函数实现

/* Render a cJSON item/entity/structure to text. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)
{
    return (char*)print(item, true, &global_hooks);
}

在cJSON_Print函数里面,他调用了print函数来实现他内部的功能,所以我们需要聚焦到print函数中

print函数

函数实现

他的函数实现非常复杂,甚至使用了goto语句

static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks)
{
    static const size_t default_buffer_size = 256;
    printbuffer buffer[1];
    unsigned char *printed = NULL;

    memset(buffer, 0, sizeof(buffer));

    /* create buffer */
    buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size);
    buffer->length = default_buffer_size;
    buffer->format = format;
    buffer->hooks = *hooks;
    if (buffer->buffer == NULL)
    {
        goto fail;
    }

    /* print the value */
    if (!print_value(item, buffer))
    {
        goto fail;
    }
    update_offset(buffer);

    /* check if reallocate is available */
    if (hooks->reallocate != NULL)
    {
        printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1);
        if (printed == NULL) {
            goto fail;
        }
        buffer->buffer = NULL;
    }
    else /* otherwise copy the JSON over to a new buffer */
    {
        printed = (unsigned char*) hooks->allocate(buffer->offset + 1);
        if (printed == NULL)
        {
            goto fail;
        }
        memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1));
        printed[buffer->offset] = '\0'; /* just to be sure */

        /* free the buffer */
        hooks->deallocate(buffer->buffer);
        buffer->buffer = NULL;
    }

    return printed;

fail:
    if (buffer->buffer != NULL)
    {
        hooks->deallocate(buffer->buffer);
        buffer->buffer = NULL;
    }

    if (printed != NULL)
    {
        hooks->deallocate(printed);
        printed = NULL;
    }

    return NULL;
}

这个print函数的主要目标是将一个cJSON对象(item)转换为其字符串表示。下面是这个函数的工作原理:

  1. 初始化:函数首先创建一个printbuffer结构体,并为其分配一块默认大小(256字节)的内存。这个printbuffer用于存储生成的字符串。

  2. 打印值:然后,函数调用print_value函数,将cJSON对象转换为字符串,并将结果存储在printbuffer中。print_value函数会根据cJSON对象的类型(如cJSON_ObjectcJSON_ArraycJSON_String等),递归地生成JSON字符串。

  3. 更新偏移量print_value函数完成后,buffer->offset将指向printbuffer中的下一个空闲位置。函数调用update_offset来更新这个偏移量。

  4. 重新分配或复制:然后,函数检查是否可以重新分配内存。如果可以(即hooks->reallocate不为NULL),那么函数就会调用hooks->reallocate来重新分配printbuffer的大小,使其刚好能够容纳生成的字符串。如果不能重新分配内存,那么函数就会分配一块新的内存,并将生成的字符串从printbuffer复制到新的内存中。

  5. 返回结果:最后,函数返回生成的字符串。如果在任何步骤中出现错误(如内存分配失败),函数就会跳转到fail标签,释放已分配的内存,并返回NULL。

print_value函数

函数实现如下:

/* Render a value to text. */
static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer)
{
    unsigned char *output = NULL;

    if ((item == NULL) || (output_buffer == NULL))
    {
        return false;
    }

    switch ((item->type) & 0xFF)
    {
        case cJSON_NULL:
            output = ensure(output_buffer, 5);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "null");
            return true;

        case cJSON_False:
            output = ensure(output_buffer, 6);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "false");
            return true;

        case cJSON_True:
            output = ensure(output_buffer, 5);
            if (output == NULL)
            {
                return false;
            }
            strcpy((char*)output, "true");
            return true;

        case cJSON_Number:
            return print_number(item, output_buffer);

        case cJSON_Raw:
        {
            size_t raw_length = 0;
            if (item->valuestring == NULL)
            {
                return false;
            }

            raw_length = strlen(item->valuestring) + sizeof("");
            output = ensure(output_buffer, raw_length);
            if (output == NULL)
            {
                return false;
            }
            memcpy(output, item->valuestring, raw_length);
            return true;
        }

        case cJSON_String:
            return print_string(item, output_buffer);

        case cJSON_Array:
            return print_array(item, output_buffer);

        case cJSON_Object:
            return print_object(item, output_buffer);

        default:
            return false;
    }
}

这个函数的作用就是根据不同的type类型进行字符串的输出
我们发现一个函数不是标准C函数:ensure,他是检查当前输出的字符串是否>buffer大小,如果大于就重新hook分配内存,这样就动态分配了内存了,这就非常迷惑了。
为什么这里重新分配内存了,就代表外面不需要再进行了啊,那么为什么print函数又判断分配一次内存(上个标题的4)

这是一个很好的问题。在print_value函数中,确实会根据需要动态地重新分配内存。然而,这个内存重新分配的过程是逐步进行的,也就是说,每次需要更多的空间时,就会增加一些额外的内存。这样做的好处是,我们可以确保在任何时候,printbuffer都有足够的空间来存储新的数据。

然而,当我们完成所有的打印操作后,printbuffer可能会包含一些未使用的额外空间。这是因为我们在每次重新分配内存时,通常会分配比当前需要的稍微多一些的空间,以防止频繁的内存重新分配。

这就是为什么在print函数的最后,我们还需要再次重新分配内存。这一次,我们知道了最终的字符串的确切长度,所以可以将内存大小调整为刚好足够的大小,从而释放那些未使用的额外空间。这样做可以帮助我们优化内存使用,特别是在处理大量数据时。

update_offset函数

函数实现:

/* calculate the new length of the string in a printbuffer and update the offset */
static void update_offset(printbuffer * const buffer)
{
    const unsigned char *buffer_pointer = NULL;
    if ((buffer == NULL) || (buffer->buffer == NULL))
    {
        return;
    }
    buffer_pointer = buffer->buffer + buffer->offset;

    buffer->offset += strlen((const char*)buffer_pointer);
}

update_offset函数在cJSON库中起着重要的作用。它的主要任务是更新printbuffer结构体中的offset字段。

在cJSON库中,printbuffer结构体用于存储生成的JSON字符串。offset字段表示当前已经使用的printbuffer的大小,也就是说,它指向printbuffer中的下一个空闲位置。

当我们向printbuffer中添加新的数据时,我们需要更新offset字段,以确保它总是指向正确的位置。这就是update_offset函数的主要任务。

通过正确地更新offset字段,我们可以确保在任何时候,printbuffer都有足够的空间来存储新的数据。这对于生成正确的JSON字符串非常重要。
当然可以。在cJSON库中,offsetprintbuffer结构体的一个字段,它表示当前已经使用的printbuffer的大小。也就是说,它指向printbuffer中的下一个空闲位置。

让我们通过一个简单的例子来理解offset的作用。假设我们有一个printbuffer,并且我们已经向其中添加了一些数据,如下所示:

printbuffer: | 'H' | 'e' | 'l' | 'l' | 'o' | ' ' | 'W' | 'o' | 'r' | 'l' | 'd' | '\0' | ... |
offset:      12

在这个例子中,printbuffer中已经存储了字符串"Hello World",并且offset的值为12,表示我们已经使用了printbuffer的前12个位置。

现在,如果我们想要向printbuffer中添加一个新的字符,比如’!',我们就可以将这个字符添加到offset所指向的位置,然后将offset加1,如下所示:

printbuffer: | 'H' | 'e' | 'l' | 'l' | 'o' | ' ' | 'W' | 'o' | 'r' | 'l' | 'd' | '!' | '\0' | ... |
offset:      13

通过这种方式,我们可以确保在任何时候,printbuffer都有足够的空间来存储新的数据。这就是offset的主要作用。

这个update_offset函数的实现原理是计算printbuffer中字符串的新长度,并更新offset。这里的offsetprintbuffer中已经使用的部分的大小,也就是下一个空闲位置的索引。

函数的步骤如下:

  • 参数检查:首先检查buffer指针和buffer->buffer是否为NULL,如果是,则直接返回,不执行任何操作。

  • 定位字符串:通过buffer->buffer + buffer->offset定位到printbuffer中当前字符串的末尾。

  • 计算长度:使用strlen函数计算从buffer_pointer指向的位置开始的字符串的长度。
    buffer->buffer + buffer->offset确实已经定位到了printbuffer中的下一个可用位置。然而,update_offset函数中的strlen调用并不是为了找到下一个可用位置,而是为了计算新添加的字符串的长度。

    print_value函数中,我们会向printbuffer中添加新的字符串。这个新的字符串可能包含多个字符,所以我们不能简单地将offset加1。相反,我们需要计算新添加的字符串的实际长度,然后将这个长度加到offset上。

    这就是为什么我们需要调用strlen(buffer_pointer)buffer_pointer指向新添加的字符串的开始位置,strlen函数会返回从这个位置开始的字符串的长度。然后,我们将这个长度加到offset上,从而正确地更新offset

  • 更新offset:将计算出的长度加到buffer->offset上,从而更新offset

这样,offset就反映了printbuffer中字符串的新长度,确保了下一次添加内容时,能够正确地追加到字符串的末尾。这个过程对于构建JSON字符串是必要的,因为它保证了字符串的连续性和正确的内存管理。希望这个解释能帮助你理解update_offset函数的作用。


总结

通过深入研究cJSON_Print函数的源码,我们可以更好地理解cJSON库是如何将内存中的JSON对象转换为字符串的。这个函数的实现虽然复杂,但却非常关键,它使得我们可以方便地将JSON对象转换为字符串,以便于存储或传输。希望这篇文章能帮助你更好地理解cJSON库的内部工作原理,以及如何在你自己的项目中使用它。