宝岛
                                      当前位置:首页 > 攻略

                                      C++和C#之间互相调用经验详谈

                                      阅读次数:更新时间:2021-02-20

                                      先说说程序大概组织逻辑。主程序有一套公用接口(其实就是纯虚类),在加载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 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 并且我有建了一个项目,就是封装一些常用功能,具体看代码把。

                                      精品应用
                                      ©2020 宝岛手机软件园 http://www.baodaoapp.com/
                                      网站地图1 网站地图2 网站地图3 网站地图4