visual-decaf / decaf-compiler

Compiler of decaf language
1 stars 3 forks source link

学习Boost.Json库的使用方法 #9

Closed DistinctWind closed 1 year ago

DistinctWind commented 1 year ago

我们需要一种序列化手段,将编译器获得的token、抽象语法树等信息序列化。而目前最常用的的序列化手段就是JSON了。Boost库又是C++的准标准库,相信这个库会比较合适。

DistinctWind commented 1 year ago

@3069795515 阅读Boost.Json官方文档和其他资料,考察其易用性。

DistinctWind commented 1 year ago

简要阅读Boost.Json库的官方文档后,提取到以下信息:

核心类

泛型容器 boost::json::value

JSON对象有若干种类型可以选择,一个字符串、一个布尔值、一个字典、一个数组都可以是一个JSON格式的对象。boost::json::value类提供了一个统一的容器,可以在这个容器中装下以上类型。

该容器有一定的自适应性,但想要精确定义数据结构,就不能依赖自动推断。

示例:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value val = {
        "This",
        "is",
        "a",
        "array"};
    std::cout << val << std::endl;
    return 0;
}

输出:

["This","is","a","array"]

注意,该JSON库中大部分类都重载了operator<<,能直接使用流输出进行序列化。

boost::json::array

根据官方文档,该类在“接口设计上”有意向std::vector看齐,作为JSON对象的数组,支持动态插入值(使用emplaceemplace_back

直接把上文的例子改成使用boost::json::array类型,得到的效果是一样的。

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::array val = {
        "This",
        "is",
        "a",
        "array"};
    std::cout << val << std::endl;
    return 0;
}

boost::json::object

表示了一个JSON对象,主要指的是JSON的字典格式,以键值对的形式定义。

在初始化的过程中,接受若干个键值对,表现为双层的initializer_list。外层为键值对列表,内层为含有两个元素的initializer_list,左边为键,右边为值。

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::object val {
        {"name", "John"},
        {"age", 18}
    };
    std::cout << val << std::endl;
    return 0;
}

输出:

{"name":"John","age":18}

注意,键必须为字符串,但是值可以为任意合理的JSON值类型,比如布尔值、数字等。

boost::json::string

是一个字符串类型,Boost.Json库假定该字符串为UTF-8格式。但这个类无关紧要,我们可以使用std::string代替他。

重点应该是命名冲突的问题,这提醒我们不能直接无脑using namespace std;

可以在函数体的范围内适当地使用using声明,但使用using指示时要额外小心。

类型嵌套

字典嵌套列表

比如说,在一个JSON对象中,需要最外层是一个字典,内层的值包含了一个列表。

此时boost::json::value的自动推导能起作用。比如我们弄一个student的JSON结构体:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value student{
        {"name", "John"},
        {"age", 18},
        {"scores", {99, 100, 87}}};
    std::cout << student << std::endl;
    return 0;
}

输出:

{"name":"John","age":18,"scores":[99,100,87]}

不依赖自动推导,则应该这样指明类型:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value student = boost::json::object{
        {"name", "John"},
        {"age", 18},
        {"scores", boost::json::array{99, 100, 87}}};
    std::cout << student << std::endl;
    return 0;
}

得到同样的结果。

注意,虽然结果相同,但是知晓其底层类型依然是重要的,因为在实际序列化时没有办法像这样将想要序列化的值硬编码到程序中,我们只能对JSON对象的具体类型进行操作。

列表嵌套字典

这个可能比较有用,使用token流的例子,我们让一个JSON字典表示一个token,那么token流就是一个JSON数组了,这个数组里的每一个元素都是token字典。

同样可以自动推导:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value tokens = {
        {
            {"type", "INT"},
            {"line", 10}
        },
        {
            {"type", "CLASS"},
            {"line", 20}
        }
    };
    std::cout << tokens << std::endl;
    return 0;
}

输出:

[{"type":"INT","line":10},{"type":"CLASS","line":20}]

或者使用完整类型:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value tokens = boost::json::array{
        boost::json::object{
            {"type", "INT"},
            {"line", 10}
        },
        boost::json::object{
            {"type", "CLASS"},
            {"line", 20}
        }
    };
    std::cout << tokens << std::endl;
    return 0;
}

得到同样的结果。

修改JSON对象

修改boost::json::object

该类被设计为像std::map一样,可以直接给键值对赋值:

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::object obj;
    obj["name"] = "john";
    obj["age"] = 18;
    std::cout << obj << std::endl;
    return 0;
}
{"name":"john","age":18}

修改boost::json::array

该类被设计为像std::vector一样,通常使用push_back或者emplace_back添加元素。

注意,emplace_back接受的构造参数会被传到boost::json::value当中,此处使用临时量(右值)移动的方式演示emplace_back

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::array arr;
    arr.push_back(10);
    arr.emplace_back(
        boost::json::value{
            {"key", "value"},
        });
    std::cout << arr << std::endl;
    return 0;
}

输出:

[10,{"key":"value"}]

序列化API设计规范:

基于以上知识,可以初步设计一个序列化API。

序列化接口serializable

接口类为纯虚基类serializable,定义如下:

class serializable {
    virtual boost::json::value to_json() = 0;
};

实现了该接口的函数负责依据自身信息构造一个json对象。

实现序列化接口的示例

比如token类应该这样序列化自身:

struct token: serializable {

    enum class type {
        INT,
        CLASS
    };

    inline static const std::map<type, std::string> type_name{
            {type::INT, "INT"},
            {type::CLASS, "CLASS"},
    };

    type _type;
    int line;

    token(type _type, int line):
        _type{_type}, line{line} {
    }

    boost::json::value to_json() override {
        using namespace boost;
        json::value result = {
                {"type", type_name.at(_type)},
                {"line", line}};
        return result;
    }
};

另一方面,对于组合了多个token的token流类(token_stream),则其序列化结果应为一个JSON数组,并且依赖于每一个token进行JSON序列化的结果。

struct token_stream: serializable {
    std::vector<token> tokens;

    token_stream(std::initializer_list<token> tokens):
        tokens{tokens} {
    }

    boost::json::value to_json() override {
        using namespace boost;
        json::array result;
        for (auto&& tok: tokens) {
            auto serialized_tok = tok.to_json();
            result.emplace_back(serialized_tok);
        }
        return result;
    }
};

此处使用emplace_back插入JSON对象。

最后,调用该API:

int main() {
    using namespace std;
    using namespace boost;

    token_stream tokenStream{
            {token::type::INT, 10},
            {token::type::CLASS, 20},
    };

    cout << tokenStream.to_json();
    return 0;
}

得到预期的结果:

[{"type":"INT","line":10},{"type":"CLASS","line":20}]
DistinctWind commented 1 year ago

补充反序列化

反序列化

通过boost::json::parse函数,接受一个字符串,得到boost::json::value类型的对象。

#include <boost/json.hpp>
#include <iostream>

int main(int argc, char* argv[]) {
    boost::json::value result = boost::json::parse(
        R"([1,2,3,{"key": "value"}])"
        );
    std::cout << result << std::endl;
    return 0;
}

输出:

[1,2,3,{"key":"value"}]

(这个示例很明显是反序列化后再序列化……仅做演示用途)

DistinctWind commented 1 year ago

补充配置说明

配置说明

Boost.Json库是Boost库中少数非Header Only的库之一。需要链接对应的库文件才能使用。幸运的是,我们已经使用VCPKG作为包管理工具,编译安装Boost库并不需要我们操心,我们只需要考虑如何链接到Boost.Json就可以了。

搜寻组件

在cmake文件中,让FindBoost模块搜寻组件json

find_package(Boost REQUIRED
        COMPONENTS json
        )

然后,Boost.Json对应的库文件就会导出到变量Boost_LIBRARIES中。

配置构建目标

接下来,给构建目标配置加上头文件依赖和链接依赖:

add_executable(boost_json_learning main.cpp)
target_include_directories(boost_json_learning PRIVATE ${Boost_INCLUDE_DIRS})
target_link_libraries(boost_json_learning PRIVATE ${Boost_LIBRARIES})

正如Catch2的使用方法一样。此时目标boost_json_learning即可使用Boost.Json库并成功编译。

DistinctWind commented 1 year ago

考察结果

经以上考察,Boost.Json库的易用性和通用性足够,可以进入项目以来当中。

DistinctWind commented 1 year ago

Boost.Json的学习已经完成