Search code examples
c++visual-c++cmakeglfwvulkan

How to make C++ header file use 3rd party headers while only exposing a limited API when said headers are included in the src folder CMakeLists file?


This is my first larger-scale project using CMake, so please bear with me because I might be going about this wrong.

I have the following code, to open a GLFWwindow, and start setting up a VkInstance with Vulkan. My problem is that this version works fine, but I can use GLFW components from the EngineDemo.cpp file. This is due to GameEngine/VulkanEngine/Renderer/src/CMakeLists.txt including the glfw3.h file as a PUBLIC header. I would like to know if there is a way to be able to use glfw3.h inside VulkanRenderer.h without exposing it to the API consumer EngineDemo.cpp. Also, CMake should be able to only give the right release version (Debug, RelWithDebugInfo, Release) of the glfw3.h and glfw3.lib to VulkanRenderer.h.

I have the Vulkan SDK with all the components (glfw3, glm) installed on this path: C:\VulkanSDK\1.3.236.0 And I have the following structure in a CLion C++ project using CMake and Microsoft's MSVC compiler version 1929:

GameEngine
 |-- Libs
 |   |-- Windows
 |   |   |-- DebugBuild
 |   |   |   |-- glfw3
 |   |   |   |   |-- lib
 |   |   |   |   |   |-- glfw3.lib
 |   |   |   |   |-- include
 |   |   |   |   |   |-- glfw3.h
 |   |   |   |   |   |-- glfw3native.h
 |   |   |-- RelWithDebInfoBuild
 |   |   |   |-- ...
 |   |   |-- ReleaseBuild
 |   |   |   |-- ...
 |   |-- Linux (same structure as Windows folder)
 |-- EngineDemo
 |   |-- CMakeLists.txt
 |   |-- include
 |   |   |-- EngineDemo
 |   |   |   |-- empty
 |   |-- src
 |   |   |-- EngineDemo.cpp
 |   |   |-- CMakeLists.txt
 |-- VulkanEngine
 |   |-- CMakeLists.txt
 |   |-- Renderer
 |   |   |-- CMakeLists.txt
 |   |   |-- include
 |   |   |   |-- Renderer
 |   |   |   |   |-- VulkanRenderer.h
 |   |   |-- src
 |   |   |   |-- VulkanRenderer.cpp
 |   |   |   |-- CMakeLists.txt
 |-- CMakeLists.txt

GameEngine/CMakeLists.txt content:

cmake_minimum_required(VERSION 3.24)
project(VulkanEngine)

set(CMAKE_CXX_STANDARD 17)
set(DEBUG_WIN32_LIB_PATH "${CMAKE_SOURCE_DIR}/Libs/Windows/DebugBuild")
set(RELEASE_WITH_DEB_INFO_WIN32_LIB_PATH "${CMAKE_SOURCE_DIR}/Libs/Windows/RelWithDebInfoBuild")
set(RELEASE_WIN32_LIB_PATH "${CMAKE_SOURCE_DIR}/Libs/Windows/ReleaseBuild")

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(GLFW3_INCLUDE_PATH "${DEBUG_WIN32_LIB_PATH}/glfw3/include")
    set(GLFW3_LIB_PATH "${DEBUG_WIN32_LIB_PATH}/glfw3/lib/glfw3.lib")

    if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # using GCC
    elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
        # using Visual Studio C++
        add_definitions(-D_ITERATOR_DEBUG_LEVEL=2)
    endif()

elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
    set(GLFW3_INCLUDE_PATH "${RELEASE_WITH_DEB_INFO_WIN32_LIB_PATH}/glfw3/include")
    set(GLFW3_LIB_PATH "${RELEASE_WITH_DEB_INFO_WIN32_LIB_PATH}/glfw3/lib/glfw3.lib")

    if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # using GCC
    elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
        # using Visual Studio C++
        add_definitions(-D_ITERATOR_DEBUG_LEVEL=0)
    endif()

elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
    set(GLFW3_INCLUDE_PATH "${RELEASE_WIN32_LIB_PATH}/glfw3/include")
    set(GLFW3_LIB_PATH "${RELEASE_WIN32_LIB_PATH}/glfw3/lib/glfw3.lib")

    if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # using GCC
    elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
        # using Visual Studio C++
        add_definitions(-D_ITERATOR_DEBUG_LEVEL=0)
    endif()
endif()

# Include sub-projects.
add_subdirectory ("VulkanEngine")
add_subdirectory ("EngineDemo")

GameEngine/EngineDemo/CMakeLists.txt content:

add_subdirectory(src)

GameEngine/EngineDemo/src/CMakeLists.txt content:

project(EngineDemo)

add_executable (${PROJECT_NAME} EngineDemo.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC ../include)

target_link_libraries(${PROJECT_NAME} PUBLIC VulkanEngineModule)

GameEngine/EngineDemo/src/EngineDemo.cpp content:

#include <iostream>
#include <cstdlib>
#include <stdexcept>
#include "Renderer/VulkanRenderer.h"

int main()
{
    VulkanRenderer app;

    try {
        app.run();
    } catch (const std::exception &exception) {
        std::cerr << exception.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

GameEngine/VulkanEngine/CMakeLists.txt content:

add_subdirectory(Renderer)

GameEngine/VulkanEngine/Renderer/CMakeLists.txt content:

add_subdirectory(src)

GameEngine/VulkanEngine/Renderer/include/Renderer/VulkanRenderer.h content:

#pragma once

#define GLFW_INCLUDE_VULKAN
#include "glfw3.h"

class VulkanRenderer {
private:
    VkInstance vkInstance;
    GLFWwindow* window = nullptr;
public:
    void run();
private:
    void initWindow();
    void initVulkan();
    void mainLoop();
    void cleanup();
    void createVulkanInstance();
};

GameEngine/VulkanEngine/Renderer/src/CMakeLists.txt content:

cmake_minimum_required (VERSION 3.24)
project(VulkanEngineModule)

add_library (${PROJECT_NAME} STATIC VulkanRenderer.cpp)

target_include_directories(${PROJECT_NAME} PUBLIC ../include)


find_package(Vulkan REQUIRED)

if(Vulkan_FOUND)
    target_include_directories(${PROJECT_NAME} PUBLIC ${Vulkan_INCLUDE_DIR})
    target_link_libraries(${PROJECT_NAME} INTERFACE ${Vulkan_LIBRARIES})
endif(Vulkan_FOUND)

if(WIN32)
    target_include_directories(${PROJECT_NAME} PUBLIC ${GLFW3_INCLUDE_PATH})
    target_link_libraries(${PROJECT_NAME} INTERFACE ${GLFW3_LIB_PATH})
endif(WIN32)

GameEngine/VulkanEngine/Renderer/src/VulkanRenderer.cpp content:

#ifdef _WIN32
#include <Windows.h>
#endif

#include "Renderer/VulkanRenderer.h"

void VulkanRenderer::run() {
    initWindow();
    initVulkan();
    mainLoop();
    cleanup();
}

void VulkanRenderer::initWindow() {
    const uint32_t WIDTH = 800;
    const uint32_t HEIGHT = 600;
    const char* WINDOW_TITLE = "Vulkan Engine";
    glfwInit();
    VkInstance c;
    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
    window = glfwCreateWindow(WIDTH, HEIGHT, WINDOW_TITLE, nullptr, nullptr);
}

void VulkanRenderer::initVulkan() {
    createVulkanInstance();
}

void VulkanRenderer::mainLoop() {
    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
    }
}

void VulkanRenderer::cleanup() {
    glfwDestroyWindow(window);
    glfwTerminate();
}

void VulkanRenderer::createVulkanInstance() {
    VkApplicationInfo appInfo{};
    appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
    appInfo.pApplicationName = "Hello Triangle";
    appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.pEngineName = "No Engine";
    appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
    appInfo.apiVersion = VK_API_VERSION_1_0;
}

If I move the VkInstance and GLFWwindow declarations and the glfw3.h header include macro to the VulkanRenderer.cpp file with the #define GLFW_INCLUDE_VULKAN alongside it and change the target_include_directories to PRIVATE for the GLFW_INCLUDE_PATH in GameEngine/VulkanEngine/Renderer/src/CMakeLists.txt an empty window opens when I run the EngineDemo.exe; this does not expose lower-level functions to EngineDemo.cpp either. But this is not the goal. I would like to declare every member variable of the VulkanRenderer class in the corresponding header file.

I tried to change GameEngine/VulkanEngine/Renderer/include/Renderer/VulkanRenderer.h to this:

#pragma once

#define GLFW_INCLUDE_VULKAN
#include "Renderer/glfw_config.h"

class VulkanRenderer {
private:
    VkInstance vkInstance;
    GLFWwindow* window = nullptr;
public:
    void run();
private:
    void initWindow();
    void initVulkan();
    void mainLoop();
    void cleanup();
    void createVulkanInstance();
};

Added a glfw_config.h.in file to the GameEngine/VulkanEngine/Renderer/src folder that looked like this:

#pragma once
#include "@GLFW3_INCLUDE_PATH@/glfw3.h"

Appended these two lines to the end of GameEngine/VulkanEngine/Renderer/src/CMakeLists.txt:

configure_file("${CMAKE_CURRENT_SOURCE_DIR}/glfw_config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/../include/Renderer/glfw_config.h" @ONLY)
target_include_directories(${PROJECT_NAME} PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/../include")

The resulting glfw_config.h file in GameEngine/cmake-build-debug-visual-studio/VulkanEngine/Renderer/include/Renderer has the following content:

#pragma once
#include "C:/Projects/GameEngine/Libs/Windows/DebugBuild/glfw3/include/glfw3.h"

The above path to the header file is the correct one, and this works just like the previous setup. If this line target_include_directories(${PROJECT_NAME} PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/../include") changes to INTERFACE or PRIVATE then Renderer/glfw_config.h cannot be found.

My idea was that if I generate the proper glfw_config.h header file with the appropriate absolute path when reloading the CMake project, I would be able to finally, use glfw3.h in all different run configurations from the VulkanRenderer.h header file and not expose it to the outside world if I set the above-mentioned visibility to INTERFACE or PRIVATE.

Is there a way to automatically generate this #include path with CMake depending on which release I am building my project on and to only keep the GLFW components found in glfw3.h and by extension, all components in vulkan.h inside the GameEngine/VulkanEngine project only, and expose a limited API to the EngineDemo project?

If this is not possible with just the use of CMake, I would also greatly appreciate any suggestions on how to restructure the project the achieve the desired effect.

I tried googling for a similar question already, but the automatic system of StackOverflow didn't detect any duplicates either.


Solution

  • After trying for a day to formulate a more generic question and researching for it, I found out about a new use case for header files here.

    This seems to work perfectly well in my application when the VkInstance and GLFWwindow * are declared in the private header instead of the public one. When the private header is the one that has these macros:

    #define GLFW_INCLUDE_VULKAN
    #include "glfw3.h"
    

    I can still easily control the propagation of the 3rd party header files with CMake to higher levels on the dependency graph.