ejson4cpp——一个使用极致简单且性能可比rapidjson的C++json解析库

Posted C+G

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ejson4cpp——一个使用极致简单且性能可比rapidjson的C++json解析库相关的知识,希望对你有一定的参考价值。

文章目录


代码仓库: https://github.com/ACking-you/ejson4cpp

ejson4cpp

ejosn4cpp :意味着这是一个使用上非常 easy,同时性能上也非常 efficiency c++ json解析库。 支持c++11及以上,并且完全的跨平台。

  • 使用 easy 体现在:

    1. api简单,你只需要关注两个函数(FromJSON、ToJSON),且支持一键json结构体互转。
    2. 引入简单,支持cmake命令一键引入项目并使用。
    3. 错误定位简单,无论是解析json还是序列化为json,任何错误的操作都会有详细的报错信息(模拟打印了堆栈信息),让错误定位更简单。
  • 性能 efficiency 体现在:
    本机benchmark(3000行json)结果如图:

    1. 反序列化(Parse)性能明显领先 nlohmann-jsonjsoncpp,但只有 rapidjson 的一半性能。
    2. 序列化(Stringify)性能遥遥领先其他所有json库一个数量级。
    3. 查找(FindMember):由于看过 rapidjson 源码,发现其内部每个元素的节点是以数组的形式组织的,并没有用到其他高深的数据结构,故专门对他进行成员查找测试,发现确实是 O(n) 级别的查找性能。

    benchmark的代码仓库:https://github.com/ACking-you/bench_json4cpp

快速开始

要求

  • C++11及以上,是跨全平台的

安装与引入

推荐用以下两种方式进行引入:

  • 方法一:通过cmake中的 FetchContent 模块引入

    1. 在项目的cmake中添加下列代码进行引入,国内如果因为网络问题无法使用可以换这个gitee的镜像源:https://gitee.com/acking-you/ejson4cpp.git

      include(FetchContent)
      
      FetchContent_Declare(
              ejson4cpp
              GIT_REPOSITORY https://github.com/ACking-you/ejson4cpp.git
              GIT_TAG v1.5.2
              GIT_SHALLOW TRUE)
      FetchContent_MakeAvailable(ejson4cpp)
      
    2. 在需要使用该库的目标中链接 ejson 即可。

      target_link_libraries(target  ejson)
      
  • 方法二:手动下载包,然后通过cmake命令引入

    1. 通过git命令下载项目源码

      git clone https://github.com/ACking-you/ejson4cpp.git
      
    2. 将该项目添加到子项目中:

      add_subdirectory(ejson4cpp)
      
    3. 在需要使用该库的目标中链接 ejson 即可。

      target_link_libraries(target  ejson)
      

开始使用

这里以解析 json 的配置文件映射到 C++ 结构体为例子来进行讲解。

假设有redis、mysql、日志服务需要通过配置文件来进行配置,我们先写下结构体如下:

struct server

   int         port;
   std::string host;
;

struct log

   std::string level;
   std::string filedir;
   std::string formatter;
;

struct config

   log    logger;
   server redis;
   server mysql;
;

一个模拟的json配置文件如下:


"logger": 
  "filedir": "home/project/1",
  "formatter": "default",
  "level": "debug"
,
"mysql": 
  "host": "192.31.1.1",
  "port": 1314
,
"redis": 
  "host": "127.0.0.1",
  "port": 1444


现在要实现的功能是读取json配置文件的数据将 config 结构体进行初始化,我们可以按照下面的步骤进行:

完整代码请看 example/example1.cc

  1. 让server、log、config这几个自定义类型支持 json 序列化,添加下列宏定义即可:

    // auto generate log/server/config to_json and from_json
    AUTO_GEN_NON_INTRUSIVE(log, level, filedir, formatter)
    AUTO_GEN_NON_INTRUSIVE(server, host, port)
    AUTO_GEN_NON_INTRUSIVE(config, logger, redis, mysql)
    
  2. 定义config变量,调用 FromFile 函数,即可完成需求:

    struct config s_config;
    // init config from config.json
    Parser::FromFile(CONFIG_PATH, s_config);
    

    如果需要重新写回文件,则可调用 ToFile 函数:

    // write config to file
    Parser::ToFile(CONFIG_PATH, s_config);
    

    如果读取json字符串的数据并初始化对应的变量(反序列化)则可以调用 FromJSON 函数:

    // init config struct from json string
    Parser::FromJSON(j, s_config);
    

    如果需要将变量转化为json字符串(序列化),则可调用 ToJSON 函数:

    auto json_str = Parser::ToJSON(s_config);
    

好的,经过以上两步,你已经学会了整个库的核心用法,没错,这个库提倡使用直接的函数而不是类来实现对应的功能,这样能减少你的记忆和思考过程。当然如果需要更为细致的使用它,你可以去了解 JObject 类的相关用法,在API介绍里面写的很详细。

常见用法

在进行后端开发的过程中,前端传来的数据很多时候是 json 数据,那么我们现在就使用该库来模拟一个简单的后端业务。

比如一个视频平台的评论区,首先映入眼帘的是一条条评论,然后是发出该条评论的用户。

那么我们可以抽离出 commentuser_info 这两个结构体表示前端需要展示的消息,那么它在我们C++后端的请看可能是下面这样的结构体:

完整代码在 example/example2.cc

struct user_info

   bool        is_follow;//是否关注
   int64_t     id;//id信息
   int64_t     follow_count;//关注的博主数量
   int64_t     follower_count;//粉丝数量
   std::string name;//用户名
;

struct comment

   int64_t     id;//id信息
   int64_t     user_id;//用户id信息
   std::string created_date;//创建时间
   std::string content;//评论内容
;

那么我们的后端逻辑可能会经历下面的过程:

  1. 从前端获取json数据(中间一般有鉴权的过程)。
  2. 接收json数据并其初始化为对应的C++结构体。
  3. 进行该次接口调用的业务逻辑处理。
  4. 保存数据到数据库。

那么我们用模拟数据来模拟上述过程:

  1. 前端的数据:

    const char* comment_json   = "\\n"
        "  "content": "这是一条\\"测试\\"评论",\\n"
        "  "created_date": "2023-01-16",\\n"
        "  "id": 1,\\n"
        "  "user_id": 10\\n"
        "";
    const char* user_info_json = "\\n"
        "  "follow_count": 12,\\n"
        "  "follower_count": 23,\\n"
        "  "id": 1,\\n"
        "  "is_follow": false,\\n"
        "  "name": "某人名字"\\n"
        "";
    
  2. 将数据转为C++的结构体:
    需要先添加下列宏让对应的结构支持json互转

    AUTO_GEN_NON_INTRUSIVE(user_info, is_follow, id, follow_count, follower_count,name)
    AUTO_GEN_NON_INTRUSIVE(comment, id, user_id, created_date, content)
    

    然后调用对应函数即可转化

    comment   cmt;
    user_info uinfo;
    Parser::FromJSON(comment_json, cmt);
    Parser::FromJSON(user_info_json, uinfo);
    
  3. 处理业务逻辑,这个跳过。

  4. 保存数据到数据库,这个模拟为保存数据到文件:
    我们可以创建一个 dict_t 类型的 JObject ,然后把刚才的结构体以 key-value 对的形式放进去,最后再调用 ToFile 函数。

    // 4.save data to database(we simulate it to local file)
    auto object = JObject::Dict();
    object.at("comment").get_from(cmt);
    object.at("user_info").get_from(uinfo);
    ejson::Parser::ToFile(DATA_PATH, object);
    

    最终得到下列json数据到文件中:

    
      "comment": 
        "content": "这是一条"测试"评论",
        "created_date": "2023-01-16",
        "id": 1,
        "user_id": 10
      ,
      "user_info": 
        "follow_count": 12,
        "follower_count": 23,
        "id": 1,
        "is_follow": false,
        "name": "某人名字"
      
    
    

API介绍

对所有类成员的描述的信息,可以点开 doc/html/index.html 进行查看。如果需要其他语言版本的文档,可以自己通过 Doxygen 进行生成。

通过命名风格识别API

  1. 所有对外暴露的静态成员函数均以 PascalCase 风格命名。如下:

    namespace ejson 
    class Parser
    
       static JObject FromJSON(const str_t &content,bool skip_comment=false);
        
       template <class T>
       static void FromJSON(string_view const &src, T &dst,bool skip_comment=false);
        
       static JObject FromFile(string_view const &filename,bool skip_comment=false);
        
       template <class T>
       static void FromFile(string_view const &filename, T &dst);
       
       template <class T>
       static std::string ToJSON(T &&src,const int indent = -1,
                            const char indent_char = ' ', bool is_esc = false);
        
       template <class T>
       static void ToFile(string_view const &filename, T const &src, 
                          const int indent = -1, const char indent_char = ' ',
                          bool is_esc = false);
        
       static void ToFile(string_view const &filename, JObject const &src,
                         const int indent = -1, const char indent_char = ' ',
                          bool is_esc = false)
    ;
    
    class JObject
    
       static auto Dict() -> JObject;
       static auto List() -> JObject;
    ;
       // namespace ejson
    
  2. 所有想要暴露的普通成员函数均以 snack_case 风格命名,如下:

    namespace ejson 
    class JObject
    
       auto type() const -> Type;
        
       auto at(const str_t &key) const -> ObjectRef;
        
       auto to_string(int indent = -1, char indent_char = ' ',
                                      bool is_esc = false) const -> string;
        
       void push_back(JObject item);
        
       void pop_back();
        
       auto has_key(const str_t &key) const -> bool;
    
       template <class T>
       auto cast() const -> T;
    ;
    
    struct ObjectRef
    
       template <class T>
       auto get_from(T const &src) -> ObjectRef &;
       template <class T>
       void get_to(T &src);
    ;
       // namespace ejson
    
  3. 其余还有两个函数,如下:

    namespace ejson_literals 
    
    auto operator""_json(const char *json, size_t len) -> JObject;
        
    auto float_d(int d) -> int;
    
       // namespace ejson_literals
    

宏定义

利用宏定义可以方便且迅速的让自定义类型支持 FromJSONToJSON 系列函数。

实际上自定义类型在使用 FromJSON 时,只需要定义对应的 from_json 函数,使用 ToJSON 时,只需定义对应的 to_json 函数。

下列是 from_json 和 to_json 的函数签名:

void from_json(const ejson::JObject& ejson_j, T& ejson_t);

void to_json(ejson::JObject& ejson_j, const T& ejson_t);

你可以像下面这样自己实现上面这两个函数来让自定义类型支持 FromJSONToJSON 函数。

struct student

   int         id;
   int         score;
   std::string name;
;

void to_json(ejson::JObject& ejson_j, const student& ejson_t)

   ejson_j.at("id").get_from(ejson_t.id);
   ejson_j.at("score").get_from(ejson_t.score);
   ejson_j.at("name").get_from(ejson_t.name);


void from_json(const ejson::JObject& ejson_j, student& ejson_t)

   ejson_j.at("id").get_to(ejson_t.id);
   ejson_j.at("score").get_to(ejson_t.score);
   ejson_j.at("name").get_to(ejson_t.name);

如果属性是 private 的,那么可以像下面这样侵入式的定义:

struct student

   friend void to_json(ejson::JObject& ejson_j, const student& ejson_t)
   
      ejson_j.at("id").get_from(ejson_t.id);
      ejson_j.at("score").get_from(ejson_t.score);
      ejson_j.at("name").get_from(ejson_t.name);
   
private:
   int         id;
   int         score;
   std::string name;
;

FROM_JSON_FUNC&FROM_JSON_FRIEND_FUNC

用于简化 from_json 函数定义的书写,例如前面对于 strudent 类型的 from_json 函数可以这样写:

struct student

   int         id;
   int         score;
   std::string name;
;
//非侵入式
FROM_JSON_FUNC(student, ejson_j, ejson_t) 
   ejson_j.at("id").get_to(ejson_t.id);
   ejson_j.at("score").get_to(ejson_t.score);
   ejson_j.at("name").get_to(ejson_t.name);


struct student

    //侵入式
   FROM_JSON_FRIEND_FUNC(student,ejson_j,ejson_t)
   
      ejson_j.at("id").get_to(ejson_t.id);
      ejson_j.at("score").get_to(ejson_t.score);
      ejson_j.at("name").get_to(ejson_t.name);
   
private:
   int         id;
   int         score;
   std::string name;
;

TO_JSON_FUNC&TO_JSON_FRIEND_FUNC

用于简化 to_json 函数定义的书写,例如前面对于 strudent 类型的 to_json 函数可以这样写:

struct student

   int         id;
   int         score;
   std::string name;
;
//非侵入式
TO_JSON_FUNC(student, ejson_j, ejson_t) 
   ejson_j.at("id").get_from(ejson_t.id);
   ejson_j.at("score").get_from(ejson_t.score);
   ejson_j.at("name").get_from(ejson_t.name);


struct student

    //侵入式
   TO_JSON_FRIEND_FUNC(student,ejson_j,ejson_t)
   
      ejson_j.at("id").get_from(ejson_t.id);
      ejson_j.at("score").get_from(ejson_t.score);
      ejson_j.at("name").get_from(ejson_t.name);
   
private:
   int         id;
   int         score;
   std::string name;
;

AUTO_GEN_NON_INTRUSIVE&AUTO_GEN_INTRUSIVE

这两个宏可以帮助你一键生成之前例子中的 to_jsonfrom_json 函数。

前面的代码可以替换为:

struct student

   int         id;
   int         score;
   std::string name;
;
//非侵入式
AUTO_GEN_NON_INTRUSIVE(student,id,score,name)
    
struct student

//侵入式
AUTO_GEN_INTRUSIVE(student,id,score,name)
    
private:
   int         id;
   int         score;
   std::string name;
;

ENABLE_JSON_COUT

自动生成对应类型的 operator<<(ostream&,T) 运算符重载,用于将对应类型支持 cout 打印出json格式。该宏可以为多个类型生成。

struct student

   int         id;
   int         score;
   std::string name;
;

struct info

   int         id;
   std::string msg;
;
//让对应类型支持json格式化
AUTO_GEN_NON_INTRUSIVE(student,id,score,name)
AUTO_GEN_NON_INTRUSIVE(info,id,msg)
    
//支持json格式cout打印
ENABLE_JSON_COUT(student,info)

FromJSON系列函数

参数说明

static JObject Parser::FromJSON(const str_t &content, bool skip_comment = false);

根据json字符串内容反序列化为JObject结构。

参数说明:

  • content:需要解析的json资源,这是一个string_view类型的参数,支持C风格字符串和 std::string
  • skip_comment:是否需要支持跳过注释,默认为false,未开启。

返回值:

  • 返回解析完的 JObject 类型。

template <class T>
static void Parser::FromJSON(string_view const &src, T &dst,bool skip_comment = false)

根据json字符串内容反序列化数据到 dst

参数说明:

  • src:需要解析的json资源,这是一个string_view类型的参数,支持C风格字符串和 std::string
  • dst:需要初始化的变量,可以是自定义类型。
  • skip_comment:是否需要支持跳过注释,默认为false,未开启。

static JObject& Parser::FromFile(string_view const &filename, bool skip_comment = false);

根据文件中的 json 数据获取 JObject&,这个JObject是thread_local变量,也就是每个线程共用一个JObject。所以请注意,当您调用此函数时,将更新这个共用的 JObject 的值。

参数说明: