1

I'm working on refactoring a 5.5k-line C++ DLL, splitting it into a number of smaller DLLs. Unfortunately, a lot of the code is tied together, and in the process of splitting the GiantDLL I introduced a couple of circular references.

More specifically,

//In emergeOSLib.h:
DLL_EXPORT std::wstring ELGetProcessIDApp(DWORD processID, bool fullName);

//In a couple of functions in emergeOSLib.cpp:
ELMessageBox(GetDesktopWindow(), messageText, (WCHAR*)TEXT("Emerge Desktop"),                       ELMB_OK|ELMB_ICONERROR|ELMB_MODAL);

//In emergeUtilityLib.h:
DLL_EXPORT int ELMessageBox(HWND hwnd, std::wstring messageText, std::wstring messageTitle, DWORD msgFlags);

//In a function in emergeUtilityLib.cpp:
out << ELGetProcessIDApp(GetCurrentProcessId(), false) << TEXT(": ") << debugText << std::endl;

And voila, one circular reference. I'm pretty sure there are more, this is just the one I'm dealing with right now.

I've done some research and found that forward declarations seem to be the way to go:
Resolve header include circular dependencies
Circular Dependency in C++

The second link even suggests forward declarations are preferable to #includes.

I have two questions at this point. First, how do I go about forward-declaring a function that's in another DLL? My understanding is that the compiler still won't be able to find the forward-declared function (since it's not compiling the second DLL as part of the first) and will complain. Second, which is generally considered better practice, #include statements or forward declarations?

Community
  • 1
  • 1
cf-
  • 8,598
  • 9
  • 36
  • 58
  • 5.5kloc is not an overly large amount for one DLL, if it is well organized (several dozen source files). – Ben Voigt Oct 31 '13 at 06:00
  • The 5.5k is one giant source file, and the functions it offers vary wildly (a sound-playing function, a messagebox wrapper, and a function to get the OS version are just three of the things you'll find here). I'm pretty sure the split is justified. – cf- Oct 31 '13 at 06:34

2 Answers2

1

Forward declarations vs #include only help with declaration circularity. But you have definition circularity. A multi-pass linker would resolve this if you were linking all the definitions together... but you aren't.

There are basically two ways to do this. First, you can change ELGetProcessIDApp to a function pointer inside your utility library. Initially it points to some stub value in the utility library, and when the OS support library loads, it overwrites that function pointer with the OS-specific implementation.

Or, you can use a linker definition file to build an import library before the DLL actually exists. This will fix your linker problems by creating a set of circularly dependent DLLs. It can work, but it may also cause surprising load order effects and exhibits the global initialization ordering fiasco.

Or, just go with a single DLL, and limit your refactoring to splitting source files.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    Is a linker definition file as a workaround generally considered good/acceptable practice, or is it something to be avoided? Since I'm supposed to be refactoring old/messy code, I'd rather do it correctly, even if that means leaving everything in one DLL with separate source files. – cf- Oct 31 '13 at 06:41
  • @computerfreaker: It depends on whether the DLLs need any initialization. Normally when a DLL loads, its entry point doesn't run until all its dependencies are loaded and completed their initialization. With circular references, that isn't possible, so at least the initialization step will have to run without accessing dependencies. That includes `DllMain`, constructors of global objects, and functions called in initialization expressions of globals (i.e. `int x = this_func_runs_at_startup();`) – Ben Voigt Oct 31 '13 at 14:00
  • Personally, I would focus on making the code maintainable by splitting it up logically, and still link it all into one DLL. – Ben Voigt Oct 31 '13 at 14:01
  • `DllMain` is almost empty in all of the mini-libs: it just loads certain Windows DLLs into memory for future use. So I'm not too worried about having no dependency access there. But my concern with putting everything back into one DLL is that it leaves sort of this catch-all `#include` instead of making the client code clearer by having it only `#include` what it really needs access to. I'm not sure how big a deal that is, compared to this circular reference problem. – cf- Oct 31 '13 at 14:24
  • @computerfreaker: You certainly have have functions in a single DLL but declared in different header files. For example, Visual C++'s runtime library does this. `#include `, `#include ` etc are all in one DLL. All the C++ headers use a single DLL also (different from the one with the C library) – Ben Voigt Oct 31 '13 at 14:30
  • Awesome! I didn't know that. Thanks very much, I'll put everything back into one DLL and expose multiple headers for it. That seems like the best solution. – cf- Oct 31 '13 at 14:33
0

how do I go about forward-declaring a function that's in another DLL?

You dont, it doesn't work. You can declare it, but the link stage will fail.

Instead, have DLL A linking against DLL B normally, and DLL B receiving function pointers (or objects featuring virtual functions) from DLL A at runtime.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Both DLLs are set up a bit like the Windows API - no objects, just a library of functions. But function pointers might work. Isn't this generally considered a band-aid fix, though? Or is it acceptable practice? – cf- Oct 31 '13 at 06:37
  • You can add objects to a library that used to be functions-only, don't see a problem here. The objects only communicate between a set of related DLLs and not for the end user. Or you can use function pointers, also perfectly good. – n. m. could be an AI Oct 31 '13 at 08:30