新的模板实例化会破坏 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

确实,如果我们检查从 coreplugin 中调用的函数的地址,我们会发现它们是不同的,尽管看起来位于 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 有关