1. 程式人生 > >如何使用C#呼叫C++類虛擬函式(即動態記憶體呼叫)

如何使用C#呼叫C++類虛擬函式(即動態記憶體呼叫)

  本文講解如何使用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++有虛擬函式表都不知道,時至今日,我已經失去了她。今後怕是一直會任由靈魂遊蕩,半夢半醒,即是人生。

相關推薦

如何使用C#呼叫C++虛擬函式動態記憶體呼叫

  本文講解如何使用C#呼叫只有.h標頭檔案的c++類的虛擬函式(非例項函式,因為非虛擬函式不存在於虛擬函式表,無法通過類物件偏移計算地址,除非用export匯出,而gcc預設是全部匯出例項函式,這也是為什麼msvc需要.lib,如果你不清楚但希望瞭解,可以選擇找我擺龍門陣),並以COM元件的c#直接呼叫(不

C++執行時通過基指標或引用呼叫派生虛擬函式的實現原理: 虛擬函式

我們知道要實現執行時的多型, 必須在基類中宣告和定義相關的虛擬函式, 並在派生類中重新實現基類中的虛擬函式. 當編譯器見到這種繼承層次結構的時候, 編譯器將為定義了虛擬函式的基類和覆蓋了基類虛擬函式的派生類分別建立一張虛擬函式表(Virtual Functi

C++學習筆記】虛擬函式

虛擬函式與過載函式的關係 我們現在來比較一下規則比較多的虛擬函式和規則比較少的過載函式之間的差別: 普通函式過載時,其函式的引數個數或者引數型別必須有所不同,函式的返回型別也可以不同。(這個不同是比較嚴格的不同,是涉及本質的) 過載函式: 要求函式名、返回型別、引

C++學習筆記】虛擬函式

12.3 昨天居然斷更了,唉,寫部落格真是很需要毅力呀,更新上今天的學習筆記。 上次我們講到多型性的定義以及簡述了實現方式以及靜態編譯和動態編譯的概念。這次,我們來具體講一講虛擬函式。 什麼是虛擬函式 在某基類中宣告為 virtual 並在一個或多個派生類中被重新定義的成

詳解C++中的純虛擬函式虛擬函式區別&多型性 以及理解

#include <iostream> #include <cstdio> using namespace std; class A { public:     void foo()     {         printf("1\n");    

第二章、Objective-c 語法,/屬性/函式iOS學習筆記,從零開始。

注*需要具備面向物件程式設計基礎。 一、OC常識 Objective-C是C的超集,也就是說C有的Objective-C都有,Objective-C多了C自身沒有的OO(面向物件)特性。Objective-C預設副檔名為 .m 。標頭檔案副檔名跟普通C一樣 .h 。O

構造方法呼叫構造方法super關鍵字的使用

package kaoshi; /** ************************************ * @author Hejing * @date 2017年12月24日 * @class fisrt.java * ***********

C++和物件.四個預設成員函式賦值運算子過載

1.(1)類的定義   類就是具有相同資料和相同操作的一組物件的集合。 學生類定義: class student {//成員變數char* name;int age;int sex;//成員函式void speak(){cout<<name<<"年

《隨筆十二》—— C++中的 “ 純虛擬函式 和 抽象

目錄 抽象類 純虛擬函式 抽象類 ●   抽象類:  不用於定義物件而只作為一種基本型別用做繼承的類,稱為抽象類, 由於它常用於做基類, 通常稱為抽象基類。  那麼凡是包含純虛擬函式的類都是抽象類, 抽象類是一種特殊的類,

c++虛擬函式override和過載函式overload的比較

1. 過載函式要求函式有相同的函式名稱,並有不同的引數序列;而虛擬函式則要求完全相同; 2. 過載函式可以是成員函式或友元函式,而虛擬函式只能是成員函式; 3. 過載函式的呼叫是以所傳遞引數的差別作為呼叫不同函式的依據,虛擬函式是根據物件動態型別的不同去呼叫不同

c++繼承和組合,多型,虛擬函式c++後期繫結的本質

組合和繼承,實現了程式碼的可擴充套件性和相容性。 多型是在虛擬函式上得到了支援。 多型的原理,函式的呼叫繫結: 把函式的呼叫和函式體相關聯稱為捆綁。當捆綁是在程式執行之前完成的,稱為早期捆綁。c語言只支援早期繫結。晚期捆綁,物件通過自身得到類的資訊,然後找

C++中的純虛擬函式、抽象、介面

純虛擬函式就是沒有函式體的虛擬函式,通常都以下列格式定義純虛擬函式: class <類名> { virtual <型別><函式名>(形參表) = 0; ... } 在虛擬函式後面加"=0",這並不代表虛擬函式返回值為

C++中的預設建構函式和初始化列表和子呼叫建構函式

預設建構函式:未提供顯式初始值時,用來建立物件的建構函式。 class testClass { public:     testClass();               

C++虛擬函式2

class x { char *p; public: x(int sz) {p=new char[sz];} virtual ~x(){cout << "~x()\n"; delete []p;} }; class y : public x {

16 More Effective C++ —— 條款23/24 (虛擬函式、虛基、多繼承、RTTI)

0 前序 由於條款23只是針對iostream和stdio.h之間,進行執行效率的對比,此處不會詳細展開。其宗旨是儘量使用C++的庫,可以提高程式的執行效率和安全性。 此篇將著重討論條論24的內容。 1 多型 C++中,多型是指使用基類指標、引用指向派生類,若基類和派生類中,

c++單繼承與多繼承包含虛擬函式與虛繼承的對比

先來個概念分析題: class Person { public: void Show() { cout<<"Person::"<<_name&l

C ++ 虛擬函式

第一節、一道簡單的虛擬函式的面試題 題目要求:寫出下面程式的執行結果? 1、當上述程式中的函式p()不是虛擬函式,那麼程式的執行結果是如何?即如下程式碼所示: class A { public: void p() { cout <&l

《隨筆十二》—— C++中的 “ 純虛擬函式 和 抽象

目錄 抽象類 純虛擬函式 抽象類 ●   抽象類:  不用於定義物件而只作為一種基本型別用做繼承的類,稱為抽象類, 由於它常用於做基類, 通常稱為抽象基類。  那麼凡是包含純虛擬函式的類都是抽

C++派生函式呼叫函式

在MFC的程式中,我們經常會看到下面的程式片段, 片段一: BOOL CClassDlg::OnInitDialog() {        CDialog::OnInitDialog();        //。。。。        //。。。。        AddTr

C++ 實用泛型程式設計之 虛擬函式C++ virtual function雜談

一 C++虛擬函式(C++ virtual function)雜談 我們在程式設計的時候,經常會遇到這樣的情況,假設有兩個物件,你要在函式中分別呼叫它們的OnDraw方法,我們以前的做法一般是這樣的。 void f(int iType) {  switch(iType)