Saturday, November 25, 2017

DLL_THREAD_ATTACH and DLL_THREAD_DETACH are not what they sound like (but are documented correctly)

 DllMain gets called four specific reasons:
  DLL_PROCESS_ATTACH
  DLL_PROCESS_DETACH
  DLL_THREAD_ATTACH
  DLL_THREAD_DETACH

 Ostensibly, THREAD_ATTACH is where you initialize
 any thread locals and THREAD_DETACH is where you clean them up.

 However they do not work as they sound.

 They are documented correctly however.

 DLL_THREAD_ATTACH is only called for threads created after a dll is loaded.
 DLL_THREAD_DETACH is only called for threads that exit while a dll is loaded.


 That leaves DLL_THREAD_ATTACH not called for threads that exist
 before the dll is loaded, and DLL_THREAD_DETACH not called for threads
 that still exist when a dll is unloaded.

 Therefore, if you have a thread local with a constructor, it can be used without being constructed.
 If you have a thread local with a destructor, it might not be called.


 Here is an example:

F:\1>type dll.cpp dll.h dll.def exe.cpp
dll.cpp

 #include <windows.h>
 #include <stdio.h>

 int constructs;
 int destroys;
 __declspec(thread) bool constructed;

 struct A
 {
 A() { constructed = true; printf(" constructed:%d on thread:%d \n", ++constructs, GetCurrentThreadId()); }

 ~A() { printf(" destroyed:%d on thread:%d \n", ++destroys, GetCurrentThreadId()); }

 void F1() { printf(" called on thread:%d constructed:%d \n", GetCurrentThreadId(), (int)constructed); }
 };

 __declspec(thread) A a;

 extern "C" void F1() { a.F1(); }

 PCSTR ReasonString(ULONG Reason)
 {
     switch (Reason)
     {
     case DLL_PROCESS_ATTACH: return "DLL_PROCESS_ATTACH";
     case DLL_PROCESS_DETACH: return "DLL_PROCESS_DETACH";
     case DLL_THREAD_ATTACH: return "DLL_THREAD_ATTACH";
     case DLL_THREAD_DETACH: return "DLL_THREAD_DETACH";
     }
     return "unknown";
 }

 BOOL __stdcall DllMain(HINSTANCE dll, ULONG  reason, PVOID reserved)
 {
     printf(" DllMain dll:%p, reason:%s reserved:%d thread:%d \n", dll, ReasonString(reason), !!reserved, GetCurrentThreadId());
     if (reason == DLL_PROCESS_DETACH)
         printf(" unloading with %d leaked constructions \n", constructs - destroys);
     return TRUE;
 }

dll.h

extern "C" void F1();

dll.def

EXPORTS
F1
exe.cpp

 #include <stdio.h>
 #include <windows.h>
 #include "dll.h"

 decltype(&F1) pf1;

 HANDLE thread[3];
 ULONG threadid[3];
 HANDLE event[3];

 ULONG __stdcall Thread(PVOID p)
 {
     size_t i = (size_t)p;

     // wait for dll/function to be available
     while (!pf1)
         Sleep(1);
     pf1();

     // Indicate this thread is done with the dll.
     SetEvent(event[i]);

     // keep doing more work on this thread -- at least pretend
     if (i == 2)
     {
         printf(" not exiting thread:%d \n", GetCurrentThreadId());
         Sleep(INFINITE);
     }

     printf(" exiting thread:%d \n", GetCurrentThreadId());
     return 0;
 }

 int main()
 {
     size_t i = 0;

     printf(" initial thread:%d \n", GetCurrentThreadId());

     // create thread before loading dll (current thread as well)
     event[i] = CreateEvent(0, 0, 0, 0);
     thread[i] = CreateThread(0, 0, &Thread, (PVOID)i, 0, &threadid[i]);
     printf(" created thread:%d \n", threadid[i]);
     ++i;

     auto const dll = LoadLibrary("dll");
     (PROC&)pf1 = GetProcAddress(dll, "F1");
     pf1();

     // create threads after loading dll
     event[i] = CreateEvent(0, 0, 0, 0);
     thread[i] = CreateThread(0, 0, &Thread, (PVOID)i, 0, &threadid[i]);
     printf(" created thread:%d \n", threadid[i]);
     ++i;

     event[i] = CreateEvent(0, 0, 0, 0);
     thread[i] = CreateThread(0, 0, &Thread, (PVOID)i, 0, &threadid[i]);
     printf(" created thread:%d \n", threadid[i]);
     ++i;

     // Wait for all calls to the dll to be done.
     WaitForMultipleObjects(3, event, TRUE, INFINITE);

     // Wait for some of the threads to exit.
     WaitForMultipleObjects(2, thread, TRUE, INFINITE);

     FreeLibrary(dll);
 }

F:\1>cl /LD /MD /Zi dll.cpp /link /def:dll.def
F:\1>cl  /MD /Zi exe.cpp

F:\1>.\exe.exe
 initial thread:80676
 created thread:70020
 constructed:1 on thread:80676
 DllMain dll:00007FFBDEFD0000, reason:DLL_PROCESS_ATTACH reserved:0 thread:80676
 called on thread:80676 constructed:1
 created thread:66288
 constructed:2 on thread:66288
 DllMain dll:00007FFBDEFD0000, reason:DLL_THREAD_ATTACH reserved:0 thread:66288
 created thread:49672
 called on thread:70020 constructed:0 called on thread:66288 constructed:1
 exiting thread:66288
 exiting thread:70020
 constructed:3 on thread:49672
 DllMain dll:00007FFBDEFD0000, reason:DLL_THREAD_ATTACH reserved:0 thread:49672
 called on thread:49672 constructed:1
 not exiting thread:49672
 destroyed:1 on thread:66288
 DllMain dll:00007FFBDEFD0000, reason:DLL_THREAD_DETACH reserved:0 thread:66288
 DllMain dll:00007FFBDEFD0000, reason:DLL_THREAD_DETACH reserved:0 thread:70020
 destroyed:2 on thread:80676
 DllMain dll:00007FFBDEFD0000, reason:DLL_PROCESS_DETACH reserved:0 thread:80676
 unloading with 1 leaked constructions 

 - Jay