llvm-mingw icon indicating copy to clipboard operation
llvm-mingw copied to clipboard

Clang templates issues

Open Alexpux opened this issue 2 years ago • 10 comments

Building openexr 3.1.5 results to not properly exports classes and class members from templates. Building with GCC is fine, while clang not exports one symbols and exports another symbols compare to GCC. @mstorsjo can you help with it?

Alexpux avatar May 20 '22 12:05 Alexpux

Building openexr 3.1.5 results to not properly exports classes and class members from templates. Building with GCC is fine, while clang not exports one symbols and exports another symbols compare to GCC. @mstorsjo can you help with it?

Sure, let me know how to reproduce the issue and I can try to have a look.

mstorsjo avatar May 20 '22 13:05 mstorsjo

@mstorsjo see WIP changes in my MINGW-PACKAGES repo: https://github.com/Alexpux/MINGW-packages/commit/e486a98e65972e7423f5e4ca3083786bbf5d5b98

Clone repo and first build and install mingw-w64-imath package. Then try build openexr package.

As I see GCC and Clang libOpenEXR-3_1.dll have different exports table. Clang export some symbols that GCC not export and also not export many symbols that GCC exports

Alexpux avatar May 20 '22 18:05 Alexpux

@mstorsjo if I can give you some needed info contact me

Alexpux avatar May 23 '22 07:05 Alexpux

@mstorsjo if I can give you some needed info contact me

Sorry, I haven't had time to try this out yet, I'll see if I can get the time to it sometime soon...

mstorsjo avatar May 24 '22 10:05 mstorsjo

Feel free to ping me for any information. I can reproduce the issue with new openexr versions.

Biswa96 avatar May 25 '22 05:05 Biswa96

@Biswa96 yes it is update to latest 3.1.5 version

Alexpux avatar May 25 '22 06:05 Alexpux

I've tried to reproduce it now. I've successfully built packages of mingw-w64-imath, and built mingw-w64-openexr for mingw64, but for clang64 the build fails.

First clang seems to error out with some internal error, and when I retry/continue the build a couple times in the src/build-CLANG64 directory, it finally seems to get past most of those but fails to link a component, with errors like this:

[ 61%] Linking CXX executable ../../../bin/exrstdattr.exe
ld.lld: error: undefined symbol: __declspec(dllimport) Imf_3_1::TypedAttribute<float>::TypedAttribute(float const&)
>>> referenced by objects.a(main.cpp.obj):(getFloat(char const*, int, char**, int&, int, std::__1::vector<SetAttr, std::__1::allocator<SetAttr> >&, void (*)(char const*, float)))

Is that what you're running into so far?

I can see that bin/libOpenEXR-3_1.dll here exports around 1533 symbols, while the GCC build exports around 1896 symbols. Next up would be to figure out why it doesn't seem to export it, as the other executable seems to expect it to be available.

mstorsjo avatar Jun 01 '22 11:06 mstorsjo

@mstorsjo yes this is problem that we see

Alexpux avatar Jun 01 '22 11:06 Alexpux

@mstorsjo yes this is problem that we see

Ok, great - so the problem isn't necessarily that Clang exports a different list of symbols than GCC, but that Clang wasn't able to link against the DLL itself built.


I've looked into this, and I've made some progress, but I'm not quite done yet.

BTW, for improved debuggability, I modified the PKGBUILD file to use the CMake Ninja generator instead of MSYS Makefiles. CMake generated makefiles are horrible IMO for debugging issues like this, when it's hard to (or maybe I just don't know how to) rerun specific builds of specific objects, while it's straightforward with Ninja.


First off, the problem that Clang isn't exporting all the template types that are necessary, is entirely self inflicted here. The PKGBUILD file applies the patch 0015-clang-template-instantiation.patch, which makes it no longer define IMF_EXPORT_TEMPLATE_TYPE to IMF_EXPORT (which expands to dllexport while building the DLL). So that patch essentially removed dllexport from template classes - thus the Clang build lacks exported template types. Remove that patch, and this particular issue goes away.


Now when removing that patch, I see the problem that probably made you do the wrong decision to try such a patch - building the OpenEXR DLL itself fails in three files.

The reason for this failure is a bit shady - the OpenEXR code tries some tricks. See e.g. this bit: https://github.com/AcademySoftwareFoundation/openexr/blob/main/src/lib/OpenEXR/ImfBoxAttribute.cpp#L16-L20 (and ImfMatrixAttribute.cpp and ImfVecAttribute.cpp similarly).

Here, the OpenEXR code does a forward declaration of classes (Vec2, Vec3, Box) from the Imath library, but it adds the IMF_EXPORT_TEMPLATE_TYPE (dllexport) attribute to them. Then later, the full templates for those classes are included, but without the corresponding dllexport attribute.

So it seems like OpenEXR is trying to re-export the template classes declared by the other library - which to me seems like a rather shady thing to do. If I comment out that block in that file (and the same in those other two files, the DLL builds successfully with Clang - and now contains most of the exported template classes as expected. (The whole OpenEXR build still builds with GCC with this modification applied too. At most it might cause the OpenEXR DLL to export a few symbols less, but those should be provided by Imath headers anyway, so I think that modification might be safe.)


FWIW, a reduced form of the issue here looks like this:

// Local app
template <class T> class __declspec(dllexport) ForeignClass;

// Foreign header
template <class T> class /*__declspec(dllexport)*/ ForeignClass {
public:
        ForeignClass() {}
        void doFoo();
private:
        T member;
};

template <> void ForeignClass<int>::doFoo() = delete;

GCC compiles this successfully, but Clang errors out:

injected-dllexport.cpp:14:37: error: explicit specialization of 'doFoo' after instantiation
template <> void ForeignClass<int>::doFoo() = delete;
                                    ^
injected-dllexport.cpp:6:52: note: implicit instantiation first required here
template <class T> class /*__declspec(dllexport)*/ ForeignClass {
                                                   ^
1 error generated.

If the second dllexport is uncommented (so that those attributes are consistent), or if the first one is omitted, this compiles successfully.


With these modifications, OpenEXR builds almost successfully - it now fails with only two missing symbols:

[1/2] Linking CXX executable bin\exrstdattr.exe
FAILED: bin/exrstdattr.exe
cmd.exe /C "cd . && C:\msys64\clang64\bin\clang++.exe -march=x86-64 -mtune=generic -O2 -pipe -O3 -DNDEBUG -pipe src/bin/exrstdattr/CMakeFiles/exrstdattr.dir/main.cpp.obj -o bin\exrstdattr.exe -Wl,--out-implib,src\bin\exrstdattr\libexrstdattr.dll.a -Wl,--major-image-version,0,--minor-image-version,0  src/lib/OpenEXR/libOpenEXR.dll.a  C:/msys64/clang64/lib/libImath.dll.a  -lm  src/lib/IlmThread/libIlmThread.dll.a  src/lib/Iex/libIex.dll.a  -pthread  C:/msys64/clang64/lib/libz.dll.a  -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32 && cd ."
ld.lld: error: undefined symbol: __declspec(dllimport) Imf_3_1::TypedAttribute<Imf_3_1::Chromaticities>::TypedAttribute()
>>> referenced by src/bin/exrstdattr/CMakeFiles/exrstdattr.dir/main.cpp.obj:(getChromaticities(char const*, int, char**, int&, int, std::__1::vector<SetAttr, std::__1::allocator<SetAttr> >&))
clang++: error: linker command failed with exit code 1 (use -v to see invocation)
[2/2] Linking CXX executable bin\OpenEXRTest.exe
FAILED: bin/OpenEXRTest.exe
[...]
ld.lld: error: undefined symbol: __declspec(dllimport) Imf_3_1::TypedAttribute<int>::TypedAttribute(Imf_3_1::TypedAttribute<int> const&)
>>> referenced by src/test/OpenEXRTest/CMakeFiles/OpenEXRTest.dir/testMultiPartSharedAttributes.cpp.obj:(testMultiPartSharedAttributes(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&))
>>> referenced by src/test/OpenEXRTest/CMakeFiles/OpenEXRTest.dir/testMultiPartSharedAttributes.cpp.obj:(void std::__1::vector<Imf_3_1::TypedAttribute<int>, std::__1::allocator<Imf_3_1::TypedAttribute<int> > >::__push_back_slow_path<Imf_3_1::TypedAttribute<int> const&>(Imf_3_1::TypedAttribute<int> const&))
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

When looking at the compilation of src/bin/exrstdattr/CMakeFiles/exrstdattr.dir/main.cpp.obj, GCC generates the function _ZN7Imf_3_114TypedAttributeINS_14ChromaticitiesEEC1Ev (which is Imf_3_1::TypedAttribute<Imf_3_1::Chromaticities>::TypedAttribute()) - as if it is a regular inline function/template - while Clang expects to load it from a dllimport.

Looking at the preprocessor output from that main.cpp, I do see this:

extern template class __attribute__((dllimport)) TypedAttribute<Imf_3_1::Chromaticities>;

So I would say it's correct of Clang to expect to be able to dllimport all members that relate to this class. But in practice, the DLL seems to be missing this particular method. When compiling the DLL side, Clang doesn't emit code for the constructor Imf_3_1::TypedAttribute<Imf_3_1::Chromaticities>::TypedAttribute(), but when compiling the calling code, Clang expects to be able to dllimport an implementation of the method. Not sure why this is...

mstorsjo avatar Jun 03 '22 21:06 mstorsjo

Sorry for the long delay in picking up on this again, it was quite daunting to dive into. Now I've successfully managed to build OpenEXR 3.1.5 with llvm-mingw. There was essentially only one other issue left - but it's related to rather non-trivial bits in exported C++ template instantiations and method specializations.

Normally when doing exported C++ template instantiations, you'd have a header that looks like this:

template<class T> class TEMPLATE_VISIBILITY MyClass {
   [...]
};

extern template class EXTERN_TEMPLATE_VISIBILITY MyClass<int>;

The file that actually exports the instantiation then looks like this:

#include "header"
template class TEMPLATE_INSTANTIATION_VISIBILITY MyClass<int>;

When callers use these, TEMPLATE_VISIBILITY and EXTERN_TEMPLATE_VISIBILITY are __declspec(dllimport) (or empty, in the case of static linking).

For exporting the instantiation, there's a big difference between MSVC and mingw mode; in MSVC mode, EXTERN_TEMPLATE_VISIBILITY is empty while TEMPLATE_INSTANTIATION_VISIBILITY is __declspec(dllexport) (i.e. dllexport in source file), while it's the reverse in mingw mode; EXTERN_TEMPLATE_VISIBILITY is __declspec(dllexport) while TEMPLATE_INSTANTIATION_VISIBILITY is empty.

Examples of how this is done in libcxx are here:

  • https://github.com/llvm/llvm-project/blob/llvmorg-15.0.0-rc3/libcxx/include/__config#L528-L534
  • https://github.com/llvm/llvm-project/blob/llvmorg-15.0.0-rc3/libcxx/include/streambuf#L491-L495
  • https://github.com/llvm/llvm-project/blob/llvmorg-15.0.0-rc3/libcxx/src/ios.instantiations.cpp#L20-L41

Now in the case of OpenEXR, they are ifdeffing out the extern template class EXTERN_TEMPLATE_VISIBILITY MyClass<int> line when they are doing the template instantiations. This is probably because they do an explicit specialization of some template method within the source file that does the instantiation, which causes errors about "explicit specialization of '(method)' after instantiation" with GCC and Clang if the extern template .. has been parsed before.

Now the explicit method specialization seems to cause Clang to no longer export the "default" methods. But if we'd readd the extern template ... after the last explicit specialization, then we get all methods exported again.


This issue can be tested and observed with this test snippet:

// .h
template<class T> class __declspec(dllexport) MyClass {
public:
        MyClass() = default;
        virtual ~MyClass();
        void specializedMethod();
};

template <class T> MyClass<T>::~MyClass() {}

template <class T> void MyClass<T>::specializedMethod() {}

// Usually also in a header, but ifdeffed out in the case of OpenEXR
// extern template class __declspec(dllexport) MyClass<int>;

// .cpp file
// Explicit specialization of a virtual method
template <> __declspec(dllexport) void MyClass<int>::specializedMethod() {}

// Not present in OpenEXR
//extern template class __declspec(dllexport) MyClass<int>;

template class MyClass<int>;

// The default constructor MyClass<int>::MyClass() isn't exported.
// (If it is made non-inline, it ends up exported though. That's
// easy for a default constructor, but a bigger mess for copy
// constructors.)

// If we uncomment the "extern template" in the .cpp file (the "Not
// present in OpenEXR" line above) after the explicit specialization,
// the default constructor ends up exported after all.

// To test: x86_64-w64-mingw32-clang++ -c -o - template-export.cpp | llvm-nm - | c++filt 

TL;DR, to make it compile, on top of the checked out OpenEXR 3.1.5 version, I added these changes. For the issue about adding extra dllexport on foreign classes (fixing it by removing it), as explained above:

diff --git a/src/lib/OpenEXR/ImfBoxAttribute.cpp b/src/lib/OpenEXR/ImfBoxAttribute.cpp
index 480e113d..b017d997 100644
--- a/src/lib/OpenEXR/ImfBoxAttribute.cpp
+++ b/src/lib/OpenEXR/ImfBoxAttribute.cpp
@@ -14,11 +14,13 @@
 #include <ImathExport.h>
 #include <ImathNamespace.h>
 
+/*
 IMATH_INTERNAL_NAMESPACE_HEADER_ENTER
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Vec2;
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Vec3;
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Box;
 IMATH_INTERNAL_NAMESPACE_HEADER_EXIT
+*/
 
 #define COMPILING_IMF_BOX_ATTRIBUTE
 
diff --git a/src/lib/OpenEXR/ImfMatrixAttribute.cpp b/src/lib/OpenEXR/ImfMatrixAttribute.cpp
index 67103a87..ad4b2507 100644
--- a/src/lib/OpenEXR/ImfMatrixAttribute.cpp
+++ b/src/lib/OpenEXR/ImfMatrixAttribute.cpp
@@ -16,10 +16,12 @@
 #include <ImathExport.h>
 #include <ImathNamespace.h>
 
+/*
 IMATH_INTERNAL_NAMESPACE_HEADER_ENTER
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Matrix33;
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Matrix44;
 IMATH_INTERNAL_NAMESPACE_HEADER_EXIT
+*/
 
 #define COMPILING_IMF_MATRIX_ATTRIBUTE
 
diff --git a/src/lib/OpenEXR/ImfVecAttribute.cpp b/src/lib/OpenEXR/ImfVecAttribute.cpp
index b80496ed..e0551daa 100644
--- a/src/lib/OpenEXR/ImfVecAttribute.cpp
+++ b/src/lib/OpenEXR/ImfVecAttribute.cpp
@@ -18,10 +18,12 @@
 #include <ImathExport.h>
 #include <ImathNamespace.h>
 
+/*
 IMATH_INTERNAL_NAMESPACE_HEADER_ENTER
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Vec2;
 template <class V> class IMF_EXPORT_TEMPLATE_TYPE Vec3;
 IMATH_INTERNAL_NAMESPACE_HEADER_EXIT
+*/
 
 #define COMPILING_IMF_VECTOR_ATTRIBUTE
 #include "ImfVecAttribute.h"

And for fixing the specialization issues:

diff --git a/src/lib/OpenEXR/ImfChromaticitiesAttribute.cpp b/src/lib/OpenEXR/ImfChromaticitiesAttribute.cpp
index b06f89e1..4db27d76 100644
--- a/src/lib/OpenEXR/ImfChromaticitiesAttribute.cpp
+++ b/src/lib/OpenEXR/ImfChromaticitiesAttribute.cpp
@@ -60,6 +60,8 @@ ChromaticitiesAttribute::readValueFrom (OPENEXR_IMF_INTERNAL_NAMESPACE::IStream
     Xdr::read <StreamIO> (is, _value.white.y);
 }
 
+extern template class IMF_EXPORT_EXTERN_TEMPLATE TypedAttribute<OPENEXR_IMF_INTERNAL_NAMESPACE::Chromaticities>;
+
 template class IMF_EXPORT_TEMPLATE_INSTANCE TypedAttribute<OPENEXR_IMF_INTERNAL_NAMESPACE::Chromaticities>;
 
 OPENEXR_IMF_INTERNAL_NAMESPACE_SOURCE_EXIT 
diff --git a/src/lib/OpenEXR/ImfIntAttribute.cpp b/src/lib/OpenEXR/ImfIntAttribute.cpp
index d07b9f3e..c0182c6b 100644
--- a/src/lib/OpenEXR/ImfIntAttribute.cpp
+++ b/src/lib/OpenEXR/ImfIntAttribute.cpp
@@ -37,6 +37,8 @@ IntAttribute::staticTypeName ()
     return "int";
 }
 
+extern template class IMF_EXPORT_EXTERN_TEMPLATE TypedAttribute<int>;
+
 template class IMF_EXPORT_TEMPLATE_INSTANCE TypedAttribute<int>;
 
 

If there are other similar issues for other classes than TypedAttribute<Chromaticities> and TypedAttribute<int>, just copy the extern template ... line from Imf*Attribute.h (which is within #ifndef COMPILING_IMF_*_ATTRIBUTE) into Imf*Attribute.cpp.

mstorsjo avatar Aug 30 '22 12:08 mstorsjo