Skip to content

Conversation

@aganea
Copy link
Member

@aganea aganea commented Dec 10, 2025

Consider the following:

struct A {
    __declspec(dllimport) __forceinline
    static const int* foo() {
        static constexpr int var = 42;
        static constexpr const int* p = &var;
        static_assert(*p == 42, "");
        return p;
    }
};

const int* (*pfoo)() = &A::foo;

int main() {
    return pfoo() == A::foo();
}

With clang-cl, this generates an error:

> clang-cl /c C:\src\git\test\test.cpp
C:\src\git\test\test.cpp(5,37): error: constexpr variable 'p' must be initialized by a constant expression
    5 |         static constexpr const int* p = &var;
      |                                     ^   ~~~~
C:\src\git\test\test.cpp(6,23): error: static assertion expression is not an integral constant expression
    6 |         static_assert(*p == 42, "");
      |                       ^~~~~~~~
C:\src\git\test\test.cpp(6,24): note: initializer of 'p' is not a constant expression
    6 |         static_assert(*p == 42, "");
      |                        ^
C:\src\git\test\test.cpp(5,37): note: declared here
    5 |         static constexpr const int* p = &var;
      |                                     ^
2 errors generated.

MSVC cl.exe does not generate such error with the same snippet.

The problem here is that the static variable 'var' inherits the dllimport attribute, and the const-init evaluation for 'p' is rejected because of the dllimport attribute.

I think it's fine to accept the example above since the body of the function will be discarded anyway; and the inlined version of the function will contain a reference to the imported function-static symbol, like MSVC does:

> cl.exe /c test.cpp /Ox
...
> dumpbin /disasm test.obj
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file test.obj

File Type: COFF OBJECT

main:
  0000000000000000: 48 83 EC 28        sub         rsp,28h
  0000000000000004: FF 15 00 00 00 00  call        qword ptr [?pfoo@@3P6APEBHXZEA]
  000000000000000A: 48 8B 0D 00 00 00  mov         rcx,qword ptr [__imp_?p@?1??foo@A@@SAPEBHXZ@4QEBHEB]
                    00
  0000000000000011: 33 D2              xor         edx,edx
  0000000000000013: 48 3B 01           cmp         rax,qword ptr [rcx]
  0000000000000016: 0F 94 C2           sete        dl
  0000000000000019: 8B C2              mov         eax,edx
  000000000000001B: 48 83 C4 28        add         rsp,28h
  000000000000001F: C3                 ret

??__Epfoo@@YAXXZ (void __cdecl `dynamic initializer for 'pfoo''(void)):
  0000000000000000: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_?foo@A@@SAPEBHXZ]
                    00
  0000000000000007: 48 89 05 00 00 00  mov         qword ptr [?pfoo@@3P6APEBHXZEA],rax
                    00
  000000000000000E: C3                 ret

> clang-cl.exe /c test.cpp /Ox

> dumpbin /disasm test.obj
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file test.obj

File Type: COFF OBJECT

main:
  0000000000000000: 48 83 EC 28        sub         rsp,28h
  0000000000000004: FF 15 00 00 00 00  call        qword ptr [?pfoo@@3P6APEBHXZEA]
  000000000000000A: 31 C9              xor         ecx,ecx
  000000000000000C: 48 3B 05 00 00 00  cmp         rax,qword ptr [__imp_?var@?1??foo@A@@SAPEBHXZ@4HB]
                    00
  0000000000000013: 0F 94 C1           sete        cl
  0000000000000016: 89 C8              mov         eax,ecx
  0000000000000018: 48 83 C4 28        add         rsp,28h
  000000000000001C: C3                 ret
  000000000000001D: 0F 1F 00           nop         dword ptr [rax]
_GLOBAL__sub_I_test.cpp:
  0000000000000020: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_?foo@A@@SAPEBHXZ]
                    00
  0000000000000027: 48 89 05 00 00 00  mov         qword ptr [?pfoo@@3P6APEBHXZEA],rax
                    00
  000000000000002E: C3                 ret

Thanks to @jfmarquis for crafting a reproducer.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Dec 10, 2025
@aganea aganea requested a review from mstorsjo December 10, 2025 14:48
@llvmbot
Copy link
Member

llvmbot commented Dec 10, 2025

@llvm/pr-subscribers-clang

Author: Alexandre Ganea (aganea)

Changes

Consider the following:

struct A {
    __declspec(dllimport) __forceinline
    static const int* foo() {
        static constexpr int var = 42;
        static constexpr const int* p = &var;
        static_assert(*p == 42, "");
        return p;
    }
};

const int* (*pfoo)() = &A::foo;

int main() {
    return pfoo() == A::foo();
}

With clang-cl, this generates an error:

>clang-cl /c C:\src\git\test\test.cpp
C:\src\git\test\test.cpp(5,37): error: constexpr variable 'p' must be initialized by a constant expression
    5 |         static constexpr const int* p = &var;
      |                                     ^   ~~~~
C:\src\git\test\test.cpp(6,23): error: static assertion expression is not an integral constant expression
    6 |         static_assert(*p == 42, "");
      |                       ^~~~~~~~
C:\src\git\test\test.cpp(6,24): note: initializer of 'p' is not a constant expression
    6 |         static_assert(*p == 42, "");
      |                        ^
C:\src\git\test\test.cpp(5,37): note: declared here
    5 |         static constexpr const int* p = &var;
      |                                     ^
2 errors generated.

The problem here is that the static variable 'var' inherits the dllimport attribute, and the const-init evaluation for 'p' is rejected because of the dllimport attribute. MSVC cl is fine with the snipped above.

I think it's fine to accept the exemple above since the body of the function will be discarded anyway; and the inlined version of the function will contain a reference to the imported function-static symbol, like MSVC does:

> cl.exe /c test.cpp /Ox
...
> dumpbin /disasm test.obj
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file test.obj

File Type: COFF OBJECT

main:
  0000000000000000: 48 83 EC 28        sub         rsp,28h
  0000000000000004: FF 15 00 00 00 00  call        qword ptr [?pfoo@@<!-- -->3P6APEBHXZEA]
  000000000000000A: 48 8B 0D 00 00 00  mov         rcx,qword ptr [__imp_?p@?1??foo@<!-- -->A@@<!-- -->SAPEBHXZ@<!-- -->4QEBHEB]
                    00
  0000000000000011: 33 D2              xor         edx,edx
  0000000000000013: 48 3B 01           cmp         rax,qword ptr [rcx]
  0000000000000016: 0F 94 C2           sete        dl
  0000000000000019: 8B C2              mov         eax,edx
  000000000000001B: 48 83 C4 28        add         rsp,28h
  000000000000001F: C3                 ret

??__Epfoo@@<!-- -->YAXXZ (void __cdecl `dynamic initializer for 'pfoo''(void)):
  0000000000000000: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_?foo@<!-- -->A@@<!-- -->SAPEBHXZ]
                    00
  0000000000000007: 48 89 05 00 00 00  mov         qword ptr [?pfoo@@<!-- -->3P6APEBHXZEA],rax
                    00
  000000000000000E: C3                 ret

&gt; clang-cl..exe /c test.cpp /Ox

&gt; dumpbin /disasm test.obj
Microsoft (R) COFF/PE Dumper Version 14.44.35222.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file test.obj

File Type: COFF OBJECT

main:
  0000000000000000: 48 83 EC 28        sub         rsp,28h
  0000000000000004: FF 15 00 00 00 00  call        qword ptr [?pfoo@@<!-- -->3P6APEBHXZEA]
  000000000000000A: 31 C9              xor         ecx,ecx
  000000000000000C: 48 3B 05 00 00 00  cmp         rax,qword ptr [__imp_?var@?1??foo@<!-- -->A@@<!-- -->SAPEBHXZ@<!-- -->4HB]
                    00
  0000000000000013: 0F 94 C1           sete        cl
  0000000000000016: 89 C8              mov         eax,ecx
  0000000000000018: 48 83 C4 28        add         rsp,28h
  000000000000001C: C3                 ret
  000000000000001D: 0F 1F 00           nop         dword ptr [rax]
_GLOBAL__sub_I_test.cpp:
  0000000000000020: 48 8B 05 00 00 00  mov         rax,qword ptr [__imp_?foo@<!-- -->A@@<!-- -->SAPEBHXZ]
                    00
  0000000000000027: 48 89 05 00 00 00  mov         qword ptr [?pfoo@@<!-- -->3P6APEBHXZEA],rax
                    00
  000000000000002E: C3                 ret

Full diff: https://github.com/llvm/llvm-project/pull/171628.diff

2 Files Affected:

  • (modified) clang/lib/AST/ExprConstant.cpp (+5-2)
  • (modified) clang/test/SemaCXX/dllimport.cpp (+34)
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index d81496ffd74e0..e5ca6912ed566 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -2407,8 +2407,11 @@ static bool CheckLValueConstantExpression(EvalInfo &Info, SourceLocation Loc,
         return false;
 
       // A dllimport variable never acts like a constant, unless we're
-      // evaluating a value for use only in name mangling.
-      if (!isForManglingOnly(Kind) && Var->hasAttr<DLLImportAttr>())
+      // evaluating a value for use only in name mangling, and unless we're a
+      // static local. For the latter case, we'd still need to evaluate the
+      // constant expression in case we're inside a (inlined) function.
+      if (!isForManglingOnly(Kind) && Var->hasAttr<DLLImportAttr>() &&
+          !Var->isStaticLocal())
         // FIXME: Diagnostic!
         return false;
 
diff --git a/clang/test/SemaCXX/dllimport.cpp b/clang/test/SemaCXX/dllimport.cpp
index b7a1a62b8725b..eaf217c8e2d3b 100644
--- a/clang/test/SemaCXX/dllimport.cpp
+++ b/clang/test/SemaCXX/dllimport.cpp
@@ -1526,6 +1526,40 @@ template <typename T> struct __declspec(dllimport) PartiallySpecializedClassTemp
 template <typename T> struct ExpliciallySpecializedClassTemplate {};
 template <> struct __declspec(dllimport) ExpliciallySpecializedClassTemplate<int> { void f() {} };
 
+// Function-local static constexpr in dllimport function (or class).
+struct DLLImportFuncWithConstexprStatic {
+#if defined(GNU)
+// expected-warning@+2{{'dllimport' attribute ignored on inline function}}
+#endif
+  __declspec(dllimport) static const int *func() {
+    static constexpr int value = 42;
+    static constexpr const int *p = &value;
+    static_assert(*p == 42, "");
+    return p;
+  }
+};
+const int* (*pFunc)() = &DLLImportFuncWithConstexprStatic::func;
+bool UsedDLLImportFuncWithConstexprStatic() {
+  return pFunc() == DLLImportFuncWithConstexprStatic::func();
+}
+
+#if !defined(PS)
+struct DLLImportInlineFuncWithConstexprStatic {
+#if defined(GNU)
+  // expected-warning@+2{{'dllimport' attribute ignored on inline function}}
+#endif
+  __declspec(dllimport) __forceinline static const int* funcForceInline() {
+    static constexpr int value = 42;
+    static constexpr const int* p = &value;
+    static_assert(*p == 42, "");
+    return p;
+  }
+};
+const int* (*pFuncForceInline)() = &DLLImportInlineFuncWithConstexprStatic::funcForceInline;
+bool UsedDLLImportInlineFuncWithConstexprStatic() {
+  return pFuncForceInline() == DLLImportInlineFuncWithConstexprStatic::funcForceInline();
+}
+#endif // !PS
 
 //===----------------------------------------------------------------------===//
 // Classes with template base classes

Copy link
Collaborator

@zmodem zmodem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I haven't thought deeply about these kinds of things in a while, so I'm struggling to wrap my head around this.

I think it's fine to accept the example above since the body of the function will be discarded anyway; and the inlined version of the function will contain a reference to the imported function-static symbol, like MSVC does

Why does it matter whether the function gets inlined or not?

Despite the __forceinline I don't think it's guaranteed to get inlined, and in your change to ExprConstant.cpp, you're not checking for it anyway?

@aganea
Copy link
Member Author

aganea commented Dec 12, 2025

Hmm, I haven't thought deeply about these kinds of things in a while, so I'm struggling to wrap my head around this.

I think it's fine to accept the example above since the body of the function will be discarded anyway; and the inlined version of the function will contain a reference to the imported function-static symbol, like MSVC does

Why does it matter whether the function gets inlined or not?

Because the behavior is not the same, however the address of the local static must remain the same in both cases:

  • If the function is dllimport'ed, the whole function body must be discarded in the current TU, and an indirect call is made, ie. call qword ptr [?pfoo@@3P6APEBHXZEA]
  • If the function is inlined, the address of the local-static must be imported, ie cmp rax,qword ptr [__imp_?var@?1??foo@A@@SAPEBHXZ@4HB]

Despite the __forceinline I don't think it's guaranteed to get inlined, and in your change to ExprConstant.cpp, you're not checking for it anyway?

The above was only a observation I couldn't make before, because of the compilation error (in the description above). That observation is unrelated with my change.

Copy link
Collaborator

@zmodem zmodem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to wrap my head around this. Part of the weirdness comes from having an initializer for a dllimport variable in the first place.

We would typically not allow:

__declspec(dllimport) int x = 42;

(error: definition of dllimport data)

But we allow it when the dllimport is inherited via the function for a static local, because otherwise a lot of code would break:

__declspec(dllimport) constexpr const int* f() {
  static constexpr int x = 42;
  return &x;
}

We already seem okay evaluating the address as a constant expression:

__declspec(dllimport) constexpr const int* f() {
  static constexpr int x = 42;
  return &x;
} 
static_assert(*f() == 42);

But not like this:

__declspec(dllimport) constexpr const int* f() {
  static constexpr int x = 42;
  static constexpr const int *p = &x;
  return p;
} 
static_assert(*f() == 42);

Even though it's really the same thing? I'm not sure why those two are different (that would be interesting to know!).

Maybe this is a simpler test case to work with?


In the updated comment, you add "For the [static local] case, we'd still need to evaluate the constant expression in case we're inside a (inlined) function."

But isn't there a risk that the expression can leak outside the function, and we way try to use it in some constexpr context where it doesn't actually work?

// A dllimport variable never acts like a constant, unless we're
// evaluating a value for use only in name mangling.
if (!isForManglingOnly(Kind) && Var->hasAttr<DLLImportAttr>())
// evaluating a value for use only in name mangling, and unless we're a
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/we're/it's/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants