Search code examples
c++cmakeg++dllimportdllexport

Exceptions don't work in dynamically loaded DLL


Context: The frontend of the C++ application I'm working on is a dynamically loaded DLL, so that different frontends can be switched out without recompiling the main application (e.g., there can be one that uses Ncurses and one that uses Raylib, and all you need to do to change from one to the other is tell the program where the DLL is). A header file provides a Frontend class, from which all frontends must inherit, so that everyone knows what functions a frontend needs to have.

The Problem: Exceptions thrown in the DLL don't work properly. When an exception is thrown in the DLL, instead of instantly discontinuing and printing a stack trace, the program hangs for around 3 seconds and then stops silently.

The complete lack of error information makes development very hard. How can I get exceptions to work normally? Am I just doing the dynamic loading wrong?

I'm on Windows 11.

How to reproduce this error (or lack of error):

File Structure:
- dlls
----- Interface.h
----- import_me.cpp
----- import_me.dll
- main.cpp
- CMakeLists.txt

Interface.h:

class Interface {
    public:
        virtual void say_hello() = 0;
        virtual void throw_exception() = 0;
};

import_me.cpp:

#include "interface.h"
#include <iostream>

class ImportMe : public Interface {
    public:
        void say_hello() override {
            std::cout << "Hello from ImportMe!" << std::endl;
        }
        void throw_exception() override {
            throw std::runtime_error("Exception from ImportMe!");
        }
};

extern "C" __declspec(dllexport) Interface* create_ImportMe() {
    return new ImportMe();
}

main.cpp:

#include "dlls/interface.h"
#include <iostream>
#include <windows.h>

// got this from chatGPT so it may not be standard procedure
// basically it's just creating a type of function that returns a pointer to an Interface
typedef Interface* (*create_ImportMe_t)();

Interface* load_ImportMe(const char * name) {
    HMODULE dll = LoadLibrary(name);
    
        // create one of those Interface-grabbing functions (it will be an ImportMe)
    create_ImportMe_t create_ImportMe = (create_ImportMe_t)GetProcAddress(dll, "create_ImportMe");
        // call it and return the pointer to the ImportMe
    return create_ImportMe();
}

int main() {
        // DLL address is hardcoded, but obviously in the real thing it's given by the user.
        // this is the path relative to silent_errors/build/Debug, where main.exe ends up when built with CMake.
    Interface* in = load_ImportMe("../../dlls/import_me.dll");
    in->say_hello();
    in->throw_exception();
    return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.4...3.27.1)
project(silent_errors)
add_executable(silent_errors main.cpp)

I build main.cpp with cmake, and compile import_me.cpp to DLL with g++ -shared -o import_me.dll import_me.cpp

When I run main.exe, I expect to get something like:
> main.exe
Hello from ImportMe!
RuntimeException: Exception from ImportMe!

but instead I get:
> main.exe
Hello from ImportMe!
<3 second pause>
<program ends>

Some information that may be of use: I tried building the DLL with CMake, in case the compiler difference was causing the problem, and I just got a pop-up window that said: "Debug Error! abort() has been called," and then the program hung and crashed just like with g++.


Solution

  • Throwing C++ exceptions across DLL boundaries is only possible when all modules use the same C++ runtime, in which case they share a heap as well. But this can be a maintenance burden, especially when libraries from multiple vendors are involved, so it is generally discouraged. I hope this helps!