Exception handling is inconsistent after switching to FetchContent

I am migrating a codebase that included external projects in its source tree to a FetchContent-based solution. There are four that I want to move out: zlib, libpng, ogg, and vorbis. I moved the zlib out and in another spot a standard library exception thrown during file lookup started terminating the game. Previously it was getting caught by a catch block (DataSource.cpp\StarsEx - Starshatter: The Open Source Project - Military space combat simulator). None of related files changed.

 terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
 terminate called recursively

After digging through code and cmake caches for two days I think I’m focused on a wrong aspect of the whole problem. I created several minimal reproduction examples which all work and catch the exception (while they shouldn’t be) to the point, where I arrived at an example that includes everything a regular game build does, but in top-level CMakeLists.txt instead of subdirectory:

 diff --git a/CMakeLists.txt b/CMakeLists.txt
 index e878572..be83384 100644
 --- a/CMakeLists.txt
 +++ b/CMakeLists.txt
 @@ -26,7 +26,17 @@ endif()
  add_subdirectory(NetEx)
  add_subdirectory(Starserver)
  add_subdirectory(StarsEx)
 -add_subdirectory(Starshatter)
 +add_executable(Starshatter WIN32 Starshatter/Main.cpp)
 +target_include_directories(Starshatter PRIVATE Starshatter/)
 +target_link_libraries(Starshatter PUBLIC StarsEx)
 +add_version_file(Starshatter.rc Starshatter/Starshatter.rc.conf)
 +target_sources(Starshatter PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/Starshatter.rc)
 +install(
 +	TARGETS Starshatter
 +	RUNTIME
 +	COMPONENT Runtime
 +	DESTINATION ${CMAKE_INSTALL_PREFIX}
 +	)
 add_version_file(version.txt version.txt.conf ALL)
 install(
    FILES NOTICE COPYING

This still does not reproduce the issue. Above patch could be considered a fix in this manner, but I want to understand what’s going on (plus I want to retain the directory structure). Removal of add_subdirectory could suggest some unusual variable propagation. The CMake caches look similar in terms of linker options but different in structure (of course).

This executable links to project internal static library (StarsEx) which contains the affected catch block. The static library links to another library (ArchiveEx) that links to zlib. This ArchiveEx also builds a tool that works with archives and std::filesystem. It works with no issues. It contains try … catch blocks, but does not use std::filesystem::filesystem_error.

I was unable to create a minimal example that reproduced the issue (which by itself suggests that there is some weird in-project dependency going on). Forgive me and allow me to link the full project this one time:

Project is targeted at MinGW i686. C++ standard is set to C++17 with GNU extensions. Some of the cmake files are getting old and were put hastily together until things started to compile after putting everything together from a somewhat broken Visual Studio solution.

I do not ask you to take a deep look at the project. That’s definitely too much. Pointing me to an issue that you remember to sounds similar will do. I did not get decent results searching around. I kind of hope it’s something trivial that can be easily caught with a fresh pair of eyes and I missed simply because my methods become too chaotic at this point. Thank you in advance for any comments.

I figured out a minimal example finally.

Top-level CMakeLists.txt:

cmake_minimum_required(VERSION 3.24)
project(MinimalExample)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_EXTENSIONS Yes)
add_subdirectory(deps)
set(EXAMPLE_NAME Sub)
add_subdirectory(example)
set(EXAMPLE_NAME Top)
include(example/CMakeLists.txt)

deps/ has everything FetchContent needs to get zlib. Same approach as here is used: declare → patch → populate → add_subdirectory.

As for example/CMakeLists.txt:

project(${EXAMPLE_NAME})
add_executable(${PROJECT_NAME} WIN32 ${CMAKE_CURRENT_LIST_DIR}/example.cpp)
target_link_libraries(${PROJECT_NAME} PUBLIC Zlib::zlib -ld3dx9)
target_compile_definitions(${PROJECT_NAME} PUBLIC _ALLOW_KEYWORD_MACROS)
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX})

And example/example.cpp:

#include <d3dx9.h>
#include <windows.h>
#include <filesystem>
#include <zlib.h>
namespace fs = std::filesystem;
void unused() {
	uncompress(nullptr, nullptr, nullptr, 0);
	D3DXGetImageInfoFromFileInMemory(nullptr, 0, nullptr);
}
int APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
	try {
		for (const auto& entry : fs::directory_iterator("does/not/exist"))
			(void) entry;
	} catch (const fs::filesystem_error&) {
		// no-op
	}
	return 0;
}

When built and ran the in-subdirectory example terminates as per expected issue and the top-level does not as I noted in previous reproduction attempts:

$ ./Sub.exe && echo ok
terminate called after throwing an instance of 'std::filesystem::__cxx11::filesystem_error'
terminate called recursively
$ ./Top.exe && echo ok
ok

I do not have proper solution just yet but at least there’s a “minimal” example now. Caches remain to look similar. The only difference so far I spotted is the order of DLLs in objdump -p output.

I have decided to go with the hypothesis from the previous post: the subdirectory structure and how targets are built and linked to dependencies somehow affects exception handling. Additionally, I have found a solution that is satisfying to me, so I’ll believe this until proven incorrect (or: until I will be forced to investigate again due to different error; or until I spend more time on Windows-based platforms).

The solution I have chosen was to: limit zlib use to one internal archive format library, link it statically there, make the library shared. This seems to work for the library, its built-in utility, and any user target that may or may not link against d3dx9.