/*
* 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