Merge "threads.h: Add C11 thread support."
diff --git a/docs/status.md b/docs/status.md
index d6a2f4c..e1a5d34 100644
--- a/docs/status.md
+++ b/docs/status.md
@@ -37,6 +37,9 @@
Current libc symbols: https://android.googlesource.com/platform/bionic/+/master/libc/libc.map.txt
+New libc functions in R (API level 30):
+ * Full C11 `<threads.h>` (available as inlines for older API levels).
+
New libc functions in Q (API level 29):
* `timespec_get` (C11 `<time.h>` addition)
* `reallocarray` (BSD/GNU extension in `<malloc.h>` and `<stdlib.h>`)
diff --git a/libc/Android.bp b/libc/Android.bp
index 931f87b..174783f 100644
--- a/libc/Android.bp
+++ b/libc/Android.bp
@@ -1154,6 +1154,7 @@
"bionic/tdestroy.cpp",
"bionic/termios.cpp",
"bionic/thread_private.cpp",
+ "bionic/threads.cpp",
"bionic/timespec_get.cpp",
"bionic/tmpfile.cpp",
"bionic/umount.cpp",
diff --git a/libc/bionic/threads.cpp b/libc/bionic/threads.cpp
new file mode 100644
index 0000000..f597580
--- /dev/null
+++ b/libc/bionic/threads.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <threads.h>
+
+#define __BIONIC_THREADS_INLINE /* Out of line. */
+#include <bits/threads_inlines.h>
diff --git a/libc/include/android/api-level.h b/libc/include/android/api-level.h
index a175857..af7cde1 100644
--- a/libc/include/android/api-level.h
+++ b/libc/include/android/api-level.h
@@ -100,6 +100,9 @@
/** Names the "Q" API level (29), for comparisons against __ANDROID_API__. */
#define __ANDROID_API_Q__ 29
+/** Names the "R" API level (30), for comparisons against __ANDROID_API__. */
+#define __ANDROID_API_R__ 30
+
/**
* Returns the `targetSdkVersion` of the caller, or `__ANDROID_API_FUTURE__`
* if there is no known target SDK version (for code not running in the
diff --git a/libc/include/android/legacy_threads_inlines.h b/libc/include/android/legacy_threads_inlines.h
new file mode 100644
index 0000000..e73ef37
--- /dev/null
+++ b/libc/include/android/legacy_threads_inlines.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+
+#if __ANDROID_API__ < __ANDROID_API_R__
+
+#define __BIONIC_THREADS_INLINE static __inline
+#include <bits/threads_inlines.h>
+
+#endif
diff --git a/libc/include/bits/threads_inlines.h b/libc/include/bits/threads_inlines.h
new file mode 100644
index 0000000..1130b3a
--- /dev/null
+++ b/libc/include/bits/threads_inlines.h
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <threads.h>
+
+#include <errno.h>
+#include <sched.h>
+#include <stdlib.h>
+
+#if !defined(__BIONIC_THREADS_INLINE)
+#define __BIONIC_THREADS_INLINE static __inline
+#endif
+
+__BEGIN_DECLS
+
+static __inline int __bionic_thrd_error(int __pthread_code) {
+ switch (__pthread_code) {
+ case 0: return 0;
+ case ENOMEM: return thrd_nomem;
+ case ETIMEDOUT: return thrd_timedout;
+ case EBUSY: return thrd_busy;
+ default: return thrd_error;
+ }
+}
+
+__BIONIC_THREADS_INLINE void call_once(once_flag* __flag,
+ void (*__function)(void)) {
+ pthread_once(__flag, __function);
+}
+
+
+
+__BIONIC_THREADS_INLINE int cnd_broadcast(cnd_t* __cnd) {
+ return __bionic_thrd_error(pthread_cond_broadcast(__cnd));
+}
+
+__BIONIC_THREADS_INLINE void cnd_destroy(cnd_t* __cnd) {
+ pthread_cond_destroy(__cnd);
+}
+
+__BIONIC_THREADS_INLINE int cnd_init(cnd_t* __cnd) {
+ return __bionic_thrd_error(pthread_cond_init(__cnd, NULL));
+}
+
+__BIONIC_THREADS_INLINE int cnd_signal(cnd_t* __cnd) {
+ return __bionic_thrd_error(pthread_cond_signal(__cnd));
+}
+
+__BIONIC_THREADS_INLINE int cnd_timedwait(cnd_t* __cnd,
+ mtx_t* __mtx,
+ const struct timespec* __timeout) {
+ return __bionic_thrd_error(pthread_cond_timedwait(__cnd, __mtx, __timeout));
+}
+
+__BIONIC_THREADS_INLINE int cnd_wait(cnd_t* __cnd, mtx_t* __mtx) {
+ return __bionic_thrd_error(pthread_cond_wait(__cnd, __mtx));
+}
+
+
+
+__BIONIC_THREADS_INLINE void mtx_destroy(mtx_t* __mtx) {
+ pthread_mutex_destroy(__mtx);
+}
+
+__BIONIC_THREADS_INLINE int mtx_init(mtx_t* __mtx, int __type) {
+ int __pthread_type = (__type & mtx_recursive) ? PTHREAD_MUTEX_RECURSIVE
+ : PTHREAD_MUTEX_NORMAL;
+ __type &= ~mtx_recursive;
+ if (__type != mtx_plain && __type != mtx_timed) return thrd_error;
+
+ pthread_mutexattr_t __attr;
+ pthread_mutexattr_init(&__attr);
+ pthread_mutexattr_settype(&__attr, __pthread_type);
+ return __bionic_thrd_error(pthread_mutex_init(__mtx, &__attr));
+}
+
+__BIONIC_THREADS_INLINE int mtx_lock(mtx_t* __mtx) {
+ return __bionic_thrd_error(pthread_mutex_lock(__mtx));
+}
+
+__BIONIC_THREADS_INLINE int mtx_timedlock(mtx_t* __mtx,
+ const struct timespec* __timeout) {
+ return __bionic_thrd_error(pthread_mutex_timedlock(__mtx, __timeout));
+}
+
+__BIONIC_THREADS_INLINE int mtx_trylock(mtx_t* __mtx) {
+ return __bionic_thrd_error(pthread_mutex_trylock(__mtx));
+}
+
+__BIONIC_THREADS_INLINE int mtx_unlock(mtx_t* __mtx) {
+ return __bionic_thrd_error(pthread_mutex_unlock(__mtx));
+}
+
+
+
+struct __bionic_thrd_data {
+ thrd_start_t __func;
+ void* __arg;
+};
+
+static inline void* __bionic_thrd_trampoline(void* __arg) {
+ struct __bionic_thrd_data __data =
+ *__BIONIC_CAST(static_cast, struct __bionic_thrd_data*, __arg);
+ free(__arg);
+ int __result = __data.__func(__data.__arg);
+ return __BIONIC_CAST(reinterpret_cast, void*,
+ __BIONIC_CAST(static_cast, uintptr_t, __result));
+}
+
+__BIONIC_THREADS_INLINE int thrd_create(thrd_t* __thrd,
+ thrd_start_t __func,
+ void* __arg) {
+ struct __bionic_thrd_data* __pthread_arg =
+ __BIONIC_CAST(static_cast, struct __bionic_thrd_data*,
+ malloc(sizeof(struct __bionic_thrd_data)));
+ __pthread_arg->__func = __func;
+ __pthread_arg->__arg = __arg;
+ int __result = __bionic_thrd_error(pthread_create(__thrd, NULL,
+ __bionic_thrd_trampoline,
+ __pthread_arg));
+ if (__result != thrd_success) free(__pthread_arg);
+ return __result;
+}
+
+__BIONIC_THREADS_INLINE thrd_t thrd_current(void) {
+ return pthread_self();
+}
+
+__BIONIC_THREADS_INLINE int thrd_detach(thrd_t __thrd) {
+ return __bionic_thrd_error(pthread_detach(__thrd));
+}
+
+__BIONIC_THREADS_INLINE int thrd_equal(thrd_t __lhs, thrd_t __rhs) {
+ return pthread_equal(__lhs, __rhs);
+}
+
+__BIONIC_THREADS_INLINE void thrd_exit(int __result) {
+ pthread_exit(__BIONIC_CAST(reinterpret_cast, void*,
+ __BIONIC_CAST(static_cast, uintptr_t, __result)));
+}
+
+__BIONIC_THREADS_INLINE int thrd_join(thrd_t __thrd, int* __result) {
+ void* __pthread_result;
+ if (pthread_join(__thrd, &__pthread_result) != 0) return thrd_error;
+ if (__result) {
+ *__result = __BIONIC_CAST(reinterpret_cast, intptr_t, __pthread_result);
+ }
+ return thrd_success;
+}
+
+__BIONIC_THREADS_INLINE int thrd_sleep(const struct timespec* __duration,
+ struct timespec* __remaining) {
+ int __rc = nanosleep(__duration, __remaining);
+ if (__rc == 0) return 0;
+ return (errno == EINTR) ? -1 : -2;
+}
+
+__BIONIC_THREADS_INLINE void thrd_yield(void) {
+ sched_yield();
+}
+
+
+
+__BIONIC_THREADS_INLINE int tss_create(tss_t* __key, tss_dtor_t __dtor) {
+ return __bionic_thrd_error(pthread_key_create(__key, __dtor));
+}
+
+__BIONIC_THREADS_INLINE void tss_delete(tss_t __key) {
+ pthread_key_delete(__key);
+}
+
+__BIONIC_THREADS_INLINE void* tss_get(tss_t __key) {
+ return pthread_getspecific(__key);
+}
+
+__BIONIC_THREADS_INLINE int tss_set(tss_t __key, void* __value) {
+ return __bionic_thrd_error(pthread_setspecific(__key, __value));
+}
+
+__END_DECLS
diff --git a/libc/include/threads.h b/libc/include/threads.h
new file mode 100644
index 0000000..2c43b0b
--- /dev/null
+++ b/libc/include/threads.h
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#pragma once
+
+/**
+ * @file threads.h
+ * @brief C11 threads.
+ */
+
+#include <sys/cdefs.h>
+
+#include <pthread.h>
+#include <time.h>
+
+#define ONCE_FLAG_INIT PTHREAD_ONCE_INIT
+#define TSS_DTOR_ITERATIONS PTHREAD_DESTRUCTOR_ITERATIONS
+
+/** The type for a condition variable. */
+typedef pthread_cond_t cnd_t;
+/** The type for a thread. */
+typedef pthread_t thrd_t;
+/** The type for a thread-specific storage key. */
+typedef pthread_key_t tss_t;
+/** The type for a mutex. */
+typedef pthread_mutex_t mtx_t;
+
+/** The type for a thread-specific storage destructor. */
+typedef void (*tss_dtor_t)(void*);
+/** The type of the function passed to thrd_create() to create a new thread. */
+typedef int (*thrd_start_t)(void*);
+
+/** The type used by call_once(). */
+typedef pthread_once_t once_flag;
+
+enum {
+ mtx_plain = 0x1,
+ mtx_recursive = 0x2,
+ mtx_timed = 0x4,
+};
+
+enum {
+ thrd_success = 0,
+ thrd_busy = 1,
+ thrd_error = 2,
+ thrd_nomem = 3,
+ thrd_timedout = 4,
+};
+
+#if !defined(__cplusplus)
+#define thread_local _Thread_local
+#endif
+
+__BEGIN_DECLS
+
+#if __ANDROID_API__ >= __ANDROID_API_R__
+// This file is implemented as static inlines before API level 30.
+
+/** Uses `__flag` to ensure that `__function` is called exactly once. */
+void call_once(once_flag* __flag, void (*__function)(void));
+
+
+
+/**
+ * Unblocks all threads blocked on `__cond`.
+ */
+int cnd_broadcast(cnd_t* __cond);
+
+/**
+ * Destroys a condition variable.
+ */
+void cnd_destroy(cnd_t* __cond);
+
+/**
+ * Creates a condition variable.
+ */
+int cnd_init(cnd_t* __cond);
+
+/**
+ * Unblocks one thread blocked on `__cond`.
+ */
+int cnd_signal(cnd_t* __cond);
+
+/**
+ * Unlocks `__mutex` and blocks until `__cond` is signaled or `__timeout` occurs.
+ */
+int cnd_timedwait(cnd_t* __cond, mtx_t* __mutex, const struct timespec* __timeout);
+
+/**
+ * Unlocks `__mutex` and blocks until `__cond` is signaled.
+ */
+int cnd_wait(cnd_t* __cond, mtx_t* __mutex);
+
+
+
+/**
+ * Destroys a mutex.
+ */
+void mtx_destroy(mtx_t* __mutex);
+
+/**
+ * Creates a mutex.
+ */
+int mtx_init(mtx_t* __mutex, int __type);
+
+/**
+ * Blocks until `__mutex` is acquired.
+ */
+int mtx_lock(mtx_t* __mutex);
+
+/**
+ * Blocks until `__mutex` is acquired or `__timeout` expires.
+ */
+int mtx_timedlock(mtx_t* __mutex, const struct timespec* __timeout);
+
+/**
+ * Acquires `__mutex` or returns `thrd_busy`.
+ */
+int mtx_trylock(mtx_t* __mutex);
+
+/**
+ * Unlocks `__mutex`.
+ */
+int mtx_unlock(mtx_t* __mutex);
+
+
+
+/**
+ * Creates a new thread running `__function(__arg)`, and sets `*__thrd` to
+ * the new thread.
+ */
+int thrd_create(thrd_t* __thrd, thrd_start_t __function, void* __arg);
+
+/**
+ * Returns the `thrd_t` corresponding to the caller.
+ */
+thrd_t thrd_current(void);
+
+/**
+ * Tells the OS to automatically dispose of `__thrd` when it exits.
+ */
+int thrd_detach(thrd_t __thrd);
+
+/**
+ * Tests whether two threads are the same thread.
+ */
+int thrd_equal(thrd_t __lhs, thrd_t __rhs);
+
+/**
+ * Terminates the calling thread, setting its result to `__result`.
+ */
+void thrd_exit(int __result) __noreturn;
+
+/**
+ * Blocks until `__thrd` terminates. If `__result` is not null, `*__result`
+ * is set to the exiting thread's result.
+ */
+int thrd_join(thrd_t __thrd, int* __result);
+
+/**
+ * Blocks the caller for at least `__duration` unless a signal is delivered.
+ * If a signal causes the sleep to end early and `__remaining` is not null,
+ * `*__remaining` is set to the time remaining.
+ *
+ * Returns 0 on success, or -1 if a signal was delivered.
+ */
+int thrd_sleep(const struct timespec* __duration, struct timespec* __remaining);
+
+/**
+ * Request that other threads should be scheduled.
+ */
+void thrd_yield(void);
+
+
+
+/**
+ * Creates a thread-specific storage key with the associated destructor (which
+ * may be null).
+ */
+int tss_create(tss_t* __key, tss_dtor_t __dtor);
+
+/**
+ * Destroys a thread-specific storage key.
+ */
+void tss_delete(tss_t __key);
+
+/**
+ * Returns the value for the current thread held in the thread-specific storage
+ * identified by `__key`.
+ */
+void* tss_get(tss_t __key);
+
+/**
+ * Sets the current thread's value for the thread-specific storage identified
+ * by `__key` to `__value`.
+ */
+int tss_set(tss_t __key, void* __value);
+
+#endif
+
+__END_DECLS
+
+#include <android/legacy_threads_inlines.h>
diff --git a/libc/libc.map.txt b/libc/libc.map.txt
index f0fccf5..4a734fc 100644
--- a/libc/libc.map.txt
+++ b/libc/libc.map.txt
@@ -1480,6 +1480,35 @@
android_mallopt; # apex
} LIBC_P;
+LIBC_R { # introduced=R
+ global:
+ call_once;
+ cnd_broadcast;
+ cnd_destroy;
+ cnd_init;
+ cnd_signal;
+ cnd_timedwait;
+ cnd_wait;
+ mtx_destroy;
+ mtx_init;
+ mtx_lock;
+ mtx_timedlock;
+ mtx_trylock;
+ mtx_unlock;
+ thrd_create;
+ thrd_current;
+ thrd_detach;
+ thrd_equal;
+ thrd_exit;
+ thrd_join;
+ thrd_sleep;
+ thrd_yield;
+ tss_create;
+ tss_delete;
+ tss_get;
+ tss_set;
+} LIBC_Q;
+
LIBC_PRIVATE {
global:
___Unwind_Backtrace; # arm
diff --git a/tests/Android.bp b/tests/Android.bp
index 71cf8a6..85bb29a 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -184,6 +184,7 @@
"system_properties_test2.cpp",
"termios_test.cpp",
"tgmath_test.c",
+ "threads_test.cpp",
"time_test.cpp",
"uchar_test.cpp",
"unistd_nofortify_test.cpp",
diff --git a/tests/headers/posix/threads_h.c b/tests/headers/posix/threads_h.c
new file mode 100644
index 0000000..c9329f4
--- /dev/null
+++ b/tests/headers/posix/threads_h.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#if __has_include(<threads.h>)
+
+#include <threads.h>
+
+#include "header_checks.h"
+
+thread_local int t;
+
+static void threads_h() {
+ MACRO(ONCE_FLAG_INIT);
+ MACRO(TSS_DTOR_ITERATIONS);
+
+ TYPE(cnd_t);
+ TYPE(thrd_t);
+ TYPE(tss_t);
+ TYPE(mtx_t);
+
+ TYPE(tss_dtor_t);
+ TYPE(thrd_start_t);
+
+ TYPE(once_flag);
+
+ int enumeration_constants = mtx_plain | mtx_recursive | mtx_timed |
+ thrd_timedout | thrd_success | thrd_busy | thrd_error | thrd_nomem;
+
+ FUNCTION(call_once, void (*f)(once_flag*, void (*)(void)));
+
+ FUNCTION(cnd_broadcast, int (*f)(cnd_t*));
+ FUNCTION(cnd_destroy, void (*f)(cnd_t*));
+ FUNCTION(cnd_init, int (*f)(cnd_t*));
+ FUNCTION(cnd_signal, int (*f)(cnd_t*));
+ FUNCTION(cnd_timedwait, int (*f)(cnd_t*, mtx_t*, const struct timespec*));
+ FUNCTION(cnd_wait, int (*f)(cnd_t*, mtx_t*));
+
+ FUNCTION(mtx_destroy, void (*f)(mtx_t*));
+ FUNCTION(mtx_init, int (*f)(mtx_t*, int));
+ FUNCTION(mtx_lock, int (*f)(mtx_t*));
+ FUNCTION(mtx_timedlock, int (*f)(mtx_t*, const struct timespec*));
+ FUNCTION(mtx_trylock, int (*f)(mtx_t*));
+ FUNCTION(mtx_unlock, int (*f)(mtx_t*));
+
+ FUNCTION(thrd_create, int (*f)(thrd_t*, thrd_start_t, void*));
+ FUNCTION(thrd_current, thrd_t (*f)(void));
+ FUNCTION(thrd_detach, int (*f)(thrd_t));
+ FUNCTION(thrd_equal, int (*f)(thrd_t, thrd_t));
+ FUNCTION(thrd_exit, void (*f)(int));
+ FUNCTION(thrd_join, int (*f)(thrd_t, int*));
+ FUNCTION(thrd_sleep, int (*f)(const struct timespec*, struct timespec*));
+ FUNCTION(thrd_yield, void (*f)(void));
+
+ FUNCTION(tss_create, int (*f)(tss_t*, tss_dtor_t));
+ FUNCTION(tss_delete, void (*f)(tss_t));
+ FUNCTION(tss_get, void* (*f)(tss_t));
+ FUNCTION(tss_set, int (*f)(tss_t, void*));
+}
+
+#define DO_NOT_INCLUDE_TIME_H
+#include "time_h.c"
+
+#endif
diff --git a/tests/threads_test.cpp b/tests/threads_test.cpp
new file mode 100644
index 0000000..5fafff3
--- /dev/null
+++ b/tests/threads_test.cpp
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <gtest/gtest.h>
+
+#if __has_include(<threads.h>)
+
+#define HAVE_THREADS_H
+#include <threads.h>
+
+static int g_call_once_call_count;
+
+static void increment_call_count() {
+ ++g_call_once_call_count;
+}
+
+static int g_dtor_call_count;
+
+static void tss_dtor(void* ptr) {
+ ++g_dtor_call_count;
+ free(ptr);
+}
+
+static int return_arg(void* arg) {
+ return static_cast<int>(reinterpret_cast<uintptr_t>(arg));
+}
+
+static int exit_arg(void* arg) {
+ thrd_exit(static_cast<int>(reinterpret_cast<uintptr_t>(arg)));
+}
+
+#endif
+
+#include <time.h>
+
+#include <thread>
+
+#include "BionicDeathTest.h"
+#include "SignalUtils.h"
+
+TEST(threads, call_once) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ once_flag flag = ONCE_FLAG_INIT;
+ call_once(&flag, increment_call_count);
+ call_once(&flag, increment_call_count);
+ std::thread([&flag] {
+ call_once(&flag, increment_call_count);
+ }).join();
+ ASSERT_EQ(1, g_call_once_call_count);
+#endif
+}
+
+TEST(threads, cnd_broadcast__cnd_wait) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+
+ cnd_t c;
+ ASSERT_EQ(thrd_success, cnd_init(&c));
+
+ std::atomic_int i = 0;
+
+ auto waiter = [&c, &i, &m] {
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ while (i != 1) ASSERT_EQ(thrd_success, cnd_wait(&c, &m));
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ };
+ std::thread t1(waiter);
+ std::thread t2(waiter);
+ std::thread t3(waiter);
+
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ i = 1;
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+
+ ASSERT_EQ(thrd_success, cnd_broadcast(&c));
+
+ t1.join();
+ t2.join();
+ t3.join();
+
+ mtx_destroy(&m);
+ cnd_destroy(&c);
+#endif
+}
+
+TEST(threads, cnd_init__cnd_destroy) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ cnd_t c;
+ ASSERT_EQ(thrd_success, cnd_init(&c));
+ cnd_destroy(&c);
+#endif
+}
+
+TEST(threads, cnd_signal__cnd_wait) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+ cnd_t c;
+ ASSERT_EQ(thrd_success, cnd_init(&c));
+
+ std::atomic_int count = 0;
+ auto waiter = [&c, &m, &count] {
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ ASSERT_EQ(thrd_success, cnd_wait(&c, &m));
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ ++count;
+ };
+ std::thread t1(waiter);
+ std::thread t2(waiter);
+ std::thread t3(waiter);
+
+ // This is inherently racy, but attempts to distinguish between cnd_signal and
+ // cnd_broadcast.
+ usleep(100000);
+ ASSERT_EQ(thrd_success, cnd_signal(&c));
+ while (count == 0) {
+ }
+ usleep(100000);
+ ASSERT_EQ(1, count);
+
+ ASSERT_EQ(thrd_success, cnd_signal(&c));
+ while (count == 1) {
+ }
+ usleep(100000);
+ ASSERT_EQ(2, count);
+
+ ASSERT_EQ(thrd_success, cnd_signal(&c));
+ while (count == 2) {
+ }
+ usleep(100000);
+ ASSERT_EQ(3, count);
+
+ t1.join();
+ t2.join();
+ t3.join();
+
+ mtx_destroy(&m);
+ cnd_destroy(&c);
+#endif
+}
+
+TEST(threads, cnd_timedwait_timedout) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_timed));
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+
+ cnd_t c;
+ ASSERT_EQ(thrd_success, cnd_init(&c));
+
+ timespec ts = {};
+ ASSERT_EQ(thrd_timedout, cnd_timedwait(&c, &m, &ts));
+#endif
+}
+
+TEST(threads, cnd_timedwait) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_timed));
+
+ cnd_t c;
+ ASSERT_EQ(thrd_success, cnd_init(&c));
+
+ std::atomic_bool done = false;
+ std::thread t([&c, &m, &done] {
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+
+ // cnd_timewait's time is *absolute*.
+ timespec ts;
+ ASSERT_EQ(TIME_UTC, timespec_get(&ts, TIME_UTC));
+ ts.tv_sec += 666;
+
+ ASSERT_EQ(thrd_success, cnd_timedwait(&c, &m, &ts));
+ done = true;
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ });
+
+ while (!done) ASSERT_EQ(thrd_success, cnd_signal(&c));
+
+ t.join();
+#endif
+}
+
+TEST(threads, mtx_init) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_timed));
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain | mtx_recursive));
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_timed | mtx_recursive));
+ ASSERT_EQ(thrd_error, mtx_init(&m, 123));
+ ASSERT_EQ(thrd_error, mtx_init(&m, mtx_recursive));
+#endif
+}
+
+TEST(threads, mtx_destroy) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+ mtx_destroy(&m);
+#endif
+}
+
+TEST(threads, mtx_lock_plain) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ ASSERT_EQ(thrd_busy, mtx_trylock(&m));
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+
+ mtx_destroy(&m);
+#endif
+}
+
+TEST(threads, mtx_lock_recursive) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain | mtx_recursive));
+
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ ASSERT_EQ(thrd_success, mtx_trylock(&m));
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+
+ mtx_destroy(&m);
+#endif
+}
+
+TEST(threads, mtx_timedlock) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_timed));
+
+ timespec ts = {};
+ ASSERT_EQ(thrd_success, mtx_timedlock(&m, &ts));
+
+ std::thread([&m] {
+ timespec ts = { .tv_nsec = 500000 };
+ ASSERT_EQ(thrd_timedout, mtx_timedlock(&m, &ts));
+ }).join();
+
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ mtx_destroy(&m);
+#endif
+}
+
+
+TEST(threads, mtx_unlock) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ mtx_t m;
+ ASSERT_EQ(thrd_success, mtx_init(&m, mtx_plain));
+ ASSERT_EQ(thrd_success, mtx_lock(&m));
+ std::thread([&m] {
+ ASSERT_EQ(thrd_busy, mtx_trylock(&m));
+ }).join();
+ ASSERT_EQ(thrd_success, mtx_unlock(&m));
+ std::thread([&m] {
+ ASSERT_EQ(thrd_success, mtx_trylock(&m));
+ }).join();
+#endif
+}
+
+TEST(threads, thrd_current__thrd_equal) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ thrd_t t1 = thrd_current();
+ // (As a side-effect, this demonstrates interoperability with std::thread.)
+ std::thread([&t1] {
+ thrd_t t2 = thrd_current();
+ ASSERT_FALSE(thrd_equal(t1, t2));
+ thrd_t t2_2 = thrd_current();
+ ASSERT_TRUE(thrd_equal(t2, t2_2));
+ }).join();
+ thrd_t t1_2 = thrd_current();
+ ASSERT_TRUE(thrd_equal(t1, t1_2));
+#endif
+}
+
+TEST(threads, thrd_create__thrd_detach) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ thrd_t t;
+ ASSERT_EQ(thrd_success, thrd_create(&t, exit_arg, reinterpret_cast<void*>(1)));
+ ASSERT_EQ(thrd_success, thrd_detach(t));
+#endif
+}
+
+TEST(threads, thrd_create__thrd_exit) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ // Similar to the thrd_join test, but with a function that calls thrd_exit
+ // instead.
+ thrd_t t;
+ int result;
+ ASSERT_EQ(thrd_success, thrd_create(&t, exit_arg, reinterpret_cast<void*>(1)));
+ ASSERT_EQ(thrd_success, thrd_join(t, &result));
+ ASSERT_EQ(1, result);
+
+ ASSERT_EQ(thrd_success, thrd_create(&t, exit_arg, reinterpret_cast<void*>(2)));
+ ASSERT_EQ(thrd_success, thrd_join(t, &result));
+ ASSERT_EQ(2, result);
+
+ // The `result` argument can be null if you don't care...
+ ASSERT_EQ(thrd_success, thrd_create(&t, exit_arg, reinterpret_cast<void*>(3)));
+ ASSERT_EQ(thrd_success, thrd_join(t, nullptr));
+#endif
+}
+
+class threads_DeathTest : public BionicDeathTest {};
+
+TEST(threads_DeathTest, thrd_exit_main_thread) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ // "The program terminates normally after the last thread has been terminated.
+ // The behavior is as if the program called the exit function with the status
+ // EXIT_SUCCESS at thread termination time." (ISO/IEC 9899:2018)
+ ASSERT_EXIT(thrd_exit(12), ::testing::ExitedWithCode(EXIT_SUCCESS), "");
+#endif
+}
+
+TEST(threads, thrd_create__thrd_join) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ // Similar to the thrd_exit test, but with a function that calls return
+ // instead.
+ thrd_t t;
+ int result;
+ ASSERT_EQ(thrd_success, thrd_create(&t, return_arg, reinterpret_cast<void*>(1)));
+ ASSERT_EQ(thrd_success, thrd_join(t, &result));
+ ASSERT_EQ(1, result);
+
+ ASSERT_EQ(thrd_success, thrd_create(&t, return_arg, reinterpret_cast<void*>(2)));
+ ASSERT_EQ(thrd_success, thrd_join(t, &result));
+ ASSERT_EQ(2, result);
+
+ // The `result` argument can be null if you don't care...
+ ASSERT_EQ(thrd_success, thrd_create(&t, return_arg, reinterpret_cast<void*>(3)));
+ ASSERT_EQ(thrd_success, thrd_join(t, nullptr));
+#endif
+}
+
+TEST(threads, thrd_sleep_signal) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ ScopedSignalHandler ssh{SIGALRM, [](int) {}};
+ std::thread t([] {
+ timespec long_time = { .tv_sec = 666 };
+ timespec remaining = {};
+ ASSERT_EQ(-1, thrd_sleep(&long_time, &remaining));
+ uint64_t t = remaining.tv_sec * 1000000000 + remaining.tv_nsec;
+ ASSERT_LE(t, 666ULL * 1000000000);
+ });
+ usleep(100000); // 0.1s
+ pthread_kill(t.native_handle(), SIGALRM);
+ t.join();
+#endif
+}
+
+TEST(threads, thrd_sleep_signal_nullptr) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ ScopedSignalHandler ssh{SIGALRM, [](int) {}};
+ std::thread t([] {
+ timespec long_time = { .tv_sec = 666 };
+ ASSERT_EQ(-1, thrd_sleep(&long_time, nullptr));
+ });
+ usleep(100000); // 0.1s
+ pthread_kill(t.native_handle(), SIGALRM);
+ t.join();
+#endif
+}
+
+TEST(threads, thrd_sleep_error) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ timespec invalid = { .tv_sec = -1 };
+ ASSERT_EQ(-2, thrd_sleep(&invalid, nullptr));
+#endif
+}
+
+TEST(threads, thrd_yield) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ thrd_yield();
+#endif
+}
+
+TEST(threads, TSS_DTOR_ITERATIONS_macro) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ ASSERT_EQ(PTHREAD_DESTRUCTOR_ITERATIONS, TSS_DTOR_ITERATIONS);
+#endif
+}
+
+TEST(threads, tss_create) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ tss_t key;
+ ASSERT_EQ(thrd_success, tss_create(&key, nullptr));
+ tss_delete(key);
+#endif
+}
+
+TEST(threads, tss_create_dtor) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ tss_dtor_t dtor = tss_dtor;
+ tss_t key;
+ ASSERT_EQ(thrd_success, tss_create(&key, dtor));
+
+ ASSERT_EQ(thrd_success, tss_set(key, strdup("hello")));
+ std::thread([&key] {
+ ASSERT_EQ(thrd_success, tss_set(key, strdup("world")));
+ }).join();
+ // Thread exit calls the destructor...
+ ASSERT_EQ(1, g_dtor_call_count);
+
+ // "[A call to tss_set] will not invoke the destructor associated with the
+ // key on the value being replaced" (ISO/IEC 9899:2018).
+ g_dtor_call_count = 0;
+ ASSERT_EQ(thrd_success, tss_set(key, strdup("hello")));
+ ASSERT_EQ(0, g_dtor_call_count);
+
+ // "Calling tss_delete will not result in the invocation of any
+ // destructors" (ISO/IEC 9899:2018).
+ // The destructor for "hello" won't be called until *this* thread exits.
+ g_dtor_call_count = 0;
+ tss_delete(key);
+ ASSERT_EQ(0, g_dtor_call_count);
+#endif
+}
+
+TEST(threads, tss_get__tss_set) {
+#if !defined(HAVE_THREADS_H)
+ GTEST_SKIP() << "<threads.h> unavailable";
+#else
+ tss_t key;
+ ASSERT_EQ(thrd_success, tss_create(&key, nullptr));
+
+ ASSERT_EQ(thrd_success, tss_set(key, const_cast<char*>("hello")));
+ ASSERT_STREQ("hello", reinterpret_cast<char*>(tss_get(key)));
+ std::thread([&key] {
+ ASSERT_EQ(nullptr, tss_get(key));
+ ASSERT_EQ(thrd_success, tss_set(key, const_cast<char*>("world")));
+ ASSERT_STREQ("world", reinterpret_cast<char*>(tss_get(key)));
+ }).join();
+ ASSERT_STREQ("hello", reinterpret_cast<char*>(tss_get(key)));
+
+ tss_delete(key);
+#endif
+}