C と C++ の間で関数や変数を共有する

C++ で実装した関数を C から利用したい場合,あるいはその逆の場合がある.同じソースの中に両方のプログラムコードを書くことはできないが,それぞれのソースをコンパイルしてオブジェクトファイルを生成し,それらをリンクすることは可能だ.ただし C++ 特有の名前空間 (namespace) や関数オーバーロードに対処する必要がある.

introduction...

簡単なプログラムの場合は,図(a)のようにひとつのソースコードコンパイルして実行ファイル (*.out / *.exe) を生成している.しかし,プログラムの規模が大きくなった場合,ライブラリを作る場合,何人かで共同開発する場合などでは,図(b)のようにソースを小分けして個別にコンパイルしてオブジェクトファイル (*.o) を生成し,最後にそれらをリンカで統合して実行ファイルを生成する*1

図の水色の枠は gcc による処理を表していて,プログラムを小分けする場合は複数回 gcc を実行する必要がある*2.このように,プログラムを小分けするとかえって手間が掛かるように見えるが,ソースコードの一部を変更してもすべてのソースを再コンパイルする必要がなくなったりするため,コンパイルの速さが向上する.何より,個々のソースが C で書かれていても C++ で書かれていても,(あるルールさえ満たしていれば)リンカでちゃんとリンクできるという利点がある.このため前述したように複数人で開発する場合や,ライブラリ化には必須の方法だ.

以下では C++ で作った関数や変数を C から利用する場合と,その逆の場合について解説する.

オブジェクトファイルの生成とリンク

本題に入る前に,簡単にオブジェクトファイルの生成とリンクの方法を解説する.

簡単な C++ のプログラム:

// test1.cpp
#include <iostream>
void hprint(void)
{
  std::cout<<"hoge"<<std::endl;
}
int main(int argc, char**argv)
{
  hprint();
  return 0;
}

を,コンパイルする.ただし,実行ファイルを生成せずに,オブジェクトファイルにとどめる.これを実現するには, g++ で -c オプションをつけて実行する:

g++ test1.cpp -c

すると,オブジェクトファイル test1.o が生成される.このオブジェクトファイルをリンカでリンクするには,次のように g++ を実行する:

g++ test1.o

ここではオブジェクトファイルは1つだけだが,複数ある場合は, test1.o のあとに続けて書く.すると,実行ファイル a.out が生成される.なお,このリンカの段階で, C++ の標準ライブラリ libstdc++ が自動的にリンクされている.

C++ で作った関数や変数を C から利用する場合

さて,図(b)のような状況で,ソースコード1で実装されている関数をソースコード2で利用する場合,関数の実装部分を include する必要は無いが,関数の宣言は必要だ.このためプログラムを小分けする場合,ヘッダファイル (*.h) と呼ばれる,宣言のみを書き出したファイルを作っておく.ソースコード1のヘッダファイル1をソースコード2から include することで,ソースコード1がどのような関数を提供するのかがわかるようになる.

テクニック1: #ifdef __cplusplus 〜 #endif を使う

このヘッダファイル1は,ソースコード1からも include される.つまり C++ からも C からも include されることになるのだ. C++ では名前空間を使うから namespace の定義があるが,これは C ではエラーだ.こういうケースを防ぐために,

#ifdef __cplusplus
namespace hoge
{
#endif

のような実装をする. __cplusplus は C++ソースコードコンパイルしているときは必ず定義されていて, #ifdef __cplusplus は __cplusplus が定義されているときには #endif までのコードをコンパイルするようにコンパイラ(厳密にはプリプロセッサ)に指示を与えるものだ. C のソースコードコンパイルしているときは __cplusplus は定義されていないから,この部分は無視される.

テクニック2: extern "C" を使う

ここで,ちょっとした実験をしてみよう.まず,簡単な C のプログラム:

/* test1.c */
#include <stdio.h>
void hprint(const char* str)
{
  printf("hoge hoge %s\n",str);
}
int main(int argc, char**argv)
{
  hprint("from c");
  return 0;
}

コンパイルし,オブジェクトファイルを作る:

gcc test1.c -c

ここで生成された test1.o ファイルを nm コマンドで調べてみる:

nm test1.o

nm コマンドは,オブジェクトファイルに含まれるシンボル(関数名とか変数名)の一覧を表示するコマンドで,この場合,

00000000 T hprint

を含むリストが表示される. hprint は上のプログラムで作った関数であり,ちゃんとオブジェクトファイルの中に含まれていることがわかる.

一方,簡単な C++ のプログラム:

// test1.cpp
#include <iostream>
void hprint(const char* str)
{
  std::cout<<"hoge hoge "<<str<<std::endl;
}
int main(int argc, char**argv)
{
  hprint("from c++");
  return 0;
}

コンパイルし,オブジェクトファイルを作って, nm でシンボルを調べてみよう:

g++ test1.cpp -c
nm test1.o

すると,

00000000 T _Z6hprintPKc

を含むリストが表示される.上のプログラムで作った関数 hprint が見て取れるが,純粋な関数名だけではない.なぜこのような形でオブジェクトファイルが作られているかと言うと, C++ では関数のオーバーロードを許しているため,識別子だけではどの関数かが特定できないからだ.よって引数の情報まで含めてオブジェクトファイルに記録する必要がある.ついでに言うと,名前空間の情報も含まれる.

よって上の例で作った C の関数と C++ の関数は,同じ宣言を有するにもかかわらず,オブジェクトファイルには異なる宣言であるかのように記録される.このため, C++ のソースをコンパイルしてできたオブジェクトファイルに定義されている関数を, C のプログラムからは利用できない(逆も然り).これを可能にするためには,C++ で定義する関数が C の形式でオブジェクトファイルに書き込まれるようにしなければならない.それには,関数宣言に extern "C" をつければよい:

// test1.cpp
#include <iostream>
extern "C" void hprint(const char* str)
{
  std::cout<<"hoge hoge "<<str<<std::endl;
}
int main(int argc, char**argv)
{
  hprint("from c++");
  return 0;
}

これをコンパイルし,オブジェクトファイルを作って, nm でシンボルを調べてみる:

g++ test1.cpp -c
nm test1.o

すると,

00000000 T hprint

となって, C とまったく同じ形で関数が書き込まれていることがわかる.これにより, C++ のソースをコンパイルしてできたオブジェクトファイルに定義されている関数を, C のプログラムから利用できるようになる.

なお, extern "C" は C++ 特有の指定子なので, C でコンパイルするときはエラーになる.これは前述の #ifdef __cplusplus で切替えればよい.

テクニック3: -lstdc++ をつけてコンパイル(リンク)する

上の C++ プログラムは iostream ライブラリを利用している.これは C++ の標準ライブラリなので自動的に libstdc++ がリンクされるため,このライブラリの関数やオブジェクトを使っていても,明示的にリンクオプションを書く必要が無い.しかし C から C++ で書かれたプログラムを使う場合は, libstdc++ が自動リンクされないため,このリンクを明示的に指示しないと,コンパイラC++ のオブジェクトや関数が定義されていない(上の例だと std::cout とか std::endl の定義がない)というエラーを吐く.

これを解決するには,コンパイラ(リンカ)に -lstdc++ オプションをつけて libstdc++ を使うことを明示すればよい(具体例は後述).

テクニック4: グローバル変数も extern "C"

基本的に使わない方がいいのだが….まったく使わないのも無理な話なので,解説する.

変数 int a はグローバル変数で,ソースコード1で「定義」されているとする.この変数をソースコード2から参照したい場合,ヘッダファイル1に a の「宣言」を書いておく必要がある.ここで,ヘッダファイル1に

int a;

と書いてしまうと,リンクの段階で,変数 a が2重定義されたというエラーになる.なぜかと言うと,ヘッダファイル1はソースコード1からも2からも include されているから,両方のソースで「定義」(実際にメモリ割り当てが行われる)されてしまっているからだ.
正しくは,ヘッダファイル1には

extern int a;

と書いて変数 a を「宣言」(メモリ割り当てはしない)し,ソースコード1で

int a;

のように,変数 a を「定義」する必要がある.

C++ と C で共有するためには,名前空間の壁があるから,ヘッダファイル1には

extern "C" int a;

と書かなくてはならない.また, C では extern "C" を認識しないから(extern は認識する),

  extern
  #ifdef __cplusplus
    "C"
  #endif
      int a;

のようにして切替える必要がある.ただし以下のサンプルのように,もっとスマートに書ける.

サンプル

それでは,簡単なプログラムで, C++ で定義した関数や変数を C から利用できることを確認しよう.

まず C++ でライブラリを作る.このライブラリは,グローバル変数 int a と,適当な文字列を出力する関数 hprint を提供するものとする.このヘッダファイル (test2.h) は,

// test2.h
#ifdef __cplusplus
namespace hoge
{
extern "C" {
#endif

  extern int a;
  void hprint(const char*);

#ifdef __cplusplus
};
};
#endif

のように書かれる.名前空間と extern "C" は C++ でしか使えないから, #ifdef __cplusplus で切替えている.ここで, extern "C" {...} は,括弧でくくった部分をすべて extern "C" 扱いにするという記法だ.

このライブラリの実装 (test2.cpp) は,例えば以下のようにすればよい:

// test2.cpp
#include <iostream>
#include "test2.h"
namespace hoge
{
int a(0);
extern "C" void hprint(const char* str)
{
  std::cout<<"hoge hoge "<<str<<std::endl;
  std::cout<<"a= "<<a<<std::endl;
}
};

このコードは C++ で書かれており,ライブラリも C++ 固有のものを使ってよい.

このライブラリをコンパイルするには,

g++ test2.cpp -c

とする. test2.o が生成される.

次に, C のプログラム (main.c) からこのライブラリを利用する:

// main.c
#include "test2.h"
int main(int argc, char**argv)
{
  a=49;
  hprint("from c");
  return 0;
}

コンパイルは,

gcc main.c -c
gcc main.o test2.o -lstdc++

のように2段階に分けてもいいし,まとめて

gcc main.c test2.o -lstdc++

としてもいい.後者は main.o を生成しない.いずれも a.out が生成される.ポイントは C++ の標準ライブラリ -lstdc++ をリンクしている点だ.これをしないといけない理由は, test2.o で iostream ライブラリを使っているからである. a.out を実行すると,

hoge hoge from c
a= 49

のように,正常に動作していることがわかる.

比較のために, C++ のプログラム (main.cpp) から利用する方法も述べておく.ソースは:

// main.cpp
#include "test2.h"
using namespace hoge;
int main(int argc, char**argv)
{
  a=100;
  hprint("from c++");
  return 0;
}

コンパイルは,

g++ main.cpp test2.o

とすればよい(Cと同様に,分けてもいい). a.out を実行すると,

hoge hoge from c++
a= 100

と出力される.

C で作った関数や変数を C++ から利用する場合

ここまでの話を理解していれば,割と簡単.これまでに述べたテクニックしか使わないので,いきなりサンプルから.

サンプル

今度は C でライブラリを作る.test2 と同様,グローバル変数 int a と,適当な文字列を出力する関数 hprint を提供する.このヘッダファイル (test3.h) は,

// test3.h
#ifdef __cplusplus
extern "C" {
#endif

  extern int a;
  void hprint(const char*);

#ifdef __cplusplus
};
#endif

のようにすれば, C++ でも使えるような設計になる.実装 (test3.c) は

// test3.c
#include <stdio.h>
#include "test3.h"
int a=0;
void hprint(const char* str)
{
  printf("hoge hoge %s\n", str);
  printf("a= %i\n", a);
}

のような感じ. C のプログラムなので, stdio.h などのライブラリを使う.

コンパイルは,

gcc test3.c -c

でいい. test3.o が生成される.

次に,これを C++ のプログラム (main.cpp) から利用する.プログラムは

// main.cpp
#include "test3.h"
int main(int argc, char**argv)
{
  a=100;
  hprint("from c++");
  return 0;
}

のように書けばよい.

g++ main.cpp test3.o

コンパイルして ./a.out で実行すると,

hoge hoge from c++
a= 100

と出力される.

C から利用するプログラム (main.c) も一応:

// main.c
#include "test3.h"
int main(int argc, char**argv)
{
  a=49;
  hprint("from c");
  return 0;
}

コンパイル

gcc main.c test3.o

以下略.

まとめ

長くなったので,簡単にまとめ.

  • テクニック1: #ifdef __cplusplus 〜 #endif を使う
    • C++ と C でコードを切替えるときに使う.
  • テクニック2: extern "C" を使う
  • テクニック3: -lstdc++ をつけてコンパイル(リンク)する
    • C++ で作ったプログラムを C から使うときには, libstdc++ をリンクする必要がある.
  • テクニック4: グローバル変数も extern "C"

参考文献

*1:オブジェクトファイルではなく,シェアドオブジェクト (*.so) もしくはダイナミックリンクライブラリ (*.dll) を作る場合もある.これらの違いは静的(=コンパイル時)にリンクするか,動的(=実行時)にリンクするか,だ.

*2:リンカの部分で gcc を使う代わりに ld でもよいのだが, gcc では内部で自動的に ld を呼び出しているから一緒.