CからC++のライブラリを呼び出す

大昔からある話だが,復習のためにやってみた.STLのlistをCから使ってみる.ポイントは,

  • C++ でCから呼び出すためのラッパを書く
  • ラッパ関数のプロトタイプ宣言は extern "C" で宣言する
  • このとき,C++で読まれる場合とCから読まれる場合を__cplusplusで切り分ける.
  • リンクはC++コンパイルドライバを使う

ラッパのヘッダファイル

まず,ラッパのヘッダファイルlist_wrapper.hを書く.list_int_t は void * としている.C++コンパイラでは__cplusplusが定義されているので,extern "C" が有効になる.これはC++での関数名のマングリングを行わないようにする仕掛け.これをいれることでC言語から同じ関数名で呼び出すことができるようになる.

typedef void * list_int_t;

#ifdef __cplusplus
extern "C" {
#endif
	list_int_t list_int_create();
	void       list_int_push_back(list_int_t, int);
	int        list_int_pop_front(list_int_t);
	void       list_int_delete(list_int_t);
#ifdef __cplusplus
};
#endif

ラッパを実装

定義したwrapperをlist_wrapper.cppで実装する.キャストしてlistのメソッドを呼び出しているだけ.

#include <list>
#include "list_wrapper.h"

using namespace std;
list_int_t list_int_create(){
	return (list_int_t) (new list<int>());
}

void list_int_push_back(list_int_t listPtr, int item){
	((list<int> *)listPtr)->push_back(item);
}

int list_int_pop_front(list_int_t listPtr){
	int tmp = ((list<int> *)listPtr)->front();
	((list<int> *)listPtr)->pop_front();
	return tmp;
}

void list_int_delete(list_int_t listPtr){
	delete ((list<int> *)listPtr);
}

Cプログラムを書く

作ったラッパを利用するCプログラムtest.cを書く.listを作って,1,2,3とプッシュ,とりだしてprintfし,listを破棄している.

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

main(){
	list_int_t l = list_int_create();

	list_int_push_back(l, 1);
	list_int_push_back(l, 2);
	list_int_push_back(l, 3);

	printf("%d\n", list_int_pop_front(l));
	printf("%d\n", list_int_pop_front(l));
	printf("%d\n", list_int_pop_front(l));

	list_int_delete(l);
}

コンパイルと実行

ラッパはg++で,テストプログラムはgccコンパイルする.リンクはg++で.実行してみるとちゃんと動くの確認できる.

$ g++ -c list_wrapper.cpp
$ gcc -c test.c
$ g++ -o test test.o list_wrapper.o
$ ./test
1
2
3

extern "C" の効果

extern "C" の効果をテストしてみる.こんなファイルを作ってテスト.

#ifdef EXTERN_C
extern "C" {
#endif
	int test(int);
#ifdef EXTERN_C
};
#endif

int test(int a){}

まずはEXTERN_Cを定義せずに,つまりextern "C" なしでコンパイル
し,nmコマンドで定義されているラベルを見る.

$ g++ -c test2.cpp
$ nm test2.o
00000000 T __Z4testi
00000000 A __Z4testi.eh

関数testが__Z4testi という名前で定義されていることが分かる.このように関数名に余分な情報が追加されるのは,引数が異なる同名関数を許すためだが,この名前だとC言語からは参照できない.

つぎにextern "C"つきでコンパイルしてラベルを見てみよう.

$ g++ -DEXTERN_C -c test2.cpp
$ nm test2.o
00000000 T _test
00000000 A _test.eh

こんどは_testという名前になっている.これでC言語から呼び出せる,というわけ.

gccでリンクすると

最後のリンクをg++でなくgccでやるとこんなエラーがでる.これは必要なライブラリがリンクされないため.またmainが呼ばれる前の初期化処理を行うcrt0もCとC++では異なるので,C++用のコンパイルドライバでリンクしないと動かない.

$ gcc -o test test.o list_wrapper.o
Undefined symbols:
  "operator new(unsigned long)", referenced from:
      _list_int_create in list_wrapper.o
      __gnu_cxx::new_allocator<std::_List_node<int> >::allocate(unsigned long, void const*)in list_wrapper.o
  "___gxx_personality_v0", referenced from:
      ___gxx_personality_v0$non_lazy_ptr in list_wrapper.o
  "operator delete(void*)", referenced from:
      _list_int_delete in list_wrapper.o
      __gnu_cxx::new_allocator<std::_List_node<int> >::deallocate(std::_List_node<int>*, unsigned long)in list_wrapper.o
  "std::_List_node_base::hook(std::_List_node_base*)", referenced from:
      std::list<int, std::allocator<int> >::_M_insert(std::_List_iterator<int>, int const&)in list_wrapper.o
  "std::__throw_bad_alloc()", referenced from:
      __gnu_cxx::new_allocator<std::_List_node<int> >::allocate(unsigned long, void const*)in list_wrapper.o
  "std::_List_node_base::unhook()", referenced from:
      std::list<int, std::allocator<int> >::_M_erase(std::_List_iterator<int>)in list_wrapper.o
ld: symbol(s) not found
collect2: ld returned 1 exit status