C与C++混编

介绍

了解一下C与C++如何合作,gcc和g++编译出来的东西有什么区别。C++为了支持重载等特性,编译出来的符号和C是不一样的。

每个公司都会有一些古老的库,几乎每个程序都在使用它,它可能是C写的,或者是C++写的,通常情况下,我们能做的就是调用里面的函数,而不能修改这个库,因为很多程序都在用它,你改了它就得测所有的程序,得不偿失。

工具使用

objdump是个好工具,可以用于查看.o文件的内容,也可以查看可执行文件的内容。主要是可以用来查看gccg++编译出来的符号有什么区别。

查看符号表
objdump -t foo.o

查看正文段
objdump -S foo.o

查看所有session
objdump -D foo.o

符号入门

先来看下面这个文件foo.c

#include <stdio.h>

void foo()
{
    printf("foo\n");
}

g++ -c foo.c编译结果如下

0000000000000000 <_Z3foov>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <_Z3foov+0xe>
   e:	90                   	nop
   f:	5d                   	pop    %rbp
  10:	c3                   	retq

gcc -c foo.c编译结果如下

0000000000000000 <foo>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <foo+0xe>
   e:	90                   	nop
   f:	5d                   	pop    %rbp
  10:	c3                   	retq 

这个文件足够简单,可以看到区别就只是函数名而已,这里我们只关注第9行。可以看出,gcc并没有改变函数名foo,而g++在前后加了一些串变成了_Z3foov。其实g++将参数信息插在函数名的尾部了,_Z3foov中的后缀v就代表了void(PS: 我不知道_Z3表示啥)。稍微改一下函数的参数,用g++编译一下看看:

  • 如果foo是有1个参数int,那函数名是_Z3fooi
  • 如果foo是有1个参数double,那函数名是_Z3food
  • 如果foo有两个参数int和double,那函数名应该是_Z3fooid。(请自行实验)

如果参数是个自定义的类呢,比如:

int foo(My my)
{
    return 0;
}

被编译成

0000000000000047 <_Z3foo2My>:
  47:	55                   	push   %rbp
  48:	48 89 e5             	mov    %rsp,%rbp
  4b:	89 7d f0             	mov    %edi,-0x10(%rbp)
  4e:	b8 00 00 00 00       	mov    $0x0,%eax
  53:	5d                   	pop    %rbp
  54:	c3                   	retq 

可以看到,直接以类名拼接在末尾(PS: 我不知道2表示啥)。

如果是个std的类呢?比如string

void foo(std::string my)
{
    printf("foo%s\n", my.c_str());
}

被编译成

000000000000001a <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE>:
  1a:	55                   	push   %rbp
  1b:	48 89 e5             	mov    %rsp,%rbp
  1e:	48 83 ec 10          	sub    $0x10,%rsp
  22:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  26:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  2a:	48 89 c7             	mov    %rax,%rdi
  2d:	e8 00 00 00 00       	callq  32 <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE+0x18>
  32:	48 89 c6             	mov    %rax,%rsi
  35:	bf 00 00 00 00       	mov    $0x0,%edi
  3a:	b8 00 00 00 00       	mov    $0x0,%eax
  3f:	e8 00 00 00 00       	callq  44 <_Z3fooNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE+0x2a>
  44:	90                   	nop
  45:	c9                   	leaveq 
  46:	c3                   	retq 

很长很长,因为类名确实很长,这个你用lstrace跑个程序就知道了,很多函数名都很长得看不懂。

C++调用C

在C++源文件中是不能直接调用C源文件中的函数的,链接的时候就会报对‘foo()’未定义的引用,因为C++源文件编译时没问题,链接时就找不到符号了,有了上面的符号基础,我们可以知道这是因为gcc和g++编译出来的符号不一致导致的。举个例子,现在有文件main.cpp、foo.h、foo.c。

main.cpp内容如下:

#include "foo.h"
int main()
{
    foo();
    return 0;
}

foo.h内容如下:

#ifndef __FOO__
#define __FOO__
void foo();
#endif

foo.c内容如下:

#include <stdio.h>
void foo()
{
    printf("foo\n");
}

现在以如下命令编译他们

g++ -c main.cpp
gcc -c foo.c
g++ -o test foo.o main.o  # 这一步会报错

报错内容:

main.c:(.text+0x10):对‘foo()’未定义的引用
collect2: error: ld returned 1 exit status

这是因为在链接两个.o文件时,找不到foo这个函数才报的错。foo确实是在foo.o里边的,只不过main.o中其实需要的是函数_Z3foov才对。

正确的做法之一是修改foo.h文件如下

#ifndef __FOO__
#define __FOO__

extern "C" {
void foo();
}
#endif

这样编译出来的foo.o没有任何区别,但是main.o就有区别了,里面的符号_Z3foov全被替换成foo了(用objdump -t查看),这样链接起来就没问题。当然了,在一些古老的库中,头文件那么多,难道要一个个改吗?况且改完头文件还得重新用gcc编译。这是没必要的,此时只需要再新建一个头文件,比如foo_cpp.h,把需要用到的C头文件写在大括号里面就可以了,比如:

#ifndef __FOO_CPP__
#define __FOO_CPP__

#ifdef __cplusplus  // 很重要
extern "C" {
#endif

#include "foo.h"

#ifdef __cplusplus
}
#endif
#endif

看到这里,extern "C"的最常见的用法也就清晰了,件即告诉g++编译器,大括号内的符号都以C的符号命名方式去调用。值得注意的是,一般需要在使用extern "C"的时候用宏__cplusplus来判断此时的编译器是不是C++的,这样的做法百利而无害。

但是,这样就改到了旧的C模块了,这通常不是我们想要的,这里介绍一个更简单的方法。直接改main.cpp,用extern "C"括起需要用到的C头文件,如下

extern "C" {
#include "foo.h"
}

int main()
{
    foo();
    return 0;
}

这种方式更加方便快捷,还不用动旧代码,所以用得比较多。

C调用C++

C调用C++稍微有些不同,因为C没有extern "C"这个东西,所以还得从C++的代码入手,使得它可以被C代码所调用。

方法1:

如果原来的C++代码可以改的话,直接在头文件里面套上extern "C"全部括起来,然后C代码中想用哪个函数就直接将函数声明拷过来,前面加上extern(告诉gcc这个函数是在其他模块定义的)。

apple.cpp内容如下

#include "apple.h"

Apple::Apple() : m_nColor(0)
{
}
 
void Apple::SetColor(int color)
{
    m_nColor = color;
}
 
int Apple::GetColor(void)
{
    return m_nColor;
}

int set_and_get_color(int n)
{
    Apple a;
    a.SetColor(n);
    return a.GetColor();
}

apple.h内容如下

#ifndef __APPLE_H__
#define __APPLE_H__

#ifdef __cplusplus       // 新加上的
extern "C" {
#endif

class Apple
{
public:
    Apple();
    int GetColor(void);
    void SetColor(int color);
    
private:
    int m_nColor;
};


int set_and_get_color(int);

#ifdef __cplusplus     // 新加上的
}
#endif
#endif

main.c内容如下

#include <stdio.h>
extern int set_and_get_color(int);  // 函数如果太多可以弄个头文件单独写

int main(void)
{
    printf("color: %d\n", set_and_get_color(5666));
    return 0;
}

按照如下命令编译它们

g++ -c apple.cpp
gcc -c main.c
gcc -o test main.o apple.o -lstdc++    # 链接

注意到,旧的C++的代码仍然是用g++编译,新的C代码是用gcc编译,最后再把它们链接起来就是可执行程序。这个方法的缺点就是需要改到旧的C++头文件,导致C++代码重新编译,问题也不是很大,可以考虑。

方法2:

如果旧的C++代码太古董了,一点都不想改的话,可以专门写个wrapper模块,单独给main.cpp用。这个wrapper模块是C++代码(g++编译)。

apple.cpp和上面方法1是一样的。
apple.h内容如下

#ifndef __APPLE_H__
#define __APPLE_H__

class Apple
{
public:
    Apple();
    int GetColor(void);
    void SetColor(int color);
    
private:
    int m_nColor;
};

int set_and_get_color(int);
#endif

wrapper.cpp的内容如下

#include "wrapper.h"
#include "apple.h"  // 这行不能放头文件,编不过的

int c_set_and_get_color(int x)
{
    return set_and_get_color(x);
}

wrapper.h的内容如下

#ifdef __cplusplus      // 这个宏判断必不可少
extern "C" {
#endif

int c_set_and_get_color(int);

#ifdef __cplusplus
}
#endif

main.c的内容如下

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

int main(void)
{
    printf("color: %d\n", c_set_and_get_color(5666));
    return 0;
}

按照如下命令编译它们

g++ -c apple.cpp
g++ -c wrapper.cpp
gcc -c main.c
gcc -o test main.o apple.o wrapper.o -lstdc++    # 链接

注意到,wrapper.o是用g++编的,main.o是用gcc编的,而且wrapper.hwrapper.omain.o中都有用到,所以__cplusplus宏判断是必不可少的,因为对于wrapper.o来说,extern "C"是需要的,对于main.o来说又是不需要的。

总结

C++调C很简单,在需要调用C接口时用extern "C"将头文件括起来就行了,不需要改原来的代码(当然了,你想改也没问题)。

C调C++较麻烦,有两种方法:

  • 改旧的C++头文件,插入extern "C"后,C代码可直接用C++接口(以extern声明一下函数即可)。
  • 用C++写个wrapper模块,提供一些C接口,里面再去调用C++接口。
posted @ 2018-11-14 20:35  xcw0754  阅读(1678)  评论(0编辑  收藏  举报