上一篇文章简单介绍了.Net平台互操作技术的面临的主要问题,以及主要的解决方案。本文将重点介绍使用相对较多的P/Invoke技术的实现:C#通过P/Invoke调用Native C++ Dll技术、C#调用Native C++代码示例、非托管内存的释放和平台调用性能提升技巧。

1 C# 通过P/Invoke调用Native C++ Dll技术

1.1 C# 中安全代码与不安全代码

通常,公共语言运行时(CLR)负责检查 Microsoft 中间语言(MSIL)代码的行为,防止任何有问题的操作。但是,有时您希望直接访问低级功能(如:Native C++模块、Win32 API调用等),表现为通过指针操作内存。为此,C#提供了对不安全(不安全指的是内存不会被管理)类型代码的支持。不安全代码必须放在源代码中的不安全代码块内。

1.1.1 unsafe 关键字

C#中不安全的代码必须用unsafe关键字标识出来。unsafe可以标识整个方法、大括号内的代码块和单个语句。下面代码演示如何使用unsafe关键字:

unsafe static void PointyMethod()
{
//unsafe function
}
static void StillPointy()
{
unsafe
{
//unsafe code block
}
int i = 10;
unsafe int* p = &i; //unsafe statement
}

1.1.2 fixed 关键字

在安全代码中,垃圾回收器在对象的生命周期内可以自由地移动对象,以组织和压缩可用资源。但是,如果代码使用了指针,则此行为可能很容易造成意外的结果,因此您可以使用fixed语句来指示垃圾回收器不要移动某些对象。下面的代码演示了使用fixed关键字以确保在执行方法中的不安全代码块时系统不会移动数组。注意:fixed 只能用于不安全的代码中:

unsafe
{
fixed (char *p = array)
{
for (int i=0; i<array.Length; i++) {//logic}
}
}

在我们的实际开发中,较少用到这两个关键字。

1.2 C#中的DllImport详细介绍

1.2.1命名空间:

using System.Runtime.InteropServices;

1.2.2 DllImport说明

1) DllImport只能放置在方法声明上。

2) DllImport具有单个定位参数:指定导入Dll的地址。

3) DllImport具有五个命名参数:

a) CallingConvention:指示入口点的调用约定。默认值:CallingConvention.Winapi

b) CharSet :指示用在入口点中的字符集。默认值:CharSet.Auto

c) EntryPoint:指示Dll中入口点的名称。默认值:方法本身的名称

d) ExactSpelling:指示EntryPoint是否必须与指示的入口点拼写完全匹配。默认值:False

e) PreserveSig:指示方法的签名应当被保留还是被转换。默认值:True

f) SetLastError:指示方法是否保留Win 32“上一错误”。默认值:False

4) DllImport是一次性属性类

5) 用DllImport属性修饰的方法必须具有extern修饰符。

1.2.3 DllImport的用法:

1) 静态调用Native C++ Dll

[DllImport("myDll.dll", EntryPoint = "fun")]
public static extern int fun(int a, int b);

2) 动态调用Native C++ Dll

[DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
public static extern int LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);
[DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
public static extern IntPtr GetProcAddress(int hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);
[DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
public static extern bool FreeLibrary(int hModule);
int hModule = NativeMethod.LoadLibrary("myDll.dll");//动态读取
if (hModule == 0) { return; }
IntPtr add = NativeMethod.GetProcAddress(hModule, "Add");
if (add == IntPtr.Zero) { return; }

1.3 数据封送处理

在托管代码对非托管函数进行平台调用时,会进行数据封送处理。封送指的是在托管内存和非托管内存之间传递数据的过程。它是一个双向的过程,不仅在托管代码向非托管代码传递参数时发生,在非托管代码向托管代码返回结果时也发生。封送过程由封送拆收器完成,主要有三项任务:首先将数据从非托管类型转换为托管类型,或者由托管类型转换为非托管类型。然后,再将经过类型转换的数据从非托管内存复制到托管内存,或者从托管内存复制到非托管内存。最后,在调用完成后,释放掉封送过程中分配的内存。

1.3.1封送字符串

由于不同的编程语言对字符串的实现机制不同,因此导致在托管代码中平台调用C/C++函数时,必须对字符串进行特殊的封送处理。主要注意一下几点:

1. 字符是ANSI格式还是Unicode格式,需要设置相应的DllImport的CharSet参数。

2. 在托管代码中使用相应的字符类型与非托管字符类型对应。

3. 注意释放非托管内存,避免内存泄漏。

4. 注意字符参数的方向属性。如果需要将非托管代码对字符串的修改返回托管代码,则必须使用StringBuilder。

1.3.2封送结构体

无论是对作为参数的结构体,还是对作为返回值的结构体进行封送,主要注意以下几点。

1. 必须在托管代码中定义一个与非托管结构体等价的托管结构体。

2. 善于使用StructLayout属性及其参数来指定结构体的内存布局和对齐方式。

1.3.3封送类

对类的封送和对结构体的封送的方式类似。他们之间的区别在于结构体是值传递,类是传递引用。对于非blittable引用类型,非托管代码对它的修改不会反应到托管代码中,除非显示地使用[In, Out]或者ref / out标识。

1.3.4封送数组

数组传递的是引用。在传递的时候适用封送类的情况。

2 C#调用Native C++代码示例

2.1参数是int类型的示例

C# Code
[DllImport("myDll.dll", EntryPoint = "fun")]
public static extern int fun(int a, int b);
C++ Code
extern "C" __declspec(dllexport) int fun(int a, int b)
{
return a+b;
}

2.2参数是int*类型,返回值是int*类型的示例

C# Code
[DllImport("myDll.dll", EntryPoint = "fun")]
unsafe public static extern IntPtr fun(ref int a, ref int b, ref int result);
C++ Code
extern "C" __declspec(dllexport) int* fun(int* a, int* b, int* result)
{
*result = (*a+*b);
return result;
}

2.3参数是char*、wchar_t*类型的示例

C# Code
[DllImport(dllPath, CallingConvention = CallingConvention.Cdecl)]
public extern static void TestStringMarshalArguments(
[MarshalAs(UnmanagedType.LPStr)] string inAnsiString,
[MarshalAs(UnmanagedType.LPWStr)] string inUnicodeString,
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder outStringBuffer,
int outBufferSize);
C++ Code
extern "C" __declspec(dllexport) void __cdecl TestStringMarshalArguments(const char* inAnsiString, const wchar_t* inUnicodeString, wchar_t* outUnicodeString, int outBufferSize)
{
size_t ansiStrLength = strlen(inAnsiString);
size_t uniStrLength = wcslen(inUnicodeString);
size_t totalSize = ansiStrLength + uniStrLength + 2;
wchar_t* tempBuffer = new(std::nothrow) wchar_t[totalSize];
if(NULL == tempBuffer)
{
return;
}
wmemset(tempBuffer, 0, totalSize);
mbstowcs(tempBuffer, inAnsiString, totalSize);
wcscat_s(tempBuffer, totalSize, L" ");
wcscat_s(tempBuffer, totalSize, inUnicodeString);
wcscpy_s(outUnicodeString, outBufferSize, tempBuffer);
delete[] tempBuffer;
}

2.4返回值是char*类型的示例

C# Code
[DllImport(dllPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public extern static IntPtr TestStringAsResult(int id);
C++ Code
extern "C" __declspec(dllexport) char* __cdecl TestStringAsResult(int id)
{
int size = 64;
char* result = (char*)CoTaskMemAlloc(size);
sprintf_s(result, size/sizeof(char), "Result of ID: %d", id);
return result;
}

2.5参数是char数组,同时也是返回值

C# Code
[DllImport(dllPath, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public extern static uint TestArrayOfChar([In, Out] char[] charArray, int arraySize);
C++ Code
extern "C" __declspec(dllexport) UINT __cdecl TestArrayOfChar(char charArray[], int arraySize)
{
int result = 0;
for(int i = 0; i < arraySize; i++)
{
if (isdigit(charArray[i]))
{
result++;
charArray[i] = '@';
}
}
return result;
}

2.6参数是Structure\Class类型,返回Structure\Class指针类型的示例

由于C++自定义类型在C#中不会被识别,解决方法是在C#中定义等价的结构体,然后传递内存地址给Native C++ Dll。需要注意的是,相同类型在C++中和C#中所占的字节是不一样的。在处理布局内存的时候,需要具体考虑。在C#中定义对应的结构时,LayoutKind可以选择:Auto、Sequential和Explicit。Explicit模式下,可以通过FieldOffset(num)显示的指定占用的字节数。示例如下:

C# Code
//Structure definition
[StructLayout(LayoutKind.Sequential)]
public struct CPerson
{
public int Age;
public double Height;
[MarshalAS(UnmanagedType.LPStr)]
public string Name;
}
//funtion in C#
[DllImport("myDll.dll", EntryPoint = "fun")]
unsafe public static extern IntPtr fun(ref CPerson a, ref CPerson b, ref CPerson result);
C++ code
//Class definition
#pragma once
class __declspec(dllexport) CPerson
{
public:
CPerson();
void SetAge(int iAge);
int GetAge();
public:
int Age;
double Height;
char* Name;
};
CPerson::CPerson() { }
void CPerson::SetAge(int iAge){ Age = iAge; }
int CPerson::GetAge(){ return Age; }
//function impletmentation in C++
extern "C" __declspec(dllexport) CPerson* fun(CPerson* cPerson1,CPerson* cPerson2,CPerson* result)
{
if(cPerson1==NULL||cPerson2==NULL){return NULL;}
result->SetAge(cPerson1->GetAge()+cPerson2->GetAge());
return result;
}

3 非托管内存的释放

在前面的例子中,为了力求简洁的说明问题,没有考虑非托管内存的释放。实际编程中,需要及时的释放非托管程序中动态申请的内存空间,否则会造成内存泄漏。

要想成功的释放非托管内存,首先要清楚它生产的方式(malloc, new etc),然后用相应的方式(free, delete etc)释放内存。在非托管环境中主要有三种申请内存的方法:new(使用delete释放内存), malloc(使用free释放内存)和CoTaskMemAlloc(使用CoTaskMemFree释放内存)。如果是前两种方式申请的内存,在托管代码中无法直接对其进行释放,必须在非托管代码中实现一个能够释放此非托管内存的方法,然后在托管代码中调用该方法对非托管内存进行释放。示例程序如下:

C# Code
[DllImport(dllPath, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern void FreeMallocMemory(IntPtr buffer);
[DllImport(dllPath, CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern IntPtr GetStringMalloc();
private static void TestGetString()
{
try
{
IntPtr stringPtr = NativeMethod.GetStringMalloc();
string str = Marshal.PtrToStringUni(stringPtr);
NativeMethod.FreeMallocMemory(stringPtr);
}
catch (Exception ex) { }
}
C++ code
extern "C" __declspec(dllexport) void FreeMallocMemory(void* buffer)
{
if(NULL != buffer)
{
free(buffer);
buffer = NULL;
}
}
extern "C" __declspec(dllexport) wchar_t* GetStringMalloc()
{
int size = 128;
wchar_t* buffer = (wchar_t*)malloc(size);
if(NULL != buffer)
{
wcscpy_s(buffer, size / sizeof(wchar_t), L"String from Malloc");
}
return buffer;
}

如果是采用CoTaskMemAlloc方式申请内存,那么封送拆收器是能够将其释放掉的。原因在于封送拆收器在对非托管内存进行处理时,会将CoTaskMemAlloc作为分配内存的默认方式。因此,当封送拆收器将一个非托管内存指针封送成.Net对象时,封送拆收器会使用非托管数据的一个复制创建一个.Net对象。由于非托管数据已经被封送拆收器获取,因此封送拆收器就会使用相应的释放内存的方式CoTaskMemFree来释放掉这块已经被封送过的非托管内存。所以,在编程托管代码与非托管代码交互的程序时,推荐使用CoTaskMemAlloc方法申请内存。

4 平台调用性能提升技巧

4.1显示地指定要调用的非托管函数的名称

通过将关键字ExactSpelling设置为true显示指定非托管函数的名称,缩短CLR寻找非托管函数的时间。否则,他将会按照一定的规则模糊的搜索非托管函数。

4.2对数据封送处理进行优化

a. 尽量使用blittable数据类型:CLR对数据封送时,有两种选择:锁定数据和复制数据。第二种方式会耗费多一些时间,因为他有一个数据转换的过程。而blittable数据类型采用的是第一种方式。

b. 尽可能的减少数据封送的次数:如:将一些循环逻辑转移到非托管代码中,而不要循环的调用非托管函数。

4.3尽量避免字符串编码转换

.Net采用的是Unicode编码,如果要调用的非托管函数采用的是ANSI编码,那么就会有类型转换的过程,耗费性能。所以,在非托管代码中应该尽可能的采用Unicode编码方式。

4.4 Native C++和托管平台基本类型位宽比较

在托管平台和非托管平台,即使是相同的数据类型。也会占用不同的位宽。了解他们之间的差异,对在数据封送和类型转换中选择合适的数据类型非常重要。例如,在Native C++中,void*类型被强制转换为char*类型,那么在C#环境中应该将void*类型强制转换为byte*类型才能达到和Native环境等价的效果。下表列出了Native环境和托管环境各基本数据类型的位宽:

数据类型 \ 平台类型

Native C++

托管环境(C#)

int

4 byte

4 byte

long

4 byte

8 byte

short

2 byte

2 byte

byte

1 byte

1 byte

char

1 byte

2 byte

wchar_t

2 byte

无此类型

double

8 byte

8 byte

float

4 byte

4 byte

void

0 byte

无此类型

.Net平台互操作技术:02. 技术介绍的更多相关文章

  1. iOS开发数据持久化技术02——plist介绍

    有疑问的请加qq交流群:390438081 我的QQ:604886384(注明来意) 微信:niuting823 1. 简单介绍:属性列表是一种xml格式的文件.扩展名.plist: 2. 特性:pl ...

  2. .Net平台互操作技术:03. 技术验证

    上面两篇文章分别介绍了.Net平台互操作技术面临的问题,并重点介绍了通过P/Invoke调用Native C++类库的技术实现.光说不做是假把式,本文笔者将设计实验来证明P/Invoke调用技术的可行 ...

  3. [转] KVM虚拟化技术生态环境介绍

    KVM虚拟化技术生态环境介绍 http://xanpeng.github.io/wiki/virt/kvm-virtulization-echosystem-intro.html kvm和qemu/q ...

  4. JSP技术的优缺点介绍

    什么是JSP?JSP可用一种简单易懂的等式表示为:HTML+Java=JSP. JSP技术使用Java编程语言编写类XML的tags和scriptlets,来封装产生动态网页的处理逻辑. 网页还能通过 ...

  5. the5fire博客对接微信公众平台接口 | the5fire的技术博客

    the5fire博客对接微信公众平台接口 | the5fire的技术博客 the5fire博客对接微信公众平台接口

  6. Java开源生鲜电商平台-系统架构与技术选型(源码可下载)

    Java开源生鲜电商平台-系统架构与技术选型(源码可下载) 1.  硬件环境 公司服务器 2.   软件环境 2.1  操作系统 Linux CentOS 6.8系列 2.2 反向代理/web服务器 ...

  7. oracle 11g 数据库恢复技术 ---02 控制文件

    oracle 11g 数据库恢复技术 ---02 控制文件 SYS@ orcl >show parameter control_file NAME TYPE VALUE ------------ ...

  8. EMIS快速开发平台 - 微服务版技术选型

    http://demo.zuoyour.com/system/login EMIS快速开发平台 - 微服务版技术选型 开发框架:Spring Boot 2.1.3.RELEASE 微服务:Spring ...

  9. Net平台下的消息队列介绍

    Net平台下的消息队列介绍   本系列主要记录最近学习消息队列的一些心得体会,打算形成一个系列文档.开篇主要介绍一下.Net平台下一些主流的消息队列框架.       RabbitMQ:http:// ...

随机推荐

  1. rt-thread的定时器管理源码分析

    1 前言 rt-thread可以采用软件定时器或硬件定时器来实现定时器管理的,所谓软件定时器是指由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务.而硬件 ...

  2. MUI框架开发HTML5手机APP

    出处:http://www.cnblogs.com/jerehedu/p/7832808.html  前  言 JRedu 随着HTML5的不断发展,移动开发成为主流趋势!越来越多的公司开始选择使用H ...

  3. 九 Vue学习 manager页面布局

    1:  登录后系统页面如下: 对应代码: <template> <div class="manage_page fillcontain"> <el-r ...

  4. fastIO模板

    freadIO整理 namespace fastIO{ #define BUF_SIZE 100000 ; inline char nc() { static char buf[BUF_SIZE],* ...

  5. AngularJs(Part 5)--与后台联系

    AngularJS内置了$http这个服务来与后台联系.(默认会把接受到的数据转换为json)当然,还有一个$resource来提供与RESTful后台联系的服务. $http服务    $http比 ...

  6. WordPress博客搭建指南

    WordPress是一个以PHP和MySQL为平台的自由开源的博客软件和内容管理系统.WordPress具有插件架构和模板系统.Alexa排行前100万的网站中有超过16.7%的网站使用WordPre ...

  7. 查看 打包秘钥的 SHA1

    keytool -v -list -keystore C:\Users\XXX\.android\debug.keystore 输入密钥库口令: android android

  8. WPF语言切换,国际化

    winform语言切换在每个窗口下面有一个.resx结尾的资源文件,在上面添加新字符串就好了: WPF语言切换跟winform不一样的地方在于需要自己添加资源文件,并且这个资源文件可以写一个,也可以写 ...

  9. VisualStudio2017中新建的ASP.NET Core项目中的各个文件的含义

     Program.cs is the entry point for the web application; everything starts from here. As we mentione ...

  10. 3damx平滑组注意事项

    需要在Editable Poly->面级别,选中(需要平滑的)面,然后去点平滑组或自动平滑 如果出现下图的情况,说明可能是有多余点,点没缝合 max中确认,确实是点没缝合导致