From c1b30c0cd297212ec17368329be4008d9b06bf37 Mon Sep 17 00:00:00 2001 From: samrach9 Date: Mon, 19 Oct 2026 14:03:24 -0400 Subject: [PATCH 1/2] [MrBot] lackey dll: no logger init/deinit in DllMain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CMakeLists.txt | 2 +- ...d_notifications_lackey_dll_requirements.md | 19 ++++++-- .../src/thread_notifications_lackey_dll.c | 22 ++------- .../thread_notif_lackey_dll_wo_init_ut.c | 15 ------ .../thread_notif_lackey_dll_wo_init_ut_pch.h | 3 -- .../thread_notifications_lackey_dll_mocked.c | 3 -- .../thread_notifications_lackey_dll_mocked.c | 3 -- .../thread_notifications_lackey_dll_ut.c | 48 +++++-------------- .../thread_notifications_lackey_dll_ut_pch.h | 3 -- 9 files changed, 33 insertions(+), 85 deletions(-) diff --git a/thread_notifications_lackey_dll/CMakeLists.txt b/thread_notifications_lackey_dll/CMakeLists.txt index 6bf824f1..8ac60819 100644 --- a/thread_notifications_lackey_dll/CMakeLists.txt +++ b/thread_notifications_lackey_dll/CMakeLists.txt @@ -15,7 +15,7 @@ set(thread_notifications_lackey_dll_def_files add_library(thread_notifications_lackey_dll SHARED ${thread_notifications_lackey_dll_c_files} ${thread_notifications_lackey_dll_h_files} ${thread_notifications_lackey_dll_def_files}) target_include_directories(thread_notifications_lackey_dll PUBLIC $) -target_link_libraries(thread_notifications_lackey_dll c_logging_v2 c_pal) +target_link_libraries(thread_notifications_lackey_dll c_pal) function(copy_thread_notifications_lackey_outputs target output_directory) add_custom_command(TARGET ${target} POST_BUILD diff --git a/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md b/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md index 8c069e02..ea15894f 100644 --- a/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md +++ b/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md @@ -67,15 +67,24 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_005: [** If `fdwReason` is `DLL_PROCESS_ATTACH`: **]** -- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_006: [** `DllMain` shall call `logger_init` to initialize logging for the module. **]** +- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [** `DllMain` shall return `TRUE`. **]** -- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_008: [** If `logger_init` fails, `DllMain` shall return FALSE. **]** +**SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_009: [** If `fdwReason` is `DLL_PROCESS_DETACH`: **]** -- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [** Otherwise, `DllMain` shall initialize the callback maintained by the module with `NULL` and return `TRUE`. **]** +- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [** `DllMain` shall return `TRUE`. **]** -**SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_009: [** If `fdwReason` is `DLL_PROCESS_DETACH`: **]** +#### DllMain design notes + +`DllMain` does no initialization or cleanup for process attach/detach. In particular it does not use logging, because the +module keeps no state that needs it and because of the following constraints from the +[`DllMain` documentation](https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain) and +[DLL best practices](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices): + +- `DllMain` runs under the loader lock and must not call `LoadLibrary` directly or indirectly. `logger_init` calls `SymInitialize`, which makes dbghelp load `symsrv.dll` when it is found next to `dbghelp.dll` (this is the case on the ARM64 build agents). +- During process termination other DLLs may already have been detached. `symsrv.dll` is loaded after this module, so it is detached before this module, and `logger_deinit` → `SymCleanup` would call into an unloaded `symsrv.dll`. The process dies before stdout is flushed and the executable's output is lost although the exit code is 0. +- The ideal `DLL_PROCESS_DETACH` handler is empty: when the process terminates the operating system reclaims resources. -- **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [** `DllMain` shall call `logger_deinit` and return `TRUE`. **]** +Failures of `thread_notifications_lackey_dll_init_callback` are reported to the caller through its return value, which the caller logs using its own (properly initialized) logger. **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_011: [** If `fdwReason` is `DLL_THREAD_ATTACH`: **]** diff --git a/thread_notifications_lackey_dll/src/thread_notifications_lackey_dll.c b/thread_notifications_lackey_dll/src/thread_notifications_lackey_dll.c index 70b85cf2..ce83d170 100644 --- a/thread_notifications_lackey_dll/src/thread_notifications_lackey_dll.c +++ b/thread_notifications_lackey_dll/src/thread_notifications_lackey_dll.c @@ -5,8 +5,6 @@ #include "macro_utils/macro_utils.h" -#include "c_logging/logger.h" - #include "c_pal/interlocked.h" #include "c_pal/ps_util.h" @@ -23,7 +21,6 @@ int thread_notifications_lackey_dll_init_callback(THREAD_NOTIFICATIONS_LACKEY_DL /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_001: [ If thread_notification_cb is NULL, thread_notifications_lackey_set_callback shall fail and return a non-zero value. ]*/ if (thread_notifications_cb == NULL) { - LogError("Invalid arguments: THREAD_NOTIFICATIONS_LACKEY_DLL_CALLBACK_FUNC thread_notifications_cb=%p", thread_notifications_cb); result = MU_FAILURE; } else @@ -33,7 +30,6 @@ int thread_notifications_lackey_dll_init_callback(THREAD_NOTIFICATIONS_LACKEY_DL if (existing_callback != NULL) { /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_003: [ If the callback was already initialized, thread_notifications_lackey_dll_init_callback shall fail and return a non-zero value. ] */ - LogError("User error: callback already set to %p, thread_notifications_cb=%p", existing_callback, thread_notifications_cb); result = MU_FAILURE; } else @@ -61,6 +57,8 @@ BOOL WINAPI DllMain( (void)hinstDLL; (void)lpvReserved; + /* DllMain runs under the loader lock and, at process exit, after other DLLs may already have detached. + It therefore does no initialization or cleanup, only forwards thread notifications. */ switch (fdwReason) { default: @@ -70,17 +68,8 @@ BOOL WINAPI DllMain( /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_005: [ If fdwReason is DLL_PROCESS_ATTACH: ]*/ case DLL_PROCESS_ATTACH: - /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_006: [ DllMain shall call logger_init to initialize logging for the module. ]*/ - if (logger_init() != 0) - { - /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_008: [ If logger_init fails, DllMain shall return FALSE. ]*/ - result = FALSE; - } - else - { - /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [ Otherwise, DllMain shall initialize the callback maintained by the module with NULL and return TRUE. ]*/ - result = TRUE; - } + /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [ DllMain shall return TRUE. ]*/ + result = TRUE; break; /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_011: [ If fdwReason is DLL_THREAD_ATTACH: ]*/ @@ -114,8 +103,7 @@ BOOL WINAPI DllMain( /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_009: [ If fdwReason is DLL_PROCESS_DETACH: ]*/ case DLL_PROCESS_DETACH: - /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [DllMain shall call logger_deinit and return TRUE.]*/ - logger_deinit(); + /* Codes_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [ DllMain shall return TRUE. ]*/ result = TRUE; break; } diff --git a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut.c b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut.c index 0780c47a..db22e4b6 100644 --- a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut.c +++ b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut.c @@ -3,16 +3,6 @@ #include "thread_notif_lackey_dll_wo_init_ut_pch.h" -#include "umock_c/umock_c_ENABLE_MOCKS.h" // ============================== ENABLE_MOCKS -#undef ENABLE_MOCKS_DECL -#include "umock_c/umock_c_prod.h" -MOCK_FUNCTION_WITH_CODE(, int, mock_logger_init); -MOCK_FUNCTION_END(0) - -MOCK_FUNCTION_WITH_CODE(, void, mock_logger_deinit); -MOCK_FUNCTION_END() -#include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS - MU_DEFINE_ENUM_STRINGS(UMOCK_C_ERROR_CODE, UMOCK_C_ERROR_CODE_VALUES) static void on_umock_c_error(UMOCK_C_ERROR_CODE error_code) @@ -26,15 +16,10 @@ TEST_SUITE_INITIALIZE(suite_init) { ASSERT_ARE_EQUAL(int, 0, umock_c_init(on_umock_c_error), "umock_c_init failed"); ASSERT_ARE_EQUAL(int, 0, umocktypes_windows_register_types(), "umocktypes_windows_register_types failed"); - - REGISTER_GLOBAL_MOCK_FAIL_RETURN(mock_logger_init, MU_FAILURE); - - ASSERT_ARE_EQUAL(int, 0, umock_c_negative_tests_init()); } TEST_SUITE_CLEANUP(suite_cleanup) { - umock_c_negative_tests_deinit(); umock_c_deinit(); } diff --git a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut_pch.h b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut_pch.h index 1f9cc28a..eec088a4 100644 --- a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut_pch.h +++ b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notif_lackey_dll_wo_init_ut_pch.h @@ -15,11 +15,8 @@ #include "macro_utils/macro_utils.h" #include "testrunnerswitcher.h" -#include "c_logging/logger.h" - #include "umock_c/umock_c.h" #include "umock_c/umocktypes_windows.h" -#include "umock_c/umock_c_negative_tests.h" #include "c_pal/interlocked.h" diff --git a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notifications_lackey_dll_mocked.c b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notifications_lackey_dll_mocked.c index 2d33bd26..3dc52e76 100644 --- a/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notifications_lackey_dll_mocked.c +++ b/thread_notifications_lackey_dll/tests/thread_notif_lackey_dll_wo_init_ut/thread_notifications_lackey_dll_mocked.c @@ -1,9 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license.See LICENSE file in the project root for full license information. -#define logger_init mock_logger_init -#define logger_deinit mock_logger_deinit - // Since we mightbe running in an environment where a DllMain exists (like running under VS CppUnittest) // we want to rename it to something else #define DllMain DllMain_under_test diff --git a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_mocked.c b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_mocked.c index 2d33bd26..3dc52e76 100644 --- a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_mocked.c +++ b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_mocked.c @@ -1,9 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license.See LICENSE file in the project root for full license information. -#define logger_init mock_logger_init -#define logger_deinit mock_logger_deinit - // Since we mightbe running in an environment where a DllMain exists (like running under VS CppUnittest) // we want to rename it to something else #define DllMain DllMain_under_test diff --git a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut.c b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut.c index 4b4e6eb8..ec7fccf3 100644 --- a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut.c +++ b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut.c @@ -11,12 +11,6 @@ MOCK_FUNCTION_END() MOCK_FUNCTION_WITH_CODE(, void, test_thread_notifications_callback_2, THREAD_NOTIFICATIONS_LACKEY_DLL_REASON, reason); MOCK_FUNCTION_END() - -MOCK_FUNCTION_WITH_CODE(, int, mock_logger_init); -MOCK_FUNCTION_END(0) - -MOCK_FUNCTION_WITH_CODE(, void, mock_logger_deinit); -MOCK_FUNCTION_END() #include "umock_c/umock_c_DISABLE_MOCKS.h" // ============================== DISABLE_MOCKS static HMODULE test_module_handle = (HMODULE)0x42; @@ -36,15 +30,11 @@ TEST_SUITE_INITIALIZE(suite_init) ASSERT_ARE_EQUAL(int, 0, umock_c_init(on_umock_c_error), "umock_c_init failed"); ASSERT_ARE_EQUAL(int, 0, umocktypes_windows_register_types(), "umocktypes_windows_register_types failed"); - REGISTER_GLOBAL_MOCK_FAIL_RETURN(mock_logger_init, MU_FAILURE); REGISTER_TYPE(THREAD_NOTIFICATIONS_LACKEY_DLL_REASON, THREAD_NOTIFICATIONS_LACKEY_DLL_REASON); - - ASSERT_ARE_EQUAL(int, 0, umock_c_negative_tests_init()); } TEST_SUITE_CLEANUP(suite_cleanup) { - umock_c_negative_tests_deinit(); umock_c_deinit(); } @@ -135,12 +125,10 @@ TEST_FUNCTION(thread_notifications_lackey_dll_deinit_callback_when_not_initializ /* DllMain */ /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_005: [ If fdwReason is DLL_PROCESS_ATTACH: ]*/ - /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_006: [ DllMain shall call logger_init to initialize logging for the module. ]*/ - /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [ Otherwise, DllMain shall initialize the callback maintained by the module with NULL and return TRUE. ]*/ + /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [ DllMain shall return TRUE. ]*/ TEST_FUNCTION(thread_notifications_lackey_DllMain_with_DLL_PROCESS_ATTACH_returns_TRUE) { // arrange - STRICT_EXPECTED_CALL(mock_logger_init()); // act BOOL result = DllMain_under_test(test_module_handle, DLL_PROCESS_ATTACH, NULL); @@ -150,39 +138,29 @@ TEST_FUNCTION(thread_notifications_lackey_DllMain_with_DLL_PROCESS_ATTACH_return ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); } -/* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_008: [ If logger_init fails, DllMain shall return FALSE. ]*/ -TEST_FUNCTION(when_underlying_calls_fail_thread_notifications_lackey_DllMain_with_DLL_PROCESS_ATTACH_returns_FALSE) +/* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_009: [ If fdwReason is DLL_PROCESS_DETACH: ]*/ + /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [ DllMain shall return TRUE. ]*/ +TEST_FUNCTION(thread_notifications_lackey_DllMain_with_DLL_PROCESS_DETACH_on_dynamic_unload_returns_TRUE) { // arrange - STRICT_EXPECTED_CALL(mock_logger_init()); - - umock_c_negative_tests_snapshot(); - - for (size_t i = 0; i < umock_c_negative_tests_call_count(); i++) - { - if (umock_c_negative_tests_can_call_fail(i)) - { - umock_c_negative_tests_reset(); - umock_c_negative_tests_fail_call(i); - // act - BOOL result = DllMain_under_test(test_module_handle, DLL_PROCESS_ATTACH, NULL); + // act + BOOL result = DllMain_under_test(test_module_handle, DLL_PROCESS_DETACH, NULL); - // assert - ASSERT_IS_FALSE(result); - } - } + // assert + ASSERT_IS_TRUE(result); + ASSERT_ARE_EQUAL(char_ptr, umock_c_get_expected_calls(), umock_c_get_actual_calls()); } /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_009: [ If fdwReason is DLL_PROCESS_DETACH: ]*/ - /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [DllMain shall call logger_deinit and return TRUE.]*/ -TEST_FUNCTION(thread_notifications_lackey_DllMain_with_DLL_PROCESS_DETACH_returns_TRUE) + /* Tests_SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [ DllMain shall return TRUE. ]*/ +TEST_FUNCTION(thread_notifications_lackey_DllMain_with_DLL_PROCESS_DETACH_during_process_termination_returns_TRUE) { // arrange - STRICT_EXPECTED_CALL(mock_logger_deinit()); + int process_termination_context = 0; // act - BOOL result = DllMain_under_test(test_module_handle, DLL_PROCESS_DETACH, NULL); + BOOL result = DllMain_under_test(test_module_handle, DLL_PROCESS_DETACH, &process_termination_context); // assert ASSERT_IS_TRUE(result); diff --git a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut_pch.h b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut_pch.h index 07368213..29b56a1e 100644 --- a/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut_pch.h +++ b/thread_notifications_lackey_dll/tests/thread_notifications_lackey_dll_ut/thread_notifications_lackey_dll_ut_pch.h @@ -15,11 +15,8 @@ #include "macro_utils/macro_utils.h" #include "testrunnerswitcher.h" -#include "c_logging/logger.h" - #include "umock_c/umock_c.h" #include "umock_c/umocktypes_windows.h" -#include "umock_c/umock_c_negative_tests.h" #include "c_pal/interlocked.h" From 7b8a1171cd0e7b9d5784560f88a6790b2a4bf1b6 Mon Sep 17 00:00:00 2001 From: samrach9 Date: Tue, 20 Oct 2026 09:38:27 -0400 Subject: [PATCH 2/2] [MrBot] lackey dll: trim DllMain note in requirements Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...hread_notifications_lackey_dll_requirements.md | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md b/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md index ea15894f..9461aeec 100644 --- a/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md +++ b/thread_notifications_lackey_dll/devdoc/thread_notifications_lackey_dll_requirements.md @@ -65,6 +65,8 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); `DllMain` implements the DLL main entry point. +`DllMain` runs under the loader lock and, at process exit, other DLLs may have already been detached. For this reason it does not initialize or clean up anything (including logging) for process attach/detach. + **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_005: [** If `fdwReason` is `DLL_PROCESS_ATTACH`: **]** - **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_007: [** `DllMain` shall return `TRUE`. **]** @@ -73,19 +75,6 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); - **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_010: [** `DllMain` shall return `TRUE`. **]** -#### DllMain design notes - -`DllMain` does no initialization or cleanup for process attach/detach. In particular it does not use logging, because the -module keeps no state that needs it and because of the following constraints from the -[`DllMain` documentation](https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain) and -[DLL best practices](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices): - -- `DllMain` runs under the loader lock and must not call `LoadLibrary` directly or indirectly. `logger_init` calls `SymInitialize`, which makes dbghelp load `symsrv.dll` when it is found next to `dbghelp.dll` (this is the case on the ARM64 build agents). -- During process termination other DLLs may already have been detached. `symsrv.dll` is loaded after this module, so it is detached before this module, and `logger_deinit` → `SymCleanup` would call into an unloaded `symsrv.dll`. The process dies before stdout is flushed and the executable's output is lost although the exit code is 0. -- The ideal `DLL_PROCESS_DETACH` handler is empty: when the process terminates the operating system reclaims resources. - -Failures of `thread_notifications_lackey_dll_init_callback` are reported to the caller through its return value, which the caller logs using its own (properly initialized) logger. - **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_011: [** If `fdwReason` is `DLL_THREAD_ATTACH`: **]** - **SRS_THREAD_NOTIFICATIONS_LACKEY_DLL_01_013: [** If a callback was passed by the user through a `thread_notifications_lackey_dll_init_callback` call, the callback shall be called with `THREAD_NOTIFICATIONS_LACKEY_DLL_REASON_THREAD_ATTACH`. **]**