问题 链接静态库时内联函数的多个定义


我有一个C ++程序,我用mingw编译(gcc for Windows)。使用包含gcc 4.4.1的mingw的TDM版本。可执行文件链接到两个静态库(.a)文件:它们是用C语言编写的第三方库;另一个是由我编写的C ++库,它使用C库提供我自己的C ++ API。

在我看来,过多的C库功能部分是在内联函数中实现的。当你使用C库的API时,你无法避免包含内联函数,但当我尝试将它们全部链接在一起时,我收到链接错误,说明所有内联函数都有多个定义 - 我都有在我的C ++包装器库中调用,而我没有调用它,基本上在头文件中内联定义的任何东西都有一个在C库和C ++库中为它创建的函数。

当包含文件在同一项目中的不同.c或.cpp文件中多次使用时,它不会导致多个定义错误;问题只是它为每个库生成一个定义。

编译器如何/为什么在两个库中为这些内联函数生成函数和符号?如何强制它停止在我的代码中生成它们?是否有一个工具可以运行以从.a文件中删除重复的函数,或者是一种使链接器忽略多个定义的方法?

(仅供参考,第三方库在其所有标题中都包含#ifdef __cplusplus和extern“C”保护;无论如何,如果这是问题,它不会导致符号的多重定义,它会导致相反的问题,因为符号会不确定或至少不同。)

值得注意的是,如果我链接到第三方C库的DLL,则不会发生链接错误;然而,我得到奇怪的运行时故障,似乎与我的代码有关,它应该从DLL调用自己的函数版本。 (好像编译器正在创建我没有要求的本地版本的函数。)

之前已经问过这个问题的类似版本,但是,我没有找到任何这些问题的答案:

这个问题的答案是海报是多重定义的 变量,我的问题是内联函数的多重定义: 重复多重定义错误在多个cpp中包含相同的标头

这是一个MSVC计划,但我正在使用mingw;此外,海报在这个问题中的问题是在标题中类主体之外定义了一个C ++类构造函数,而我的问题是内联的C函数: 静态Lib多重定义问题

这个傻瓜将他的所有C代码重命名为C ++文件,而他的C代码不是C ++ - 安全: 链接时批量std :: functions的多个定义

这个只是想知道为什么违反一个定义规则不是一个错误: 具有不同定义的内联函数的不可预测行为


6490
2018-02-07 17:47


起源

请注意,C99内联语义与C ++不同:在C中,如果明确指定了其中一个内联函数声明 extern,它创建了一个外部定义 - 不再是内联定义。这些外部定义不能在程序中多次出现。在C ++中是一个明确的 extern 对这样的功能没有任何影响。你做得最好 static inline 在C. - Johannes Schaub - litb
通常,内联函数定义标记为“弱”符号,链接器应该删除所有重复项。 - Omnifarious
感谢Johannes:使用预处理器定义,C库的头文件在C库的.c文件中声明这些函数为“inline”,在我的项目中为“extern inline”。但我不确定他们为什么这样做。 C库确实有一些代码使用函数的地址作为唯一键值。作为一名C ++程序员,我认为这是一种不好的做法,我真的祈祷他们没有使用内联函数的地址。虽然我仍然无法编译程序,但我觉得这些评论中有足够的信息可以用来回答。 - Dennis
如果它在某处使用函数的地址作为密钥并依赖于TU上的相同地址,则无法使用 static inline 了。你可以用 -fgnu89-inline 并使用 extern inline 即使在C模式下(参见 gcc.gnu.org/onlinedocs/gcc/Inline.html 。如果你没有编译 -std=c99,它甚至可能已经是有效的模式,我想)。 - Johannes Schaub - litb
我在这个问题中添加了“C”标签,并且(因为一个不能有6个标签)删除了“多重定义错误”,这似乎也被“链接器错误”所覆盖。 - Johannes Schaub - litb


答案:


首先,你必须了解C99内联模型 - 也许你的标题有问题。内联函数有两种定义,具有外部(非静态)链接

  • 外部定义
    函数的这个定义只能在指定的TU中在整个程序中出现一次。它提供了可以从其他TU使用的导出功能。

  • 内联定义
    这些出现在每个TU中,其中声明为单独的定义。定义有  需要彼此相同或与外部定义相同。如果在库中使用内部函数,则可以省略对函数参数的检查,否则这些参数将在外部定义中完成。

函数的每个定义都有自己的局部静态变量,因为它们的本地声明没有链接(它们不像C ++那样共享)。如果,非静态内联函数的定义将是内联定义

  • TU中的每个函数声明都包含说明符 inline,和
  • TU中没有函数声明包含说明符 extern

否则,必须出现在该TU中的定义(因为内联函数必须在声明的相同TU中定义)是外部定义。在调用内联函数时 未指定是使用外部定义还是内联定义。但是,因为在所有情况下定义的函数仍然相同(因为它具有外部链接),所以无论出现多少内联定义,它的地址在所有情况下都相等。因此,如果您使用函数的地址,则编译器可能会解析为外部定义(特别是在禁用优化的情况下)。

一个演示错误使用的例子 inline,因为它在两个TU中包含两次函数的外部定义,导致多重定义错误

// included into two TUs
void f(void); // no inline specifier
inline void f(void) { }

以下程序很危险,因为编译器可以自由使用外部定义,但程序不提供

// main.c, only TU of the program
inline void g(void) {
  printf("inline definition\n");
}

int main(void) {
  g(); // could use external definition!
}

我使用GCC进行了一些测试用例,进一步证明了这种机制:

main.c中

#include <stdio.h>

inline void f(void);

// inline definition of 'f'
inline void f(void) {
  printf("inline def main.c\n");
}

// defined in TU of second inline definition
void g(void);

// defined in TU of external definition
void h(void);

int main(void) {
  // unspecified whether external definition is used!
  f();
  g();
  h();

  // will probably use external definition. But since we won't compare
  // the address taken, the compiler can still use the inline definition.
  // To prevent it, i tried and succeeded using "volatile". 
  void (*volatile fp)() = &f;
  fp();
  return 0;
}

main1.c

#include <stdio.h>

inline void f(void);

// inline definition of 'f'
inline void f(void) {
  printf("inline def main1.c\n");
}

void g(void) {
  f();
}

main2.c

#include <stdio.h>

// external definition!
extern inline void f(void);

inline void f(void) {
  printf("external def\n");
}


void h(void) {
  f(); // calls external def
}

现在,该计划输出了我们的预期!

$ gcc -std=c99 -O2 main.c main1.c main2.c
inline def main.c
inline def main1.c
external def
external def

查看符号表,我们将看到不导出内联定义的符号(来自 main1.o),导出外部定义(来自 main2.o)。


现在,如果您的静态库每个都有其内联函数的外部定义(正如它们所应),它们将自然地相互冲突。解决方案是使内联函数静态或仅重命名它们。这些将始终提供外部定义(因此它们是完整的定义),但它们不会导出,因为它们具有内部链接,因此不会冲突

static inline void f(void) {
  printf("i'm unique in every TU\n");
}

14
2018-02-07 19:43



这是一个非常好的答案。遗憾的是,我仍然无法应用这些知识来成功编译我的程序 - 尝试将外部内联更改为静态内联会使其失败,因为它混合了同一函数的静态和非静态定义。也许我可以在我自己的代码中使用宏来更改包含标题的函数的名称,但标题中大约有100个这样的东西...... - Dennis
有关详细信息,此讨论表明编译器优化级别可能会影响该问题: lkml.indiana.edu/hypermail/linux/kernel/0408.0/1787.html - Dennis
我在使用MinGW gcc构建时发现类似的东西,并发现MinGW标头似乎与-std = gnu99选项不兼容。这造成了一个 批量 当我尝试链接时,多重定义的符号,因为函数的定义被放置在以这种方式构建的每个目标文件中。 - David Gardner
...所以我发现在我们的情况下使用-fgnu89-inline是一个合适的解决方法,因为我们需要在for循环计数器变量上使用gnu99作用域。 (啊) - David Gardner