本文講解如何使用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 extern IntPtr CreateObject();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void voidMethod1(IntPtr thisPtr);
static void Main(string[] args)
{
IntPtr dummyClass = CreateObject();
IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
voidMethod(dummyClass);
Console.ReadKey();
}
}
}
(因為調用的是c++的函數,所以this指針是第一個參數,當然,不同調用約定時它入棧方式和順序不一樣)
下面有一種另外的寫法
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
namespace ConsoleApp2
{
class Program
{
[DllImport("Dll1", EntryPoint = "newObj")]
static extern IntPtr CreateObject();
//[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
//delegate void voidMethod1(IntPtr thisPtr);
static void Main(string[] args)
{
IntPtr dummyClass = CreateObject();
IntPtr vfptr = Marshal.ReadIntPtr(dummyClass);
IntPtr funcPtr = Marshal.ReadIntPtr(vfptr);
/*voidMethod1 voidMethod = (voidMethod1)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(voidMethod1));
voidMethod(dummyClass);*/
AssemblyName MyAssemblyName = new AssemblyName();
MyAssemblyName.Name = "DummyAssembly";
AssemblyBuilder MyAssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(MyAssemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder MyModuleBuilder = MyAssemblyBuilder.DefineDynamicModule("DummyModule");
MethodBuilder MyMethodBuilder = MyModuleBuilder.DefineGlobalMethod("DummyFunc", MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { typeof(int) });
ILGenerator IL = MyMethodBuilder.GetILGenerator();
IL.Emit(OpCodes.Ldarg, 0);
IL.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32());
IL.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(void), new Type[] { typeof(int) });
IL.Emit(OpCodes.Ret);
MyModuleBuilder.CreateGlobalFunctions();
MethodInfo MyMethodInfo = MyModuleBuilder.GetMethod("DummyFunc");
MyMethodInfo.Invoke(null, new object[] { dummyClass.ToInt32() });
Console.ReadKey();
}
}
}
上文中的方法雖然復雜了一點,但……就是沒什么用。不用懷疑!
文章寫到這里,可能有童鞋就要發問了。你說這么多,tmd到底有啥用?那接下來,我舉一個栗子,activex組件的直接調用!
以前,我們調用activex組件需要做很多復雜的事情,首先需要使用命令行調用regsvr32將dll注冊到系統,然后回到vs去引用com組件是吧
仔細想想,需要嗎?並不需要,因為兩個原因:
- COM組件規定DLL需要給出一個DllGetClassObject函數,它就可以為我們在DLL內部new一個所需對象
- COM組件返回的對象其實就是一個只有虛函數的C++類對象(COM組件規定屬性和事件用getter/setter方式實現)
- COM組件其實不需要用戶手動注冊,執行regsvr32會操作注冊表,而且32位/64位會混淆,其實regsvr32只是調用了DLL導出函數DllRegisterServer,而這個函數的實現一般只是把自己注冊到注冊表中,這一步可有可無(特別是對於我們已經知道某個activex的dll存在路徑且它能提供的服務時,如果你非要注冊,使用p/invoke調用該dll的DllRegisterServer函數是一樣的效果)
因此,假如我們有一個activex控件(例如vlc),我們希望把它嵌入我們程序中,我們先看看常規的做法(本文沒有討論帶窗體的vlc,因為窗體這塊兒又復雜一些),直接貼圖:
看起來很簡單,但當我們需要打包給客戶使用時就很麻煩,涉及到嵌入vlc的安裝程序。而當我們會動態內存調用之后,就可以不注冊而使用vlc的功能,我先貼出代碼:
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp3
{
class Program
{
[DllImport("kernel32")]
static extern IntPtr LoadLibraryEx(string path, IntPtr hFile, int dwFlags);
[DllImport("kernel32")]
static extern IntPtr GetProcAddress(IntPtr dll, string func);
delegate int DllGetClassObject(Guid clsid, Guid iid, ref IntPtr ppv);
delegate int CreateInstance(IntPtr _thisPtr, IntPtr unkown, Guid iid, ref IntPtr ppv);
delegate int getVersionInfo(IntPtr _thisPtr, [MarshalAs(UnmanagedType.BStr)] out string bstr);
static void Main(string[] args)
{
IntPtr dll = LoadLibraryEx(@"D:\Program Files\VideoLAN\VLC\axvlc.dll", default, 8);
IntPtr func = GetProcAddress(dll, "DllGetClassObject");
DllGetClassObject dllGetClassObject = (DllGetClassObject)Marshal.GetDelegateForFunctionPointer(func, typeof(DllGetClassObject));
Guid vlc = new Guid("2d719729-5333-406c-bf12-8de787fd65e3");
Guid clsid = new Guid("9be31822-fdad-461b-ad51-be1d1c159921");
Guid iidClassFactory = new Guid("00000001-0000-0000-c000-000000000046");
IntPtr objClassFactory = default;
dllGetClassObject(clsid, iidClassFactory, ref objClassFactory);
CreateInstance createInstance = (CreateInstance)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(objClassFactory) + IntPtr.Size * 3), typeof(CreateInstance));
IntPtr obj = default;
createInstance(objClassFactory, default, vlc, ref obj);
getVersionInfo getVersion = (getVersionInfo)Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(Marshal.ReadIntPtr(obj) + IntPtr.Size * 18), typeof(getVersionInfo));
string versionInfo;
getVersion(obj, out versionInfo);
Console.ReadKey();
}
}
}
上文中的代碼有幾處可能大家不容易懂,特別是指針偏移量的運算,這里面有比較復雜的地方,文章篇幅有限,下來咱們細細研究。
從11年下半年開始學習編程到現在已經很久了,有時候會覺得沒什么奔頭。其實人生,無外乎兩件事,愛情和青春,我希望大家能有抓住的,就不要放手。兩年前,我為了要和一個女孩子多說幾句話,給人家講COM組件,其實我連c++有虛函數表都不知道,時至今日,我已經失去了她。今后怕是一直會任由靈魂游盪,半夢半醒,即是人生。