.Net平台互操作技术:02. 技术介绍
上一篇文章简单介绍了.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. 技术介绍的更多相关文章
- iOS开发数据持久化技术02——plist介绍
有疑问的请加qq交流群:390438081 我的QQ:604886384(注明来意) 微信:niuting823 1. 简单介绍:属性列表是一种xml格式的文件.扩展名.plist: 2. 特性:pl ...
- .Net平台互操作技术:03. 技术验证
上面两篇文章分别介绍了.Net平台互操作技术面临的问题,并重点介绍了通过P/Invoke调用Native C++类库的技术实现.光说不做是假把式,本文笔者将设计实验来证明P/Invoke调用技术的可行 ...
- [转] KVM虚拟化技术生态环境介绍
KVM虚拟化技术生态环境介绍 http://xanpeng.github.io/wiki/virt/kvm-virtulization-echosystem-intro.html kvm和qemu/q ...
- JSP技术的优缺点介绍
什么是JSP?JSP可用一种简单易懂的等式表示为:HTML+Java=JSP. JSP技术使用Java编程语言编写类XML的tags和scriptlets,来封装产生动态网页的处理逻辑. 网页还能通过 ...
- the5fire博客对接微信公众平台接口 | the5fire的技术博客
the5fire博客对接微信公众平台接口 | the5fire的技术博客 the5fire博客对接微信公众平台接口
- Java开源生鲜电商平台-系统架构与技术选型(源码可下载)
Java开源生鲜电商平台-系统架构与技术选型(源码可下载) 1. 硬件环境 公司服务器 2. 软件环境 2.1 操作系统 Linux CentOS 6.8系列 2.2 反向代理/web服务器 ...
- oracle 11g 数据库恢复技术 ---02 控制文件
oracle 11g 数据库恢复技术 ---02 控制文件 SYS@ orcl >show parameter control_file NAME TYPE VALUE ------------ ...
- EMIS快速开发平台 - 微服务版技术选型
http://demo.zuoyour.com/system/login EMIS快速开发平台 - 微服务版技术选型 开发框架:Spring Boot 2.1.3.RELEASE 微服务:Spring ...
- Net平台下的消息队列介绍
Net平台下的消息队列介绍 本系列主要记录最近学习消息队列的一些心得体会,打算形成一个系列文档.开篇主要介绍一下.Net平台下一些主流的消息队列框架. RabbitMQ:http:// ...
随机推荐
- rt-thread的定时器管理源码分析
1 前言 rt-thread可以采用软件定时器或硬件定时器来实现定时器管理的,所谓软件定时器是指由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务.而硬件 ...
- MUI框架开发HTML5手机APP
出处:http://www.cnblogs.com/jerehedu/p/7832808.html 前 言 JRedu 随着HTML5的不断发展,移动开发成为主流趋势!越来越多的公司开始选择使用H ...
- 九 Vue学习 manager页面布局
1: 登录后系统页面如下: 对应代码: <template> <div class="manage_page fillcontain"> <el-r ...
- fastIO模板
freadIO整理 namespace fastIO{ #define BUF_SIZE 100000 ; inline char nc() { static char buf[BUF_SIZE],* ...
- AngularJs(Part 5)--与后台联系
AngularJS内置了$http这个服务来与后台联系.(默认会把接受到的数据转换为json)当然,还有一个$resource来提供与RESTful后台联系的服务. $http服务 $http比 ...
- WordPress博客搭建指南
WordPress是一个以PHP和MySQL为平台的自由开源的博客软件和内容管理系统.WordPress具有插件架构和模板系统.Alexa排行前100万的网站中有超过16.7%的网站使用WordPre ...
- 查看 打包秘钥的 SHA1
keytool -v -list -keystore C:\Users\XXX\.android\debug.keystore 输入密钥库口令: android android
- WPF语言切换,国际化
winform语言切换在每个窗口下面有一个.resx结尾的资源文件,在上面添加新字符串就好了: WPF语言切换跟winform不一样的地方在于需要自己添加资源文件,并且这个资源文件可以写一个,也可以写 ...
- VisualStudio2017中新建的ASP.NET Core项目中的各个文件的含义
Program.cs is the entry point for the web application; everything starts from here. As we mentione ...
- 3damx平滑组注意事项
需要在Editable Poly->面级别,选中(需要平滑的)面,然后去点平滑组或自动平滑 如果出现下图的情况,说明可能是有多余点,点没缝合 max中确认,确实是点没缝合导致