Skip to content

C++ modules appear to be exceedignly strict with intrinsic headers #98021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
chriselrod opened this issue Jul 8, 2024 · 36 comments · Fixed by #104701
Closed

C++ modules appear to be exceedignly strict with intrinsic headers #98021

chriselrod opened this issue Jul 8, 2024 · 36 comments · Fixed by #104701
Assignees
Labels
clang:modules C++20 modules and Clang Header Modules

Comments

@chriselrod
Copy link

That is, code using immintrin.h tends to fail to compile when using modules while working fine with headers.
I'll try to produce a minimal example in the next few hours.
For now, I have an example using boost_unordered.
When problems showed up in my own code using intrinsics, I could generally fix it by declaring all arguments as variables, and then passing the lvalues to the intriinsic function.

Hello.cxxm:

#ifndef USE_HEADERS
module;
#endif
#include <boost/unordered/unordered_flat_map.hpp>
#include <iostream>

#ifndef USE_HEADERS
export module Hello;
export {
#endif
  void hello() { std::cout << "Hello World!\n"; }
  template <typename K, typename V> using map = boost::unordered_flat_map<K, V>;
#ifndef USE_HEADERS
}
#endif

user.cpp:

#ifndef USE_HEADERS
import Hello;
#else
#include "hello.cxxm"
#endif

int main() {
  hello();
  int x = 0;
  long y = 0;
  map<int*,long*> m;
  m[&x] = &y;
  [[maybe_unused]] auto f = m.find(&x);
  return 0;
}

Compiling with headers:

$ clang++ -std=c++23 use.cpp -DUSE_HEADERS -o Hello.out
$ ./Hello.out
Hello World!

With modules:

$ clang++ -std=c++23 --precompile hello.cxxm -o M-hello.pcm
$ clang++ -std=c++23 use.cpp -fmodule-file=Hello=M-hello.pcm M-hello.pcm -o Hello_mod.out

results in

/usr/include/boost/unordered/detail/foa/core.hpp:293:7: error: no matching function for call to '_mm_cmpeq_epi8'
  293 |       _mm_cmpeq_epi8(load_metadata(),_mm_setzero_si128()))&0x7FFF;
      |       ^~~~~~~~~~~~~~
/usr/include/boost/unordered/detail/foa/core.hpp:309:14: note: in instantiation of member function 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>::match_available' requested here
  309 |     return (~match_available())&0x7FFF;

I could file this as a boost_unordered issue or make a PR there, as I've generally found I can work around the problem.
But I'll see about creating a minimal reproducer using #include <immintrin.h> directlry that works with headers but fails with modules.

@EugeneZelenko EugeneZelenko added clang:modules C++20 modules and Clang Header Modules and removed new issue labels Jul 8, 2024
@llvmbot
Copy link
Member

llvmbot commented Jul 8, 2024

@llvm/issue-subscribers-clang-modules

Author: Chris Elrod (chriselrod)

That is, code using `immintrin.h` tends to fail to compile when using modules while working fine with headers. I'll try to produce a minimal example in the next few hours. For now, I have an example using boost_unordered. When problems showed up in my own code using intrinsics, I could generally fix it by declaring all arguments as variables, and then passing the lvalues to the intriinsic function.

Hello.cxxm:

#ifndef USE_HEADERS
module;
#endif
#include &lt;boost/unordered/unordered_flat_map.hpp&gt;
#include &lt;iostream&gt;

#ifndef USE_HEADERS
export module Hello;
export {
#endif
  void hello() { std::cout &lt;&lt; "Hello World!\n"; }
  template &lt;typename K, typename V&gt; using map = boost::unordered_flat_map&lt;K, V&gt;;
#ifndef USE_HEADERS
}
#endif

user.cpp:

#ifndef USE_HEADERS
import Hello;
#else
#include "hello.cxxm"
#endif

int main() {
  hello();
  int x = 0;
  long y = 0;
  map&lt;int*,long*&gt; m;
  m[&amp;x] = &amp;y;
  [[maybe_unused]] auto f = m.find(&amp;x);
  return 0;
}

Compiling with headers:

$ clang++ -std=c++23 use.cpp -DUSE_HEADERS -o Hello.out
$ ./Hello.out
Hello World!

With modules:

$ clang++ -std=c++23 --precompile hello.cxxm -o M-hello.pcm
$ clang++ -std=c++23 use.cpp -fmodule-file=Hello=M-hello.pcm M-hello.pcm -o Hello_mod.out

results in

/usr/include/boost/unordered/detail/foa/core.hpp:293:7: error: no matching function for call to '_mm_cmpeq_epi8'
  293 |       _mm_cmpeq_epi8(load_metadata(),_mm_setzero_si128()))&amp;0x7FFF;
      |       ^~~~~~~~~~~~~~
/usr/include/boost/unordered/detail/foa/core.hpp:309:14: note: in instantiation of member function 'boost::unordered::detail::foa::group15&lt;boost::unordered::detail::foa::plain_integral&gt;::match_available' requested here
  309 |     return (~match_available())&amp;0x7FFF;

I could file this as a boost_unordered issue or make a PR there, as I've generally found I can work around the problem.
But I'll see about creating a minimal reproducer using #include &lt;immintrin.h&gt; directlry that works with headers but fails with modules.

@chriselrod
Copy link
Author

Here is the minimal example using immintrin.h:

hello.cxxm:

#ifndef USE_HEADERS
module;
#endif

#include <concepts>
#include <immintrin.h>
#include <iostream>

#ifndef USE_HEADERS
export module Hello;
export {
#endif
  void hello() { std::cout << "Hello World!\n"; }
  template <typename T> auto vload128(const T *p) {
    if constexpr (std::same_as<T, float>) {
      return _mm_loadu_ps(p);
    } else if constexpr (std::same_as<T, double>) {
      return _mm_loadu_pd(p);
    } else {
      return _mm_loadu_si128(reinterpret_cast<const __m128i *>(p));
    }
  }
#ifndef USE_HEADERS
}
#endif

use.cpp:

#ifndef USE_HEADERS
import Hello;
#else
#include "hello.cxxm"
#endif

int main() {
  hello();
  float x[4];
  [[maybe_unused]] auto v = vload128(x);
  return 0;
}

Compiling with headers:

$ clang++ -std=c++23 use.cpp -DUSE_HEADERS -o Hello.
out
$ ./Hello.out
Hello World!

Compiling with modules:

$ clang++ -std=c++23 --precompile hello.cxxm -o M-he
llo.pcm
$ clang++ -std=c++23 use.cpp -fmodule-file=Hello=M-hell
o.pcm M-hello.pcm -o Hello_mod.out
In file included from use.cpp:2:
/home/chriselrod/Documents/progwork/cxx/experiments/modules/hello.cxxm:16:14: error: no matching function for call to '_mm_loadu_ps'
   16 |       return _mm_loadu_ps(p);
      |              ^~~~~~~~~~~~
use.cpp:10:29: note: in instantiation of function template specialization 'vload128<float>' requested here
   10 |   [[maybe_unused]] auto v = vload128(x);
      |                             ^
1 error generated.

@chriselrod
Copy link
Author

chriselrod commented Jul 8, 2024

using something like

template <typename T> auto vload128(const T *p) {
  if constexpr (std::same_as<T, float>) {
    const float *fp = p;
    return _mm_loadu_ps(fp);
  } else if constexpr (std::same_as<T, double>) {
    const double *dp = p;
    return _mm_loadu_pd(dp);
  } else {
    const __m128i *ip = reinterpret_cast<const __m128i *>(p);
    return _mm_loadu_si128(ip);
  }
}

instead allows it to compile, even though fp and dp should have the same type as p.

@chriselrod
Copy link
Author

Another workaround is to declare explicit instantiations of the template within the module that defines it.

@ChuanqiXu9 ChuanqiXu9 self-assigned this Jul 9, 2024
@ChuanqiXu9
Copy link
Member

I can't reproduce this in my local environment with trunk (0182f51).

My standard library is libstdc++ 10.2 (this may not be relevent) in linux.

Can you try again with trunk?

@jiixyj
Copy link
Contributor

jiixyj commented Jul 22, 2024

I just stumbled across this bug as well, upgrading from a clang trunk from around end of June to this commit: ChuanqiXu9/clangd-for-modules@e583176. I'm also using <boost/unordered/unordered_flat_map.hpp> in my code.

I managed to bisect it to 91d40ef . Maybe something in that commit inadvertently touched some code related to name lookup or module visibility?

It is a bit weird though that the date of that commit is after the report date of this bug...

@jiixyj
Copy link
Contributor

jiixyj commented Jul 22, 2024

I managed to work around it for now by re-applying 91d40ef on top of ChuanqiXu9/clangd-for-modules@e583176 . I guess reapplying it on main should also work.

@jiixyj
Copy link
Contributor

jiixyj commented Jul 22, 2024

After reapplying 91d40ef , the reproducer from here now fails again, but luckily my code doesn't seem to trigger that case.

@ChuanqiXu9
Copy link
Member

@jiixyj I've relanded #102287. Can you verify if this is still a problem?

@jiixyj
Copy link
Contributor

jiixyj commented Aug 8, 2024

@ChuanqiXu9 : Sadly, with 3c9e345 I still get the errors when including <boost/unordered/unordered_flat_map.hpp>:

/home/jan/.conan2/p/b/boost77375cc29ad40/p/include/boost/unordered/detail/foa/core.hpp:291:12: error: no matching function for call to '_mm_movemask_epi8'
  291 |     return _mm_movemask_epi8(
      |            ^~~~~~~~~~~~~~~~~

and

/home/jan/.conan2/p/b/boost77375cc29ad40/p/include/boost/unordered/detail/foa/core.hpp:1070:36: error: no matching function for call to 'size_index_for'                                        
 1070 |     auto         groups_size_index=size_index_for<group_type,size_policy>(n);                                                                                                           
      |                                    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                                                                                                               

...and some more similar ones.

I looked at the differences between the original PR and the relanded version. It turns out if I make Decl::isInAnotherModuleUnit() look like the following, the errors go away:

bool Decl::isInAnotherModuleUnit() const {
  auto *M = getOwningModule();

#if 1
  if (!M || !M->isNamedModule())
    return false;
#else
  if (!M)
    return false;

  // FIXME or NOTE: maybe we need to be clear about the semantics
  // of clang header modules. e.g., if this lives in a clang header
  // module included by the current unit, should we return false
  // here?
  //
  // This is clear for header units as the specification says the
  // header units live in a synthesised translation unit. So we
  // can return false here.
  M = M->getTopLevelModule();
  if (!M->isNamedModule())
    return false;
#endif

  return M != getASTContext().getCurrentNamedModule();
}

The code between #else and #endif is the current code in the main branch.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 8, 2024

This is just a shot in the dark as I'm really not familiar with this code, but maybe there should be a check for the global module again? Like this:

 bool Decl::isInAnotherModuleUnit() const {
   auto *M = getOwningModule();
 
   if (!M)
     return false;
 
+  if (M->isGlobalModule())
+    return false;
+
   // FIXME or NOTE: maybe we need to be clear about the semantics
   // of clang header modules. e.g., if this lives in a clang header
   // module included by the current unit, should we return false
   // here?
   //
   // This is clear for header units as the specification says the
   // header units live in a synthesised translation unit. So we
   // can return false here.
   M = M->getTopLevelModule();
   if (!M->isNamedModule())
     return false;
 
   return M != getASTContext().getCurrentNamedModule();
 }

Otherwise, getTopLevelModule() turns e.g. my.module.<global> into my.module.

Also I'm wondering if any change in isInAnotherModuleUnit() should be mirrored in isInCurrentModuleUnit()?

@ChuanqiXu9
Copy link
Member

@jiixyj Thanks for the analysis, it is helpful. But the suggested check may not be the case. Since the GMF may be in the current module unit semantically. I'll try if the minimal reproducer from @chriselrod works. If yes I guess I can fix it quickly. But if not, maybe it will be better to get a reproducer from you.

Also I'm wondering if any change in isInAnotherModuleUnit() should be mirrored in isInCurrentModuleUnit()?

Maybe not. Since a translation unit may not be a module unit. Otherwise we can implement isInAnotherModuleUnit() as !isInCurrentModuleUnit()

@jiixyj
Copy link
Contributor

jiixyj commented Aug 10, 2024

Here is a minimal reproducer. I used cvise to create this, plus some simplifications by hand.

map.cppm:

module;

static void fun(long);

template <typename = void> struct a {
  a() { fun(load()); }
  long load();
};

export module map;

export using map = a<>;

map.cpp:

import map;
map m;

Run it like:

$ clang++ -std=c++23 --precompile map.cppm -o M-map.pcm && clang++ -std=c++23 -c map.cpp -fmodule-file=map=M-map.pcm
In file included from map.cpp:1:
map.cppm:6:9: error: no matching function for call to 'fun'
    6 |   a() { fun(load()); }
      |         ^~~
map.cpp:2:5: note: in instantiation of member function 'a<>::a' requested here
    2 | map m;
      |     ^
1 error generated.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 10, 2024

For the record, here is the resulting map.cppm from cvise before my manual simplifications:

module;
static void _mm_movemask_epi8(long);
template <template <typename> class> struct group15 {
  void match_occupied() { _mm_movemask_epi8(load_metadata()); }
  long load_metadata();
};
template <typename Group> struct table_core {
  table_core() {
    for_all_elements([] {});
  }
  static void match_really_occupied() {
    Group pg;
    pg.match_occupied();
  }
  template <typename F> void for_all_elements(F) { match_really_occupied; }
};
template <typename> struct plain_integral;
template <typename, typename, typename, typename>
using table_core_impl = table_core<group15<plain_integral>>;
struct Trans_NS_unordered_unordered_flat_map {
  table_core_impl<int, int, int, int> table_;
};
export module map;
export template <typename, typename>
using map = Trans_NS_unordered_unordered_flat_map;

...and here the "start" map.cppm provided as input to cvise (before macro expansion with -save-temps):

module;
#include <boost/unordered/unordered_flat_map.hpp>

export module map;

export template <typename K, typename V>
using map = boost::unordered_flat_map<K, V>;

@chriselrod
Copy link
Author

I remember calling the intrinsics a red herring somewhere, as I later learned it seems to actually be about static, as in your minimal example.

Out of curiosity, have you also gotten a lot of linker errors that only show up when using modules, not when using headers?

@jiixyj
Copy link
Contributor

jiixyj commented Aug 10, 2024

So if I understand correctly, _mm_movemask_epi8 is considered "TU-local" according to https://eel.is/c++draft/basic.link#15. Therefore, "exposing" it from the module (via inline or a function template) is ill-formed.

This is the definition of _mm_movemask_epi8 from Clang 18 (/usr/lib/clang/18/include/emmintrin.h):

static __inline__ int __DEFAULT_FN_ATTRS _mm_movemask_epi8(__m128i __a) {
  return __builtin_ia32_pmovmskb128((__v16qi)__a);
}

__DEFAULT_FN_ATTRS defines the __always_inline__ attribute among some others.

Because this is static inline, this becomes "TU-local" when used in a module unit. In the traditional C++ compilation model this is no problem, as each translation unit gets its own copy of the static inline method.

Interestingly, GCC defines their _mm_* intrinsics without static:

extern __inline int __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_movemask_epi8 (__m128i __A)
{
  return __builtin_ia32_pmovmskb128 ((__v16qi)__A);
}

I'm not sure what the exact semantics of extern in combination with __inline (with a leading double underscore) and __always_inline__ are, but at least it would not be TU-local I guess.

Maybe Clang could define their intrinsics to be more "modules friendly", like GCC? Or, maybe Clang could just "make modules work" as an exception for __always_inline__ static inline functions -- the __always_inline__ means the function should "disappear" anyways.

Or could static inline in the GMF be made to "just work" in general? static inline is a common idiom especially in C when you want to have code in headers without any ODR issues.


On the other hand, the size_index_for error seems to be a "bug" in boost::unordered. It is defined like this:

template<typename Group,typename SizePolicy>
static inline std::size_t size_index_for(std::size_t n)
{
  /* n/N+1 == ceil((n+1)/N) (extra +1 for the sentinel) */
  return SizePolicy::size_index(n/Group::N+1);
}

It should probably be just inline, not static inline. Again, in the old translation model this wouldn't make any difference, but with modules it does (IIUC).


Out of curiosity, have you also gotten a lot of linker errors that only show up when using modules, not when using headers?

Yes, I also got linker errors here and there when trying to debug my issue with boost's unordered_map when including it in a module unit. This is just a guess, but maybe Clang gets confused with all those static inline functions floating around combined with being in a "modules context". Maybe things like reachability (examples: https://eel.is/c++draft/module.global.frag#6) enter the picture as well... All compiles "successfully" (even though it is actually ill-formed according to the standard), but then it fails at the linking stage because the code for some of those functions is not emitted?

In #78173 @ChuanqiXu9 also mentioned that Clang's diagnostics when static/static inline is mixed with modules are not the best currently.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 10, 2024

I can fix the compiler errors if I replace static with extern for _mm_movemask_epi8 and _mm_cmpeq_epi8 in "llvm-prefix/lib/clang/20/include/emmintrin.h", and remove the static from boost::unordered's size_index_for.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 10, 2024

There is an interesting discussion thread from when GCC decided to convert all their static inline SIMD intrinsics to extern inline: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=34000

It appears that using static inline functions inside inline functions is problematic even in C, so this class of issue is not just limited to C++20 modules.

edit: Another interesting discussion about static vs. extern for SIMD intrinsics: https://stackoverflow.com/questions/57105290/static-vs-external-intrinsics The accepted answer is from a GCC developer.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 11, 2024

What do you think about something like the following patch to Sema::AddOverloadCandidate? The intention is to have a special case for static inline functions that are defined in the GMF and don't make them invisible to name lookup. This should hopefully be minimally invasive as it just handles the common idiom of static inline functions from (C) headers.

I'm not sure if this is blessed by the standard. But maybe it's OK as the GMF is intended as a compatibility mechanism for headers.

diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp
index fd88b6a74297..103eec3c371f 100644
--- a/clang/lib/Sema/SemaOverload.cpp
+++ b/clang/lib/Sema/SemaOverload.cpp
@@ -6872,21 +6872,33 @@ void Sema::AddOverloadCandidate(
 
   // Functions with internal linkage are only viable in the same module unit.
   if (getLangOpts().CPlusPlusModules && Function->isInAnotherModuleUnit()) {
     /// FIXME: Currently, the semantics of linkage in clang is slightly
     /// different from the semantics in C++ spec. In C++ spec, only names
     /// have linkage. So that all entities of the same should share one
     /// linkage. But in clang, different entities of the same could have
     /// different linkage.
-    NamedDecl *ND = Function;
-    if (auto *SpecInfo = Function->getTemplateSpecializationInfo())
+    NamedDecl *ND;
+    bool IsInlineFunctionInGMF;
+    if (auto *SpecInfo = Function->getTemplateSpecializationInfo()) {
       ND = SpecInfo->getTemplate();
+      IsInlineFunctionInGMF = false;
+    } else {
+      ND = Function;
+      /// As a special case, don't remove a "static inline" function declared
+      /// in the GMF from the overload set since this is a common pattern in C
+      /// code.
+      IsInlineFunctionInGMF =
+          Function->getOwningModule() &&
+          Function->getOwningModule()->isGlobalModule() &&
+          Function->isInlineSpecified();
+    }
 
-    if (ND->getFormalLinkage() == Linkage::Internal) {
+    if (ND->getFormalLinkage() == Linkage::Internal && !IsInlineFunctionInGMF) {
       Candidate.Viable = false;
       Candidate.FailureKind = ovl_fail_module_mismatched;
       return;
     }
   }
 
   if (isNonViableMultiVersionOverload(Function)) {
     Candidate.Viable = false;

@ChuanqiXu9
Copy link
Member

@jiixyj as you mentioned, this is due to the limitations of TU locals from the standard. Or in another word, the limitation was introduced by the nature of static entities. I feel the proposed suggestion is somewhat hack. I guess the best solution here is to mark all the intrinsics as inline.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 12, 2024

I agree, this is more of a hack. I do wonder though why the error only appears when the call to the static inline function is dependent on a template parameter for the error to appear. Here is a simpler reproducer:

"map.cppm":

module;

static inline void fun(long) {}
template <typename T = long> void a() { fun(T{}); }

export module map;
export using ::a;

"map.cpp":

import map;
auto m = (a(), 0);

If I either:

  • make a() a non-template function
  • or call fun() qualified like this: ::fun()

...the problem goes away.

I guess it is like this because dependent names are looked up at the point of template instantiation -- and at that point, the static inline function is "outside" the current module so it is not considered for name lookup (?). I wonder why the error also goes away when I qualify ::fun(), though. Is ADL involved as well?

@ChuanqiXu9
Copy link
Member

I wonder why the error also goes away when I qualify ::fun(), though. Is ADL involved as well?

Yeah, I think so.

I guess it is like this because dependent names are looked up at the point of template instantiation -- and at that point, the static inline function is "outside" the current module so it is not considered for name lookup (?).

Yeah, according to the codes you cited, it may be the case.

"map.cppm":

module;

static inline void fun(long) {}
template <typename T = long> void a() { fun(T{}); }

export module map;
export using ::a;

"map.cpp":

import map;
auto m = (a(), 0);

For this reproducer, my feeling is, if we implement the standard strictly, the compiler may not have a chance to look at map.cpp, it will emit errors when compiling map.cppm.

But this is the reason why I didn't implement the check for such a long time. We have too many static functions in headers in this ecosystem. I fear it will make a lot of things gets broken.

@ChuanqiXu9
Copy link
Member

Now I changed my mind slightly. I think you can submit your change as a hack workaround due to the current C++ ecosystem.

And it is not conflicting with the change we need to made for intrinsics and give warnings for such leaked TU-locals.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 13, 2024

Thank you for your feedback! I'll try to create a PR and to write some tests.

It looks like this was discussed in the committee before: cplusplus/nbballot#427 There was one paper, but it wasn't accepted. Still, the committee recognizes the problem and they encourage further work on this.

And in Clang it actually works most of the time! In MSVC as well as far as I can see. But finding wording for this that can go into the standard is a hard problem it looks like.

edit: correction: MSVC fails this:

module;

static inline int fun() { return 0; }
template <typename G = void> int a() { return fun(); }

export module map;
export using ::a;
import map;
auto _ = (a(), 0);

...with:

map.cppm(3): error C2129: static function 'int fun(void)' declared but not defined
map.cppm(3): note: see declaration of 'fun'

@ChuanqiXu9
Copy link
Member

It looks like this was discussed in the committee before: cplusplus/nbballot#427 There was one paper, but it wasn't accepted. Still, the committee recognizes the problem and they encourage further work on this.

This is another topic. It is about header units not the named modules. As far as I remember, I didn't see such disucssion in commitee.

@jiixyj
Copy link
Contributor

jiixyj commented Aug 18, 2024

I opened a PR: #104701

I expanded the fix/hack for implicit instantiations of templates with internal linkage, as people write code like that: https://github.com/boostorg/unordered/blob/a39cf60e93ab7ee1782bae4ced211dc9f6eff751/include/boost/unordered/detail/foa/core.hpp#L855

With this change, my usage of Boost's unordered_flap_{map,set} now works without errors.

ChuanqiXu9 pushed a commit that referenced this issue Dec 31, 2024
In C, it is a common pattern to have `static inline` functions in
headers to avoid ODR issues. Currently, when those headers are included
in a GMF, the names are not found when two-phase name lookup and ADL is
involved. Those names are removed by `Sema::AddOverloadCandidate`.

Similarly, in C++, sometimes people use templates with internal linkage
in headers.

As the GMF was designed to be a transitional mechanism for headers,
special case those functions in `Sema::AddOverloadCandidate`.

This fixes <#98021>.
github-actions bot pushed a commit to arm/arm-toolchain that referenced this issue Jan 10, 2025
…4701)

In C, it is a common pattern to have `static inline` functions in
headers to avoid ODR issues. Currently, when those headers are included
in a GMF, the names are not found when two-phase name lookup and ADL is
involved. Those names are removed by `Sema::AddOverloadCandidate`.

Similarly, in C++, sometimes people use templates with internal linkage
in headers.

As the GMF was designed to be a transitional mechanism for headers,
special case those functions in `Sema::AddOverloadCandidate`.

This fixes <llvm/llvm-project#98021>.
@chriselrod
Copy link
Author

I'm finally back to converting one of my projects to using modules, and am hitting this again:

cmake --build buildclang/modules/
[0/2] Re-checking globbed directories...
[1/3] Building CXX object CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o
FAILED: CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o 
/home/chriselrod/.local/bin/clang++ -DUSE_MODULE -I/home/chriselrod/Documents/progwork/cxx/LoopModels/test -I/home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math -I/home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/include -isystem /home/chriselrod/Documents/progwork/cxx/LoopModels/buildclang/modules/_deps/googletest-src/googletest/include -isystem /home/chriselrod/Documents/progwork/cxx/LoopModels/buildclang/modules/_deps/googletest-src/googletest -stdlib=libc++ -g -std=gnu++23 -fno-exceptions -fno-omit-frame-pointer -fno-rtti -fstack-protector-strong -ferror-limit=8 -fcolor-diagnostics -ftime-trace -Wall -Wpedantic -Wextra -Wshadow -D_GLIBCXX_ASSERTIONS -march=native -mprefer-vector-width=512 -MD -MT CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o -MF CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o.d @CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o.modmap -o CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o -c /home/chriselrod/Documents/progwork/cxx/LoopModels/test/triangular_solve_test.cpp
In module 'ArrayParse' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/test/triangular_solve_test.cpp:33:
In module 'StaticArray' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Utilities/MatrixStringParse.cxx:17:
In module 'Array' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/StaticArrays.cxx:43:
In module 'AssignExprTemplates' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/Array.cxx:48:
In module 'Indexing' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/ArrayOps.cxx:31:
In module 'SIMD' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/Indexing.cxx:26:
In module 'SIMD:Intrin' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/SIMD/SIMD.cxx:18:
/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:56: error: definition with same mangled name '_ZL17_mm_setzero_si128v' as another definition
 3880 | static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                        ^
/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:56: note: previous definition is here
 3880 | static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                        ^
1 error generated.
ninja: build stopped: subcommand failed.
make: *** [Makefile:45: clangmodules] Error 1

I'm on a relatively recent commit of clang++ (this is a roughly 10-day old build of main):

clang++ --version
clang version 21.0.0git ([email protected]:llvm/llvm-project.git 7dad8b91bc94741034052a4eb06ef45e7cb47c06)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/chriselrod/.local/stow/llvm/bin

@jiixyj
Copy link
Contributor

jiixyj commented May 11, 2025

I think this might be related to #119947 . Can you try to remove the static from static __inline__ ...? This at least should get you further...

@chriselrod
Copy link
Author

If I understand correctly, it isn't the same problem as #119947. That seems to be two separate template instantiations with different structs named s produce the same mangled name, but here, we have

static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
  return __extension__(__m128i)(__v2di){0LL, 0LL};
}

so we should always have the exact same function _mm_setzero_si128 being called (i.e., both instances should be exact duplicates)

@jiixyj
Copy link
Contributor

jiixyj commented May 11, 2025

Ah, you are right!
What happens if you add __attribute__((__always_inline__)) (and keep the static)? Does the error go away?

@jiixyj
Copy link
Contributor

jiixyj commented May 11, 2025

I still have the same problem, btw. It's this template here: https://github.com/boostorg/unordered/blob/a64d81a3789b6f5deb8d8569503e6fe129df93a6/include/boost/unordered/detail/foa/core.hpp#L856 I removed the static to test if it makes the problem go away and then promptly forgot about it.

@jiixyj
Copy link
Contributor

jiixyj commented May 11, 2025

In my case it's this error that is hit:

getDiags().Report(D->getLocation(), diag::err_duplicate_mangled_name)

Here is a dump of the two involved Decls:

FunctionDecl 0x55d5d74a2ed8 </home/jan/.conan2/p/b/boost92dac37a4f13d/p/include/boost/unordered/detail/foa/core.hpp:855:1, line:859:1> line:855:27 imported in ldgr.balance_sheet.<global> hidden used size_index_for 'std::size_t (std::size_t)' implicit_instantiation static inline
|-TemplateArgument type 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>'
| `-RecordType 0x55d5ce61bbd0 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>' imported
|   `-ClassTemplateSpecialization 0x55d5ce61b1e8 'group15'
|-TemplateArgument type 'boost::unordered::detail::foa::pow2_size_policy'
| `-RecordType 0x55d5ce65fb10 'boost::unordered::detail::foa::pow2_size_policy' imported
|   `-CXXRecord 0x55d5ce65f9f8 'pow2_size_policy'
|-ParmVarDecl 0x55d5d74a3028 <col:42, col:54> col:54 imported in ldgr.balance_sheet hidden used n 'std::size_t':'unsigned long'
`-CompoundStmt 0x55d5d74a3708 <line:856:1, line:859:1>
  `-ReturnStmt 0x55d5d74a36f8 <line:858:3, col:45>
    `-CallExpr 0x55d5d74a36d0 <col:10, col:45> 'std::size_t':'unsigned long'
      |-ImplicitCastExpr 0x55d5d74a36b8 <col:10, col:22> 'std::size_t (*)(std::size_t)' <FunctionToPointerDecay>
      | `-DeclRefExpr 0x55d5d74a3618 <col:10, col:22> 'std::size_t (std::size_t)' lvalue CXXMethod 0x55d5d5dd6690 'size_index' 'std::size_t (std::size_t)'
      |   `-NestedNameSpecifier TypeSpec 'boost::unordered::detail::foa::pow2_size_policy'
      `-BinaryOperator 0x55d5d74a35f8 <col:33, col:44> 'unsigned long' '+'
        |-BinaryOperator 0x55d5d74a35d8 <col:33, col:42> 'unsigned long' '/'
        | |-ImplicitCastExpr 0x55d5d74a35c0 <col:33> 'std::size_t':'unsigned long' <LValueToRValue>
        | | `-DeclRefExpr 0x55d5d74a35a0 <col:33> 'std::size_t':'unsigned long' lvalue ParmVar 0x55d5d74a3028 'n' 'std::size_t':'unsigned long'
        | `-ImplicitCastExpr 0x55d5d74a3588 <col:35, col:42> 'std::size_t':'unsigned long' <LValueToRValue>
        |   `-DeclRefExpr 0x55d5d74a34e8 <col:35, col:42> 'const std::size_t':'const unsigned long' lvalue Var 0x55d5cc4c12f8 'N' 'const std::size_t':'const unsigned long' non_odr_use_constant
        |     `-NestedNameSpecifier TypeSpec 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>'
        `-ImplicitCastExpr 0x55d5d74a34d0 <col:44> 'unsigned long' <IntegralCast>
          `-IntegerLiteral 0x55d5d74a34b0 <col:44> 'int' 1

FunctionDecl 0x55d5d5dd5508 </home/jan/.conan2/p/b/boost92dac37a4f13d/p/include/boost/unordered/detail/foa/core.hpp:855:1, line:859:1> line:855:27 imported in ldgr.balance.<global> hidden used size_index_for 'std::size_t (std::size_t)' implicit_instantiation static inline
|-TemplateArgument type 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>'
| `-RecordType 0x55d5ce61bbd0 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>' imported
|   `-ClassTemplateSpecialization 0x55d5ce61b1e8 'group15'
|-TemplateArgument type 'boost::unordered::detail::foa::pow2_size_policy'
| `-RecordType 0x55d5ce65fb10 'boost::unordered::detail::foa::pow2_size_policy' imported
|   `-CXXRecord 0x55d5ce65f9f8 'pow2_size_policy'
|-ParmVarDecl 0x55d5d5dd5658 <col:42, col:54> col:54 imported in ldgr.balance hidden used n 'std::size_t':'unsigned long'
`-CompoundStmt 0x55d5d5dd6868 <line:856:1, line:859:1>
  `-ReturnStmt 0x55d5d5dd6858 <line:858:3, col:45>
    `-CallExpr 0x55d5d5dd6830 <col:10, col:45> 'std::size_t':'unsigned long'
      |-ImplicitCastExpr 0x55d5d5dd6818 <col:10, col:22> 'std::size_t (*)(std::size_t)' <FunctionToPointerDecay>
      | `-DeclRefExpr 0x55d5d5dd65e8 <col:10, col:22> 'std::size_t (std::size_t)' lvalue CXXMethod 0x55d5d5dd6690 'size_index' 'std::size_t (std::size_t)'
      |   `-NestedNameSpecifier TypeSpec 'boost::unordered::detail::foa::pow2_size_policy'
      `-BinaryOperator 0x55d5d5dd65c8 <col:33, col:44> 'std::size_t':'unsigned long' '+'
        |-BinaryOperator 0x55d5d5dd65a8 <col:33, col:42> 'std::size_t':'unsigned long' '/'
        | |-ImplicitCastExpr 0x55d5d5dd6590 <col:33> 'std::size_t':'unsigned long' <LValueToRValue>
        | | `-DeclRefExpr 0x55d5d5dd6570 <col:33> 'std::size_t':'unsigned long' lvalue ParmVar 0x55d5d5dd5658 'n' 'std::size_t':'unsigned long'
        | `-ImplicitCastExpr 0x55d5d5dd6558 <col:35, col:42> 'std::size_t':'unsigned long' <LValueToRValue>
        |   `-DeclRefExpr 0x55d5d5dd64c0 <col:35, col:42> 'const std::size_t':'const unsigned long' lvalue Var 0x55d5cc4c12f8 'N' 'const std::size_t':'const unsigned long' non_odr_use_constant
        |     `-NestedNameSpecifier TypeSpec 'boost::unordered::detail::foa::group15<boost::unordered::detail::foa::plain_integral>'
        `-ImplicitCastExpr 0x55d5d5dd64a8 <col:44> 'std::size_t':'unsigned long' <IntegralCast>
          `-IntegerLiteral 0x55d5d5dd6488 <col:44> 'int' 1

So the ldgr.balance_sheet module gets two separate FunctionDecls for the same static inline template instantiation, once by directly including <boost/unordered/unordered_flat_map.hpp> in the GMF, and once by importing another module ldgr.balance that also includes <boost/unordered/unordered_flat_map.hpp> in its GMF.

@chriselrod
Copy link
Author

What happens if you add __attribute__((__always_inline__)) (and keep the static)? Does the error go away?

Removing static fixes it.
Keeping static and adding the attribute does not:

/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:91: error: definition with same mangled name '_ZL17_mm_setzero_si128v' as another definition
 3880 | __attribute__((__always_inline__)) static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                                                           ^
/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:91: note: previous definition is here
 3880 | __attribute__((__always_inline__)) static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                                                           ^
1 error generated.
[101/103] Building CXX object CMakeFiles/LoopModelsTests.dir/dependence_test.cpp.o
ninja: build stopped: subcommand failed.
make: *** [Makefile:63: clang-modules] Error 1

I still have the same problem, btw. It's this template here: https://github.com/boostorg/unordered/blob/a64d81a3789b6f5deb8d8569503e6fe129df93a6/include/boost/unordered/detail/foa/core.hpp#L856 I removed the static to test if it makes the problem go away and then promptly forgot about it.

I'm also including <boost/unordered/unordered_flat_map.hpp>. I was trying explicit instantiations when building into module, as these let it compile before.
I recall getting such errors with intrinsics, but also recall such errors with static functions in the unordered library.

Ideally, static would work with modules.
Maybe something like

#if __GNUC__ >= 4
#define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
#else
#define DLL_LOCAL
#endif
  • inline would be a good workaround for libraries to use instead, assuming the main point of static is to avoid exporting symbols (and that you aren't already using -fvisibility-hidden, which header-only libraries probably can't assume). https://gcc.gnu.org/wiki/Visibility

I imagine removing static fixed the problem?

So the ldgr.balance_sheet module gets two separate FunctionDecls for the same static inline template instantiation, once by directly including <boost/unordered/unordered_flat_map.hpp> in the GMF, and once by importing another module ldgr.balance that also includes <boost/unordered/unordered_flat_map.hpp> in its GMF.

Thanks for the deep dive.
I suspect my problem is similar.
I try to wrap header-only libraries in modules that place all the inclides in the GMF, and export the symbols I need.
Then other modules can use import boost_unordered; or import LLVM;.
I have a module that includes immintrin.h, then boost/unordered does as well. I suspect I'm getting collisions like this.
Testing smaller sets of modules in isolation works, but the larger tests that try to link more modules together fail (due to bringing in the duplicate definitions).

@jiixyj
Copy link
Contributor

jiixyj commented May 11, 2025

One hacky (?) fix is to just disable this error for decls from the GMF:

diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 3469676b74bc..cb2e1fa1b0e1 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -4746,24 +4746,36 @@ llvm::Constant *CodeGenModule::GetOrCreateLLVMFunction(
 
     // Handle dropped DLL attributes.
     if (D && shouldDropDLLAttribute(D, Entry)) {
       Entry->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass);
       setDSOLocal(Entry);
     }
 
     // If there are two attempts to define the same mangled name, issue an
     // error.
     if (IsForDefinition && !Entry->isDeclaration()) {
+
+      bool SkipCheck = false;
+      if (D->isFromGlobalModule()) {
+        SkipCheck = true;
+      } else if (const FunctionDecl *FD = cast_or_null<FunctionDecl>(D)) {
+        if (const FunctionTemplateDecl *FunTmpl = FD->getPrimaryTemplate()) {
+          if (FunTmpl->isFromGlobalModule()) {
+            SkipCheck = true;
+          }
+        }
+      }
+
       GlobalDecl OtherGD;
       // Check that GD is not yet in DiagnosedConflictingDefinitions is required
       // to make sure that we issue an error only once.
-      if (lookupRepresentativeDecl(MangledName, OtherGD) &&
+      if (!SkipCheck && lookupRepresentativeDecl(MangledName, OtherGD) &&
           (GD.getCanonicalDecl().getDecl() !=
            OtherGD.getCanonicalDecl().getDecl()) &&
           DiagnosedConflictingDefinitions.insert(GD).second) {
         getDiags().Report(D->getLocation(), diag::err_duplicate_mangled_name)
             << MangledName;
         getDiags().Report(OtherGD.getDecl()->getLocation(),
                           diag::note_previous_definition);
       }
     }
 

With this my program compiles and all tests run successfully.

@ChuanqiXu9
Copy link
Member

I'm finally back to converting one of my projects to using modules, and am hitting this again:

cmake --build buildclang/modules/
[0/2] Re-checking globbed directories...
[1/3] Building CXX object CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o
FAILED: CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o 
/home/chriselrod/.local/bin/clang++ -DUSE_MODULE -I/home/chriselrod/Documents/progwork/cxx/LoopModels/test -I/home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math -I/home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/include -isystem /home/chriselrod/Documents/progwork/cxx/LoopModels/buildclang/modules/_deps/googletest-src/googletest/include -isystem /home/chriselrod/Documents/progwork/cxx/LoopModels/buildclang/modules/_deps/googletest-src/googletest -stdlib=libc++ -g -std=gnu++23 -fno-exceptions -fno-omit-frame-pointer -fno-rtti -fstack-protector-strong -ferror-limit=8 -fcolor-diagnostics -ftime-trace -Wall -Wpedantic -Wextra -Wshadow -D_GLIBCXX_ASSERTIONS -march=native -mprefer-vector-width=512 -MD -MT CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o -MF CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o.d @CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o.modmap -o CMakeFiles/LoopModelsTests.dir/triangular_solve_test.cpp.o -c /home/chriselrod/Documents/progwork/cxx/LoopModels/test/triangular_solve_test.cpp
In module 'ArrayParse' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/test/triangular_solve_test.cpp:33:
In module 'StaticArray' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Utilities/MatrixStringParse.cxx:17:
In module 'Array' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/StaticArrays.cxx:43:
In module 'AssignExprTemplates' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/Array.cxx:48:
In module 'Indexing' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/ArrayOps.cxx:31:
In module 'SIMD' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/Math/Indexing.cxx:26:
In module 'SIMD:Intrin' imported from /home/chriselrod/Documents/progwork/cxx/LoopModels/extern/Math/mod/SIMD/SIMD.cxx:18:
/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:56: error: definition with same mangled name '_ZL17_mm_setzero_si128v' as another definition
 3880 | static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                        ^
/home/chriselrod/.local/stow/llvm/lib/clang/21/include/emmintrin.h:3880:56: note: previous definition is here
 3880 | static __inline__ __m128i __DEFAULT_FN_ATTRS_CONSTEXPR _mm_setzero_si128(void) {
      |                                                        ^
1 error generated.
ninja: build stopped: subcommand failed.
make: *** [Makefile:45: clangmodules] Error 1

I'm on a relatively recent commit of clang++ (this is a roughly 10-day old build of main):

clang++ --version
clang version 21.0.0git ([email protected]:llvm/llvm-project.git 7dad8b91bc94741034052a4eb06ef45e7cb47c06)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/chriselrod/.local/stow/llvm/bin

I think this worth another issue.

@ChuanqiXu9
Copy link
Member

One hacky (?) fix is to just disable this error for decls from the GMF:

diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 3469676b74bc..cb2e1fa1b0e1 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -4746,24 +4746,36 @@ llvm::Constant *CodeGenModule::GetOrCreateLLVMFunction(

 // Handle dropped DLL attributes.
 if (D && shouldDropDLLAttribute(D, Entry)) {
   Entry->setDLLStorageClass(llvm::GlobalValue::DefaultStorageClass);
   setDSOLocal(Entry);
 }

 // If there are two attempts to define the same mangled name, issue an
 // error.
 if (IsForDefinition && !Entry->isDeclaration()) {
  •  bool SkipCheck = false;
    
  •  if (D->isFromGlobalModule()) {
    
  •    SkipCheck = true;
    
  •  } else if (const FunctionDecl *FD = cast_or_null<FunctionDecl>(D)) {
    
  •    if (const FunctionTemplateDecl *FunTmpl = FD->getPrimaryTemplate()) {
    
  •      if (FunTmpl->isFromGlobalModule()) {
    
  •        SkipCheck = true;
    
  •      }
    
  •    }
    
  •  }
    
  •  GlobalDecl OtherGD;
     // Check that GD is not yet in DiagnosedConflictingDefinitions is required
     // to make sure that we issue an error only once.
    
  •  if (lookupRepresentativeDecl(MangledName, OtherGD) &&
    
  •  if (!SkipCheck && lookupRepresentativeDecl(MangledName, OtherGD) &&
         (GD.getCanonicalDecl().getDecl() !=
          OtherGD.getCanonicalDecl().getDecl()) &&
         DiagnosedConflictingDefinitions.insert(GD).second) {
       getDiags().Report(D->getLocation(), diag::err_duplicate_mangled_name)
           << MangledName;
       getDiags().Report(OtherGD.getDecl()->getLocation(),
                         diag::note_previous_definition);
     }
    
    }

With this my program compiles and all tests run successfully.

I feel the CodeGen part is correct. The problem part is the producer. We shouldn't contain the template instantiation including TULocal template parameters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:modules C++20 modules and Clang Header Modules
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants