13

The abridged problem (Y)

Suppose you need to know, from within a function, whether this function has been called as part of the initialization of a static object, or not. Is there a standard or platform-specific way to do so?

The backstory (X)

I'm knee-deep into the source code of a DLL that is used by many applications. This DLL exposes an Init function, and this function should construct a boost::asio::deadline_timer to be used later (but the DLL can work without it in a degraded mode).

The issue is that the timer cannot be constructed during the initialization of static objects (DLL load time), lest its constructor deadlock.

Of course, everyone and their cat calls Init from everywhere (yes, multiple times!), including static constructors from source code I shan't edit, hence the need to detect that situation and bail out.

After pragmatism overcame disgust, I ended up walking up the callstack to try and find wWinMain and deduce that static initialization is over. It's awful and doesn't work with dynamically-loaded binaries, which thankfully was outside the scope of my particular case. I sure wish there were a cleaner way.

Quentin
  • 58,778
  • 7
  • 120
  • 175
  • 1
    Have you tried setting a global variable at an appropriate time in DLLMain to indicate that the static initialization is complete? – 1201ProgramAlarm Nov 22 '17 at 16:39
  • @1201ProgramAlarm yes! But `DllMain` is in fact called *before* the DLL's static initialization... Which sounds dumb but kind of makes sense since it receives the `DLL_PROCESS_ATTACH` notification. – Quentin Nov 22 '17 at 16:40
  • Are you using post `C++11`? – Galik Nov 22 '17 at 16:48
  • Hmm, this is a chicken-and-egg problem. Suppose you *can* detect this and somehow can't debug the deadlock, *what are you going to do about it?* If you know what to do about it then just always do it. – Hans Passant Nov 22 '17 at 16:50
  • @Galik yes! --- – Quentin Nov 22 '17 at 16:51
  • I was thinking that somehow this here may be of use: http://en.cppreference.com/w/cpp/thread/call_once Also static variable inside functions are initialized in a thread safe way after `C++11` – Galik Nov 22 '17 at 16:55
  • @HansPassant why, I'll just skip it and try again next time `Init` is called of course! At least I'm reasonably sure that all of the applications end up calling it somewhere at the beginning of their `main` :) -- The whole situation *is* a mess, but now I'm curious whether the core problem of detecting static initialization can be solved in general. – Quentin Nov 22 '17 at 16:56
  • @Galik I'm afraid that wouldn't help... Sure, I could skip redundant calls to `Init`, but the issue is that the first one is made too early. And of course, I can't detect the last one... – Quentin Nov 22 '17 at 17:05
  • All static initialisation must be complete before entering main. I think that is the best we can do. This is probably why we still have libraries that require you to call an initialization function in main. – Justin Finnerty Nov 22 '17 at 17:15
  • 1
    @JustinFinnerty He's writing a library, he doesn't have any control over `main()`. – Barmar Nov 22 '17 at 17:16
  • @Barmar, I realise that. If you know your library is not used in the definition of any static variables you can handle this problem by detecting use of your library. But this is obviously not a general solution, and I doubt if there is one. – Justin Finnerty Nov 22 '17 at 17:25
  • 1
    @JustinFinnerty The question specifically says that his `Init()` method is called from static constructors. – Barmar Nov 22 '17 at 17:27
  • 1
    Is postponing creation of the `deadline_timer` until the first time it is needed unfeasible? Or when some other (non-Init) function is called, but before the deadline_timer is needed? And creating a one-shot timer to create it 1 or 2 or 42 seconds after Init is called isn't 100% reliable, so that's out. – 1201ProgramAlarm Nov 22 '17 at 18:08
  • @1201ProgramAlarm in fact (I forgot to mention this at first, my bad) the timer is not required for the DLL to operate. It's an autosave-in-the-background-regularly deal. There's no issue with skipping it within static initialization because `Init()` does the flush before anything else, but it should end up available for use during the application's lifetime, after the `Init()` fest. The one-shot timer could actually end up being okay-ish... if starting a timer wasn't the issue to begin with ;) – Quentin Nov 22 '17 at 18:23
  • 1
    Ah, right, because `SetTimer` is in `User32.dll` which you can't call during the load lock. Is there some other function, or small collection of functions, that users of your DLL would call, like a Create or Open type function that isn't called frequently but on some level is necessary? You could create the timer then if it hasn't been created yet. – 1201ProgramAlarm Nov 22 '17 at 18:32
  • @Quentin The answer to the question you linked about deadlock said that the initialization was done before `DllMain` was called. Is this not actually true? – Daniel H Nov 22 '17 at 18:51
  • I assume you mean to ask whether the function was called as part of the initialization of static variables. (The term "static initialization" excludes cases where the initializer is not a constant expression; those are known as "dynamic initialization") – M.M Nov 22 '17 at 23:21
  • 1
    The standard doesn't cover the behaviour of DLLs. For example, I have observed that calling FreeLibrary means static objects are destroyed (and may even be recreated by calling LoadLibrary again), which is at odds with the standard. I'd suggest adding platform tags. – M.M Nov 22 '17 at 23:28
  • 1
    @DanielH indeed, that looks like an error over there... Here is a [pastebin](https://pastebin.com/e7DPbD0h) of the callstack from a breakpoint triggered on `DLL_PROCESS_ATTACH`. You can see that I'm still inside the initialization of the DLL. That could've been the last call in the procedure, but constructing the `io_service` from `DllMain` deadlocks as well, hence my conclusion. – Quentin Nov 23 '17 at 12:48
  • @M.M yes, I'll revise the question. – Quentin Nov 23 '17 at 12:48
  • @1201ProgramAlarm I don't have any obvious candidate function at hand -- other initialization functions suffer from the same issue, and stuffing that in an unrelated function sounds even worse to me than what I already have :p – Quentin Nov 23 '17 at 12:53
  • 1
    @M.M I do wish to remain close to a theoretical "can we spot static initialization in the general case". Even though I happened to come across the issue with a DLL, I feel like it's only a detail here. – Quentin Nov 23 '17 at 13:00
  • @Quentin in the general case you can just set a global flag as the first line of `main` (or whatever the platform entry point is) – M.M Nov 23 '17 at 23:54

1 Answers1

1

For the moment, let's assume your DLL has two entry points: Init and Frobnicate.

Init effectively does nothing. Frobnicate checks a global boolean to find out if the DLL has ever really initialized. If not, it first does the real initialization, which also sets the I've-really-initialized flag, before actually frobnicating.

Likely, you have more entry points (Frobnicate2, Fronbnicate3, ...), so you'd have to add this logic into every one of those. This is tedious but not unheard of. Back in the day, we used to have to write our own delay-load mechanisms, which were very similar. Of course, we only presented C-style interfaces from DLLs back then, not C++-style objects with mangled method names, but it must still be do-able.

This assumes that it's OK to delay initialization until the first non-Init call into the DLL. That may or may not be a good assumption.

Other hacky ideas:

  • Find a way to detect the loader lock (other than actually deadlocking). If the loader lock is held when Init is called, then this is one of those "too soon" moments. This is on par with your walk-the-stack-to-find-WinMain solution.
  • Do something that guarantees a deadlock when Init is called too soon, and make your clients fix their damn code.
  • Create a hidden message-only window and post a message to it. Assuming the client is a traditional GUI app that will pump messages on the same thread, then, by the time you receive the message, it should be safe to do real initialization (and, hopefully, not too late).
Adrian McCarthy
  • 41,073
  • 12
  • 108
  • 157
  • 1
    Can confirm... have published a DLL with two-stage init where every single entry point started with `if ( init_has_been_called() )` – M.M Nov 23 '17 at 23:55
  • The problem remains if Frobnicate is called in the Init code of another DLL. – Justin Finnerty Nov 24 '17 at 02:14
  • @Justin Finnerty: Yeah, it's a no-win situation. The clients never should have called anything from static initialization methods, but they did and we're trying our best to keep that from breaking things. If they're doing "real work" form static initialization, then all hope is probably lost. – Adrian McCarthy Nov 26 '17 at 00:16