/* * Sanity check of libcdecl multithread safety. * * Copyright © 2024 Nick Bowler * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "cdecl-internal.h" #include "errmsg.h" #include "tap.h" /* * Prior returned value from cdecl_get_error in the main thread. */ static const struct cdecl_error *thread1_err; /* * Check that the error code and message matches expectations (noting that * this application does not call setlocale to enable translations). */ static void check_simple_err(const struct cdecl_error *err, unsigned t, unsigned exp_code, unsigned msg_id) { static const char errmsgs[] = STRTAB_INITIALIZER; const char *exp_msg; if (!tap_result(err->code == exp_code, "thread[%u] err->code", t)) { tap_diag("Failed, unexpected result"); tap_diag(" Received: %u", err->code); tap_diag(" Expected: %u", exp_code); } exp_msg = &errmsgs[msg_id]; if (!tap_result(!strcmp(err->str, exp_msg), "thread[%u] err->str", t)) { tap_diag("Failed, unexpected result"); tap_diag(" Received: %.*s", (int)strlen(exp_msg), err->str); tap_diag(" Expected: %s", exp_msg); } } #if USE_POSIX_THREADS || USE_ISOC_AND_POSIX_THREADS #define THREAD_API "posix" #include #define DEFINE_SIMPLE_THREAD_TEST(run_func, thread_entry_func) \ static void *run_func##_(void *p) { thread_entry_func(); return 0; } \ static void run_func(void) \ { \ pthread_t t; int err; \ if (!(err = pthread_create(&t, 0, run_func##_, 0))) \ if (!(err = pthread_join(t, 0))) \ return; \ tap_bail_out("%s failed: %s", #run_func, strerror(err)); \ } static void check_init_once(void) { tap_result(1, "init_once # SKIP test not implemented"); } #elif USE_ISOC_THREADS #define THREAD_API "isoc" #include #define DEFINE_SIMPLE_THREAD_TEST(run_func, thread_entry_func) \ static int run_func##_(void *p) { thread_entry_func(); return 0; } \ static void run_func(void) \ { \ thrd_t t; \ if (thrd_create(&t, run_func##_, 0) == thrd_success) \ if (thrd_join(t, 0) == thrd_success) \ return; \ tap_bail_out("%s failed", #run_func); \ } static void check_init_once(void) { tap_result(1, "init_once # SKIP test not implemented"); } #elif USE_WINDOWS_THREADS #define THREAD_API "windows" #define WIN32_LEAN_AND_MEAN #include #define DEFINE_SIMPLE_THREAD_TEST(run_func, thread_entry_func) \ static DWORD WINAPI run_func##_(LPVOID p) \ { thread_entry_func(); return 0; } \ static void run_func(void) \ { \ HANDLE h; DWORD rc; \ if ((h = CreateThread(0, 0, run_func##_, 0, 0, &rc))) do { \ if (GetExitCodeThread(h, &rc) && rc != STILL_ACTIVE) { \ CloseHandle(h); \ return; \ } \ } while (WaitForSingleObject(h, INFINITE) != WAIT_FAILED); \ tap_bail_out("%s failed (%lu)", #run_func, GetLastError()); \ } /* Also include init_once sanity test on Windows */ #define TEST_W32_NO_DLLMAIN 1 #include "thread-w32.h" HANDLE init_semaphore; static void init_once_cb(void) { tap_result(1, "init_once_cb start"); ReleaseSemaphore(init_semaphore, 2, NULL); Sleep(1000); tap_diag("init_once_cb exit"); } static DWORD WINAPI init_thread(LPVOID p) { WaitForSingleObject(init_semaphore, INFINITE); tap_diag("init_thread start"); init_once(); tap_diag("init_thread exit"); return 0; } static void check_init_once(void) { HANDLE threads[3]; DWORD rc; int i; if (!(init_semaphore = CreateSemaphore(NULL, 0, 10, NULL))) tap_bail_out("check_init_once failed (%lu)", GetLastError()); if (!(threads[0] = CreateThread(NULL, 0, init_thread, NULL, 0, &rc))) tap_bail_out("check_init_once failed (%lu)", GetLastError()); if (!(threads[1] = CreateThread(NULL, 0, init_thread, NULL, 0, &rc))) tap_bail_out("check_init_once failed (%lu)", GetLastError()); if (!(threads[2] = CreateThread(NULL, 0, init_thread, NULL, 0, &rc))) tap_bail_out("check_init_once failed (%lu)", GetLastError()); ReleaseSemaphore(init_semaphore, 1, NULL); WaitForMultipleObjects(3, threads, TRUE, INFINITE); for (i = 0; i < 3; i++) CloseHandle(threads[i]); CloseHandle(init_semaphore); for (i = 0; i < 10000000; i++) init_once(); } #else #undef THREAD_API int main(void) { tap_skip_all("multithreading support disabled"); } #endif #ifdef THREAD_API static void thread_result(void) { tap_result(1, "thread runs"); } DEFINE_SIMPLE_THREAD_TEST(check_thread_create, thread_result) static void err_in_thread(void) { const struct cdecl_error *err; cdecl__errmsg(CDECL__ENOTYPE); err = cdecl_get_error(); /* * Ensure that the error returned in this new thread is distinct from * the error returned in the main thread. */ tap_diag("thread[2] err: %p", (void *)err); tap_result(thread1_err != err, "thread[2] new state"); check_simple_err(err, 2, CDECL_ENOPARSE, CDECL__ENOTYPE); tap_diag("thread[2] exit"); } DEFINE_SIMPLE_THREAD_TEST(check_err_in_thread, err_in_thread) int main(void) { size_t test_live_allocations(void); const struct cdecl_error *err; tap_diag("using thread API: " THREAD_API); tap_plan(11); /* Create/join a thread to tickle DllMain on Windows. */ check_thread_create(); /* Simulate an error in the main thread. */ cdecl__errmsg(CDECL__ENOMEM); thread1_err = cdecl_get_error(); tap_diag("thread[1] err: %p", (void *)thread1_err); check_simple_err(thread1_err, 1, CDECL_ENOMEM, CDECL__ENOMEM); check_err_in_thread(); /* * Back in the main thread, the error previously returned by * cdecl_get_error() should still be valid. */ check_simple_err(thread1_err, 1, CDECL_ENOMEM, CDECL__ENOMEM); /* * Moreover, cdecl_get_error should return the same pointer it did * last time (undocumented implementation detail). */ if (!tap_result((err = cdecl_get_error()) == thread1_err, "thread[1] unchanged state")) { tap_diag("Failed, unexpected result"); tap_diag(" Received: %p", (void *)err); tap_diag(" Expected: %p", (void *)thread1_err); } /* * Main thread allocation should be the only one left. */ tap_result(test_live_allocations() == 1, "thread cleanup"); /* * Basic init_once sanity check for Windows. */ check_init_once(); tap_done(); } #endif