老生常谈之C++和C#之间互相调用 收藏 此文于2011-04-22被推荐到CSDN首页 最近实在是太不给力了,算了,这些就不这里抱怨了,直接进入正题把。 关于C++和C#之间相互调用,不管是C++做父C#做子,还是C#为父C++为子(这里的父与子是指调用和被调用的关系,通常情况下 exe为父,调用子dll相关功能),网上随便一搜,遍地都是,而且有的把调用方式划分的也相当细。 那为什么我还要谈呢?其实有以下几点原因。 1. 因为工作需要,平台需要支持.NET模块,即VB.NET、C#、WPF、有可能还要支持Silverlight。这些要完全嵌入到我们的软件中。 2. 网上资料绝大部分都不是动态加载,即都在编译时指定要加载的DLL或者类型库信息。没有了运行时动态加载,软件当然逊色不少,这种静态绑定东西不是我所要的。 3. 在正常的父与子交互中,没有统一、完全的讲解和实例。 4. 父与子类型转换没有讲解。即C++和.NET 类型如何对应。 5. 在子需要调用父相关功能时,父需要将某些已存在的功能再次封装为一个dll,供子静态绑定调用。这是C++为父C#为子的一个典型情况。这显然太多余了。这里我们将采用非常规情况来实现同样功能。 6. 兴趣。 先提前说明下,此文章并不是强调实现两者相互调用的具体方法以及具体的技术,因为这部分网上已经有太多,这里只不过是本人对两者的一些理解,以及自己是如何实现的(当然部分内容少不了也参考了网上的)这一点提前注明下,省的说我标题党。当然既然是自己的理解,错误之处在所难免。能指出错误并加以修改是我希望看到的。 先说说程序大概组织逻辑。主程序有一套公用接口(其实就是纯虚类),在加载DLL时候将此接口传到DLL中,这样子模块在需要的时候就可以调用父的逻辑了,至于父调子,那就更简单了,主程序有一个纯虚类,子模块都继承此接口,并进行重写,主程序按照一定的顺序分别调用,这样父与子的逻辑交互就完成了,这些对都是C++程序来说,这当然没问题。现在问题是,要嵌入.NET的类库,由此引发一系列问题。。。。。 软件是以C++为父,DLL作为子的项目。 开发环境:WIN7 64BIT+VS2010+MFC+ATL+COM。 .NET环境下先以C#为例,其他的大部分一样下,不排除做一些简单或者复杂的修改。 下面正式开始把。 1. 动态加载 即父调子。 COM确实是好东西(他的褒与贬我们无作评论),她的语言无关性,不仅是我们实现动态加载的关键,更是实现加载其他.NET类库的核心。如VB.NET。有了她,才是这一切皆有可能。 由于.NET下的类库(DLL),和传统的WIN DLL 不太一样,毕竟托管的东西。她一些函数对外是不可见的,但对COM可见。因为我们就以COM方式定义一套接口,并把此接口当成普通C++的纯虚接口,来完成父到子的调用。 这一点不论在理论上、代码上都比较简单,而且网上大多也是这样子,所以我们直接上代码。 如下为COM接口定义。 [ComVisible(true), Guid("B86D71F4-FE07-4B60-8246-F5AE283ED2A3"), InterfaceType(ComInterfaceType.InterfaceIsDual) ] public interface IHMI { [PreserveSig, DispId(1)] void OnCreate(int a); [PreserveSig, DispId(2)] void SetRect(int left, int top, int width, int height); //其他接类似 } [ ComVisible(true), ClassInterface(ClassInterfaceType.AutoDual), ProgId("xxxxxxx.xxxxxxx") //ProgId 主程序根据此,运行时动态创建。 ] C#在使用时要继承并实现接口逻辑,如下类似。 public class CustomCOMClient : IHMI { public CustomCOMClient() { } [DispId(1)] public void OnCreate(int a) { //逻辑 } [DispId(2)] public void SetRect(int left, int top, int width, int height) { //逻辑 } } 当然了,在建项目时,项目类型要为类库。至此类库部分已经完毕。接下来再看看主程序如何加载,以及如何调用把。 其中在动态创建时,ProgId是关键。这一部分对搞过COM,在加上ATL的人来说,可能太简单了,‘可能’这个词也许用的不太恰当,因为她不是‘可能’,她确实简单。不信看代码。 ::CoInitialize(NULL); const OLECHAR lpszProgID[]=OLESTR("xxxxxxx.xxxxxxx"); //ProgID CComPtr<IDispatch> m_NetCustomer; HRESULT hr = m_NetCustomer.CoCreateInstance(lpszProgID); if(SUCCEEDED(hr)) { const LPCOLESTR szMember=OLESTR("OnCreate"); VARIANT v; v.vt = VT_I4; v.lVal = 1024; hr = m_NetCustomer.Invoke1(szMember,&v); if(SUCCEEDED(hr)) { } } ::CoUninitialize(); 怎么样?没有撒谎把,几行代码就把创建、调用搞定了。 郁闷,从C++拷出来代码没有格式,还的手工加。。。。 2. 回调 即子调父。 主程序肯定按照自己的逻辑顺序依次调用子模块的接口,如先创建、子的相关逻辑、最后销毁。如果说在实际运用中,子模块完全不会在调用父的相关功能,那么此时框架已经完全实现了,我们之前做的工作就是。难道不是吗?,但应用程序往往也有父与子相互调用,下面就来看看,子如何回调父的功能把 前面也说过,子调父往往是这样,从父身上分离出部分代码,重新封装一个dll,由子静态绑定,这步最简单、最方便。不过这显然不是正道,让人觉得别扭。 同时维护两份相同功能代码? 也许你会说,主程序从此也可以调用DLL啊,那不就一致了,你要真这样说,我的回答是,“我只是在说明问题,不涉及到架构问题” 还有每个子模块都静态绑定这个DLL? 还有你在分离这个DLL时,如果依赖主程序太多,你怎么办? 还有你能保证分离后的稳定性吗?回带来其他的问题吗? 还有你仅仅是为了满足功能,才这样做的? 你觉得这样看着顺眼吗? 等等。反正我觉得是古怪之急。 接下来就要需找其他替代方案了。 先考虑下在C++中这一部分是如何实现的把。 父传给子一个虚接口(虚类),子在适当的时候调用。仅此而已。让我们把调用函数想的深入一点。直接看汇编代码把。 看代码之前,还要先简单说一下函数调用相关信息。在汇编层调用一个函数无非也就是JMP、CALL 之类的指令,若函数还有参数就是一些PUSH指令。好了知道这些就足够了,下面看看在VC中的伪代码。 __asm {//类虚函数的汇编模拟调用,函数无参数、无返回值。 mov eax,xxxxx //存放函数地址 mov ecx,xxxxx //this指针 call eax //调用 } 这样调用就完成了,其实真正的调用也如此,只不过指令多几条而已。因为她要得到某些信息。 好了,如果说.NET支持内敛汇编,那我们完全可以自己模拟虚函数调用,不用在封装什么DLL,这所有的一切都可以搞定,但可惜的时,常规下内敛汇编是不支持的。不错,我说的是常规,那非常规呢?答案是肯定的。 关于内敛汇编网上也是一大片,底层思想是,在内存开辟一段空间,并放入相应指令,到时侯执行这一部分逻辑即可,这样就可以完成内敛汇编了。 其中网上有一个封装好的DLL(AsmClassLibrary.dll),提供接口编写汇编代码,用Reflector 查看了发现其最后执行采用远程线程注入方式,对于嵌入一两个模块的,可以这样做,但如果模块很多的话,毕竟注入涉及到安全的问题,这一点不太好,当然这也太另类了,我可不想应用程序到处以这种方式来执行。 所以我们采用Marshal.GetDelegateForFunctionPointer方式。 因为从底层上讲,是不分什么语言编写,只认机器指令的,因此只要我们模拟的合理、正确,这一点是没有问题的。 好了,现在我们目标很明确,用内敛方式在C#模拟虚函数的调用。 在给出代码之前,也先说下思路。 根据之前所讲以及常规知识,以下几点是必须的。 A 类对象指针,因为我们要将此值给ECX。 B 成员函数地址,当然了,我们要CALL嘛。 C 参数,这值是在C#中使用的。 这就是主要内容,实现他们方式有很多种,以下是我的方案。 因为接口会很多,因此我将this指针、函数地址都放到数组中,然后在传递给C#中,其实按道理说,只传递一个this指针就够了,其他部分应该在C#中实现,但操作指针C++中比较简便,所以这部分代码就在C++中做了。 得到this指针 太简单啦,根据虚表布局得到其地址也很简单。如下。 接口定义如下。 class CInterface { public: virtual void test1( LPSTR p) virtual void test2(); virtual void test3( int a); }; 得到this指针及成员函数地址。 CInterface *pInterface = new CInterface; DWORD base_proc = (*((DWORD *)(pInterface))); //虚表指针 DWORD f1 = *(( DWORD *)base_proc); //第1个 DWORD f2 = *(( DWORD *)(base_proc + 4)); //第2个 DWORD f3 = *(( DWORD *)(base_proc + 8)); //第三个 到时将值赋值到SAFEARRAY 安全数组中,在传递到C#中。 看看在C#中时如何使用的把。当然这一部分的内敛、委托、开辟内存、托管到非托管转换时少不了的,老规矩,看代码把。 先定义委托和内敛。 //委托 参数分别为 this指针 成员函数地址 参数 delegate void testcall(int pthis, int pfun, int param); byte[] codetest = { // 0xCC, 0x8B, 0x5C, 0x24, 0x0C, //mov ebx,[esp+0Ch] 第三个参数 @@ 0x8B, 0x44, 0x24, 0x08, //mov eax,[esp+08h] 函数地址 0x8B, 0x4C, 0x24, 0x04, //mov ecx,[esp+04h] this 指针 0x53, //push ebx 参数入栈 @@ 0xFF, 0xD0, //call eax 0xC3 // ret }; 书写内敛汇编当然可以考研我们的功底啦,看看你知道不知道底层是如何实现的、如何入栈、出栈、传值、传指针、传引用、堆栈平衡等。还有一点,书写汇编虽容易,但是机器指令我们并不都知道,山人自有妙计,汇编代码贴到VC中,ALT+8看反汇编,在拷贝回来即可。 以上代码中,完成接口第三个函数调用,带有一个整形参数,并且传值。 注释掉@@部分完成接口第二个函数调用,无参数。 为了简便都写在一个里面,实际运用中,你可以按照不同格式分开。 接下来看看如何调用,,主要代码如下。 VirtualAlloc。。。。。。之前肯定得先开辟内存啊 Marshal.Copy(codetest, 0, handle, codetest.Length); testcall Customer = Marshal.GetDelegateForFunctionPointer(handle, typeof(testcall)) as testcall; int bb = 22; Customer (fun[0], fun[4], bb); 不错,这就是子模块调用父相关逻辑的主要实现。 3. 后话 这就是相互调用的所有部分吗?这次答案是否定,实际上远远不至于此,我们此次实现的,只是最最基本的部分,尤其在参数上,我们用的最简单的类型 int,实际使用中,对于两者之间都存在的基本类型,还好说一点,当涉及到字符串、数组、结构体等这些类型时,真的会让你很麻烦的,尤其是字符串,两边还不一样。。。。。 其中对参数类型来说,我们用的是传值方式,直接将值push,对于引用或者指针要把其地址push,就可以实现了,当然还是针对最基本的类型来说的。 对于字符串参数的,我用全局函数实现了一个接口(具体的可以看代码),这样其中大部分转换操作,对我们就透明了,为何不自己搞?我有时间在补充进去把,这些就留给你们了,同样你们搞出来之后要告诉我啊,这里给大家一个建议,处理字符串时,在C#中最好使用char数组,但在书写内敛汇编时要注意,数组前面可有数组的大小,要偏移过去。 。。。。 。。。。 等把这一切都搞定之后,动态创建、嵌入VB的、C#的、WPF的以及她3D部分、硬件加速部分。。。。。。。。。 不错,如此看来,现在才刚刚开始。。。。。。。。。。。。。 希望能给大家起到一个抛砖引玉作用。 最后附一个类型转换的帖,供使用参考,类型转换我就不啰嗦了。 http://topic.csdn.net/u/20090225/15/a6bc50ad-9721-4749-b189-dc4a4bc045a1.html 再附效果图一张,图中部分为嵌入C#的类型。 为了嵌入到父窗口上,使用了API SetParent 并且我有建了一个项目,就是封装一些常用功能,具体看代码把。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gangyilovevc/archive/2011/04/21/6339003.aspx 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gangyilovevc/archive/2011/04/21/6339003.aspx |
|
来自: orion360doc > 《混合编程》