如何在CMake中覆盖宏定义

3
我是Windows 10上的用户,使用Visual Studio 2015。假设我正在使用类似下面的CMakeLists构建库A:
cmake_minimum_required(VERSION 3.7)
project(A)

set(DLLIMPORT "__declspec(dllimport)")
set(DLLEXPORT "__declspec(dllexport)")

set(PROJECT_SRCS
${PROJECT_SOURCE_DIR}/src/TestA.cpp)

set(PROJECT_INCS
${PROJECT_SOURCE_DIR}/include/TestA.h)

add_library(${PROJECT_NAME} SHARED ${PROJECT_SRCS} ${PROJECT_INCS})

target_compile_definitions(${PROJECT_NAME} INTERFACE
                          WINDOWS_DLL_API=${DLLIMPORT})

target_compile_definitions(${PROJECT_NAME} PRIVATE
                          WINDOWS_DLL_API=${DLLEXPORT})

target_include_directories(${PROJECT_NAME} PUBLIC
                          $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
                          $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include>)

我正在定义宏WINDOWS_DLL_APIdllexport,用于构建库A,并且在链接库A的外部应用程序中将WINDOWS_DLL_API定义为dllimport。问题是当我有另一个库B也链接到A时,我不知道如何将WINDOWS_DLL_API覆盖回dllexport。下面是我为库B编写的CMakeLists尝试:
cmake_minimum_required(VERSION 3.7)
project(B)

set(DLLEXPORT "__declspec(dllexport)")

set(PROJECT_SRCS
${PROJECT_SOURCE_DIR}/src/TestB.cpp)

set(PROJECT_INCS
${PROJECT_SOURCE_DIR}/include/TestB.h)

add_library(${PROJECT_NAME} SHARED ${PROJECT_SRCS} ${PROJECT_INCS})

target_include_directories(${PROJECT_NAME} PUBLIC
    $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include>)

target_link_libraries(${PROJECT_NAME} A)

# does not work
target_compile_definitions(${PROJECT_NAME} PRIVATE
                          WINDOWS_DLL_API=${DLLEXPORT})

什么是正确的做法?

WINDOWS_DLL_API 定义为dllimport,供链接库 A 的外部应用程序使用。B 是“外部”库,它链接了 A,因此它获得了这个定义。如果 B 不需要这个定义,只需将其定义为INTERFACE - Tsyvarev
@Tsyvarev 如果我不定义它,那么如果我有一个链接到A并包含TestA.h的应用程序(add_executable),WINDOWS_DLL_API将未被定义。 - user3667089
这可能会有用GenerateExportHeader。它与_dllexport/__dllimport一起工作,并且还处理使用-fvisibility=hidden的GCC构建。 - Nazar554
@Nazar554 不,它不支持dllimport,你必须在头文件中强制使用dllexport。请参见https://blog.kitware.com/create-dlls-on-windows-without-declspec-using-new-cmake-export-all-feature/。不幸的是,仍然需要dll宏。 - user3667089
2个回答

4

INTERFACE选项的概念是为了强制所有库的用户(包括可执行文件和库)都遵循某些规定,适用于target_compile_definitions(以及其他target_* CMake命令)。

如果意图是要至少清除单个库用户的强制性,则说明该概念被错误地使用了。应该使用其他方法代替。

在这种情况下,您需要为库AB使用不同的宏名称。最好完全删除INTERFACE选项,这样即使是不使用CMake的库用户也会感到满意。

TestA.h:

#ifdef BUILD_A
#define WINDOWS_DLL_API_A __declspec(dllexport)
#else
#define WINDOWS_DLL_API_A __declspec(dllimport)
#endif

...
WINDOWS_DLL_API_A void foo(void);
...

TestB.h:

#ifdef BUILD_B
#define WINDOWS_DLL_API_B __declspec(dllexport)
#else
#define WINDOWS_DLL_API_B __declspec(dllimport)
#endif

// Assume usage of A is here.
#include <TestA.h>
...
WINDOWS_DLL_API_B void bar(void);

A/CMakeLists.txt:

cmake_minimum_required(VERSION 3.7)
project(A)

...    

add_library(${PROJECT_NAME} SHARED ...)

target_compile_definitions(${PROJECT_NAME} PRIVATE "BUILD_${PROJECT_NAME}=1")

B/CMakeLists.txt:

cmake_minimum_required(VERSION 3.7)
project(B)

...    

add_library(${PROJECT_NAME} SHARED ...)

target_compile_definitions(${PROJECT_NAME} PRIVATE "BUILD_${PROJECT_NAME}=1")

target_link_libraries(${PROJECT_NAME} A)

请参考这个答案,该答案提供了更详细的标题,在Windows平台上也可以使用。
需要注意的是,当库B包含来自A的标题时,它将foo()视为已导入,这是正确的:函数定义在A中,而不是在B中。使用您的方法(即使您设法为B重新定义WINDOWS_DLL_API),库B将错误地将foo()视为已导出。
这是概念的优点:试图克服概念表明您正在做错事

哇,这个可以工作,但比我预期的要更混乱,更不用说如果我想让我的代码在Linux上编译,我还需要添加一些条件来使WINDOWS_DLL_API等于空。 - user3667089
是的,如果需要支持多个平台(操作系统),头文件会变得更加复杂。例如,请参见此答案(我也已将此引用添加到我的帖子中)。 - Tsyvarev

1

我想分享一下我正在使用的代码(适用于CMake 2.8.12之前的版本)。

在我的根CMakeLists.txt文件中,我有:

if (MSVC)
    add_definitions(-DWINDOWS_DLL_API=__declspec\(dllexport\))
else()
    add_definitions(-DWINDOWS_DLL_API=)
endif()

在(子)项目的CMakeLists.txt中,我使用了DLL,我放置了以下内容:
if (MSVC)
    remove_definitions(-DWINDOWS_DLL_API=__declspec\(dllexport\))
    add_definitions(-DWINDOWS_DLL_API=__declspec\(dllimport\))
endif()

在我的情况下,MSVC检查是必要的,因为我还进行交叉编译。 参考资料

只是想指出,新的 target_compile_definitions 更受欢迎的原因是旧的 add_definitions 方法在导出目标时无法正常工作。 - user3667089

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接