第二章 EXI协议原理与实现--9.6 cJSON库改进--支持64位长整型

9.6 cJSON库改进--支持64位长整型

cJSON本身不支持64位的整形数据解析和增加,对于部分应用场景不适用(例如时间戳,需要64位整数)。原因在于cJSON使用double来存储中间数据,转换为整形可能存在精度问题。本节作者提出了改进方案,让cJSON能够支持uint64类型。

9.6.1 基础知识和原因分析

float和double的范围是由指数的位数来决定的。

float的指数位有8位,而double的指数位有11位,分布如下:

float:
1bit(符号位)
8bits(指数位)
23bits(尾数位)

double:
1bit(符号位)
11bits(指数位)
52bits(尾数位)

精度

float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。

float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;

double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。

长整型

在java语言中,long表示长整型,默认是8字节。但是在C语言中,long是与short对应的整型,都归属于int类型,一般的测试结果如下:

sizeof(int) =4;
sizeof(short) =2;
sizeof(long) =4;

要想表达长整型必须定义:long long, 这个测试结果:

sizeof(long long) =8

8字节的long long数字范围是: 十进制有效数字大约19位 

-2^(64-1) ~ 2^(64-1)-1 (即 -9223372036854775808 ~ 9223372036854775807)

如果是无符号长整型,有效数字就达到了20位。

看到这里就能明白了,对于一个长整型数字,cJSON中用double表达精度有限只能达到15位,所以不能满足19位长整数的要求。

在V2G消息的XML中可能出现的大数字: timestamp,协议中规定的64位数字, 要想有效的使用cJSON解析和编码必须扩展支持int64。

9.6.2 扩展int64的思路和方法

在下列文章中提出了一种方法,但是由于cJSON源码版本更新,原来针对parse_number函数的实现方法已经没用了,需要另外实现。

参考文章:cJSON支持64位数据解析  https://blog.csdn.net/zh7152106/article/details/115506265

作者提出了一种思路:在cJSON类型中增加新成员 valuelonglong, 用于存储长整型数字,在解析数字时同时计算长整型数字。使用时可以根据需要自由调用哪一种类型(int、double、longlong)。

改造的代码如下(黄色部分): 为了简化已经屏蔽了宏定义,读者可以自由打开。

cJSON.h

#ifndef cJSON__h
#define cJSON__h

#ifdef __cplusplus
extern "C"
{
#endif

/* 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
    
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512


//#define CONFIG_CJSON_SUPPORT_64BIT
//#ifdef CONFIG_CJSON_SUPPORT_64BIT
    #define NUMBER_DOUBLE       1
    #define NUMBER_LONGLONG     2
//#endif


/* The cJSON structure: */
typedef struct cJSON {
    struct cJSON *next,*prev;    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *child;        /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */


    int type;                    /* The type of the item, as above. */


    char *valuestring;            /* The item's string, if type==cJSON_String */
    long long valueint;                /* The item's number, if type==cJSON_Number */
    double valuedouble;            /* The item's number, if type==cJSON_Number */


    char *string;                /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */




//#ifdef CONFIG_CJSON_SUPPORT_64BIT
     /* The type of number,double,long long or ull */
    int numbertype;
    long long valuelonglong;
//#endif


} cJSON;

......

//#ifdef CONFIG_CJSON_SUPPORT_64BIT
    extern int cJSON_Get_LongLong(const cJSON * const object, const char * key, long long* out);
    extern cJSON * cJSON_CreateLongLong(long long num);
    extern void cJSON_AddLongLongToObject(cJSON * const object, const char * const name, const long long valuell);
//#endif

#ifdef __cplusplus
}
#endif

#endif

cJSON.c

// created by Tom-hongtao.gao
static const char* parse_number(cJSON *item, const char *num) {
    long long m=0;
    double n = 0, sign = 1, scale = 0;
    int subscale = 0, signsubscale = 1;


    if (*num == '-')
        sign = -1, num++; /* Has sign? */
    if (*num == '0')
        num++; /* is zero */
    if (*num >= '1' && *num <= '9'){
        do{
            n = (n * 10.0) + (*num - '0');
            m = (m * 10) + (*num - '0');
            num++;
        }while (*num >= '0' && *num <= '9'); /* Number? */
    }
    if (*num == '.' && num[1] >= '0' && num[1] <= '9') {
        num++;
        do{
            n = (n * 10) + (*num++ - '0'), scale--;
        }while (*num >= '0' && *num <= '9');
    } /* Fractional part? */
    if (*num == 'e' || *num == 'E') /* Exponent? */
    {
        num++;
        if (*num == '+')
            num++;
        else if (*num == '-')
            signsubscale = -1, num++; /* With sign? */
        while (*num >= '0' && *num <= '9')
            subscale = (subscale * 10) + (*num++ - '0'); /* Number? */
    }


    n = sign * n * pow(10.0, (scale + subscale * signsubscale)); /* number = +/- number.fraction * 10^+/- exponent */


//#ifdef CONFIG_CJSON_SUPPORT_64BIT
    if(scale==0 && subscale==0){
        item->valuelonglong = m;
        item->numbertype = NUMBER_LONGLONG;
    }else{
        item->numbertype = NUMBER_DOUBLE;
    }
//#endif


    item->valuedouble = n;
    item->valueint = (long long) n;
    item->type = cJSON_Number;


    return num;
}

parse_number()函数中,如果没有小数点和E指数部分,就表示是个纯粹的整数。 同步使用m计算长整数,结果就表示在valuelonglong, 同时类型也设置为NUMBER_LONGLONG。

print_number()函数,需要判断是否是长整型, 是的话就输出valuelonglong。

【注意】但凡是print_xxx函数,都会在内部分配内存,调用者自己负责手动释放指针。否则就会出现内存泄漏。

/* Render the number nicely from the given item into a string. */
static char *print_number(cJSON *item,printbuffer *p)
{
    char *str=0;
    double d=item->valuedouble;

//#ifdef  CONFIG_CJSON_SUPPORT_64BIT
    if(item->numbertype == NUMBER_LONGLONG)
    {
        str=(char*)cJSON_malloc(30);   //30 chars is enough
        sprintf((char*)str, "%lld", item->valuelonglong);
        return str;
    }
//#endif

    if (d==0)
    {
        if (p)    str=ensure(p,2);
        else    str=(char*)cJSON_malloc(2);    /* special case for 0. */
        if (str) strcpy(str,"0");
    }
    else if (fabs(((double)item->valueint)-d)<=DBL_EPSILON && d<=INT_MAX && d>=INT_MIN)
    {
        if (p)    str=ensure(p,21);
        else    str=(char*)cJSON_malloc(21);    /* 2^64+1 can be represented in 21 chars. */
        if (str)    sprintf(str,"%lld",item->valueint);
    }
    else
    {
        if (p)    str=ensure(p,64);
        else    str=(char*)cJSON_malloc(64);    /* This is a nice tradeoff. */
        if (str)
        {
            if (fabs(floor(d)-d)<=DBL_EPSILON && fabs(d)<1.0e60)sprintf(str,"%.0f",d);
            else if (fabs(d)<1.0e-6 || fabs(d)>1.0e9)            sprintf(str,"%e",d);
            else                                                sprintf(str,"%f",d);
        }
    }

    return str;
}

还得要提供几个读写接口函数:

//#ifdef CONFIG_CJSON_SUPPORT_64BIT

int cJSON_Get_LongLong(const cJSON * const object, const char * key, long long* out)
{
    cJSON* sub_obj = NULL;

    sub_obj = cJSON_GetObjectItem(object,key);
    if(out == NULL || sub_obj == NULL || sub_obj->numbertype != NUMBER_LONGLONG)
     {
        return -1;
     }

    *out = sub_obj->valuelonglong;
    return 0;
}


cJSON * cJSON_CreateLongLong(long long num)
{
    cJSON *item = cJSON_New_Item();
    if(item)
     {
        item->type = cJSON_Number;
        item->valuedouble = (double)num;

        item->numbertype = NUMBER_LONGLONG;
        item->valuelonglong = num;
     }
    return item;
}


void cJSON_AddLongLongToObject(cJSON * const object, const char * const name, const long long valuell)
{
    cJSON *number_item = cJSON_CreateLongLong(valuell);
    cJSON_AddItemToObject(object, name, number_item);
}


//#endif

到此就修改完毕了,下面我们来进行测试。

9.6.3 测试长整型解析和输出

测试数据:

充电桩ISO15118-2协议的SessionSetupRes命令实例: 我故意把时间戳数据写成了这么长19位。

{
    "V2G_Message": {
        "Header": {
            "SessionID": "C08CDF36985A7190"
        },
        "Body": {
            "SessionSetupRes": {
                "ResponseCode": "OK_NewSessionEstablished",
                "EVSEID": "UK123E1234",
                "EVSETimeStamp": 1234567890123456789
            }
        }
    }
}

该命令对应的schema规范中定义了EVSETimeStamp的具体类型位long,因此必须要用到int64类型解析。

测试代码:

cJSON * cjson = cJSON_Parse(json); 
printf("json格式化后:\n");
char * pp = cJSON_Print(cjson);
printf("%s\n\n", pp);
free(pp);
cJSON_Delete(cjson);

//构造长整型数据:
cJSON_AddLongLongToObject(json_SessionSetupRes, "EVSETimeStamp", (long long)exiDoc->V2G_Message.Body.SessionSetupRes.EVSETimeStamp);

运行结果,正确输出了时间戳,说明我们的改造是成功的。

json格式化后:
{
        "V2G_Message":  {
                "Header":       {
                        "SessionID":    "C08CDF36985A7190"
                },
                "Body": {
                        "SessionSetupRes":      {
                                "ResponseCode": "OK_NewSessionEstablished",
                                "EVSEID":       "UK123E1234",
                                "EVSETimeStamp":        1234567890123456789
                        }
                }
        }
}

lret = 0, timeStamp=1234567890123456789
1111 SessionSetupRes.EVSETimeStamp=1234567890123456789

你可能感兴趣的:(ISO15118,EXI)