[ping]:[PATCH v2] libstdc++: Reimplement std:call_once with an ABI tag.
Iain Sandoe <[email protected]> Tue, 4 Aug 2026 09:10:06 +0100
| Newsgroups | gmane.comp.gcc.patches,gmane.comp.gcc.libstdc++.devel |
|---|---|
| Message-ID | <[email protected]> |
a gentle ping here .. I have downstream users looking forward to a fix if it is approved, thanks Iain > On 23 Jun 2026, at 07:53, Iain Sandoe <[email protected]> wrote: > > Hi Jonathan, > > thanks for the off-list discussions .. > >>> 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. > > Well, a selling point of doing this is that it is opt-in and currently > restricted to Darwin. A second target with futexes (IDK if Darwin will > ever have these) could just choose to declare the ABI2 as one using them. > > .... > >>> 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). > > ah... so tempting :) > >> 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. > > I have re-named it "__c1v2" which is also cryptic, but short. > > <snip, various typographical fixes made> > >>> + __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. > > As discussed off-list, that is not going to work because ___condvar > has a non-constexpr constructor. > >>> + { >>> + if (__flag._M_state_ == 2) > >> This should be read with the mutex locked, or all accesses to the >> variable should be atomic. > > I read some wisdom somewhere that says that atomic access is not enough > in this case, the accesses must be guarded by the mutex (done). > > <snip typographical fixes done> > >>> + // 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? > > I do not claim to have such in-depth knowledge of pthread, therefore it must > be input read from the Open pages. When I re-checked yesterday, I could not > find this exact statement - but: > > "The pthread_cond_broadcast() or pthread_cond_signal() functions may be > called by a thread whether or not it currently owns the mutex that threads > calling pthread_cond_wait() or pthread_cond_timedwait() have associated > with the condition variable during their waits; however, if predictable > scheduling behavior is required, then that mutex shall be locked by the > thread calling pthread_cond_broadcast() or pthread_cond_signal()." > > IDK whether deterministic can be equated to missing wake-ups, but it does > seem plausible - since if a wakeup is sent with no sleepers, it is ignored. > >>> + _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. > > Good point. > > I have hopefully caught all the muscle-memory extra spaces between function > names and opening parentheses. > > Re-tested on Darwin9 (approx 100 progressions) and Darwin 24 (no changes), > but not asked the macports folks to re-test yet. > > As for deployment, we're between a rock and an hard place .. what's there > now fails completely on earlier systems, so a replacement seems essential. > That means a careful roll-out .. > > OK for trunk? > 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 (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. > > The patch here reimplements std::call_once avoiding 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. > > 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 '__c1v2'. (call_once): Likewise. > * 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 | 3 + > .../config/os/bsd/darwin/os_defines.h | 4 ++ > libstdc++-v3/include/std/mutex | 54 +++++++++++++++- > libstdc++-v3/src/c++11/mutex.cc | 61 ++++++++++++++++++- > 4 files changed, 120 insertions(+), 2 deletions(-) > > diff --git a/libstdc++-v3/config/abi/pre/gnu.ver b/libstdc++-v3/config/abi/pre/gnu.ver > index 3a6afac8308..c2443143c40 100644 > --- a/libstdc++-v3/config/abi/pre/gnu.ver > +++ b/libstdc++-v3/config/abi/pre/gnu.ver > @@ -2632,6 +2632,9 @@ GLIBCXX_3.4.36 { > _ZNSt7__cxx1112basic_stringI[cw]St11char_traitsI[cw]ESaI[cw]EE*_S_allocate_*; > _ZNSt7__cxx1112basic_stringI[cw]St11char_traitsI[cw]ESaI[cw]EE*_M_create_*; > > + # Re-implementation of call_once > + _ZNSt6__c1v29once_flag14__do_call_onceEPFvPvES1_; > + > } GLIBCXX_3.4.35; > > # Symbols in the support library (libsupc++) have their own tag. > 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..c00615d749d 100644 > --- a/libstdc++-v3/include/std/mutex > +++ b/libstdc++-v3/include/std/mutex > @@ -791,6 +791,58 @@ _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 __c1v2 __attribute__((__abi_tag__ ("__c1v2"))) { > + 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; > + __gthread_mutex_t _M_mutx = __GTHREAD_MUTEX_INIT; > + __gthread_cond_t _M_condv = __GTHREAD_COND_INIT; > + > + 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) > + { > + __gthread_mutex_lock(&__flag._M_mutx); > + bool done = (__flag._M_state == 2); > + __gthread_mutex_unlock(&__flag._M_mutx); > + if (done) > + 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)); > +} > +} // namespace __c1v2 > +#else // ! _GLIBCXX_ONCE_CALL_ABI2 > /// Flag type used by std::call_once > struct once_flag > { > @@ -923,7 +975,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..d6e588f08d0 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 __c1v2 __attribute__((__abi_tag__ ("__c1v2"))) { > +// 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). > + __gthread_mutex_lock(&_M_mutx); > + _M_state = 2; > + __gthread_cond_signal(&_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_signal(&_M_condv); > + __gthread_mutex_unlock(&_M_mutx); > + // ... and pass the exception to our caller. > + throw; > + } > +} > +} // namespace __c1v2 > +#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) >