On Mon, 22 Jun 2026, 09:25 Iain Sandoe, <[email protected]> wrote: > > With the greater use of call_once by both the library and user code > the existing code bug has become a show-stopper for GCC on some of > our downstream distribution packages. > > This has been tested by me on both latest and oldest Darwin and by > one of the macports folks on a number of relevant packages (e.g. > ICU, fish, btop). I have not made an abi_tag(ged) change before > how does it look? > thanks > Iain > > --- 8< --- > > The current implementation as used on (particularly older) Darwin > does not function properly. It is possible (even likely) that there > are bugs in the pthreads implementations. However, Posix does say > "The behaviour of pthread_once() is undefined if once_control has > automatic storage duration or is not initialised by PTHREAD_ONCE_INIT." > The pthread_once Linux manual page has similar wording. > > However, at least from my reading, std::once_flag does not make such a > requirement
Correct. I think the original intent was for call_once to be a thin wrapper over pthread_once, but that was not actually possible, and was a mistake. Implementing it with a mutex and condvar makes it very large, the optimal implementation is just a futex but we don't have that on all targets. > (although the examples in the WD and, for example, > cppreference all show suitable file-scope vars in use). > > We also have known issues with exceptions in some implementations > and the presence of global state means that we cannot nest call_once. > > The patch here reimplements std::call_once to avoid the use of global > state and pthread_once. > > Since this is an ABI break, the new implementation is wrapped in an > abi_tag and the library continues to provide the existing symbols, and > their implementation. Surely the name of the tag and namespace should be "twice" not "once v2" (just kidding). I do think ocv2 is a bit cryptic, but keeping the mangled name shorter is useful. But I think we're supposed to use reserved names for abi-tags because we share the tag "namespace" with user code. > > libstdc++-v3/ChangeLog: > > * config/abi/pre/gnu.ver: Export __do_call_once. > * config/os/bsd/darwin/os_defines.h > (_GLIBCXX_ONCE_CALL_ABI2): Enable for Darwin. > * include/std/mutex (once_flag): Revised impl. > placed in abi_tag 'ocv2. (call_once): Likewise. Missing apostrophe. > * src/c++11/mutex.cc > (once_flag::__do_call_once): Revised impl. > > Signed-off-by: Iain Sandoe <[email protected]> > --- > libstdc++-v3/config/abi/pre/gnu.ver | 2 + > .../config/os/bsd/darwin/os_defines.h | 4 ++ > libstdc++-v3/include/std/mutex | 51 +++++++++++++++- > libstdc++-v3/src/c++11/mutex.cc | 61 ++++++++++++++++++- > 4 files changed, 116 insertions(+), 2 deletions(-) > > diff --git a/libstdc++-v3/config/abi/pre/gnu.ver > b/libstdc++-v3/config/abi/pre/gnu.ver > index 3a6afac8308..a79dc1fa8b3 100644 > --- a/libstdc++-v3/config/abi/pre/gnu.ver > +++ b/libstdc++-v3/config/abi/pre/gnu.ver > @@ -1323,6 +1323,8 @@ GLIBCXX_3.4.11 { > _ZNKSt10lock_error4whatEv; > > _ZSt11__once_call; > + _ZNSt6__ocv29once_flag14__do_call_onceEPFvPvES1_; This must be added in the latest symver, not 3.4.11 And that means no backports (maybe not an issue for darwin if you're using the versioned namespace now?) > + # old implementation > _ZSt15__once_callable; > _ZSt14__once_functor; > _ZSt23__get_once_functor_lockv; > diff --git a/libstdc++-v3/config/os/bsd/darwin/os_defines.h > b/libstdc++-v3/config/os/bsd/darwin/os_defines.h > index b6a5b76de21..151ee6cd169 100644 > --- a/libstdc++-v3/config/os/bsd/darwin/os_defines.h > +++ b/libstdc++-v3/config/os/bsd/darwin/os_defines.h > @@ -57,4 +57,8 @@ > // read(2) can return EINVAL for n >= INT_MAX. > #define _GLIBCXX_MAX_READ_SIZE (__INT_MAX__ - 1) > > +// Use the V2 ABI for once_call, the pthreads version does not work for > +// OS versions less than 10.11 (darwin15). > +#define _GLIBCXX_ONCE_CALL_ABI2 1 > + > #endif > diff --git a/libstdc++-v3/include/std/mutex b/libstdc++-v3/include/std/mutex > index fb5b5073834..94cf6f355c4 100644 > --- a/libstdc++-v3/include/std/mutex > +++ b/libstdc++-v3/include/std/mutex > @@ -791,6 +791,55 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > #endif // __cpp_lib_scoped_lock > > #ifdef _GLIBCXX_HAS_GTHREADS > +#ifdef _GLIBCXX_ONCE_CALL_ABI2 > + // The revised ABI does not use TLS > + /// Flag type used by std::call_once > + inline namespace __ocv2 __attribute__((__abi_tag__ ("ocv2"))) { > + struct once_flag > + { > + /// Constructor > + constexpr once_flag() = default; > + > + /// Deleted copy constructor > + once_flag(const once_flag&) = delete; > + /// Deleted assignment operator > + once_flag& operator=(const once_flag&) = delete; > + > + private: > + // call state: 0 = init, 1 = someone is trying, 2 = done. > + unsigned int _M_state_ = 0; No trailing underscore on these names. > + __gthread_mutex_t _M_mutx_ = __GTHREAD_MUTEX_INIT; > + __gthread_cond_t _M_condv_ = __GTHREAD_COND_INIT; We could use std::mutex and std::__condvar here, then use a unique_lock to lock it. > + > + void __do_call_once(void (*)(void*), void*); > + > + template<typename _Callable, typename... _Args> > + friend void > + call_once(once_flag& __once, _Callable&& __f, _Args&&... __args); > + }; > + > + /// Invoke a callable and synchronize with other calls using the same flag > + template<typename _Callable, typename... _Args> > + void > + call_once (once_flag& __flag, _Callable&& __f, _Args&&... __args) No space before the opening paren on a function definition > + { > + if (__flag._M_state_ == 2) This should be read with the mutex locked, or all accesses to the variable should be atomic. > + return; > + > + // Closure type that runs the original function with the supplied args. > + auto __callable = [&] { > + std::__invoke(std::forward<_Callable>(__f), > + std::forward<_Args>(__args)...); > + }; > + // Trampoline to call the actual fn; we will pass in the closure address. > + void (*__oc_tramp)(void*) > + = [] (void *ca) { (*static_cast<decltype(__callable)*>(ca))(); }; > + // Attempt to do it and synchronize with any other threads that are also > + // trying. > + __flag.__do_call_once (__oc_tramp, std::__addressof(__callable)); No space before paren > +} > +} // namespace ocv2 > +#else // ! _GLIBCXX_ONCE_CALL_ABI2 > /// Flag type used by std::call_once > struct once_flag > { > @@ -923,7 +972,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION > if (int __e = __gthread_once(&__once._M_once, &__once_proxy)) > __throw_system_error(__e); > } > - > +#endif // _GLIBCXX_ONCE_CALL_ABI2 > #else // _GLIBCXX_HAS_GTHREADS > > /// Flag type used by std::call_once > diff --git a/libstdc++-v3/src/c++11/mutex.cc b/libstdc++-v3/src/c++11/mutex.cc > index 8f04494620b..01c8b4f737c 100644 > --- a/libstdc++-v3/src/c++11/mutex.cc > +++ b/libstdc++-v3/src/c++11/mutex.cc > @@ -30,6 +30,66 @@ namespace std _GLIBCXX_VISIBILITY(default) > { > _GLIBCXX_BEGIN_NAMESPACE_VERSION > > +#ifdef _GLIBCXX_ONCE_CALL_ABI2 > +inline namespace __ocv2 __attribute__((__abi_tag__ ("ocv2"))) { > +// Version 2 ABI without global state, is callable recursively. > +// This calls the trampoline lambda, passing the address of the closure > +// repesenting the original function and its arguments. > +void > +once_flag::__do_call_once (void (*func)(void*), void *arg) > +{ > + __gthread_mutex_lock(&_M_mutx_); > + while (_M_state_ == 1) > + __gthread_cond_wait(&_M_condv_, &_M_mutx_); > + > + // mutex locked, the most likely outcome is that the once-call completed > + // on some other thread, so we are done. > + if (_M_state_ == 2) > + { > + __gthread_mutex_unlock(&_M_mutx_); > + return; > + } > + > + // mutex locked; if we get here, we expect the state to be 0, this would > + // correspond to an exception throw by the previous thread that tried to > + // do the once_call. > + __glibcxx_assert (_M_state_ == 0); > + > + try > + { > + // mutex locked. > + _M_state_ = 1; > + __gthread_mutex_unlock (&_M_mutx_); > + func (arg); > + // We got here without an exception, so the call is done. > + // If the underlying implementation is pthreads, then it is possible > + // to trigger a sequence of events where wake-ups are lost - unless the > + // mutex associated with the condition var is locked around the > relevant > + // broadcast (or signal). Is that true? > + __gthread_mutex_lock(&_M_mutx_); > + _M_state_ = 2; > + __gthread_cond_broadcast (&_M_condv_); > + __gthread_mutex_unlock (&_M_mutx_); > + } > + catch (...) > + { > + // mutex unlocked. > + // func raised an exception, let someone else try ... > + // See above. > + __gthread_mutex_lock(&_M_mutx_); > + _M_state_ = 0; > + __gthread_cond_broadcast (&_M_condv_); Do we want to wake all waiters here? Only one of them will be able acquire the mutex and try the once-call again. We could wake just one, I think. > + __gthread_mutex_unlock (&_M_mutx_); > + // ... and pass the exeception to our caller. "exception" typo > + throw; > + } > +} > +} // namespace ocv2 > +#endif // _GLIBCXX_ONCE_CALL_ABI2 > + > +// Unless we have a versioned library, provide the symbols for the previous > +// once call impl. > + > #ifdef _GLIBCXX_HAVE_TLS > __thread void* __once_callable; > __thread void (*__once_call)(); > @@ -115,7 +175,6 @@ namespace > callable(); > } > #endif // ! TLS > - > _GLIBCXX_END_NAMESPACE_VERSION > } // namespace std > > -- > 2.50.1 (Apple Git-155) >
