Як експортувати поля struct C++ у JSON за допомогою clang та boost::json

Також дивіться версію цієї публікації мовою Rust: How to export C++ struct fields to JSON using Rust & clang

У нашій попередній публікації How to parse C++ struct definition from source code using clang, ми навчилися розбирати код C/C++ та вилучати інформацію про визначення struct за допомогою бібліотеки clang.

У згаданій публікації інформація виводилася у вільному, зручному для читання форматі, проте:

struct_parse_example.txt
Struct: myParameters
  Field: a (Type: double)
  Field: b (Type: double)

У цій публікації ми розширимо попередній приклад, щоб експортувати поля struct у формат JSON за допомогою бібліотеки boost::json.

Повний вихідний код

export_structs_to_json.cpp
#include <clang-c/Index.h>
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <fstream>
#include <filesystem>
#include <boost/json.hpp>

boost::json::value extractStructFields(CXCursor cursor) {
    CXCursorKind kind = clang_getCursorKind(cursor);
    if (kind == CXCursor_StructDecl) {
        CXString structName = clang_getCursorSpelling(cursor);
        std::string structNameStr = clang_getCString(structName);
        clang_disposeString(structName);

        boost::json::object structObj;
        structObj["name"] = structNameStr;
        boost::json::array fieldsArray;

        struct FieldCollector {
            boost::json::array& fields;
            int currentIndex;
        };

        FieldCollector collector = {fieldsArray, 0};

        clang_visitChildren(cursor, [](CXCursor c, CXCursor parent, CXClientData client_data) {
            auto* collector = static_cast<FieldCollector*>(client_data);
            CXCursorKind kind = clang_getCursorKind(c);

            if (kind == CXCursor_FieldDecl) {
                boost::json::object fieldObj;

                CXString fieldName = clang_getCursorSpelling(c);
                CXType fieldType = clang_getCursorType(c);
                CXString typeName = clang_getTypeSpelling(fieldType);

                fieldObj["index"] = collector->currentIndex++;
                fieldObj["name"] = clang_getCString(fieldName);
                fieldObj["type"] = clang_getCString(typeName);

                collector->fields.push_back(fieldObj);

                clang_disposeString(fieldName);
                clang_disposeString(typeName);
            }
            return CXChildVisit_Continue;
        }, &collector);

        structObj["fields"] = fieldsArray;
        return structObj;
    }
    return boost::json::value(nullptr);
}

int main(int argc, char** argv) {
    CXIndex index = clang_createIndex(0, 0);

    // Прочитати код з argv[1]
    if (argc < 2) {
        std::cerr << "Використання: " << argv[0] << " <вихідний файл>" << std::endl;
        return 1;
    }
    const char* codeFilename = argv[1];
    if(!std::filesystem::exists(codeFilename)) {
        std::cerr << "Файл не існує: " << codeFilename << std::endl;
        return 1;
    }
    std::ifstream codeFile(codeFilename);
    if (!codeFile) {
        std::cerr << "Помилка відкриття файлу: " << codeFilename << std::endl;
        return 1;
    }
    std::string code((std::istreambuf_iterator<char>(codeFile)), std::istreambuf_iterator<char>());
    codeFile.close();
    if (code.empty()) {
        std::cerr << "Файл порожній: " << codeFilename << std::endl;
        return 1;
    }

    CXUnsavedFile unsavedFile = {"test.cpp", code.c_str(), code.size()};
    CXTranslationUnit unit = clang_parseTranslationUnit(index, "test.cpp", nullptr, 0, &unsavedFile, 1, CXTranslationUnit_None);
    if (unit == nullptr) {
        std::cerr << "Не вдалося розібрати одиницю трансляції." << std::endl;
        return 1;
    }

    CXCursor cursor = clang_getTranslationUnitCursor(unit);
    boost::json::array structsArray;

    clang_visitChildren(cursor, [](CXCursor c, CXCursor parent, CXClientData client_data) {
        auto* structs = static_cast<boost::json::array*>(client_data);
        boost::json::value structJson = extractStructFields(c);

        if (!structJson.is_null()) {
            structs->push_back(structJson);
        }
        return CXChildVisit_Continue;
    }, &structsArray);

    boost::json::object result;
    result["structs"] = structsArray;
    std::cout << boost::json::serialize(result) << std::endl;

    clang_disposeTranslationUnit(unit);
    clang_disposeIndex(index);
    return 0;
}

Як скомпілювати

У цьому випадку ми використали cmake для збирання проєкту. Файл CMakeLists.txt виглядає так:

CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(parse_structs)

# Встановити стандарт C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Знайти компоненти LLVM/Clang
find_package(LLVM 19.1.1 REQUIRED CONFIG)
find_package(Clang REQUIRED CONFIG)
find_package(Boost REQUIRED COMPONENTS json)

# Додати каталоги include
include_directories(${LLVM_INCLUDE_DIRS})
include_directories(${CLANG_INCLUDE_DIRS})

# Створити виконуваний файл
add_executable(parse_structs parse_structs.cpp)

# Додати визначення LLVM
target_compile_definitions(parse_structs PUBLIC ${LLVM_DEFINITIONS})

# Зв'язати з libclang
target_link_libraries(parse_structs PUBLIC libclang ${Boost_LIBRARIES})

Як запустити

run_parse_structs.sh
./parse_structs test.cpp | jq

| jq використовується для красивого виведення JSON. Ви можете не використовувати його, якщо не хочете форматувати вивід.

де test.cpp містить, наприклад, такий код:

test.cpp
struct myParameters {
  double a;
  double b;
};

Приклад виводу

export_structs_output.json
{
  "structs": [
    {
      "name": "myParameters",
      "fields": [
        {
          "index": 0,
          "name": "a",
          "type": "double"
        },
        {
          "index": 1,
          "name": "b",
          "type": "double"
        }
      ]
    }
  ]
}

Дивіться схожі статті за категоріями: C/C++, Clang