News: LimeReport new version 1.5 has been released
Вышла новая версия LimeReport 1.5

Author Topic: Повреждение кучи при совместном использовании LimeReport и QQmlEngine (Qt 6)  (Read 368 times)

maestro

  • Newbie
  • *
  • Posts: 2
    • View Profile
1. Окружение

ОС: Debian 13 (Trixie), amd64
Qt: 6.10.1 (x86_64, shared, GCC 14.2.0)
LimeReport: последняя версия из репозитория, собрана с опцией -DQT_VERSION_MAJOR=6 -DLIMEREPORT_DEMO=ON
Компилятор: GCC 14.2.0
Система сборки: CMake + Ninja

2. Описание проблемы

При одновременном использовании в одном процессе QQmlEngine (с загруженным QML-компонентом) и LimeReport ReportEngine происходит повреждение кучи (heap corruption). Программа завершается с сообщением:

Quote
corrupted size vs. prev_size while consolidating

Краш происходит в деструкторах при завершении программы — в момент уничтожения синглтона
LimeReport::ScriptEngineManager, который владеет собственным QJSEngine (и, соответственно, QV4::ExecutionEngine).

3. Шаги для воспроизведения
3.1. Скрипт сборки LimeReport

Code: [Select]
#! /bin/bash
cmake -G "Ninja" \
    -B build \
    -S src \
    -DQT_VERSION_MAJOR=6 \
    -DLIMEREPORT_DEMO=ON \
    -DCMAKE_INSTALL_PREFIX=install \
    -DCMAKE_TOOLCHAIN_FILE=/root/toolchain.cmake

cmake --build build --config Release --parallel --target install

3.2. CMakeLists.txt тестового проекта

Code: [Select]
cmake_minimum_required(VERSION 3.14)
project(limereport-tests)

enable_testing()
find_package(Qt6 REQUIRED COMPONENTS Core Test Widgets PrintSupport Qml)
find_library(limereport NAMES limereport limereport-qt6)
find_path(LIME_REPORT_INCLUDE_DIRS
    NAMES "LimeReport/LimeReport" "limereport/LimeReport"
    PATH_SUFFIXES "include" HINTS ENV CMAKE_PREFIX_PATH)

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)

qt_add_executable(tst_limereport_heap tst_limereport_heap.cpp)
add_test(NAME tst_limereport_heap COMMAND tst_limereport_heap)

target_link_libraries(tst_limereport_heap PRIVATE
    Qt6::Test Qt6::Core Qt6::Widgets Qt6::PrintSupport Qt6::Qml
    ${limereport}
)
target_include_directories(tst_limereport_heap PRIVATE ${LIME_REPORT_INCLUDE_DIRS})

# Файл отчёта содержит один элемент — ReportHeader
qt_add_resources(tst_limereport_heap "resources"
    PREFIX "/" FILES ReportWithHeadBand.lrxml)

set_target_properties(tst_limereport_heap PROPERTIES
    CXX_STANDARD 23 CXX_STANDARD_REQUIRED ON)


3.3. Тестовый файл tst_limereport_heap.cpp

Code: [Select]
#include <QObject>
#include <QtTest>
#include <QGraphicsScene>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QVariant>
#include "limereport/LimeReport"

using namespace LimeReport;

// Файл отчёта .lrxml встроен в ресурсы приложения.
// Отчёт содержит единственный элемент — ReportHeader.
static const char ResourceFileName[] = ":/ReportWithHeadBand.lrxml";

class TestLimereportHeap: public QObject
{
    Q_OBJECT

private slots:

    // Контрольный тест: только QQmlEngine без загрузки компонента.
    // Ошибка НЕ воспроизводится — одного создания движка недостаточно.
    void testQV4Init()
    {
        auto qmlEngine = std::make_unique<QQmlEngine>();

        std::unique_ptr<ReportEngine> report = std::make_unique<ReportEngine>();
        report->loadFromFile(ResourceFileName);
        std::shared_ptr<QGraphicsScene> scene(report->createPreviewScene());
        // Деструкторы вызываются в порядке: scene, report, qmlEngine
    }

    // Минимальный тест, воспроизводящий ошибку.
    // Ключевое отличие от testQV4Init: загружается QML-компонент.
    // Это инициализирует QV4::ExecutableCompilationUnit и связанные
    // глобальные структуры QV4, что приводит к конфликту с внутренним
    // QJSEngine библиотеки LimeReport при завершении программы.
    void testQV4Compile()
    {
        auto qmlEngine = std::make_unique<QQmlEngine>();

        // Загрузка минимального QML-компонента из строки.
        // Именно этот шаг является триггером ошибки.
        QQmlComponent component(qmlEngine.get());
        component.setData("import QtQuick\nItem {}", QUrl());
        qDebug() << component.status() << component.errorString();

        // ReportEngine создаёт ScriptEngineManager (синглтон),
        // который внутри владеет собственным QJSEngine.
        std::unique_ptr<ReportEngine> report = std::make_unique<ReportEngine>();
        qDebug() << report->loadFromFile(ResourceFileName);
        std::shared_ptr<QGraphicsScene> scene(report->createPreviewScene());

        // При выходе из функции уничтожаются scene, report и qmlEngine.
        // Синглтон ScriptEngineManager уничтожается позже — через atexit,
        // после того как QV4-инфраструктура qmlEngine уже освобождена.
        // Это вызывает heap corruption.
    }
};

QTEST_MAIN(TestLimereportHeap)
#include "tst_limereport_heap.moc"

4. Фактический результат

Тест testQV4Compile проходит (все assertions успешны), однако после завершения тестового фреймворка процесс падает с сообщением в stderr:

Quote
********* Start testing of TestLimereportHeap *********
Config: Using QtTest library 6.10.1, Qt 6.10.1 (x86_64-little_endian-lp64
        shared (dynamic) release build; by GCC 14.2.0), debian 13
PASS   : TestLimereportHeap::initTestCase()
QDEBUG : TestLimereportHeap::testQV4Compile() true ""
PASS   : TestLimereportHeap::testQV4Compile()
PASS   : TestLimereportHeap::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 260ms
********* Finished testing of TestLimereportHeap *********
corrupted size vs. prev_size while consolidating

Стек вызовов на момент краша:

Quote
libc!malloc_printerr
libc!_int_free_merge_chunk
libc!__GI___libc_free
libQt6Qml!QV4::ExecutableCompilationUnit::~ExecutableCompilationUnit
libQt6Qml!QV4::ExecutionEngine::~ExecutionEngine
libQt6Qml!QJSEngine::~QJSEngine
liblimereport-qt6!LimeReport::ScriptEngineManager::~ScriptEngineManager
liblimereport-qt6!LimeReport::Singleton<ScriptEngineManager>::destroy
libc!__run_exit_handlers   <-- вызов через atexit

5. Ожидаемый результат

Процесс завершается без ошибок. LimeReport корректно сосуществует с QQmlEngine в одном процессе.

6. Анализ причины

Проблема воспроизводится при выполнении всех трёх условий одновременно:
  • В процессе создаётся и уничтожается QQmlEngine с загруженным QML-компонентом (QQmlComponent::setData / load). Простое создание QQmlEngine или QJSEngine без загрузки компонента ошибки не вызывает.
  • Создаётся LimeReport::ReportEngine, который лениво инициализирует синглтон ScriptEngineManager, содержащий собственный QJSEngine.
  • Синглтон ScriptEngineManager регистрируется через механизм atexit и уничтожается после выхода из main — то есть позже, чем QQmlEngine, управляемый умным указателем внутри теста.
При загрузке QML-компонента Qt компилирует его в байткод и создаёт QV4::ExecutableCompilationUnit, который регистрирует данные в глобальном состоянии QV4 (таблицы типов, строковые пулы и т.д.). При уничтожении QQmlEngine это глобальное состояние частично освобождается. Впоследствии, когда atexit вызывает деструктор синглтона ScriptEngineManager, его внутренний QJSEngine обращается к уже освобождённым структурам QV4, что вызывает повреждение кучи.