新的模板实例化会破坏 ABI 吗?
Do new template instantiations break ABI?
概述
我有一个 core
可以动态加载不同的插件。我显式实例化我导出的模板,然后在 link 插件针对某些版本的 core
时使用它们。我的期望是,当我添加新类型和模板实例化时,它不会破坏 ABI,并且现有插件不必重新link编辑。
事实并非如此。当我添加新实例时,我得到从不同地址(而不是 core
)内执行的符号。
例子
可以找到项目回购 here。
这是代码示例。
CMakeLists.txt
cmake_minimum_required(VERSION 3.7.2)
project(template_abi_test)
set(CMAKE_CXX_STANDARD 17)
# old core
add_executable(core core.cpp)
target_compile_definitions(core
PRIVATE BUILD_DLL
PUBLIC DYN_LINK
)
set_target_properties(core PROPERTIES ENABLE_EXPORTS 1)
# new core
add_executable(core_new core.cpp)
target_compile_definitions(core_new
PRIVATE BUILD_DLL NEW_VERSION
PUBLIC DYN_LINK
)
set_target_properties(core_new PROPERTIES ENABLE_EXPORTS 1)
# plugin
add_library(plugin SHARED plugin.cpp)
target_link_libraries(plugin PRIVATE core)
core.hpp
#pragma once
#if defined(_WIN32)
#if defined(DYN_LINK)
#if defined(CORE_SOURCE)
#define CORE_DECL __declspec(dllexport)
#else
#define CORE_DECL __declspec(dllimport)
#endif
#endif
#endif
#ifndef CORE_DECL
#define CORE_DECL
#endif
CORE_DECL int non_template();
template<typename>
struct foo_template {
CORE_DECL static int get();
};
core_impl.ipp // 这是 MinGW64 的解决方法。否则它不会为 in-class 实现的函数导出符号。
#pragma once
#define CORE_SOURCE
#include "core.hpp"
template<typename T>
int foo_template<T>::get()
{
static int val = 0;
return ++val;
}
export_types.hpp - 此文件包含插件的导出类型linked 到核心的“旧”版本
#pragma once
#include "core.hpp"
struct old{};
extern template struct foo_template<old>;
plugin.cpp
#include "core.hpp"
#include "export_types.hpp"
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif
extern "C"
{
DLL_EXPORT int foo_class_template();
}
int foo_class_template()
{
return foo_template<old>::get();
}
core.cpp
#include "core_impl.ipp"
#include "core.hpp"
#include "export_types.hpp"
template struct foo_template<old>;
#ifdef NEXT_VERSION
struct new_0 {
};
template struct foo_template<new_0>;
#endif
#include <filesystem>
#include <iostream>
#include <string_view>
#include <vector>
#include <Windows.h>
int main(int argc, char **argv)
{
// preincrement all the values
int fooClassTemplate = foo_template<old>::get();
auto plugin = LoadLibraryA("plugin.dll");
auto pFooClassTemplate = reinterpret_cast<int(*)()>(GetProcAddress(plugin, "foo_class_template"));
int pluginFooClassTemplate = pFooClassTemplate();
std::cout << fooClassTemplate + 1 << " : " << pluginFooClassTemplate << "\n";
return 0;
}
因此,当执行使用 MSVC、MinGW64 或 Clang 编译的旧版本(在 Windows 上,尚未检查 Ubuntu 时),输出符合预期:
2 : 2
但是当我 运行 core_new
加载相同的插件时,linked 到旧版本的 core
,结果是不同的:
2 : 1
确实,如果我们检查从 core
和 plugin
中调用的函数的地址,我们会发现它们是不同的,尽管看起来位于 core.exe
中
// code executed within main
(gdb) info sym 0x00007ff67da46be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
(gdb) info sym 0x00007ff67da4a0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
// code executed within plugin's functions
(gdb) info sym 0x00007ff6c5cc6be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
(gdb) info sym 0x00007ff6c5cca0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
这里总结就是一个table:
核心地址
插件地址
foo_template::获取
0x00007ff67da46be0
0x00007ff6c5cc6be0
foo_template::get::val
0x00007ff67da4a0e0
0x00007ff6c5cca0e0
由于地址位于executable中,我认为原因是符号地址,因此起初我比较符号tables。
为什么会这样?可以避免吗?
这是原文。我认为符号地址的差异是原因,但正如评论中的人指出的那样,符号地址并不重要。所以这是一个错误的方向。
考虑以下动态库示例代码:
template <typename>
struct foo_template
{
static inline int get()
{
static int val = 0;
return ++val;
}
};
struct foo
{
template <typename>
static inline int get()
{
static int val = 0;
return ++val;
}
};
template <typename>
inline int get()
{
static int val = 0;
return ++val;
}
struct old{};
template struct foo_template<old>;
template int foo::get<old>();
template int get<old>();
#ifdef NEXT_VERSION
struct new_0{};
template struct foo_template<new_0>;
template int foo::get<new_0>();
template int get<new_0>();
struct new_1{};
template struct foo_template<new_1>;
template int foo::get<new_1>();
template int get<new_1>();
struct new_2{};
template struct foo_template<new_2>;
template int foo::get<new_2>();
template int get<new_2>();
struct new_3{};
template struct foo_template<new_3>;
template int foo::get<new_3>();
template int get<new_3>();
#endif
在新版本中,当添加新的模板实例时,class 模板和模板成员函数的 ABI 被破坏,而现有代码是完整的。
新实例以严格的顺序添加 - 仅在现有实例之后添加。
This 是通过 objdump -t
输出的“旧”和“下一个”版本之间的比较。
可以看出,只有函数模板符号int get
没变:
// Old
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
// New
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
但是class模板和模板静态成员函数改变了它们的地址:
// Old
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000012f0 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000000012f0 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000080 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001310 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001310 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000090 .data$_ZZN3foo3getI3oldEEivE3val
// New
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001370 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x0000000000001370 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000000c0 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001410 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001410 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000110 .data$_ZZN3foo3getI3oldEEivE3val
符号按名称链接,因此更改共享库中符号的位置不会更改 ABI。
您的插件链接到 core
,因此它使用 core
中的符号,core_new
中定义的符号是分开的,因此您违反了 ODR。编译你的 repo,删除 core.exe
,将 core_new.exe
重命名为 core.exe
这样只有一个副本,然后 运行 core.exe
现在输出你的期望值。 None 其中与 ABI 有关
概述
我有一个 core
可以动态加载不同的插件。我显式实例化我导出的模板,然后在 link 插件针对某些版本的 core
时使用它们。我的期望是,当我添加新类型和模板实例化时,它不会破坏 ABI,并且现有插件不必重新link编辑。
事实并非如此。当我添加新实例时,我得到从不同地址(而不是 core
)内执行的符号。
例子
可以找到项目回购 here。
这是代码示例。
CMakeLists.txt
cmake_minimum_required(VERSION 3.7.2)
project(template_abi_test)
set(CMAKE_CXX_STANDARD 17)
# old core
add_executable(core core.cpp)
target_compile_definitions(core
PRIVATE BUILD_DLL
PUBLIC DYN_LINK
)
set_target_properties(core PROPERTIES ENABLE_EXPORTS 1)
# new core
add_executable(core_new core.cpp)
target_compile_definitions(core_new
PRIVATE BUILD_DLL NEW_VERSION
PUBLIC DYN_LINK
)
set_target_properties(core_new PROPERTIES ENABLE_EXPORTS 1)
# plugin
add_library(plugin SHARED plugin.cpp)
target_link_libraries(plugin PRIVATE core)
core.hpp
#pragma once
#if defined(_WIN32)
#if defined(DYN_LINK)
#if defined(CORE_SOURCE)
#define CORE_DECL __declspec(dllexport)
#else
#define CORE_DECL __declspec(dllimport)
#endif
#endif
#endif
#ifndef CORE_DECL
#define CORE_DECL
#endif
CORE_DECL int non_template();
template<typename>
struct foo_template {
CORE_DECL static int get();
};
core_impl.ipp // 这是 MinGW64 的解决方法。否则它不会为 in-class 实现的函数导出符号。
#pragma once
#define CORE_SOURCE
#include "core.hpp"
template<typename T>
int foo_template<T>::get()
{
static int val = 0;
return ++val;
}
export_types.hpp - 此文件包含插件的导出类型linked 到核心的“旧”版本
#pragma once
#include "core.hpp"
struct old{};
extern template struct foo_template<old>;
plugin.cpp
#include "core.hpp"
#include "export_types.hpp"
#ifdef _WIN32
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif
extern "C"
{
DLL_EXPORT int foo_class_template();
}
int foo_class_template()
{
return foo_template<old>::get();
}
core.cpp
#include "core_impl.ipp"
#include "core.hpp"
#include "export_types.hpp"
template struct foo_template<old>;
#ifdef NEXT_VERSION
struct new_0 {
};
template struct foo_template<new_0>;
#endif
#include <filesystem>
#include <iostream>
#include <string_view>
#include <vector>
#include <Windows.h>
int main(int argc, char **argv)
{
// preincrement all the values
int fooClassTemplate = foo_template<old>::get();
auto plugin = LoadLibraryA("plugin.dll");
auto pFooClassTemplate = reinterpret_cast<int(*)()>(GetProcAddress(plugin, "foo_class_template"));
int pluginFooClassTemplate = pFooClassTemplate();
std::cout << fooClassTemplate + 1 << " : " << pluginFooClassTemplate << "\n";
return 0;
}
因此,当执行使用 MSVC、MinGW64 或 Clang 编译的旧版本(在 Windows 上,尚未检查 Ubuntu 时),输出符合预期:
2 : 2
但是当我 运行 core_new
加载相同的插件时,linked 到旧版本的 core
,结果是不同的:
2 : 1
确实,如果我们检查从 core
和 plugin
中调用的函数的地址,我们会发现它们是不同的,尽管看起来位于 core.exe
中
// code executed within main
(gdb) info sym 0x00007ff67da46be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
(gdb) info sym 0x00007ff67da4a0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core_new.exe
// code executed within plugin's functions
(gdb) info sym 0x00007ff6c5cc6be0
foo_template<old>::get() in section .text of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
(gdb) info sym 0x00007ff6c5cca0e0
foo_template<old>::get()::val in section .data of C:\dev\builds\template_abi_test\Debug-MinGW-w64\core.exe
这里总结就是一个table:
核心地址 | 插件地址 | |
---|---|---|
foo_template::获取 | 0x00007ff67da46be0 | 0x00007ff6c5cc6be0 |
foo_template::get::val | 0x00007ff67da4a0e0 | 0x00007ff6c5cca0e0 |
由于地址位于executable中,我认为原因是符号地址,因此起初我比较符号tables。
为什么会这样?可以避免吗?
这是原文。我认为符号地址的差异是原因,但正如评论中的人指出的那样,符号地址并不重要。所以这是一个错误的方向。
考虑以下动态库示例代码:
template <typename>
struct foo_template
{
static inline int get()
{
static int val = 0;
return ++val;
}
};
struct foo
{
template <typename>
static inline int get()
{
static int val = 0;
return ++val;
}
};
template <typename>
inline int get()
{
static int val = 0;
return ++val;
}
struct old{};
template struct foo_template<old>;
template int foo::get<old>();
template int get<old>();
#ifdef NEXT_VERSION
struct new_0{};
template struct foo_template<new_0>;
template int foo::get<new_0>();
template int get<new_0>();
struct new_1{};
template struct foo_template<new_1>;
template int foo::get<new_1>();
template int get<new_1>();
struct new_2{};
template struct foo_template<new_2>;
template int foo::get<new_2>();
template int get<new_2>();
struct new_3{};
template struct foo_template<new_3>;
template int foo::get<new_3>();
template int get<new_3>();
#endif
在新版本中,当添加新的模板实例时,class 模板和模板成员函数的 ABI 被破坏,而现有代码是完整的。
新实例以严格的顺序添加 - 仅在现有实例之后添加。
This 是通过 objdump -t
输出的“旧”和“下一个”版本之间的比较。
可以看出,只有函数模板符号int get
没变:
// Old
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
// New
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 90](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x00000000000012d0 _Z3getI3oldEiv
[ 91](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000070 .data$_ZZ3getI3oldEivE3val
但是class模板和模板静态成员函数改变了它们的地址:
// Old
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000012f0 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x00000000000012f0 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000080 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001310 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001310 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000090 .data$_ZZN3foo3getI3oldEEivE3val
// New
File
[ 77](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001370 .text$_ZN12foo_templateI3oldE3getEv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 79](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 1) 0x0000000000001370 _ZN12foo_templateI3oldE3getEv
AUX tagndx 0 ttlsiz 0x0 lnnos 0 next 0
[ 81](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x00000000000000c0 .data$_ZZN12foo_templateI3oldE3getEvE3val
AUX scnlen 0x4 nreloc 0 nlnno 0 checksum 0x0 assoc 0 comdat 3
[ 83](sec 1)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000001410 .text$_ZN3foo3getI3oldEEiv
AUX scnlen 0x1b nreloc 3 nlnno 0 checksum 0x0 assoc 0 comdat 2
[ 85](sec 1)(fl 0x00)(ty 20)(scl 2) (nx 0) 0x0000000000001410 _ZN3foo3getI3oldEEiv
[ 86](sec 2)(fl 0x00)(ty 0)(scl 3) (nx 1) 0x0000000000000110 .data$_ZZN3foo3getI3oldEEivE3val
符号按名称链接,因此更改共享库中符号的位置不会更改 ABI。
您的插件链接到 core
,因此它使用 core
中的符号,core_new
中定义的符号是分开的,因此您违反了 ODR。编译你的 repo,删除 core.exe
,将 core_new.exe
重命名为 core.exe
这样只有一个副本,然后 运行 core.exe
现在输出你的期望值。 None 其中与 ABI 有关