アプリケーション開発ポータルサイト
ServerNote.NET
カテゴリー【C/C++
C言語からC++クラスを生成/呼出/破棄する
POSTED BY
2021-03-01

共同開発において自分はC++で作っているが、他の人はC言語という場合、自分の作ったC++クラスライブラリをそのまま渡すことはできないので、C言語形式のライブラリ関数化して渡すことが通例となる。

つまり、C言語側から見ると、C++を意識せずにCのライブラリ関数として、

・ライブラリ初期化関数を呼びライブラリハンドルを受け取る

・ライブラリハンドルを指定してライブラリ各機能関数を呼ぶ

・ライブラリ終了化関数を呼び破棄する

のような感じでC++クラス機能を使うことになる。これを実現するには、C++側で上記C言語形式のライブラリ関数を実装し、extern Cでエクスポートすればよい。そしてC言語側はリンクの際gccでなくg++を使えばよい。

1、コーディング

まずは、C言語側でインクルードしてもらうライブラリ関数ヘッダを用意する。

C/C++test.hGitHub Source
/*
 C++クラス生成/呼出/破棄 C言語インタフェース
*/
#ifndef __test_h__
#define __test_h__

#include <stdio.h>
#include <stdarg.h>

#ifdef __cplusplus
extern "C" {
#endif

extern void* test_new(void);
extern int test_print(const void *handle, const char *fmt, ...);
extern int test_delete(void *handle);

#ifdef __cplusplus
};
#endif

#endif /* __test_h__ */

関数をextern Cで囲えば、コンパイル時にC言語のルールで関数オブジェクト名が定義されるため、そのままCで呼ぶことができるという仕組み。

・test_newはC++側内部でnew関数でクラスを生成しそのポインタを返す。

・test_printは実体はC++クラスポインタであるvoid*ポインタを受け取りクラスのprint関数を呼んでC++の機能を使用する。

・test_deleteは不要になったC++クラスポインタを指定しC++側内部でdeleteが呼ばれる。

次に、C++側独自のヘッダーファイルを定義する。これはC側からは意識しないC++クラス定義ヘッダファイルとなる。

C/C++test_cpp.hGitHub Source
//
// テストC++クラス定義ヘッダ
//
#ifndef __test_cpp_h__
#define __test_cpp_h__

#include <string>
#include "test.h"

class CTest {
public:
  CTest(void);
  ~CTest(void);
  void print( const std::string &str );
};

#endif // __test_cpp_h__

そして次のファイルで、test.h, test_cpp.h の実体をコーディングする。

C/C++test.cppGitHub Source
//
// テストC++クラス定義
//
#include <iostream>
#include <string>
#include "test_cpp.h"

CTest::CTest(void) {
  std::cout << "CTest::constructor" << std::endl;
}

CTest::~CTest(void) {
  std::cout << "CTest::destructor" << std::endl;
}

void CTest::print( const std::string &str ) {
  std::cout << "CTest::print" << std::endl;
  std::cout << str;
}

extern void* test_new(void) {

  CTest *handle = new CTest();

  return (void*)handle;
}

extern int test_print(const void *handle, const char *fmt, ...) {
  int r = (-1);

  if(handle) {

    va_list va; char buf[2048];
    va_start( va,fmt );
    r = vsnprintf( buf,sizeof(buf),fmt,va );
    va_end( va );

    if(r >= 0) {
      std::string s = std::string(buf);
      ((CTest*)handle)->print(s);
    }
  }
  
  return r;
}

extern int test_delete(void *handle) {

  if(handle) {

    delete (CTest*)handle;

  }

  return 0;
}

まずはC++クラスCTestのコンストラクタ、デストラクタ、std::stringをstd::coutするprint関数を実装する。それぞれのメソッドが呼ばれたらstd::coutでその旨を出力している。

そして最後に、エクスポートするC言語形式関数の実体をコーディングする。

test_newはCTestをnewし(void*)にキャストしてC言語側に返却

test_printはvoid*ハンドルをCTestポインタにキャストしてクラスメソッドprintを呼ぶ(C++標準クラス std::string, coutの機能を使う)

test_deleteはvoid*ハンドルをCTestポインタにキャストしてdelete破棄する(デストラクタを呼ぶ)。

これでC言語側に使ってもらうべきクラスライブラリが完成したので、C言語側は以下main.cでtest.hのみを参照しコーディングする。

C/C++main.cGitHub Source
/*
 C++クラス生成/呼出/破棄 C言語メインプログラム
*/
#include "test.h"

extern int main(int argc, char **argv) {

  void *handle = test_new();

  if(handle) {

    test_print(handle, "てすとぷりんと\n");

    test_delete(handle);

  }

  return 0;
}

2、コンパイル、テスト

まずはC++クラスライブラリ側をコンパイル。

本来であればMakefileを書いてライブラリファイルを出力(.a, .so)するところだが、ここでは簡単にオブジェクトファイルの出力としている。

/usr/bin/g++ -c test.cpp

出来上がったtest.oオブジェクトファイルの名前マップをnmで確認

nm test.o

00000000000002d7 t _GLOBAL__sub_I__ZN5CTestC2Ev
                 U _Unwind_Resume
000000000000029a t _Z41__static_initialization_and_destruction_0ii
0000000000000054 T _ZN5CTest5printERKSs
0000000000000000 T _ZN5CTestC1Ev
0000000000000000 T _ZN5CTestC2Ev
000000000000002a T _ZN5CTestD1Ev
000000000000002a T _ZN5CTestD2Ev
                 U _ZNSaIcEC1Ev
                 U _ZNSaIcED1Ev
                 U _ZNSolsEPFRSoS_E
                 U _ZNSsC1EPKcRKSaIcE
                 U _ZNSsD1Ev
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 b _ZStL8__ioinit
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
                 U _ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSbIS4_S5_T1_E
0000000000000038 r _ZZL18__gthread_active_pvE20__gthread_active_ptr
                 U _ZdlPv
                 U _Znwm
                 U __cxa_atexit
                 U __dso_handle
                 U __gxx_personality_v0
                 w __pthread_key_create
0000000000000261 T test_delete
0000000000000093 T test_new
00000000000000dc T test_print
                 U vsnprintf

extern Cの効果により、C言語形式で呼びたい関数test_delete, test_new, test_print, ついでにvsnprintfには、_ZN5などの接尾辞がついておらず、Cからそのまま呼べる名前になっているのがわかる。

そして、C言語側メインプログラムをコンパイル&test.oをリンクする。実体がC++であるtest.oをリンクするのでgccでなくg++で行う

なおこれらも本来であればMakefileを書いてコンパイル・リンクするところだが、ここでは簡単に手動コマンドを打っている。

/usr/bin/g++ main.c test.o

出来上がった実行ファイルを実行してテスト

./a.out

CTest::constructor
CTest::print
てすとぷりんと
CTest::destructor

無事、CからC++の機能を使えていることが確認できる。

しかしながら、newしたポインタをC言語側にvoid*で返して使いまわして良いものかどうか??という不安は残る。
さらに、newポインタでなくstd::shared_ptrを使いたい場合はどうするのかというのも要調査ポイント。

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【webmaster/管理人】
自営業プログラマー
ご連絡は以下アドレスまで★

☆お仲間ブログ1↓
匠のコーヒーブレイク
☆お仲間ブログ2↓
一人社長の不動産業務日誌
【キーワード検索】