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)
>

Reply via email to