本文讲解如何使用C#调用只有.h头文件的c++类的虚函数(非实例函数,因为非虚函数不存在于虚函数表,无法通过类对象偏移计算地址,除非用export导出,而gcc默认是全部导出实例函数,这也是为什么msvc需要.lib,如果你不清楚但希望了解,可以选择找我摆龙门阵),并以COM组件的c#直接调用(不需要引用生成introp.dll)举例。

  我们都知道,C#支持调用非托管函数,使用P/Inovke即可方便实现,例如下面的代码

[DllImport("msvcrt", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl)] public static extern void memcpy(IntPtr dest, IntPtr src, int count);

不过使用DllImport只能调用某个DLL中标记为导出的函数,我们可以使用一些工具查看函数导出,如下图

一般会导出的函数,都是c语言格式的。

  C++类因为有多态,所以内存中维护了一个虚函数表,如果我们知道了某个C++类的内存地址,也有它的头文件,那么我们就能自己算出想要调用的某个函数的内存地址从而直接call,下面是一个简单示例

#include <iostream>  class A_A_A { public:     virtual void hello() {         std::cout << "hello from A\n";     }; };  //typedef void (*HelloMethod)(void*);  int main() {     A_A_A* a = new A_A_A();     a->hello();      //HelloMethod helloMthd = *(HelloMethod *)*(void**)a;          //helloMthd(a);     (*(void(**)(void*))*(void**)a)(a);      int c;     std::cin >> c; }

(上文中将第23行注释掉,然后将其他注释行打开也是一样的效果,可能更便于阅读)
从代码中大家很容易看出,c++的类的内存结构是一个虚函数表二级指针(数组,多重继承时可能有多个),每个虚函数表又是一个函数二级指针(数组,多少个虚函数就有多少个指针)。上文中我们假使只知道a是一个类对象,它的第一个虚函数是void (*) (void)类型的,那么我们可以直接call它的函数。

  接下来开始骚操作,我们尝试用c#来调用一个c++的虚函数,首先写一个c++的dll,并且我们提供一个c格式的导出函数用于提供一个new出的对象(毕竟c++的new操作符很复杂,而且实际中我们经常是可以拿到这个new出来的对象,后面的com组件调用部分我会详细说明),像下面这样

dll.h

class DummyClass { private:     virtual void sayHello(); };

dll.cpp

#include "dll.h" #include <stdio.h>  void DummyClass::sayHello() {     printf("Hello World\n"); }  extern "C" __declspec(dllexport) DummyClass* __stdcall newObj() {     return new DummyClass(); }

我们编译出的dll长这样

让我们编写使用C#来调用sayHello

using System; using System.Runtime.InteropServices;  namespace ConsoleApp2 {     class Program     {         [DllImport("Dll1", EntryPoint = "newObj")]         static