老生常谈之C++和C#之间互相调用

老生常谈之C++C#之间互相调用

最近实在是太不给力了,算了,这些就不这里抱怨了,直接进入正题把。

关于C++C#之间相互调用,不管是C++做父C#做子,还是C#为父C++为子(这里的父与子是指调用和被调用的关系,通常情况下 exe为父,调用子dll相关功能),网上随便一搜,遍地都是,而且有的把调用方式划分的也相当细。

那为什么我还要谈呢?其实有以下几点原因。

1.       因为工作需要,平台需要支持.NET模块,即VB.NETC#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++中这一部分是如何实现的把。 父传给子一个虚接口(虚类),子在适当的时候调用。仅此而已。让我们把调用函数想的深入一点。直接看汇编代码把。

看代码之前,还要先简单说一下函数调用相关信息。在汇编层调用一个函数无非也就是JMPCALL 之类的指令,若函数还有参数就是一些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 并且我有建了一个项目,就是封装一些常用功能,具体看代码把。

源码下载地址

评论 58
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值