C#调用C++Dll封装时遇到的一系列问题

来源:互联网 发布:好用的数据采集器软件 编辑:程序博客网 时间:2024/05/22 04:33

最近帮底层开发的同时用C#重新封装一下dll,也就是用C#类来封装C++Dll里的方法,以供用户使用。

之前也用到过类似的应用,大多数问题都出在类型转换上,但是这次的应用层出不穷,所以在这里总结一下,以供自己以后查阅,也希望对大家能够有所帮助。

  

首先,重复一下一些基本使用方法。具体的那些方式在这里就不重复讲了,网上很多的。比如http://blog.csdn.net/sunboyljp/archive/2009/12/31/5110639.aspx

c++ 头文件中的定义:

NPD_API int NP_Init();

C#中定义函数

[DllImport("npd_api.dll")]

public static extern int NP_Init();

基本类型转换见下表(我用到过的):

BSTR——StringBuilder

LPCTSTR ——StringBuilder

LPCWSTR ——IntPtr

handle ——IntPtr

hwnd ——IntPtr

char * ——string

int * ——ref int

int & ——ref int

void * ——IntPtrs

unsigned char * ——ref byte

BOOL ——bool

DWORD ——uint或int(我用的是uint,没出过什么问题)

我的问题来了,长期的经验教训我知道了:

1、指针做参数时在C#中一定要使用ref 或out关键字,尤其是结构体指针,要不会报内存读取错误,即使不报错数据也是不太对的。呵呵

   SIPCLIENT_API void WINAPI SCCleanup(SipClient * psip);

   [DllImport("sipclient.dll")]
public static extern void SCCleanup(ref SipClient psip);

  其中SipClient是一个结构体。 

2、重写结构体的时候,之前有指明类型长度或数组长度的地方,也要进行相应的标注,要不也会导致内存错误。       

代码
 typedef struct {      char sDVRIP[16]; /* DVR IP地址 */      char sDVRIPMask[16]; /* DVR IP地址掩码 */      DWORD dwNetInterface; /* 10M/100M自适应,索引 */      WORD wDVRPort; /* 端口号 */      BYTE byMACAddr[MACADDR_LEN]; /* 服务器的物理地址 */  }NET_POSA_ETHERNET;
   public struct NET_POSA_ETHERNET { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string sDVRIP; //DVR IP地址 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string sDVRIPMask; // DVR IP地址掩码 public uint dwNetInterface; //网络接口 1-10MBase-T 2-10MBase-T全双工 3-100MBase-TX 4-100M全双工 5-10M/100M自适应 public uint wDVRPort; //端口号 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public byte[] byMACAddr; //[MACADDR_LEN]; //PPPoE用户名//服务器的物理地址 }

3、遇到这样一个问题,折腾了大半天时间——http://space.cnblogs.com/q/16616/。

//////////////////////////////////////////////////////////////////////////////////////////////////

一直都觉得c#调用c++dll这块很麻烦,不过要用也没办法。刚开始类型转换头疼了一阵,不过也都解决了,今天遇到的这个问题,是怎么都过不去了,希望大家指点指点!

C++原型:

struct SipClient{    char *    realm;    void *    lpvoid;    char *    from_ip;    int        from_port;    char *    to_ip;    int        to_port;    char *    client_id;    char *    server_id;        //平台编码    int        reg_count;        SipClient(){memset(this, 0, sizeof(SipClient));}    ~SipClient()    {        delete[] from_ip;        delete[] realm;        delete[] client_id;        delete[] to_ip;        delete[] server_id;    }};SIPCLIENT_API SipClient* SCInit(const char * reaml, const char * from_ip, int from_port,  const char * to_ip, int to_port, const char * server_id, const char * user_id, const char * user_name, void * user_obj_param);

C#里是这么些的

public struct SipClient { public string realm; //鉴权码 public IntPtr lpvoid; //用户参数 public string from_ip; //本地IP public int from_port; //本地端口 public string to_ip; //SIP服务器IP public int to_port; //SIP服务器端口 public string client_id; //用户的编码 public string server_id; //SIP服务器的编码 public int reg_id; //保留,暂不使用 } [DllImport("sipclient.dll")] public static extern SipClient SCInit(string reaml, string from_ip, int from_port, string to_ip, int to_port, string server_id, string user_id, string user_name, IntPtr user_obj_param);
//函数调用
private const string USER_ID = "180400000090000000"; private const string SERVER_ID = "180400000099000000";
SIPCLIENT_API.SCInit("192.168.10.23", "192.168.10.32", 5092, "192.168.10.23", 5090, SERVER_ID, USER_ID, USER_ID, IntPtr.Zero);

黄色标记处写作SipClient时,运行时函数调用异常:

Additional information: 方法的类型签名与 PInvoke 不兼容。

想按照网上一种做法,将SipClient改为IntPtr,运行时还是出错,不过异常是:

Additional information: 无法在 DLL“sipclient.dll”中找到名为“SCInit”的入口点。

我能够想到的办法都用了,还是不能解决这些问题,期待大家的回答!

 

/////////////////////////////////////////////////////////////////////////////////////////////////

  最后是在C++那边做了修改解决的,通过制定模块定义 (.def) 文件,统一制定导出函数对应的名称。返回值为结构体指针的函数用IntPtr也能使用了。  

代码
SIPCLIENT_API SipClient* SCInit(const char * reaml,
  const char * from_ip, int from_port,
  const char * to_ip, int to_port, const char * server_id,
  const char * user_id, const char * user_name, void * user_obj_param);
[DllImport("sipclient.dll")] public static extern IntPtr SCInit(string reaml, string from_ip, int from_port, string to_ip, int to_port, string server_id, string user_id, string user_name, IntPtr user_obj_param);
  IntPtr client = IntPtr.Zero;
client = SIPCLIENT_API.SCInit(REALM, CLIENT_IP, CLIENT_PORT, SERVER_IP,
     SERVER_PORT, SERVER_ID, USER_ID, USER_ID, IntPtr.Zero);
if (client != IntPtr.Zero)
sipclient = (SipClient)Marshal.PtrToStructure(client, typeof(SipClient));
else
MessageBox.Show("SipClient初始化失败!");

4、后来还遇到个回调函数导致的崩溃问题,又耽误了大半天时间,下班了还耽搁了会终于找的解决发办法了。

////////////////////////////////////////////

WINAPI见windef.h这个头文件
#define WINAPI __stdcall
  默认情况下,我们的函数调用都是遵循__stdcall这个规则的。当然,也有诸如__cdecl、__pascal等规则。
  使用__stdcall还是__cdecl或__pascal,在纯Windows编程下并非特别需要。
__stdcall:
  1、进行函数调用,函数参数的入栈方式是最右边先入栈。
  2、同时__stdcall规定,调用者负责栈的回收。当然,这些工作是应用程序自己完成的,不需要编写者动手。汇编语言另当别论(POP SP POP BP等)
  题外话:__pascal的调用规则是从左到右,正好与__stdcall相反。
  3、C调用约定(即用__cdecl关键字说明)(The C default calling convention)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数 vararg的函数(如printf)只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。 _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。
是MFC缺省调用约定。
  在函数调用过程中,会使用栈。__stdcall与__cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数弹出栈,以及产生函数修饰名的方法。关于这两个调用约定的详细信息,读者可参看MSDN。对于参数个数可变的函数,例如printf,使用的是__cdecl调用约定,Win32的API函数都遵循__stdcall调用约定。在VC++开发环境中,默认的编译选项是__cdecl,对于那些需要__stdcall调用约定的函数,在声明时必须显式地加上__stdcall。在Windows程序中,回调函数必须遵循__stdcall调用约定,所以我们在声明回调函数时要使用CALLBACK。使用CALLBACK而不是__stdcall的原因是为了告诉我们这是一个回调函数。注意,在Windows 98和Windows 2000下,声明窗口过程函数时,即使不使用CALLBACK也不会出错,但在Windows NT4.0下,则会出错

////////////////////////////////////////////

  刚开始同事分析出了崩溃的原因,都是回收方式惹的祸,可参见http://www.hudong.com/wiki/WINAPI,尝试使用__stdcall,但是还是没有解决问题

  后来实践证明,程序是很严谨的,半点差错都不能出才不会导致错误,思路还是__stdcall,只不过少改了东西,有两个地方需要改,才能保证不出错。

  参考http://hi.baidu.com/tease/blog/item/1fe7213802780f22b9998f5a.html。

 

  关键就是这两句话

  typedef void (_stdcall *CiCiCallBack) (bool started, void* client,char *message);
  将导出函数修改为:
  extern "C" _declspec(dllexport) bool _stdcall Test(char* fileName, CiCiCallBack callback)

  一开始的时候就只修改了定义那,却忘记了导出时的修改,差点就放弃了这条解决思路了,不过还好,所谓坚持就是胜利!

  
5、后来封装好拿到用户那里用,却总是提示说找不到C++那些dll.

  网上一查,初步定位是开发环境引起的,跟环境部署有关系。我们的开发环境是vs2008,而客户使用的vs2010,通过几次尝试,问题终于了。

  首先考虑是缺少某些C++必备的运行库,存在相互依赖关系,所以导致找不到dll。用查看Dependency Walker查看才发现真的是客户机子上少了一些东西。

    但是此路不通,将缺少的那些东西拷贝到可执行程序目录下,问题依旧没有解决。但是依旧坚持这条路~

  尝试安装vcredist_x86.exe,以排除是否还是缺少了某些运行库的可能,问题依然存在。

  后来我想起来之前搜索问题的时候,看到好像跟dll的Releas\Debug版本还有关系,所有又尝试提议让同事将他们的c++dll改为Release版的。

    因为项目是多个人一起做了,编译Release版还花了不少时间,不过好歹问题终于解决了!

  总结:直接安装vcredist_x86.exe,所有dll必须使用Release版的。如果使用Debug版的就必须保证可执行程序目录下的dll是完整的,缺一不可!

  网上详细的讲解也很多,感觉这个总结的很好http://hi.baidu.com/fairysky/blog/item/e7a8366dbaa735f3431694c8.html。

做程序就怕出现问题,出现问题就怕不知道原因,知道原因了就好找解决的办法啦!

0 0
原创粉丝点击