MinGW64不导出类内定义成员函数及跨编译器符号导出差异问询
我还为类方法补充了实现逻辑,但这对MinGW和Clang的符号导出行为无任何影响(未更新objdump输出,其内容本质上无明显变化)。
我遇到了一个从可执行文件导出符号的棘手问题,测试代码如下:
# CMakeLists.txt add_executable(app main.cpp) set_target_properties(app PROPERTIES ENABLE_EXPORTS 1) add_library(lib SHARED main.cpp)
// main.cpp class __declspec(dllexport) foo { public: int bar() { static int val = 3; return ++val; } static int buz() { static int val = 814; return ++val; } }; template <typename T> class __declspec(dllexport) foo_t { public: int bar() { static int val = 3; return ++val; } static int buz() { static int val = 814; return ++val; } }; template class foo_t<int>; int main() { return 0; }
MSVC
编译时可执行文件和动态库都会生成带导出符号的xxx.lib文件,可用于后续链接。
// DUMPBIN.exe /EXPORTS app.lib (与lib.lib输出完全一致) Dump of file lib.lib File Type: LIBRARY Exports ordinal name ??4?$foo_t@H@@QEAAAEAV0@$$QEAV0@@Z (public: class foo_t<int> & __cdecl foo_t<int>::operator=(class foo_t<int> &&)) ??4?$foo_t@H@@QEAAAEAV0@AEBV0@@Z (public: class foo_t<int> & __cdecl foo_t<int>::operator=(class foo_t<int> const &)) ??4foo@@QEAAAEAV0@$$QEAV0@@Z (public: class foo & __cdecl foo::operator=(class foo &&)) ??4foo@@QEAAAEAV0@AEBV0@@Z (public: class foo & __cdecl foo::operator=(class foo const &)) ?bar@?$foo_t@H@@QEAAXXZ (public: void __cdecl foo_t<int>::bar(void)) ?bar@foo@@QEAAXXZ (public: void __cdecl foo::bar(void)) ?buz@?$foo_t@H@@SAXXZ (public: static void __cdecl foo_t<int>::buz(void)) ?buz@foo@@SAXXZ (public: static void __cdecl foo::buz(void))
可见foo类和模板类foo_t的方法均被导出。
Clang
Dump of file libapp.dll.a File Type: LIBRARY Exports ordinal name _ZN5foo_tIiE3barEv _ZN5foo_tIiE3buzEv _ZN5foo_tIiEaSERKS0_
可见仅foo_t的方法被导出,且仅因显式模板实例化才导出。移除显式实例化后不会生成libapp.dll.a,动态库对应的liblib.dll.a会生成但EXPORTS段为空。
MinGW64
Dump of file liblib.dll.a File Type: LIBRARY Exports ordinal name main _ZN5foo_tIiE3buzEv _ZN5foo_tIiE3barEv
与Clang一致仅导出显式实例化的模板方法,但与Clang、MSVC不同的是,可执行文件不会生成对应的libapp.dll.a。
// objdump -t app.exe File [116](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000016c0 .text$_ZN5foo_tIiE3barEv AUX scnlen 0xb nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 2 [118](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000000016c0 _ZN5foo_tIiE3barEv AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0 [120](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000016d0 .text$_ZN5foo_tIiE3buzEv AUX scnlen 0x7 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 2 [122](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000016d0 _ZN5foo_tIiE3buzEv
可执行文件自身包含显式实例化的foo_t方法符号,为何没有为可执行文件生成对应的静态库?
问题解答
1. MinGW64不为可执行文件生成对应导入库的原因
MinGW使用的GNU ld链接器默认仅为SHARED类型的目标自动生成导入库(.dll.a文件),这是基于Windows生态的常规使用场景设计:绝大多数导出符号的需求都来自动态库,可执行文件导出符号的场景非常少见,因此默认关闭了可执行文件导入库的生成逻辑。如果需要生成可执行文件的导入库,可在链接参数中手动添加-Wl,--out-implib=libapp.dll.a强制生成。
2. MSVC独有的非模板隐式内联方法导出行为的原因,以及是否可跨平台依赖
MSVC对__declspec(dllexport)标记的类采用强制导出逻辑:只要类被标记为dllexport,编译器就会为所有非模板成员生成独立的外部符号并导出,哪怕是类内定义的隐式内联函数也不例外。而Clang、GCC、MinGW的符号模型中,隐式内联函数默认是仅当前编译单元可见的弱符号,没有外部引用的前提下不会生成外部符号,哪怕加了dllexport标记也不会强制导出。
该行为是MSVC的专有扩展,不符合标准C++的符号模型,跨平台应用绝对不能依赖该行为。
3. 模板类与非模板类隐式内联方法导出行为不一致的原因,以及模板类导出是否可靠
模板类显式实例化时,会强制编译器为所有类成员生成外部可见的全局符号,此时dllexport标记会正常生效,将这些符号加入导出表。而非模板类的隐式内联方法没有显式实例化的触发逻辑,Clang、GCC不会为其生成外部符号,自然无法导出。
模板类方法的导出也不能作为跨平台的可靠保证:不同编译器、不同版本对模板类dllexport的处理逻辑仍有差异,比如旧版本Clang不会导出模板类的隐式内联静态成员。如果需要稳定的跨平台导出行为,应当统一将方法实现移到类外,显式标记dllexport,不要依赖类内隐式内联的自动导出逻辑。
内容的提问来源于stack exchange,提问作者Sergey Kolesnik

