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)可以用来唯一标识每一个机器人。
static std::map<int, jkrobot *> HD;
static std::mutex G_HD_MTX;//使用了 std::mutex 来确保对 HD 容器的线程安全访问。每次添加、删除或查找 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语言函数:
#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函数:
#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 中的 Dlllmport属性通过平台调用(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(一个整数引用,返回创建的处理器标识符),并返回一个整数,表示调用的结果(可能是成功或失败的标志)。
[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语言约定一致。
需要注意以下两点:
确保熟悉 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 | 使用 ref 或 out 修饰符代替 C 的指针。 |
struct | struct (需 StructLayout ) | 自定义结构体需要使用 StructLayout 指定布局方式。 |
对于C版本中相对复杂的结构体需要在 C# 中重新声明,并确保字段的内存布局与 C 一致。
例如 C 中有以下结构体:
/**
* @brief cartesian position with orientation
*/
typedef struct {
double x; ///< x axis,unit: mm
double y; ///< y axis,unit: mm
double z; ///< z axis,unit: mm
} CartesianTran;
在 C# 中需要用StructLayout重新定义:
/**
* @brief Cartesian spatial position data type
*/
[StructLayout(LayoutKind.Sequential)]
public struct CartesianTran
{
public double x; ///< x axis,unit: mm
public double y; ///< y axis,unit: mm
public double z; ///< z axis,unit: 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 方法释放分配的内存。
对返回的非托管资源(如指针)需调用相应释放函数。