2024-02-28
共同開発において自分はC++で作っているが、他の人はC言語という場合、自分の作ったC++クラスライブラリをそのまま渡すことはできないので、C言語形式のライブラリ関数化して渡すことが通例となる。
つまり、C言語側から見ると、C++を意識せずにCのライブラリ関数として、
・ライブラリ初期化関数を呼びライブラリハンドルを受け取る
・ライブラリハンドルを指定してライブラリ各機能関数を呼ぶ
・ライブラリ終了化関数を呼び破棄する
のような感じでC++クラス機能を使うことになる。これを実現するには、C++側で上記C言語形式のライブラリ関数を実装し、extern Cでエクスポートすればよい。そしてC言語側はリンクの際gccでなくg++を使えばよい。
1、コーディング
まずは、C言語側でインクルードしてもらうライブラリ関数ヘッダを用意する。
C/C++ | test.h | GitHub 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.h | GitHub 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.cpp | GitHub 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.c | GitHub 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を使いたい場合はどうするのかというのも要調査ポイント。
※本記事内容の無断転載を禁じます。
ご連絡は以下アドレスまでお願いします★
Intel Macbook2020にBootCampで入れたWindows11 Pro 23H2のBluetoothを復活させる
Windowsのデスクトップ画面をそのまま配信するための下準備
WindowsでGPUの状態を確認するには(ASUS系監視ソフトの自動起動を停止する)
CORESERVER v1プランからさくらインターネットスタンダートプランへ引っ越しメモ
さくらインターネットでPython MecabをCGIから使う
さくらインターネットのPHPでAnalytics-G4 APIを使う
インクルードパスの調べ方
【Git】特定ファイルを除外する.gitignore
【Ubuntu/Debian】NVIDIA関係のドライバを自動アップデートさせない
【Windows10】リモートデスクトップ間のコピー&ペーストができなくなった場合の対処法
【Apache】サーバーに同時接続可能なクライアント数を調整する
Windows版Google Driveが使用中と言われアンインストールできない場合
Windows11+WSL2でUbuntuを使う【2】ブリッジ接続+固定IPの設定
【C/C++】小数点以下の切り捨て・切り上げ・四捨五入
GitLabにHTTPS経由でリポジトリをクローン&読み書きを行う
cannot guess build type; you must specify oneと言われた場合
VirtualBoxの仮想マシンをWindows起動時に自動起動し終了時に自動サスペンドする
Pythonで処理にかかった時間を計測するには