CMake: Place resources into static libraries, not object libraries

During the Qt 5 -> Qt 6 and qmake -> CMake porting time frame, it was
decided to keep resources in an object file (object library), rather
than putting them directly into a static library when doing a static
Qt build, so that the build system can take care of linking the
object file directly into the executable and thus not forcing
project developers to manually initialize resources with
the Q_INIT_RESOURCE() macro in project code.

This worked for most qmake and cmake projects, but it created
difficulties for other build systems, in the sense that these projects
would have to manually link to the resource object files, otherwise
they would get link time errors about undefined resource symbols,
assuming they kept the Q_INIT_RESOURCE() calls.
If the project code didn't contain Q_INIT_RESOURCE calls, the
situation would be even worse, the linker would not error out,
and the missing resources would only be discovered at runtime.

It's also an issue in CMake projects that try to link to the
library files directly instead of using the library target names,
which means the object files would not be automatically linked in.
Many projects try to do that because we don't yet offer a convenient
way to install libraries and reuse them in other projects (the SDK
case), so projects end up shipping only the libraries, without the
resource object files.

We can improve the situation by moving the resources back into their
associated static libraries, and only keeping a static initializer as
a separate object file / object library, which references the actual
resource initializer symbol, to ensure it does not get discarded
during linking.

This way, projects that link using targets get no behavior difference,
whereas projects linking to static libraries directly can still
successfully build as long as their sources have all the necessary
Q_INIT_RESOURCE calls.

To ensure the resource symbols do not get discarded, we use a few new
private macros. We declare the resource init symbols we want to keep as
extern symbols and then assign the symbol addresses to volatile
variables.
This prevents discarding the symbols with the compilers / linkers we
care about.

It comes at the cost of an additional static initializer per resource,
but we would get the same + a bigger performance hit if we just used
Q_INIT_RESOURCE twice (once in the object lib and once in project
code), which internally needs to traverse a linked list of all
resources to check if a resource was initialized or not.

For GHS / Integrity, we also need to use a GHS-specific pragma to keep
the symbols, which we currently use in qtdeclarative to ensure qml
plugin symbols are not discarded.

The same macros will be used in a qtdeclarative change to prevent
discarding of resources when linking to static qml plugins.

A cmake-based test case is added to verify that linking to static
libraries directly, without linking to the resource initializer
object libraries, works fine as long as the project code calls
Q_INIT_RESOURCE for the relevant resource.

Fixes: QTBUG-91448
Task-number: QTBUG-110243
Change-Id: I39c325aac91e36d53c3576a39f881949c3b21e3f
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Alexandru Croitor 2023-04-17 13:36:27 +02:00
parent 86fe84f5f7
commit ce8874fc3b
9 changed files with 192 additions and 8 deletions

View File

@ -87,6 +87,7 @@ qt_internal_add_module(Core
global/qtpreprocessorsupport.h
global/qtrace_p.h
global/qtresource.h
global/qtsymbolmacros.h
global/qttranslation.h
global/qttypetraits.h
global/qtversionchecks.h
@ -342,11 +343,12 @@ qt_internal_add_module(Core
PUBLIC_LIBRARIES
Qt::Platform
EXTRA_CMAKE_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CTestMacros.cmake"
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CoreConfigureFileTemplate.in"
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CoreDeploySupport.cmake"
"${config_build_dir}/QtInstallPaths.cmake"
${corelib_extra_cmake_files}
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CTestMacros.cmake"
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CoreConfigureFileTemplate.in"
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CoreResourceInit.in.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Qt6CoreDeploySupport.cmake"
"${config_build_dir}/QtInstallPaths.cmake"
${corelib_extra_cmake_files}
POLICIES
QTP0002
)

View File

@ -1846,11 +1846,24 @@ function(__qt_propagate_generated_resource target resource_name generated_source
math(EXPR resource_count "${resource_count} + 1")
set_target_properties(${target} PROPERTIES _qt_generated_resource_target_count ${resource_count})
__qt_internal_generate_init_resource_source_file(
resource_init_file ${target} ${resource_name})
set(resource_target "${target}_resources_${resource_count}")
add_library("${resource_target}" OBJECT "${generated_source_code}")
add_library("${resource_target}" OBJECT "${resource_init_file}")
# Needed so that qtsymbolmacros.h and its dependent headers are already created / syncqt'ed.
if(TARGET Core_sync_headers)
set(headers_available_target "Core_sync_headers")
else()
set(headers_available_target "${QT_CMAKE_EXPORT_NAMESPACE}::Core")
endif()
add_dependencies(${resource_target} ${headers_available_target})
target_compile_definitions("${resource_target}" PRIVATE
"$<TARGET_PROPERTY:${QT_CMAKE_EXPORT_NAMESPACE}::Core,INTERFACE_COMPILE_DEFINITIONS>"
)
target_include_directories("${resource_target}" PRIVATE
"$<TARGET_PROPERTY:${QT_CMAKE_EXPORT_NAMESPACE}::Core,INTERFACE_INCLUDE_DIRECTORIES>"
)
_qt_internal_set_up_static_runtime_library("${resource_target}")
# Special handling is required for the Core library resources. The linking of the Core
@ -1869,7 +1882,7 @@ function(__qt_propagate_generated_resource target resource_name generated_source
# .rcc/qrc_qprintdialog.cpp
file(RELATIVE_PATH generated_cpp_file_relative_path
"${CMAKE_CURRENT_BINARY_DIR}"
"${generated_source_code}")
"${resource_init_file}")
set_property(TARGET ${resource_target} APPEND PROPERTY
_qt_resource_generated_cpp_relative_path "${generated_cpp_file_relative_path}")
@ -1883,8 +1896,31 @@ function(__qt_propagate_generated_resource target resource_name generated_source
set(${output_generated_target} "${resource_target}" PARENT_SCOPE)
else()
set(${output_generated_target} "" PARENT_SCOPE)
target_sources(${target} PRIVATE ${generated_source_code})
endif()
target_sources(${target} PRIVATE ${generated_source_code})
endfunction()
function(__qt_internal_generate_init_resource_source_file out_var target resource_name)
set(template_file "${__qt_core_macros_module_base_dir}/Qt6CoreResourceInit.in.cpp")
# Gets replaced in the template
set(RESOURCE_NAME "${resource_name}")
set(resource_init_path "${CMAKE_CURRENT_BINARY_DIR}/.rcc/qrc_${resource_name}_init.cpp")
configure_file("${template_file}" "${resource_init_path}" @ONLY)
set(scope_args "")
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.18")
set(scope_args TARGET_DIRECTORY ${target})
endif()
set_source_files_properties(${resource_init_path} ${scope_args} PROPERTIES
SKIP_AUTOGEN TRUE
SKIP_UNITY_BUILD_INCLUSION TRUE
SKIP_PRECOMPILE_HEADERS TRUE
)
set(${out_var} "${resource_init_path}" PARENT_SCOPE)
endfunction()
# Make file visible in IDEs.

View File

@ -0,0 +1,14 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: BSD-3-Clause
// This file was generated by the qt_add_resources command.
#include <QtCore/qtsymbolmacros.h>
QT_DECLARE_EXTERN_RESOURCE(@RESOURCE_NAME@);
namespace {
struct resourceReferenceKeeper {
resourceReferenceKeeper() { QT_KEEP_RESOURCE(@RESOURCE_NAME@); }
} resourceReferenceKeeperInstance;
}

View File

@ -0,0 +1,65 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QTSYMBOLMACROS_H
#define QTSYMBOLMACROS_H
#if 0
# pragma qt_sync_stop_processing
#endif
// For GHS symbol keeping.
#include <QtCore/qcompilerdetection.h>
#include <QtCore/qtpreprocessorsupport.h>
// For handling namespaced resources.
#ifdef QT_NAMESPACE
# define QT_RCC_MANGLE_NAMESPACE0(x) x
# define QT_RCC_MANGLE_NAMESPACE1(a, b) a##_##b
# define QT_RCC_MANGLE_NAMESPACE2(a, b) QT_RCC_MANGLE_NAMESPACE1(a,b)
# define QT_RCC_MANGLE_NAMESPACE(name) QT_RCC_MANGLE_NAMESPACE2( \
QT_RCC_MANGLE_NAMESPACE0(name), QT_RCC_MANGLE_NAMESPACE0(QT_NAMESPACE))
#else
# define QT_RCC_MANGLE_NAMESPACE(name) name
#endif
// GHS needs special handling to keep a symbol around.
#if defined(Q_CC_GHS)
# define Q_GHS_KEEP_REFERENCE(S) QT_DO_PRAGMA(ghs reference S ##__Fv)
#else
# define Q_GHS_KEEP_REFERENCE(S)
#endif
// Macros to ensure a symbol is not dropped by the linker even if it's not used.
#define QT_DECLARE_EXTERN_SYMBOL(NAME, RETURN_TYPE) \
extern RETURN_TYPE NAME(); \
Q_GHS_KEEP_REFERENCE(NAME)
#define QT_DECLARE_EXTERN_SYMBOL_INT(NAME) \
QT_DECLARE_EXTERN_SYMBOL(NAME, int)
#define QT_DECLARE_EXTERN_SYMBOL_VOID(NAME) \
QT_DECLARE_EXTERN_SYMBOL(NAME, void)
#define QT_KEEP_SYMBOL_VAR_NAME(NAME) NAME ## _keep
#define QT_KEEP_SYMBOL_HELPER(NAME, VAR_NAME) \
volatile auto VAR_NAME = &NAME; \
Q_UNUSED(VAR_NAME)
#define QT_KEEP_SYMBOL(NAME) \
QT_KEEP_SYMBOL_HELPER(NAME, QT_KEEP_SYMBOL_VAR_NAME(NAME))
// Similar to the ones above, but for rcc resource symbols specifically.
#define QT_GET_RESOURCE_INIT_SYMBOL(NAME) \
QT_RCC_MANGLE_NAMESPACE(qInitResources_ ## NAME)
#define QT_DECLARE_EXTERN_RESOURCE(NAME) \
QT_DECLARE_EXTERN_SYMBOL_INT(QT_GET_RESOURCE_INIT_SYMBOL(NAME))
#define QT_KEEP_RESOURCE(NAME) \
QT_KEEP_SYMBOL(QT_GET_RESOURCE_INIT_SYMBOL(NAME))
#endif // QTSYMBOLMACROS_H

View File

@ -222,6 +222,7 @@ _qt_internal_test_expect_pass(test_multiple_find_package)
_qt_internal_test_expect_pass(test_add_resources_delayed_file)
_qt_internal_test_expect_pass(test_add_binary_resources_delayed_file BINARY test_add_binary_resources_delayed_file)
_qt_internal_test_expect_pass(test_qt_add_resources_rebuild)
_qt_internal_test_expect_pass(test_resource_without_obj_lib BINARY test_resource_without_obj_lib)
if(NOT NO_GUI)
_qt_internal_test_expect_pass(test_private_includes)

View File

@ -0,0 +1,33 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(test_resource_without_obj_lib)
if (EXISTS "${CMAKE_CURRENT_LIST_DIR}/FindPackageHints.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/FindPackageHints.cmake")
endif()
find_package(Qt6 REQUIRED
COMPONENTS Core Test
HINTS ${Qt6Tests_PREFIX_PATH}
)
qt6_add_library(helper_lib STATIC helper_lib.cpp)
qt6_add_resources(helper_lib "helper_res" FILES resource.txt PREFIX "/")
# Link to Core, to ensure both the helper_lib and the main executable
# inherit the QT_NAMESPACE if it is set, otherwise we get undefined
# linker errors due to the mismatch in symbol names.
target_link_libraries(helper_lib PRIVATE Qt6::Core)
set(CMAKE_AUTOMOC ON)
qt6_add_executable(test_resource_without_obj_lib main.cpp)
target_link_libraries(test_resource_without_obj_lib PRIVATE Qt6::Core Qt6::Test)
# Link against the library file and not the target, so that we can confirm
# the ability to manually initialize the resource via Q_INIT_RESOURCE.
target_link_libraries(test_resource_without_obj_lib PRIVATE $<TARGET_FILE:helper_lib>)

View File

@ -0,0 +1,4 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
void nothing() {}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtCore/qtresource.h>
#include <QtTest/QtTest>
class TestManualResourceInit : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void resourceExistsAfterManualInit();
};
void TestManualResourceInit::initTestCase()
{
// Manually initialize the resource like we used to do it in qt5 + qmake times.
Q_INIT_RESOURCE(helper_res);
}
void TestManualResourceInit::resourceExistsAfterManualInit()
{
QVERIFY(QFile::exists(":/resource.txt"));
}
QTEST_MAIN(TestManualResourceInit)
#include "main.moc"

View File

@ -0,0 +1 @@
Test resource