從Emgu學(xué)習(xí)C#調(diào)用C++庫(1)

最近,圖像處理組之前專門做MFC的同事離職了,這開發(fā)界面的事情就落到了我們自己的頭上。

這幾乎是大家的共識:用C++調(diào)用OpenCV寫函數(shù)底層庫很方便;但是用MFC寫上層的GUI程序卻很麻煩。因?yàn)镸FC太不C++了。我成為一只程序猿以來,從來沒有任何一刻喜歡上MFC。而我家那位一直用的是C#開發(fā)界面的,再加上另一位基友的推薦,因此我便想嘗試這用C#來做界面的開發(fā)。但底層還是得用C/C++來做圖像處理啊,這就牽扯到了C#調(diào)用非托管的C/C++動態(tài)庫的問題。

C#,我是小白一只。但好在各路大神的幫忙以及它本身的便捷性,編寫個界面啥的也是沒有什么問題。但用C#來調(diào)用C/C++ 動態(tài)庫,朋友們用的不多,我就只能自己艱苦奮斗啦~但是我是很有信心的。因?yàn)槲抑溃琌penCV有一個C#的版本,叫Emgu,它實(shí)際上也是調(diào)用的OpenCV的C++ DLL。Emgu在這個動態(tài)庫之上又封裝了一層,形成了C#的動態(tài)庫,使得在C#界面上可以輕松調(diào)用OpenCV。既然Emgu能做到,我就也能做到。

我也不打算把Emgu全部解析一遍,我既沒有這個能力,也沒有這個時間,咱們就實(shí)用主義,來看看Emgu是如何調(diào)用OpenCV的動態(tài)庫的吧。

拿一個我們最常用的函數(shù)作為例子吧。cvCreateImage。

在Emgu的教程里,第一個例子就是說,

The CvInvoke class provides a way to directly invoke OpenCV function within .NET languages. Each method in this class corresponds to a function in OpenCV of the same name. For example, a call to

IntPtr image = CvInvoke.cvCreateImage(new System.Drawing.Size(400, 300), CvEnum.IPL_DEPTH.IPL_DEPTH_8U, 1);

is equivalent to the following function call in C

 IplImage* image = cvCreateImage(cvSize(400, 300), IPL_DEPTH_8U, 1);

在Emgu的源碼里面,可以看到CvInvoke.cvCreateImage是這樣子的:

      #region Initialization
      /// <summary>
      /// Creates the header and allocates data. 
      /// </summary>
      /// <param name="size">Image width and height.</param>
      /// <param name="depth">Bit depth of image elements</param>
      /// <param name="channels">
      /// Number of channels per element(pixel). Can be 1, 2, 3 or 4. The channels are interleaved, for example the usual data layout of a color image is:
      /// b0 g0 r0 b1 g1 r1 ...
      /// </param>
      /// <returns>A pointer to IplImage </returns>
      [DllImport(OPENCV_CORE_LIBRARY, CallingConvention = CvInvoke.CvCallingConvention)]
      public static extern IntPtr cvCreateImage(
         Size size,
         CvEnum.IPL_DEPTH depth,
         int channels);  

下面通過一些官方的文檔或渠道來注解這段代碼。首先是DllImport

來自MSDN
直接從 C# 調(diào)用 DLL 導(dǎo)出:若要聲明一個方法使其具有來自 DLL 導(dǎo)出的實(shí)現(xiàn),請執(zhí)行下列操作:

  • 使用 C# 關(guān)鍵字 staticextern 聲明方法。
  • 將 DllImport 屬性附加到該方法。DllImport 屬性允許您指定包含該方法的 DLL 的名稱。通常的做法是用與導(dǎo)出的方法相同的名稱命名 C# 方法,但也可以對 C# 方法使用不同的名稱。
  • 還可以為方法的參數(shù)和返回值指定自定義封送處理信息,這將重寫 .NET Framework 的默認(rèn)封送處理。

示例:

// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;
 
class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(string c);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();
 
    public static void Main()
    {
        puts("Test");
        _flushall();
    }
}

輸出:

Test

代碼討論
前面的示例顯示了聲明在非托管 DLL 中實(shí)現(xiàn)的 C# 方法的最低要求。PlatformInvokeTest.puts 方法用 static 和 extern 修飾符聲明并且具有 DllImport 屬性,該屬性使用默認(rèn)名稱 puts 通知編譯器此實(shí)現(xiàn)來自 msvcrt.dll。若要對 C# 方法使用不同的名稱(如 putstring),則必須在 DllImport 屬性中使用 EntryPoint 選項(xiàng),如下所示:

[DllImport("msvcrt.dll", EntryPoint="puts")]

官網(wǎng)上沒有詳細(xì)說的是CallingConvention。這個是調(diào)用慣例??梢灾苯涌丛创a:

    public enum CallingConvention
    {
        // 摘要:
        //     此成員實(shí)際上不是調(diào)用約定,而是使用了默認(rèn)平臺調(diào)用約定。例如,在 Windows 上默認(rèn)為 System.Runtime.InteropServices.CallingConvention.StdCall,在
        //     Windows CE.NET 上默認(rèn)為 System.Runtime.InteropServices.CallingConvention.Cdecl。
        Winapi = 1,
        //
        // 摘要:
        //     調(diào)用方清理堆棧。這使您能夠調(diào)用具有 varargs 的函數(shù)(如 Printf),使之可用于接受可變數(shù)目的參數(shù)的方法。
        Cdecl = 2,
        //
        // 摘要:
        //     被調(diào)用方清理堆棧。這是使用平臺 invoke 調(diào)用非托管函數(shù)的默認(rèn)約定。
        StdCall = 3,
        //
        // 摘要:
        //     第一個參數(shù)是 this 指針,它存儲在寄存器 ECX 中。其他參數(shù)被推送到堆棧上。此調(diào)用約定用于對從非托管 DLL 導(dǎo)出的類調(diào)用方法。
        ThisCall = 4,
        //
        // 摘要:
        //     不支持此調(diào)用約定。
        FastCall = 5,
    }  

支持的有Winapi, Cdecl, StdCall, ThisCall, FastCall。我一般使用的都是Stdcall。調(diào)用慣例要保持一致,C++的代碼上用的什么調(diào)用慣例,則C#上就使用什么調(diào)用慣例。

至此,DllImport(OPENCV_CORE_LIBRARY, CallingConvention = CvInvoke.CvCallingConvention),這一行也就差不多明白了。再來往下看。

當(dāng)從 C# 代碼中調(diào)用非托管函數(shù)時,公共語言運(yùn)行庫必須封送參數(shù)和返回值。

對于每個 .NET Framework 類型均有一個默認(rèn)非托管類型,公共語言運(yùn)行庫將使用此非托管類型在托管到非托管的函數(shù)調(diào)用中封送數(shù)據(jù)。例如,C# 字符串值的默認(rèn)封送處理是封送為 LPTSTR(指向 TCHAR 字符緩沖區(qū)的指針)類型??梢栽诜峭泄芎瘮?shù)的 C# 聲明中使用 MarshalAs 屬性重寫默認(rèn)封送處理。

示例 2
本示例使用 DllImport 屬性輸出一個字符串。它還顯示如何通過使用 MarshalAs 屬性重寫函數(shù)參數(shù)的默認(rèn)封送處理。

// Marshal.cs
using System;
using System.Runtime.InteropServices;
 
class PlatformInvokeTest
{
    [DllImport("msvcrt.dll")]
    public static extern int puts(
        [MarshalAs(UnmanagedType.LPStr)]
        string m);
    [DllImport("msvcrt.dll")]
    internal static extern int _flushall();
 
 
    public static void Main()
    {
        puts("Hello World!");
        _flushall();
    }
}

輸出

Hello World!

代碼討論

在前面的示例中,puts 函數(shù)的參數(shù)的默認(rèn)封送處理已從默認(rèn)值 LPTSTR 重寫為 LPSTR。

MarshalAs屬性可以放置在方法參數(shù)、方法返回值以及結(jié)構(gòu)和類的字段上。若要設(shè)置方法返回值的封送處理,請將 MarshalAs 屬性與返回屬性位置重寫一起放置在方法上的屬性塊中。例如,若要顯式設(shè)置 puts 方法返回值的封送處理:

...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
...

再來看一下,在OpenCV中,cvCreateImage是這樣的簽名:

CVAPI(IplImage*)  cvCreateImage( CvSize size, int depth, int channels );
```

而在C#中,是這樣的:
```.cs
public static extern IntPtr cvCreateImage(Size size, CvEnum.IPL_DEPTH depth, int channels);  

```

光從類型來看,最好理解的是`int channels`,完全一樣。
比較難理解的是`CvEnum.IPL_DEPTH`和`IntPtr`。我們看一下`CvEnum.IPL_DEPTH`的定義:

```.cs
public enum IPL_DEPTH : uint{

...
}
```

這我們就知道了,其實(shí)是一致的。

而指針類型,都可以用`IntPtr`來代替,也沒有什么問題。事實(shí)上,我們可以在網(wǎng)上找到一張C#和C++之間的mapping[表格](http://blog.csdn.net/yangzhenping/article/details/6549150)。

最為費(fèi)解的是Size。在C++的版本中,是用的CvSize
```.c
typedef struct CvSize
{
    int width;
    int height;
}
CvSize;

```

而C#中,用的卻是Size,這是System.Drawing里面的一個類。Emgu的[教程](http://www.emgu.com/wiki/index.php/Tutorial)上面一句話帶過:

Emgu CV also borrows some existing structures in .Net to represent structures in OpenCV:

| .Net Structure | OpenCV structure |
| ------------------ | ----------------------- |
| System.Drawing.Point | CvPoint |
| System.Drawing.PointF | CvPoint2D32f |
| System.Drawing.Size | CvSize |
| System.Drawing.Rectangle | CvRect |

可是,究竟為什么可以這樣借用呢?查了很久,也沒查出個所以然來,但是最后我用Marshal.SizeOf函數(shù)測試Size的大小,得到的結(jié)果是8。但這也無法窺得Mashal機(jī)制的全貌來~或許問題就在于文中提到過的一句話:

>  .NET Framework 類型均有一個默認(rèn)非托管類型。

那么,`System.Drawing.Size`所對應(yīng)的非托管類型是什么,又是在哪里定義這個非托管類型的呢?就無從知曉了。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,533評論 19 139
  • 動態(tài)調(diào)用動態(tài)庫方法c/c++linuxwindows 關(guān)于動態(tài)調(diào)用動態(tài)庫方法說明 一、 動態(tài)庫概述 1、 動態(tài)庫的...
    KINGZ1993閱讀 14,192評論 0 10
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 12,332評論 6 13
  • 《騰訊桌球:客戶端總結(jié)》 本次分享總結(jié),起源于騰訊桌球項(xiàng)目,但是不僅僅限于項(xiàng)目本身。雖然基于Unity3D,很多東...
    吳秦閱讀 25,255評論 12 143
  • 上邪 寫給過往 上帝啊 既然不能與他結(jié)緣 為什么又偏偏讓...
    不系之舟a閱讀 382評論 1 1

友情鏈接更多精彩內(nèi)容