原文中的接口定义可参考本系列的前一篇文章,我们直接来看在 C# 中如何定义。
接口定义
使用 DllImport 特性来声明对 C 动态库的调用,并将回调函数作为参数传递。
[DllImport("XXX.dll")]
public static extern IntPtr Init(string pcPayDeviceIP, int usTlsPort, OnPayResult onPayResult);
委托定义
回调函数在 C# 中通过委托来表示。为了确保与 C 代码的调用约定一致,必须使用 UnmanagedFunctionPointer 特性并指定 CallingConvention.Cdecl。这是 平台调用 中处理回调的关键步骤。
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void OnPayResult(IntPtr pstPayResult);
委托绑定接口及调用
定义一个委托实例,并将其传递给原生的 Init 函数。
OnPayResult onPayResult += new OnPayResult(OnPayResult);
Init(pcPayDeviceIP, usTlsPort, onPayResult);
接口定义
在 OnPayResult 方法内部,核心任务是将从 C 层传来的指针数据安全地转换为 C# 可用的结构。这里定义了一个抽象方法 PayResult 供子类实现具体业务逻辑。
protected abstract void PayResult(PayResult payResult);
private void OnPayResult(IntPtr pstPayResult)
{
PayResultTmp st = (PayResultTmp)Marshal.PtrToStructure(pstPayResult, typeof(PayResultTmp));
PayResult payResult = new PayResult();
payResult.place = enPlace.SD_D_PLACE_BUTT;
payResult.place = (enPlace)Marshal.ReadInt32(pstPayResult);
payResult.errCode = PtrToUtf8String(st.errCode);
payResult.errInfo = PtrToUtf8String(st.errInfo);
payResult.seqId = PtrToUtf8String(st.seqId);
payResult.merOrderId = PtrToUtf8String(st.merOrderId);
payResult.srcReserve = PtrToUtf8String(st.srcReserve);
payResult.attachedData = PtrToUtf8String(st.attachedData);
payResult.totalAmount = PtrToUtf8String(st.totalAmount);
payResult.couponAmount = PtrToUtf8String(st.couponAmount);
payResult.payAmount = PtrToUtf8String(st.payAmount);
payResult.payTime = PtrToUtf8String(st.payTime);
payResult.orderCreateTime = PtrToUtf8String(st.orderCreateTime);
payResult.status = PtrToUtf8String(st.status);
PayResult(payResult);
}
由于回调函数传过来的是 C 语言申请的内存,其内存布局与 C# 的结构体可能不一致,因此必须通过 Marshal 类提供的方法进行显式转换。如果直接进行强制类型转换,极有可能引发内存访问异常,导致程序崩溃。这正是 C/C++ 与托管环境交互时需要特别注意的细节。
结构体定义
需要定义两个结构体。PayResultTmp 用于精确匹配 C 层的内存布局,其字符串字段均为 IntPtr 类型。而 PayResult 则是供 C# 业务逻辑使用的友好结构,字段为 string 类型。
[StructLayout(LayoutKind.Sequential)]
private struct PayResultTmp
{
public enPlace place; //收单机构
public IntPtr errCode; //错误码
public IntPtr errInfo; //错误信息
public IntPtr seqId; //平台流水号
public IntPtr merOrderId; //商户订单号
public IntPtr srcReserve; //请求系统预留字段
public IntPtr attachedData; //商户附加数据
public IntPtr totalAmount; //订单金额
public IntPtr couponAmount; //优惠金额
public IntPtr payAmount; //实付金额
public IntPtr payTime; //支付时间
public IntPtr orderCreateTime; //订单创建时间
public IntPtr status; //交易状态
}
[StructLayout(LayoutKind.Sequential)]
public struct PayResult
{
public enPlace place; //收单机构
public string errCode; //错误码
public string errInfo; //错误信息
public string seqId; //平台流水号
public string merOrderId; //商户订单号
public string srcReserve; //请求系统预留字段
public string attachedData; //商户附加数据
public string totalAmount; //订单金额
public string couponAmount; //优惠金额
public string payAmount; //实付金额
public string payTime; //支付时间
public string orderCreateTime; //订单创建时间
public string status; //交易状态
}
实现
最后,通过继承包含抽象方法 PayResult 的基类,并重写该方法,即可实现回调的具体业务逻辑。这样就将底层的 C 回调与上层的 C# 业务逻辑清晰地连接了起来。
protected override void PayResult(PayResult payResult)
{
Console.WriteLine("errCode:{0}, errInfo:{1}", payResult.errCode, payResult.errInfo);
}