彭某的技术折腾笔记

彭某的技术折腾笔记

关键字 extern C 详解

2024-05-14

关键字 extern C 详解

2024年5月14日

摘要

在 C/C++ 的开发中,会出现两种语言互相调用的情况,此时会用到 extern "C" 这样的用法,本文将对其进行详细的讲解。

关于 GCC

在编译 C/C++ 程序时,最常用的工具链是 GCC(GNU Compiler Collection),这是一个编译器集合,可以编译 C,C++,Fortran,Pascal,Objective-C 等语言。而小写的 gcc 则是 GNU C Compiler,和大写的不是一个东西,只是GCC 中的一个组件,g++ 则是 GNU C++ Compiler。

当我们在命令行中调用 gcc 时,调用的其实是大写的 GCC,他会根据输入源代码的后缀名是 .c 还是 .cpp 来选择按照 C 还是 C++ 的规则进行编译。而调用 g++ 时,则一律按照 C++ 的规则进行编译。

当然,GCC 也提供了命令行选项用于指定规则,例如 -xc 是指定按照 C 的规则编译,-xc++ 则是按照 C++ 的规则进行编译。

C++ 调用 C

假如存在三个待编译的文件:

cfun.h

##### cfun.h

#ifndef _C_FUN_H_
#define _C_FUN_H_

void cfun(int i);

#endif

cfun.c

##### cfun.c

#include <stdio.h>
#include "cfun.h"

void cfun(int i)
{
    printf("cfun(%d)\n", i);
}

main.cpp

##### main.cpp

#include "cfun.h"

int main()
{
    cfun(2);
    return 0;
}

我们调用:

gcc cfun.c main.cpp

则会出现以下报错:

Undefined symbols for architecture arm64:
  "cfun(int)", referenced from:
      _main in main-1dbef1.o
   NOTE: found '_cfun' in cfun-4451cb.o, declaration possibly missing 'extern "C"'
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

其原因就是 .c 的源文件和 .cpp 的源文件中,同一个函数被采用不同的规则生成了不同的符号,在链接阶段无法被正常匹配链接所导致的。为了验证这个原因,我们可以通过 gcc -S 来生成汇编文件观察编译生成的符号名,但由于汇编代码较长,不便于放入博客,因此我们生成更简短的符号表来观察:

gcc -c cfun.c main.cpp

此时生成了 cfun.omain.o 两个对象文件,我们使用 objdump -t 命令来观察符号表:

objdump -t cfun.o
cfun.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000038 l     O __TEXT,__cstring l_.str
0000000000000038 l     O __TEXT,__cstring ltmp1
0000000000000048 l     O __LD,__compact_unwind ltmp2
0000000000000000 g     F __TEXT,__text _cfun
0000000000000000         *UND* _printf
objdump -t main.o
main.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000030 l     O __LD,__compact_unwind ltmp1
0000000000000000 g     F __TEXT,__text _main
0000000000000000         *UND* __Z4cfuni

可以看到,C 源文件 cfun.ovoid cfun(int i); 按照 C 规则生成的符号是 _cfun,而 C++ 源文件 main.cppvoid cfun(int i); 按照 C++ 规则生成的符号是 __Z4cfuni(根据 #include 进来的函数声明里产生),二者无法对应。

因此,有两个解决办法:修改引用文件和修改被引用文件。

修改引用文件

我们可以在 C++ 源文件 main.cpp#include 的地方添加 extern "C" 关键字来解决:

#ifdef __cplusplus
extern "C" {
#endif

#include "cfun.h"

#ifdef __cplusplus
}
#endif


int main()
{
    cfun(2);
    return 0;
}

其中 __cplusplus 关键字将在编译器启用 C++ 编译规则时自动定义,此时,extern "C" 关键字将生效,包含在 cfun.h 中的函数声明将自动被此关键字包裹,编译器将在编译 main.cpp 这个 C++ 源文件时,把 extern "C" 内部声明的函数和变量按照 C 的规则生成符号,此时编译 main.cppobjdump 的结果如下:

gcc -c main.cpp
objdump -t main.o
main.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000030 l     O __LD,__compact_unwind ltmp1
0000000000000000 g     F __TEXT,__text _main
0000000000000000         *UND* _cfun

此时,则可以完美与 cfun.c 生成的符号对应,再次将两个源文件一起编译也可产生正确结果。

修改被引用文件

另一个更通用也是更推荐的办法则是直接修改头文件,直接将头文件除了防止重复包含的宏以外的部分全部用 extern "C" 包裹,即可让此头文件在 C 和 C++ 中都可直接被引用,且自适应的生成符号。只需按照如下方法修改 cfun.h

#ifndef _C_FUN_H_
#define _C_FUN_H_

#ifdef __cplusplus
extern "C" {
#endif

void cfun(int i);

#ifdef __cplusplus
}
#endif

#endif

即可正常按照:

gcc cfun.c main.cpp

完成编译且正常输出。

C 调用 C++

假如存在三个待编译的文件:

cppfun.hpp(也可以是 .h,但使用 .hpp 能更清晰表明这是个 C++ 头文件):

##### cppfun.hpp

#ifndef __CPP_FUN_HPP_
#define __CPP_FUN_HPP_

void cppfun(int i);

#endif

cfun.cpp

##### cppfun.cpp

#include <stdio.h>
#include "cppfun.hpp"

void cppfun(int i)
{
    printf("cppfun: %d\n", i);
}

main.c

##### main.c

#include "cppfun.hpp"

int main()
{
    cppfun(2);
    return 0;
}

我们调用:

gcc cppfun.cpp main.c

则会出现以下报错:

Undefined symbols for architecture arm64:
  "_cppfun", referenced from:
      _main in main-a23963.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

其原因就是 .cpp 的源文件和 .c 的源文件中,同一个函数被采用不同的规则生成了不同的符号,在链接阶段无法被正常匹配链接所导致的。为了验证这个原因,我们可以通过 gcc -S 来生成汇编文件观察编译生成的符号名,但由于汇编代码较长,不便于放入博客,因此我们生成更简短的符号表来观察:

gcc -c cppfun.cpp main.c

此时生成了 cppfun.omain.o 两个对象文件,我们使用 objdump -t 命令来观察符号表:

objdump -t cppfun.o
cppfun.o:       file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000038 l     O __TEXT,__cstring l_.str
0000000000000038 l     O __TEXT,__cstring ltmp1
0000000000000048 l     O __LD,__compact_unwind ltmp2
0000000000000000 g     F __TEXT,__text __Z6cppfuni
0000000000000000         *UND* _printf
objdump -t main.o
main.o: file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000030 l     O __LD,__compact_unwind ltmp1
0000000000000000 g     F __TEXT,__text _main
0000000000000000         *UND* _cppfun

可以看到,C++ 源文件 cppfun.ovoid cppfun(int i); 按照 C++ 规则生成的符号是 __Z6cppfuni,而 C 源文件 main.cvoid cfun(int i); 按照 C 规则生成的符号是 _cppfun(根据 #include 进来的函数声明里产生),二者无法对应。

因此,有两个解决办法:修改引用文件和修改被引用文件。

修改引用文件

我们可以在 C++ 源文件 cppfun.cpp#include 的地方添加 extern "C" 关键字来解决:

#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

#include "cppfun.hpp"

#ifdef __cplusplus
}
#endif

void cppfun(int i)
{
    printf("cppfun: %d\n", i);
}

其中 __cplusplus 关键字将在编译器启用 C++ 编译规则时自动定义,此时,extern "C" 关键字将生效,包含在 cppfun.hpp 中的函数声明将自动被此关键字包裹,编译器将在编译 cppfun.cpp 这个 C++ 源文件时,把 extern "C" 内部声明的函数和变量按照 C 的规则生成符号,此时编译 cppfun.cppobjdump 的结果如下:

gcc -c cppfun.cpp
objdump -t cppfun.o
cppfun.o:       file format mach-o arm64

SYMBOL TABLE:
0000000000000000 l     F __TEXT,__text ltmp0
0000000000000038 l     O __TEXT,__cstring l_.str
0000000000000038 l     O __TEXT,__cstring ltmp1
0000000000000048 l     O __LD,__compact_unwind ltmp2
0000000000000000 g     F __TEXT,__text _cppfun
0000000000000000         *UND* _printf

此时,则可以完美与 main.c 生成的符号对应,再次将两个源文件一起编译也可产生正确结果。

修改被引用文件

另一个更通用也是更推荐的办法则是直接修改头文件,直接将头文件除了防止重复包含的宏以外的部分全部用 extern "C" 包裹,即可让此头文件在 C 和 C++ 中都可直接被引用,且自适应的生成符号。只需按照如下方法修改 cfun.hpp

#ifndef __CPP_FUN_HPP_
#define __CPP_FUN_HPP_

#ifdef __cplusplus
extern "C" {
#endif

void cppfun(int i);

#ifdef __cplusplus
}
#endif

#endif

即可正常按照:

gcc cppfun.cpp main.c

完成编译且正常输出。

其他办法

单独声明

我们还可以单独对某一个声明加上 extern "C"

extern "C" void func(int num);

条件编译

还可以定义一个宏进行条件编译:

#ifdef __cplusplus
    #define EXTERN_C extern "C"
#else
    #define EXTERN_C
#endif

EXTERN_C void func(int char);

总结

由于 extern "C" 是一个 C++ 关键词,因此一定是用在 C++ 源文件一侧,让其按照 C 的规则生成符号。当然,C++ 的函数本身还是按照 C++ 的方式进行编译,只是生成的接口符合 C 的规范而已。并不是所有的函数都能 extern "C",因为有些类型无法按照 C 的格式表达。

即使 #include 时也可 extern "C",但最好的办法还是写在头文件里,方便在 C/C++ 中都可调用。

  • 0