[libcxx-commits] [libcxx] [libc++] Introduce a new attribute keyword for Clang improves compatibility with Mingw-GCC (PR #141040)
Tomohiro Kashiwada via libcxx-commits
libcxx-commits at lists.llvm.org
Thu May 22 04:10:15 PDT 2025
https://github.com/kikairoya created https://github.com/llvm/llvm-project/pull/141040
A new ABI annotation keyword `_LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1` needs to be introduced and attached to `ostream::sentry::sentry`, `ostream::sentry::~sentry` and `istream::sentry` to improve binary compatibility on MinGW platform.
In MinGW environment, Clang handles dllexport attribute of internal class that defined in class template in different way from GCC. This incompatibility should be fixed but breaks ABI of libc++, so we need to introduce the new keyword to keep ABI in MinGW environment with old and patched Clang and to stay ABI compatible on other platforms.
This attribute is attached only for `basic_ostream::sentry::sentry`, `basic_ostream::sentry::~sentry` and `basic_istream::sentry::sentry`. Other entities won't be affected by patching Clang so doesn't need to be annotate.
# Background
Clang (targeting MinGW a.k.a. windows-gnu, slightly different from windows-msvc) handles template instantiation:
- When exporting: `extern template __declspec(dllexport) class TheTemplateClass<T>;`
allows exporting the outer template instantiation, but not its nested types (e.g., InnerClass).
- When importing: `extern template __declspec(dllimport) class TheTemplateClass<T>;`
try to import the outer template instantiation, but not its nested types - they will be instantiated in client object.
But MinGW-GCC handles template instantiation differently:
- When exporting: `extern template __declspec(dllexport) class TheTemplateClass<T>;`
allows exporting the outer template instantiation, but not its nested types (e.g., InnerClass).
- When importing: `extern template __declspec(dllimport) class TheTemplateClass<T>;`
causes MinGW-GCC to also try importing nested types such as TheTemplateClass<T>::InnerClass,
even if they were never exported. This leads to linker errors like: `undefined reference to TheTemplateClass<T>::InnerClass::...`
This difference causes link-time problems ( duplicated symbol or undefined reference ) or run-time problems ( illegal memory access, crash or other strange errors ) as reported in https://github.com/llvm/llvm-project/issues/135910 , so we are trying to align the behavior of Clang to MinGW-GCC.
But modifying Clang breaks libc++:
```
ld.lld: error: undefined symbol: std::__1::basic_ostream<char, std::__1::char_traits<char>>::sentry::sentry(std::__1::basic_ostream<char, std::__1::char_traits<char>>&)
>>> referenced by tools/clang/utils/TableGen/CMakeFiles/clang-tblgen.dir/NeonEmitter.cpp.obj:(std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:nn200100]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned int))
>>> referenced by tools/clang/utils/TableGen/CMakeFiles/clang-tblgen.dir/NeonEmitter.cpp.obj:((anonymous namespace)::Intrinsic::emitReverseVariable((anonymous namespace)::Variable&, (anonymous namespace)::Variable&))
ld.lld: error: undefined symbol: std::__1::basic_ostream<char, std::__1::char_traits<char>>::sentry::~sentry()
>>> referenced by tools/clang/utils/TableGen/CMakeFiles/clang-tblgen.dir/NeonEmitter.cpp.obj:(std::__1::basic_ostream<char, std::__1::char_traits<char>>& std::__1::__put_character_sequence[abi:nn200100]<char, std::__1::char_traits<char>>(std::__1::basic_ostream<char, std::__1::char_traits<char>>&, char const*, unsigned int))
>>> referenced by tools/clang/utils/TableGen/CMakeFiles/clang-tblgen.dir/NeonEmitter.cpp.obj:((anonymous namespace)::Intrinsic::emitReverseVariable((anonymous namespace)::Variable&, (anonymous namespace)::Variable&))
```
so we need to fix symbol visibility annotation in libc++ prior to patch Clang.
# Effects
What attaching `_LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1` to `ostream::sentry::sentry`s does:
- in MinGW environment:
Expanded to `_LIBCPP_HIDE_FROM_ABI`.
- While building a DLL:
Virtually no-op while Clang and MinGW-GCC doesn't export them from a past.
- Using a DLL from client-code:
Forces instantiate in client code and prohibits trying to import from DLL.
This is same to what former Clang does and gains compatibility with patched Clang and MinGW-GCC.
- for all other platforms including MSVC:
Expanded to `_LIBCPP_HIDE_FROM_ABI_V1`. Keeps V1 ABI stable.
Thus, this change introduces no significant side-effects except for need to add `inline` together.
# Other options that were not adopted
1. Modify GCC's behavior:
@mstorsjo reported to GCC as defect over 5 years ago but there is no response yet: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=89087 .
There seems to be no chance of fixing it.
2. Attaching `_LIBCPP_EXPORTED_FROM_ABI` like:
--- a/libcxx/include/__ostream/basic_ostream.h
+++ b/libcxx/include/__ostream/basic_ostream.h
@@ -71,7 +71,7 @@ protected:
public:
// 27.7.2.4 Prefix/suffix:
- class sentry;
+ class _LIBCPP_EXPORTED_FROM_ABI sentry;
// 27.7.2.6 Formatted output:
inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 basic_ostream& operator<<(basic_ostream& (*__pf)(basic_ostream&)) {
This is simply wrong. Breaks ABI and doesn't work with template argument other than char or wchar_t.
3. Declaring `ostream::sentry`s with `__attribute__((exclude_from_explicit_instantiation))` if `__MINGW32__` and when client-side with a new keyword like (empty if in other conditions):
--- a/libcxx/include/__config
+++ b/libcxx/include/__config
@@ -365,6 +365,11 @@ typedef __char32_t char32_t;
# define _LIBCPP_CLASS_TEMPLATE_INSTANTIATION_VIS
# define _LIBCPP_OVERRIDABLE_FUNC_VIS
# define _LIBCPP_EXPORTED_FROM_ABI
+# if !__has_attribute(exclude_from_explicit_instantiation) || defined(_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS)
+# define _LIBCPP_INNER_CLASS_IN_TEMPLATE_VIS
+# else
+# define _LIBCPP_INNER_CLASS_IN_TEMPLATE_VIS __attribute__((__exclude_from_explicit_instantiation__))
+# endif
# elif defined(_LIBCPP_BUILDING_LIBRARY)
# if defined(__MINGW32__)
# define _LIBCPP_EXTERN_TEMPLATE_TYPE_VIS __declspec(dllexport)
and
--- a/libcxx/include/__ostream/basic_ostream.h
+++ b/libcxx/include/__ostream/basic_ostream.h
@@ -72,7 +72,7 @@ protected:
public:
// 27.7.2.4 Prefix/suffix:
- class sentry;
+ class _LIBCPP_INNER_CLASS_IN_TEMPLATE_VIS sentry;
// 27.7.2.6 Formatted output:
inline _LIBCPP_HIDE_FROM_ABI_AFTER_V1 basic_ostream& operator<<(basic_ostream& (*__pf)(basic_ostream&)) {
This works well but leaves GCC incompatible, so this idea is not perfect.
4. Instantiate inner class explicitly
# if defined(__MINGW32__) || defined(__CYGWIN__)
extern template _LIBCPP_EXTERN_TEMPLATE_TYPE_VIS basic_ostream<char>::sentry::sentry(basic_ostream<char>& __os);
extern template _LIBCPP_EXTERN_TEMPLATE_TYPE_VIS basic_ostream<char>::sentry::~sentry();
# endif
This works for both of Clang and GCC but scattering `# if` blocks is not a good style.
5. Similar to this proposal but expand to nothing for non-MinGW platforms like this:
# if defined(__MINGW32__) || defined(__CYGWIN__)
# define _LIBCPP_HIDE_FROM_ABI_IF_MINGW inline _LIBCPP_HIDE_FROM_ABI
# else
# define _LIBCPP_HIDE_FROM_ABI_IF_MINGW
# endif
This works fine. If keeping non-inline is preferred to keeping consistency of presence or absence of `inline` between platforms, this is an option.
# Conclusion
Due to incompatible behavior on MinGW platform, Clang needs to be modified. But patching Clang breaks libc++ so adjusting visibility of some symbols is required. Any keyword already exist can't be suitable so we're going to introduce a new keyword named `_LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1`.
>From 4581bdbb38146cc6fcff3379eafea1758d245a00 Mon Sep 17 00:00:00 2001
From: kikairoya <kikairoya at gmail.com>
Date: Thu, 22 May 2025 06:42:34 +0900
Subject: [PATCH] [libc++] Introduce a new attribute keyword for Clang improves
compatibility with Mingw-GCC
The new ABI annotation keyword _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1
is introduced.
In MinGW environment, Clang handles dllexport attribute of internal
class that defined in class template in different way from GCC.
This incompatibility should be fixed but breaks ABI of libc++, so
introduce a new keyword to keep ABI in MinGW environment with
old and patched Clang and to stay ABI compatible on other platforms.
This attribute is attached only for basic_ostream::sentry::sentry,
basic_ostream::sentry::~sentry and basic_istream::sentry::sentry.
Other entities won't be affected by patching Clang so doesn't need
to be annotate.
At a time to introduce a new (static or non-static) member function
is added to an inner class within a class as a non-template, all of
such member functions needs to be attached _LIBCPP_HIDE_FROM_ABI
Otherwise, that member functions contained in DLL will be
inaccessible on MinGW environment.
---
libcxx/include/__config | 42 ++++++++++++++++++++++++
libcxx/include/__ostream/basic_ostream.h | 4 +--
libcxx/include/istream | 2 +-
3 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/libcxx/include/__config b/libcxx/include/__config
index 57223e4f1db18..118f97cc77d7d 100644
--- a/libcxx/include/__config
+++ b/libcxx/include/__config
@@ -530,6 +530,48 @@ typedef __char32_t char32_t;
# define _LIBCPP_HIDE_FROM_ABI_AFTER_V1 _LIBCPP_HIDE_FROM_ABI
# endif
+// _LIBCPP_HIDE_FROM_ABI is required for member functions defined within an inner class of a class template
+// (e.g., std::basic_ostream<...>::sentry::sentry(...)), due to inconsistent behavior in MinGW-GCC (and
+// Cygwin as well, in all relevant cases) regarding template instantiation and symbol visibility when combined
+// with __declspec(dllexport/dllimport).
+//
+// Previous versions of Clang did not exhibit this issue, but upcoming versions are expected to align with
+// GCC's behavior for compatibility. This is particularly important because some of libstdc++ packages
+// compiled with --with-default-libstdcxx-abi=gcc4-compatible are incompatible with Clang, resulting in linking
+// errors or runtime crushes.
+//
+// A few such member functions already exist (here are ostream::sentry::sentry, ostream::~sentry and
+// istream::sentry::sentry) were not previously marked with _LIBCPP_HIDE_FROM_ABI but should be to avoid symbol
+// visibility issues. However, adding the macro unconditionally would break the ABI on other platforms.
+//
+// Therefore, a dedicated macro _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 is introduced. This macro expands to
+// _LIBCPP_HIDE_FROM_ABI only when targeting MinGW, and to _LIBCPP_HIDE_FROM_ABI_AFTER_V1 on all other platforms.
+//
+// Going forward, whenever a new (static or non-static) member function is added to an inner class within a
+// class template, it must be annotated with _LIBCPP_HIDE_FROM_ABI to ensure proper symbol visibility when
+// targeting MinGW. Otherwise, the resulting DLL will be unusable due to missing symbols.
+//
+// The underlying issue arises from how MinGW-GCC handles template instantiation:
+//
+// - When exporting: 'extern template __declspec(dllexport) class TheTemplateClass<T>;'
+// allows exporting the outer template instantiation, but not its nested types (e.g., InnerClass).
+//
+// - When importing: 'extern template __declspec(dllimport) class TheTemplateClass<T>;'
+// causes MinGW-GCC to also try importing nested types such as TheTemplateClass<T>::InnerClass,
+// even if they were never exported. This leads to linker errors like:
+// 'undefined reference to TheTemplateClass<T>::InnerClass::...'
+//
+// This differs from Clang's historical behavior, which did not import nested classes implicitly.
+// However, to ensure ABI compatibility with the MinGW-GCC toolchain (commonly used in the MinGW ecosystem),
+// Clang will adopt this behavior as well.
+//
+// Note: As of this writing, Clang does not yet implement this behavior, since doing so would break libc++.
+# if defined(__MINGW32__) || defined(__CYGWIN__)
+# define _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 _LIBCPP_HIDE_FROM_ABI
+# else
+# define _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 _LIBCPP_HIDE_FROM_ABI_AFTER_V1
+# endif
+
// TODO: Remove this workaround once we drop support for Clang 16
# if __has_warning("-Wc++23-extensions")
# define _LIBCPP_CLANG_DIAGNOSTIC_IGNORED_CXX23_EXTENSION _LIBCPP_CLANG_DIAGNOSTIC_IGNORED("-Wc++23-extensions")
diff --git a/libcxx/include/__ostream/basic_ostream.h b/libcxx/include/__ostream/basic_ostream.h
index 09ad401a48d60..8c146840becf2 100644
--- a/libcxx/include/__ostream/basic_ostream.h
+++ b/libcxx/include/__ostream/basic_ostream.h
@@ -187,8 +187,8 @@ class basic_ostream<_CharT, _Traits>::sentry {
basic_ostream<_CharT, _Traits>& __os_;
public:
- explicit sentry(basic_ostream<_CharT, _Traits>& __os);
- ~sentry();
+ explicit inline _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 sentry(basic_ostream<_CharT, _Traits>& __os);
+ inline _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 ~sentry();
sentry(const sentry&) = delete;
sentry& operator=(const sentry&) = delete;
diff --git a/libcxx/include/istream b/libcxx/include/istream
index 4596eebf5ed22..6bbeccaa2de55 100644
--- a/libcxx/include/istream
+++ b/libcxx/include/istream
@@ -310,7 +310,7 @@ class basic_istream<_CharT, _Traits>::sentry {
bool __ok_;
public:
- explicit sentry(basic_istream<_CharT, _Traits>& __is, bool __noskipws = false);
+ explicit inline _LIBCPP_HIDE_FROM_ABI_MINGW_OR_AFTER_V1 sentry(basic_istream<_CharT, _Traits>& __is, bool __noskipws = false);
// ~sentry() = default;
_LIBCPP_HIDE_FROM_ABI explicit operator bool() const { return __ok_; }
More information about the libcxx-commits
mailing list