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を使いたい場合はどうするのかというのも要調査ポイント。
※本記事内容の無断転載を禁じます。
ご連絡は以下アドレスまでお願いします★
Wav2Lipのオープンソース版を改造して外部から呼べるAPI化する
Wav2Lipのオープンソース版で静止画の口元のみを動かして喋らせる
【iOS】アプリアイコン・ロゴ画像の作成・設定方法
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【2】
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【1】
【Xcode】iPhone is not available because it is unpairedの対処法
【Let's Encrypt】Failed authorization procedure 503の対処法
【Debian】古いバージョンでapt updateしたら404 not foundでエラーになる場合
ファイアウォール内部のWindows11 PCにmacOS Sequoiaからリモートデスクトップする
【Windows10】リモートデスクトップ間のコピー&ペーストができなくなった場合の対処法
Windows11+WSL2でUbuntuを使う【2】ブリッジ接続+固定IPの設定
【C/C++】小数点以下の切り捨て・切り上げ・四捨五入
Windows11のコマンドプロンプトでテキストをコピーする
【Apache】サーバーに同時接続可能なクライアント数を調整する
GitLabにHTTPS経由でリポジトリをクローン&読み書きを行う
緯度経度の度単位10進数表現とミリ秒表現の相互変換
apt upgradeしたあとnvidia-smiがダメになった場合
Googleスプレッドシートを編集したら自動で更新日時を入れる