ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

参数类型不一致导致extern "C"不起作用

extern "C": 指示编译器以C形式进行链接

某些时候,我们需要将一些C++的库封装成C风格的接口供外部使用,所谓“C风格”的接口,是指该接口的参数、返回值中不包含类、作用域等C++的元素,但其实现中是可以有的。比如说,我们有一个类 Wav ,用来处理wave文件

#ifndef _WAV_H_
#define _WAV_H_

class Wav
{
public:
    Wav();
    Wav(string wav_file);
    virtual ~Wav();
    bool load_wav(string wav_file);
    private:
    short *data;
    int sampel_rate;
};

#endif /* _WAV_H_ */

然后我们需要利用这个类实现几个音频处理接口,提供给 C程序 使用,由于C语言中是没有“类”这个特性的,我们不能直接用C语言去实现这些接口,这个时候就要用到extern "C"了.

#ifndef _WAVPROC_H_
#define _WAVPROC_H_

#ifdef __cplusplus
extern "C"{
#endif
    int read_wav(const char *wav_file, double *data);
    int denoise(double *data);
    int cutscene(double *data);
    int feature_extract(double *data, double *feature);
#ifdef __cplusplus
}
#endif

#endif /* _WAVPROC_H_ */

然后在"wavproc.cpp"中实现这些接口,此时在"wavproc.cpp"中是可以使用上面定义的类以及其他C++语言的特性的。实现以后以静态库/动态库的形式提供接口,对于使用接口的C程序来说,只需要

  1. wavproc.h
  2. 对应的静态库/动态库,如: libwavproc.a

nm: 查看目标文件中的符号是C形式还是C++形式

nm是一个查看目标文件中符号列表的工具,其具体使用这里不会详谈,只讲一下用nm来判断使用extern "C"是否起作用。举个栗子,对下面这个函数:

#include "fs.h"

int file_size(const char *file)
{
    int fs = 0;
    // do something
    return fs;
}

以上内容在"fs.cpp"中,其对应的头文件为:

#ifndef _FS_H_
#define _FS_H_

int file_size(const char *file);

#endif

用g++将"fs.cpp"编译成目标文件后,用nm查看其中的符号信息:

nm ~/test/cppcode/fs.o

得到的输出是:

0000000000000000 T _Z9file_sizePKc

我们在"fs.c"中实现同样一个函数

#include "fs.h"

int file_size(const char *file)
{
    int fs = 0;
    /* do something */
    return fs;
}

其对应的头文件与"fs.cpp"的头文件一样。然后用gcc将"fs.c"编译为目标文件后,同样用nm查看符号信息:

nm ~/test/ccode/fs.o

得到结果为:

0000000000000000 T file_size

可以看到,同样一个函数,在C++程序和C程序中,被编译后产生的符号类型是不一样的。C++为了支持重载,在编译的时候会对函数符号添加前缀和后缀,后缀是参数列表的缩写,用来区分重载;而C编译器则不会对函数符号添加其他信息。

当我们需要将某些用C++实现的接口提供给C程序使用时,要在其头文件(接口定义)中用extern "C"来告诉编译器,按C的形式导出符号表——这个是之前提到过的,现在我们对"fs.cpp"的头文件"fs.h"进行这样的处理:

#ifndef _FS_H_
#define _FS_H_

#ifdef __cpulsplus
extern "C"{
#endif

    int file_size(const char *file);

#ifdef __cplusplus
}
#endif

#endif

然后用g++将"fs.cpp"编译为目标文件,并使用nm查看符号信息:

nm ~/test/cppcode/fs.o

得到的结果为:

0000000000000000 T file_size

可以看到,这次得到的符号类型是C形式的符号了,是可以被C程序所使用的。

"undefined reference": 诡异的错误

好了,终于到了标题提到的内容了。

事情是这样的,我负责的一个项目A是C++实现的,然后需要将其中的一些功能以接口的形式提供给另外一个项目B使用——项目B是C实现的。那么,理所当然的,我为项目A编写API时,要用extern "C"去处理头文件。在实现好接口后,我以静态库的方式将接口提供给了项目B,然而在编译项目B时,总是报错:

undeined reference: xxxx

意即有一个函数是未定义的引用,而该函数是项目A的API中提供的接口函数中的一个。我折腾了近一天时间,始终无法找到这个问题的原因所在——这个时候我还不知道使用nm来进行检查的方法。无奈之下向老大请教,老大遂用nm查了一下提供的静态库,发现导出的符号还是C++形式的,但诡异的是,同一个头文件中提供的若干个函数,只有这一个的符号还是C++形式的,而其他的都被导出成C形式的符号了。

老大也很迷惑,不过他说:先别怀疑编译器,应该还是有什么细节上处理不当。

果然,后来经过几番检查,发现是由于头文件中函数的参数类型和实现中函数的参数类型不一致导致了这个问题,在头文件中,该函数为:

void InitDiarizationSpace(Dia *env, const char *file);

而在实现中,该函数为:

void InitDiarizationSpace(Dia *env, char *file) {
    // ...
}

……

对此我表示很羞愧。