look at the offset of m_derived in the below compiler explorer link. For msvc it is 48. For clang it is 40
Steps to Reproduce
Hello,
We have discovered an incompatibility between clang 19.1.7 and MSVC 14.44.
The bug occurs when memory alignment is applied for empty base classes as for classes inheriting from these base classes the generated assembly code generated by clang is using a different byte offset than msvc, meaning you’re reading from incorrect data which best case scenario leads to a crash, worst case scenario leads to a crazy bug that likely requires days of investigation.
I’m raising this here more for awareness than anything else as this version of clang is not banned in the SDK json files and perhaps it should be?
Note: this is reproducable outside of UE as well. Repro link: Compiler Explorer
Issue has been reported to LLVM github repo as well: https://github.com/llvm/llvm\-project/issues/170479
Thanks. Did you check if the issue also occurs with clang 20 which is currently the preferred version for UE5.7? When we updated we jumped from preferring 18 right to 20 and skipped 19. Internally I know we saw some odd crashes in third party libraries where we resolved them by compiling another set of clang static libs to be used when clang is set to be the compiler, it sounds like this is probably related to the reason we needed to do that. I’ll let the person who was investigating those issues know.
Yes, we have confirmed it happens on v18, v19, v20 and v21
Compiles should be passing --target=x86_64-pc-windows-msvc for x64 Microsoft platforms when clang is in use: https://github.com/EpicGames/UnrealEngine/blob/a72b36b0a5d956a3f5933212259bec08bef6fb70/Engine/Source/Programs/UnrealBuildTool/Platform/Windows/VCToolChain.cs\#L647
I can confirm, this appears to resolve the issue: https://gcc.godbolt.org/z/bWEE8j1ha
I’ll report back and verify if this parameter is truly added for the instance where it crashed
We have discovered this issue is happening due to the pressence of /Zp8 commandline argument when using clang in UE. https://learn.microsoft.com/en\-us/windows/win32/midl/\-zp
After looking into this in detail today, I’ve come to following conclusion.
the pack pragma or zp switch is used to put the next member of a struct on the smaller of either the size of the member type or the value specified. In the thirdparty code where we hit this issue, it was put back to 16 (overwriting Epic’s default of 8).
I’m not sure what the effect is as on “normal” hardware, there are no basic types larger than 8 bytes that I know off. Only in special cases is a long long 16 bytes. I think the confusion here comes from the class alignment and the member alignment.
class alignment is to make sure classes are always aligned on an address, this is done by adding padding bytes to the back (meaning they’re aligned in arrays etc). This is set to 16 for some havok classes.
Then there’s the struct member alignment as well, which for this code is also set to 16. This is to make sure that the members themselves are aligned to this value or their own size, whichever is smaller. I believe it’s a combination of the 2 that’s causing some kind of confusion.
see the following results with a struct member alignment of 8
- Base: not aligned
- Task: 16 byte aligned
- RefTask: 16 byte aligned
- Derived: not aligned
class Derived size(48):
+---
0 | +--- (base class RefTask)
0 | | +--- (base class Base)
0 | | | {vfptr}
8 | | | m_sf
16 | | | m_rc
| | +---
32 | | +--- (base class Task)
32 | | | {vfptr}
| | | <alignment member> (size=8)
| | +---
| | <alignment member> (size=8)
| +---
40 | m_derived
+---
Derived::$vftable@Base@:
| &Derived_meta
| 0
0 | &Derived::{dtor}
Derived::$vftable@Task@:
| -32
0 | &thunk: this-=32; goto Derived::{dtor}
1 | &Derived::doWork
Derived::{dtor} this adjustor: 0
Derived::doWork this adjustor: 32
Derived::__delDtor this adjustor: 0
Derived::__vecDelDtor this adjustor: 0
reading through that, we see the followingBase class:
- total of 24 bytes. 1 vtable ptr and 2 fields of 8 bytes each. Makes sense
Task class:
- first it gets aligned to 16 bytes so it start offset (24) becomes 32, also makes sense
- vtable is at that start, so at 32 byte offset, makes sense
- an 8 byte padding is applied to the class is aligned again to 16 bytes, that makes sense
This is where the tricky bit comes in and is likely just a fault in the class dump layout. RefTask needs to be 16 byte aligned and Task is, but Base is not. Without extra padding, the start of RefTask would be 24 + 16 bytes ( == 40), which is not 16 byte aligned, so the compiler adds another 8 bytes, at least that’s what it says in the layout dump, but that doesn’t seem to be happening.
An 8 byte alignment member is added, but the size does not increase as m_derived member is located at 40 bytes and not at 48.
Now what happens if we set the struct field alignment to 16?
class Derived size(64):
+---
0 | +--- (base class RefTask)
0 | | +--- (base class Base)
0 | | | {vfptr}
8 | | | m_sf
16 | | | m_rc
| | +---
32 | | +--- (base class Task)
32 | | | {vfptr}
| | | <alignment member> (size=8)
| | +---
| | <alignment member> (size=8)
| +---
48 | m_derived
| <alignment member> (size=8)
+---
Derived::$vftable@Base@:
| &Derived_meta
| 0
0 | &Derived::{dtor}
Derived::$vftable@Task@:
| -32
0 | &thunk: this-=32; goto Derived::{dtor}
1 | &Derived::doWork
Derived::{dtor} this adjustor: 0
Derived::doWork this adjustor: 32
Derived::__delDtor this adjustor: 0
Derived::__vecDelDtor this adjustor: 0
Pretty much everything is the except for that final member alignment that wasn’t applied before, it is applied now, meaning the offset of m_derived becomes 48 instead of 40 and there’s an extra alignment added after m_derived which I’m not sure why, but I do think it’s because m_derived needs to be 16 byte aligned that it’s now offseted and 16 byte aligned.
The attached file is using pragma packs to increase member field alignment and not C++11 standard `alignas `. Changing it to use `alignas` and aligning the m_derived member with that as well, moves the member into padding bytes of its base class, putting it at an offset of 40 bytes for both compilers
This leads me to think that it should actually be at 40 bytes and there’s a discrepancy between pragma pack on msvc and using alignas.
I will open up a ticket with Microsoft about this to get some more information
There is a setting to control that “WindowsPlatform.StructMemberAlignment”, we’ve had it on our radar to investigate changing the default to not pass that flag since for x64 it would default to 16 rather than 8, but as that investigation hasn’t happened yet I can’t comment on the effect it will have.
It does also look like there is a preprocessor macro PRAGMA_PUSH_PLATFORM_DEFAULT_PACKING to reset the struct packing back to default in the event there is some incompatibility for some specific code, it mostly looks like this is used when including windows sdk headers.
I’m speaking with the vendor of the thirdparty software to discuss strategy. Fixing it for one class or set of classes doesn’t mean it won’t show up somewhere else. A global fix should be applied here and a ticket should likely be entered with llvm to discuss the incompatibility between clang and msvc even with the target param provided.
Issue has been entered with LLVM: https://github.com/llvm/llvm\-project/issues/171432
This is not an issue within Unreal Engine itself, but I do think it’s something to be aware off as it is a compiler bug. Feel free to close this ticket as it likely requires a follow up with LLVM devs.
If a change is made in unreal engine that related to this issue, I’m sure it’ll be announced in the release notes of 5.7.2 or any future updates