首页 > 解决方案 > 新模板实例化会破坏 ABI 吗?

问题描述

概述

我有一个core动态加载不同的插件。我明确地实例化我导出的模板,然后在将插件链接到某些版本的core. 我的期望是,当我添加新类型和模板实例时,它不会破坏 ABI,并且不必重新链接现有插件。
事实证明并非如此。当我添加新的实例时,我会得到从不同(而不是在core)地址中执行的符号。

例子

项目 repo 可以在这里找到。
这是代码示例。

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)

核心.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 的解决方法。否则它不会为类内实现的函数导出符号。

#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 - 此文件包含链接到核心“旧”版本的插件的导出类型

#pragma once

#include "core.hpp"

struct old{};

extern template struct foo_template<old>;

插件.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();
}

核心.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加载相同的插件时,它链接到旧版本的core,结果是不同的:

2 : 1

事实上,如果我们检查从coreand中调用的函数的地址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

总结一下,这里有一张表:

核心地址 插件地址
foo_template::get 0x00007ff67da46be0 0x00007ff6c5cc6be0
foo_template::get::val 0x00007ff67da4a0e0 0x00007ff6c5cca0e0

由于地址位于可执行文件中,我认为原因是符号地址,因此一开始我是在比较符号表。

为什么会这样?可以避免吗?


这是原文。我认为符号地址的差异是原因,但正如评论中的人指出的那样,符号地址并不重要。所以这是一个错误的方向。

考虑以下动态库的示例代码:

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

在新版本中,当添加新模板实例时,类模板和模板成员函数的 ABI 会被破坏,而现有代码是完整的。
新的实例以严格的顺序添加 - 仅在现有实例之后。

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

但是类模板和模板静态成员函数改变了它们的地址:

// 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

标签: c++templatesdllpluginsabi

解决方案


符号按名称链接,因此更改共享库中符号的位置不会更改 ABI。

您的插件已链接到,core因此它使用来自core的符号,其中定义的符号core_new是分开的,因此您违反了 ODR。编译您的 repo、删除core.exe、重命名core_new.exe为,core.exe以便只有一个副本,然后运行core.exe现在输出您的预期值。这些都与 ABI 无关


推荐阅读