diff --git a/libc/config/linux/api.td b/libc/config/linux/api.td --- a/libc/config/linux/api.td +++ b/libc/config/linux/api.td @@ -301,6 +301,12 @@ ]; } +def OnceFlag : TypeDecl<"once_flag"> { + let Decl = [{ + typedef unsigned int once_flag; + }]; +} + def MtxT : TypeDecl<"mtx_t"> { let Decl = [{ typedef struct { @@ -314,8 +320,20 @@ let Decl = "typedef int (*thrd_start_t)(void *);"; } +def CallOnceFuncT : TypeDecl<"__call_once_func_t"> { + let Decl = [{ + typedef void(*__call_once_func_t)(void); + }]; +} + def ThreadsAPI : PublicAPI<"threads.h"> { + let Macros = [ + SimpleMacroDef<"ONCE_FLAG_INIT", "0">, + ]; + let TypeDeclarations = [ + OnceFlag, + CallOnceFuncT, MtxT, ThreadStartT, ]; @@ -332,6 +350,7 @@ ]; let Functions = [ + "call_once", "mtx_init", "mtx_lock", "mtx_unlock", diff --git a/libc/lib/CMakeLists.txt b/libc/lib/CMakeLists.txt --- a/libc/lib/CMakeLists.txt +++ b/libc/lib/CMakeLists.txt @@ -35,6 +35,7 @@ libc.src.sys.mman.munmap # threads.h entrypoints + libc.src.threads.call_once libc.src.threads.mtx_init libc.src.threads.mtx_lock libc.src.threads.mtx_unlock diff --git a/libc/spec/stdc.td b/libc/spec/stdc.td --- a/libc/spec/stdc.td +++ b/libc/spec/stdc.td @@ -8,6 +8,11 @@ RestrictedPtrType CharRestrictedPtr = RestrictedPtrType; ConstType ConstCharRestrictedPtr = ConstType; + NamedType OnceFlagType = NamedType<"once_flag">; + PtrType OnceFlagTypePtr = PtrType; + // TODO(sivachandra): Remove this non-standard type when a formal + // way to describe callable types is available. + NamedType CallOnceFuncType = NamedType<"__call_once_func_t">; NamedType MtxTType = NamedType<"mtx_t">; PtrType MtxTTypePtr = PtrType; NamedType ThrdStartTType = NamedType<"thrd_start_t">; @@ -267,8 +272,12 @@ HeaderSpec Threads = HeaderSpec< "threads.h", - [], // Macros [ + Macro<"ONCE_FLAG_INIT">, + ], + [ + OnceFlagType, + CallOnceFuncType, MtxTType, ThrdStartTType, ThrdTType, @@ -285,6 +294,14 @@ ], [ FunctionSpec< + "call_once", + RetValSpec, + [ + ArgSpec, + ArgSpec, + ] + >, + FunctionSpec< "mtx_init", RetValSpec, [ diff --git a/libc/src/threads/CMakeLists.txt b/libc/src/threads/CMakeLists.txt --- a/libc/src/threads/CMakeLists.txt +++ b/libc/src/threads/CMakeLists.txt @@ -3,6 +3,13 @@ endif() add_entrypoint_object( + call_once + ALIAS + DEPENDS + .${LIBC_TARGET_OS}.call_once +) + +add_entrypoint_object( thrd_create ALIAS DEPENDS diff --git a/libc/src/threads/call_once.h b/libc/src/threads/call_once.h new file mode 100644 --- /dev/null +++ b/libc/src/threads/call_once.h @@ -0,0 +1,20 @@ +//===-- Implementation header for call_once function ------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_LIBC_SRC_THREADS_CALL_ONCE_H +#define LLVM_LIBC_SRC_THREADS_CALL_ONCE_H + +#include "include/threads.h" + +namespace __llvm_libc { + +void call_once(once_flag *flag, __call_once_func_t func); + +} // namespace __llvm_libc + +#endif // LLVM_LIBC_SRC_THREADS_CALL_ONCE_H diff --git a/libc/src/threads/linux/CMakeLists.txt b/libc/src/threads/linux/CMakeLists.txt --- a/libc/src/threads/linux/CMakeLists.txt +++ b/libc/src/threads/linux/CMakeLists.txt @@ -8,6 +8,19 @@ ${LIBC_TARGET_MACHINE}/thread_start_args.h.in ) +add_entrypoint_object( + call_once + SRCS + call_once.cpp + HDRS + ../call_once.h + DEPENDS + .threads_utils + libc.config.linux.linux_syscall_h + libc.include.sys_syscall + libc.include.threads +) + add_header_library( threads_utils HDRS diff --git a/libc/src/threads/linux/call_once.cpp b/libc/src/threads/linux/call_once.cpp new file mode 100644 --- /dev/null +++ b/libc/src/threads/linux/call_once.cpp @@ -0,0 +1,58 @@ +//===-- Linux implementation of the call_once function --------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "config/linux/syscall.h" // For syscall functions. +#include "include/sys/syscall.h" // For syscall numbers. +#include "include/threads.h" // For call_once related type definition. +#include "src/__support/common.h" +#include "src/threads/linux/thread_utils.h" + +#include +#include +#include + +namespace __llvm_libc { + +static constexpr unsigned START = 0x11; +static constexpr unsigned WAITING = 0x22; +static constexpr unsigned FINISH = 0x33; + +void LLVM_LIBC_ENTRYPOINT(call_once)(once_flag *flag, __call_once_func_t func) { + FutexData *futex_word = reinterpret_cast(flag); + unsigned int not_called = ONCE_FLAG_INIT; + + // The C standard wording says: + // + // The completion of the function func synchronizes with all + // previous or subsequent calls to call_once with the same + // flag variable. + // + // What this means is that, the call_once call can return only after + // the called function |func| returns. So, we use futexes to synchronize + // calls with the same flag value. + if (::atomic_compare_exchange_strong(futex_word, ¬_called, START)) { + func(); + auto status = ::atomic_exchange(futex_word, FINISH); + if (status == WAITING) { + __llvm_libc::syscall(SYS_futex, futex_word, FUTEX_WAKE_PRIVATE, + INT_MAX, // Wake all waiters. + 0, 0, 0); + } + return; + } + + unsigned int status = START; + if (::atomic_compare_exchange_strong(futex_word, &status, WAITING) || + status == WAITING) { + __llvm_libc::syscall(SYS_futex, futex_word, FUTEX_WAIT_PRIVATE, + WAITING, // Block only if status is still |WAITING|. + 0, 0, 0); + } +} + +} // namespace __llvm_libc diff --git a/libc/test/src/threads/CMakeLists.txt b/libc/test/src/threads/CMakeLists.txt --- a/libc/test/src/threads/CMakeLists.txt +++ b/libc/test/src/threads/CMakeLists.txt @@ -1,6 +1,22 @@ add_libc_testsuite(libc_threads_unittests) add_libc_unittest( + call_once_test + SUITE + libc_threads_unittests + SRCS + call_once_test.cpp + DEPENDS + libc.include.threads + libc.src.threads.call_once + libc.src.threads.mtx_init + libc.src.threads.mtx_lock + libc.src.threads.mtx_unlock + libc.src.threads.thrd_create + libc.src.threads.thrd_join +) + +add_libc_unittest( thrd_test SUITE libc_threads_unittests diff --git a/libc/test/src/threads/call_once_test.cpp b/libc/test/src/threads/call_once_test.cpp new file mode 100644 --- /dev/null +++ b/libc/test/src/threads/call_once_test.cpp @@ -0,0 +1,111 @@ +//===-- Unittests for call_once -------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "include/threads.h" +#include "src/threads/call_once.h" +#include "src/threads/mtx_init.h" +#include "src/threads/mtx_lock.h" +#include "src/threads/mtx_unlock.h" +#include "src/threads/thrd_create.h" +#include "src/threads/thrd_join.h" +#include "utils/UnitTest/Test.h" + +#include + +static constexpr unsigned int num_threads = 5; +static atomic_uint thread_count; + +static unsigned int call_count; +static void call_once_func() { ++call_count; } + +static int func(void *) { + static once_flag flag = ONCE_FLAG_INIT; + __llvm_libc::call_once(&flag, call_once_func); + + ++thread_count; // This is a an atomic update. + + return 0; +} + +TEST(CallOnceTest, CallFrom5Threads) { + // Ensure the call count and thread count are 0 to begin with. + call_count = 0; + thread_count = 0; + + thrd_t threads[num_threads]; + for (unsigned int i = 0; i < num_threads; ++i) { + ASSERT_EQ(__llvm_libc::thrd_create(threads + i, func, nullptr), + static_cast(thrd_success)); + } + + for (unsigned int i = 0; i < num_threads; ++i) { + int retval; + ASSERT_EQ(__llvm_libc::thrd_join(threads + i, &retval), + static_cast(thrd_success)); + ASSERT_EQ(retval, 0); + } + + EXPECT_EQ(static_cast(thread_count), 5U); + EXPECT_EQ(call_count, 1U); +} + +static mtx_t once_func_blocker; +static void blocking_once_func() { + __llvm_libc::mtx_lock(&once_func_blocker); + __llvm_libc::mtx_unlock(&once_func_blocker); +} + +static atomic_uint start_count; +static atomic_uint done_count; +static int once_func_caller(void *) { + static once_flag flag; + ++start_count; + __llvm_libc::call_once(&flag, blocking_once_func); + ++done_count; + return 0; +} + +// Test the synchronization aspect of the call_once function. +// This is not a fool proof test, but something which might be +// useful when we add a flakiness detection scheme to UnitTest. +TEST(CallOnceTest, TestSynchronization) { + start_count = 0; + done_count = 0; + + ASSERT_EQ(__llvm_libc::mtx_init(&once_func_blocker, mtx_plain), + static_cast(thrd_success)); + // Lock the blocking mutex so that the once func blocks. + ASSERT_EQ(__llvm_libc::mtx_lock(&once_func_blocker), + static_cast(thrd_success)); + + thrd_t t1, t2; + ASSERT_EQ(__llvm_libc::thrd_create(&t1, once_func_caller, nullptr), + static_cast(thrd_success)); + ASSERT_EQ(__llvm_libc::thrd_create(&t2, once_func_caller, nullptr), + static_cast(thrd_success)); + + while (start_count != 2) + ; // Spin until both threads start. + + // Since the once func is blocked, the threads should not be done yet. + EXPECT_EQ(static_cast(done_count), 0U); + + // Unlock the blocking mutex so that the once func blocks. + ASSERT_EQ(__llvm_libc::mtx_unlock(&once_func_blocker), + static_cast(thrd_success)); + + int retval; + ASSERT_EQ(__llvm_libc::thrd_join(&t1, &retval), + static_cast(thrd_success)); + ASSERT_EQ(retval, 0); + ASSERT_EQ(__llvm_libc::thrd_join(&t2, &retval), + static_cast(thrd_success)); + ASSERT_EQ(retval, 0); + + ASSERT_EQ(static_cast(done_count), 2U); +}