C++をC&Cにラップする
C++をC&Cにラップする
C版SDK実装の説明
JAKA C版SDKは、C++版SDKを基にラップして実装されたものであり、以下にC++実装をCインターフェースとしてラップする方法を紹介します。
C版データ構造の定義
C++版SDKでは、JAKAZuRobotクラスが定義されており、ロボットのすべてのデータとメソッドをカプセル化しています。しかし、C言語自体にはクラスやオブジェクトの概念がありません。したがって、C++オブジェクトをC言語で操作可能な構造体にラップする必要があります。実装方法は以下の通りです。
typedef struct _jkrobot {
JAKAZuRobot *robot = nullptr;
std::string ip = "";
} jkrobot;
// jkrobot構造体には、C++クラス JAKAZuRobot へのポインタ(robot)とそのロボットインスタンスのIPアドレス(ip)が含まれます。このようにして、C++のオブジェクト(JAKAZuRobot)をC言語で扱える構造体としてラップしました。```
### 複数ロボットインスタンスの管理
複数のロボットインスタンスを管理するために、グローバルコンテナ std::map<int, jkrobot \*> HD を使用します。キー(key)は整数で、値(value)はjkrobot構造体へのポインタです。このコンテナは、ロボットのハンドル(handle)の保存および検索に使用されます。複数ロボットを管理する場合、ハンドル(handle)は各ロボットを一意に識別するために使用できます。
```c++
static std::map<int, jkrobot *> HD;
static std::mutex G_HD_MTX;// HDコンテナへのスレッド安全なアクセスを保証するために std::mutex を使用します。HDコンテナ内の要素を追加、削除、または検索する際には、ロック(lock)とアンロック(unlock)を行う必要があります。```
マルチスレッド環境下で HD コンテナを操作する際のデータ競合を防ぐため、コードでは std::mutex を用いてロック処理を行っています。G\_HD\_MTX.lock() および G\_HD\_MTX.unlock() によって、HDコンテナの各変更がスレッド安全な環境下で実行されることが保証されます。
### ロボットハンドルの作成と破棄
create\_handler関数によって、C++オブジェクトの作成と初期化をラップします。関数内でjkrobotオブジェクトを生成し、JAKAZuRobot::login\_inを呼び出します。成功した場合、ハンドルを割り当ててグローバルハンドルテーブルを更新し、成功を返します。失敗した場合は、クリーンアップを行ってエラーコードを返します。対応するように、destory\_handler関数を通じてロボットインスタンスを破棄します。この関数はhandleを介してロボットオブジェクトを見つけ、login\_outメソッドを呼び出し、最後にオブジェクトを削除してメモリを解放します。
### C++クラスメソッドをCインターフェースにラップする説明
C++のメソッドは通常オブジェクト指向であり、C言語ではクラスやオブジェクトを直接操作できないため、構造体を介して間接的にC++クラスのメソッドを呼び出します。これを実現するために、コードはマクロ定義によってC++メソッドをC言語インターフェースとしてラップします。マクロ定義を使用して繰り返しコードを簡略化します。
例えば、JAKA\_C\_API\_DEF\_0 マクロ定義は power\_on() のような引数なしのC++メソッドをC言語関数にラップします:
```c++
#define JAKA_C_API_DEF_0(funcname) \
errno_t funcname(const JKHD *handle) { \
HANDLER_CHECK(); \
return HD[*handle]->robot->funcname(); \
}
// ここで、JKHD は型エイリアスであり、ロボットハンドルを表します。HANDLER_CHECK() マクロはロボットハンドルが有効かどうか(対応する jkrobot 構造体ポインタが nullptr でないか)を確認するために使用されます。ハンドルが有効な場合、HD[*handle]->robot->funcname() によって対応するC++メソッドを呼び出します。
JAKA_C_API_DEF_0(power_on) //C++のpower_onメソッドを呼び出します。
JAKA_C_API_DEF_0(enable_robot)//C++のenable_robotメソッドを呼び出します。```
同様に、JAKA\_C\_API\_DEF\_1 マクロ定義はget\_robot\_status()のような1つの引数を持つC++メソッドをC関数としてラップします:
```c++
#define JAKA_C_API_DEF_1(funcname, type0) \
errno_t funcname(const JKHD *handle, type0 _v0) { \
HANDLER_CHECK(); \
return HD[*handle]->robot->funcname(_v0); \
}
JAKA_C_API_DEF_1(get_robot_status, RobotStatus *)//C++のget_robot_statusメソッドを呼び出します。
JAKA_C_API_DEF_1(set_debug_mode, BOOL)//C++のset_debug_modeメソッドを呼び出します。```
その他の複数引数を持つ関数のラップも同様で、funcnameはC言語の関数として直接C++のメンバー関数を呼び出し、対応する引数を渡すことができます。
## C#版SDKの実装説明
JAKA C#版SDKはC版SDKを基にしたラップ実装です。C の関数を C# のインターフェースとしてラッピングすることは、一般的なクロス言語呼び出しの方法であり、通常は .NET の DllImport 属性を使用してプラットフォーム呼び出し(P/Invoke)メカニズムによって実現されます。
* P/Invoke の概要:P/Invoke(Platform Invocation Services)は、マネージコード(例:C#)とネイティブコード(例:C または C++)の間の相互運用を可能にします。P/Invoke を使用すると、C# コードは C または C++ で記述された DLL 内の関数を直接呼び出すことができ、複雑な相互運用コードを手動で記述する必要がありません。
* DllImport 属性:DllImport 属性は、外部の動的リンクライブラリ(DLL)からインポートされる関数を宣言するために使用されます。これにより、C# コードは C または C++ で記述された関数を呼び出し、その関数にデータを渡すことができます。
以下では、C バージョンの SDK を基に C# ライブラリをラッピングする実装方法を詳しく説明します。
### DllImport を使用して C インターフェースをラッピングする
次のサンプルコードは、create_handler という名前の C 関数を宣言しています。この関数は、IP アドレス(文字配列 char[] または文字列 string)と handle(整数の参照であり、作成されたハンドラー識別子を返す)を受け取り、呼び出しの結果を表す整数(成功または失敗のフラグ)を返します。
```c#
[DllImport("jakaAPI.dll", EntryPoint = "create_handler", ExactSpelling = false, CallingConvention = CallingConvention.Cdecl)]
public static extern int create_handler(char[] ip, ref int handle, bool use_grpc = false);
DllImport パラメーターの説明:
"jakaAPI.dll":呼び出す DLL ファイル名を指定します。
EntryPoint = "create_handler":DLL 内で呼び出す具体的な関数名を指定します。
ExactSpelling = false:P/Invoke に大文字小文字の厳密な一致を求めないよう指示します。
CallingConvention = CallingConvention.Cdecl:呼び出し規約として C 言語標準の cdecl を指定します。これにより、引数と戻り値のスタック上での処理方法が C 言語の慣習と一致します。
次の2点に注意が必要です:
C の動的ライブラリ内の関数宣言を十分に理解しておくこと。具体的には、関数名、引数および戻り値の型、呼び出し規約(例:__cdecl または __stdcall)など。
System.Runtime.InteropServices 名前空間を参照する必要があります。この名前空間はクロスプラットフォーム呼び出しの中核的なサポートを提供します。
ラッピング処理の注意点
データ型のマッピング
C と C# のデータ型は異なるため、クロス言語呼び出し時にはデータ型を対応させる処理が必要です。
一般的な型対応表:
| C 型 | C# 型 | 説明 |
|---|---|---|
整数(int) | 整数(int) | 整数型は直接対応します。 |
double | double | 浮動小数点型の直接マッピング。 |
char* | string または char[] | C# の string に対応し、メモリ処理のため MarshalAs 属性を指定する必要があります。 |
int* | ref int または out int | C のポインタの代わりに ref または out 修飾子を使用します。 |
struct | struct(StructLayout が必要``) | カスタム構造体は StructLayout を使用してレイアウト方式を指定する必要があります。 |
C バージョンで比較的複雑な構造体は、C# 側で再宣言し、フィールドのメモリレイアウトが C と一致していることを確認する必要があります。
例えば、C には次の構造体があります:
/**
* @brief 方向付きの直交座標位置
*/
typedef struct {
double x; ///< x 軸、単位:mm
double y; ///< y 軸、単位:mm
double z; ///< z 軸、単位:mm
} CartesianTran;
C# では StructLayout を用いて再定義する必要があります:
/**
* @brief 直交空間位置データ型
*/
[StructLayout(LayoutKind.Sequential)]
public struct CartesianTran
{
public double x; ///< x 軸、単位:mm
public double y; ///< y 軸、単位:mm
public double z; ///< z軸、単位:mm
};
- [StructLayout(LayoutKind.Sequential)]:フィールドを宣言順に配置し、C のデフォルトのメモリアラインメント方式に一致させます。
オプション引数とデフォルト値のカプセル化
C# はデフォルト値付きパラメーターをサポートしていますが、C では通常、同様の機能を明示的に引数を渡して実現します。カプセル化時に C# メソッド内で直接デフォルト値を定義できます:
[DllImport("jakaAPI.dll", EntryPoint = "create_handler", CallingConvention = CallingConvention.Cdecl)]
public static extern int create_handler([MarshalAs(UnmanagedType.LPStr)] string ip, ref int handle, bool use_grpc = false);
// このように、呼び出し元は毎回 use_grpc パラメーターを渡す必要がなく、デフォルト値が自動的に有効になります。```
#### アンマネージドリソースの解放
関数がメモリ割り当て(文字列ポインターや構造体ポインターなど)を伴う場合、リソースの解放に注意が必要です:
* Marshal クラスの FreeHGlobal メソッドを使用して割り当てられたメモリを解放します。
* 返されるアンマネージドリソース(ポインターなど)には対応する解放関数を呼び出す必要があります。