从Emgu学习C#调用C++库(1)

最近,图像处理组之前专门做MFC的同事离职了,这开发界面的事情就落到了我们自己的头上。

这几乎是大家的共识:用C++调用OpenCV写函数底层库很方便;但是用MFC写上层的GUI程序却很麻烦。因为MFC太不C++了。我成为一只程序猿以来,从来没有任何一刻喜欢上MFC。而我家那位一直用的是C#开发界面的,再加上另一位基友的推荐,因此我便想尝试这用C#来做界面的开发。但底层还是得用C/C++来做图像处理啊,这就牵扯到了C#调用非托管的C/C++动态库的问题。

C#,我是小白一只。但好在各路大神的帮忙以及它本身的便捷性,编写个界面啥的也是没有什么问题。但用C#来调用C/C++ 动态库,朋友们用的不多,我就只能自己艰苦奋斗啦~但是我是很有信心的。因为我知道,OpenCV有一个C#的版本,叫Emgu,它实际上也是调用的OpenCV的C++ DLL。Emgu在这个动态库之上又封装了一层,形成了C#的动态库,使得在C#界面上可以轻松调用OpenCV。既然Emgu能做到,我就也能做到。

我也不打算把Emgu全部解析一遍,我既没有这个能力,也没有这个时间,咱们就实用主义,来看看Emgu是如何调用OpenCV的动态库的吧。

拿一个我们最常用的函数作为例子吧。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# 调用 DLL 导出:若要声明一个方法使其具有来自 DLL 导出的实现,请执行下列操作:

  • 使用 C# 关键字 staticextern 声明方法。
  • 将 DllImport 属性附加到该方法。DllImport 属性允许您指定包含该方法的 DLL 的名称。通常的做法是用与导出的方法相同的名称命名 C# 方法,但也可以对 C# 方法使用不同的名称。
  • 还可以为方法的参数和返回值指定自定义封送处理信息,这将重写 .NET Framework 的默认封送处理。

示例:

// 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 中实现的 C# 方法的最低要求。PlatformInvokeTest.puts 方法用 static 和 extern 修饰符声明并且具有 DllImport 属性,该属性使用默认名称 puts 通知编译器此实现来自 msvcrt.dll。若要对 C# 方法使用不同的名称(如 putstring),则必须在 DllImport 属性中使用 EntryPoint 选项,如下所示:

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

官网上没有详细说的是CallingConvention。这个是调用惯例。可以直接看源代码:

    public enum CallingConvention
    {
        // 摘要:
        //     此成员实际上不是调用约定,而是使用了默认平台调用约定。例如,在 Windows 上默认为 System.Runtime.InteropServices.CallingConvention.StdCall,在
        //     Windows CE.NET 上默认为 System.Runtime.InteropServices.CallingConvention.Cdecl。
        Winapi = 1,
        //
        // 摘要:
        //     调用方清理堆栈。这使您能够调用具有 varargs 的函数(如 Printf),使之可用于接受可变数目的参数的方法。
        Cdecl = 2,
        //
        // 摘要:
        //     被调用方清理堆栈。这是使用平台 invoke 调用非托管函数的默认约定。
        StdCall = 3,
        //
        // 摘要:
        //     第一个参数是 this 指针,它存储在寄存器 ECX 中。其他参数被推送到堆栈上。此调用约定用于对从非托管 DLL 导出的类调用方法。
        ThisCall = 4,
        //
        // 摘要:
        //     不支持此调用约定。
        FastCall = 5,
    }  

支持的有Winapi, Cdecl, StdCall, ThisCall, FastCall。我一般使用的都是Stdcall。调用惯例要保持一致,C++的代码上用的什么调用惯例,则C#上就使用什么调用惯例。

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

当从 C# 代码中调用非托管函数时,公共语言运行库必须封送参数和返回值。

对于每个 .NET Framework 类型均有一个默认非托管类型,公共语言运行库将使用此非托管类型在托管到非托管的函数调用中封送数据。例如,C# 字符串值的默认封送处理是封送为 LPTSTR(指向 TCHAR 字符缓冲区的指针)类型。可以在非托管函数的 C# 声明中使用 MarshalAs 属性重写默认封送处理。

示例 2
本示例使用 DllImport 属性输出一个字符串。它还显示如何通过使用 MarshalAs 属性重写函数参数的默认封送处理。

// 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 函数的参数的默认封送处理已从默认值 LPTSTR 重写为 LPSTR。

MarshalAs属性可以放置在方法参数、方法返回值以及结构和类的字段上。若要设置方法返回值的封送处理,请将 MarshalAs 属性与返回属性位置重写一起放置在方法上的属性块中。例如,若要显式设置 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{

...
}
```

这我们就知道了,其实是一致的。

而指针类型,都可以用`IntPtr`来代替,也没有什么问题。事实上,我们可以在网上找到一张C#和C++之间的mapping[表格](http://blog.csdn.net/yangzhenping/article/details/6549150)。

最为费解的是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函数测试Size的大小,得到的结果是8。但这也无法窥得Mashal机制的全貌来~或许问题就在于文中提到过的一句话:

>  .NET Framework 类型均有一个默认非托管类型。

那么,`System.Drawing.Size`所对应的非托管类型是什么,又是在哪里定义这个非托管类型的呢?就无从知晓了。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 动态调用动态库方法c/c++linuxwindows 关于动态调用动态库方法说明 一、 动态库概述 1、 动态库的...
    KINGZ1993阅读 13,880评论 0 10
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,861评论 6 13
  • 《腾讯桌球:客户端总结》 本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身。虽然基于Unity3D,很多东...
    吴秦阅读 24,380评论 12 142
  • 上邪 写给过往 上帝啊 既然不能与他结缘 为什么又偏偏让...
    纶纶妈阅读 329评论 1 1