C#调用非托管代码(转)
对于托管的资源的回收工作,是不需要人工干预回收的,而且你也无法干预他们的回收,所能够做的
只是了解.net CLR如何做这些操作。也就是说对于您的应用程序创建的大多数对象,可以依靠 .NET
Framework 的垃圾回收器隐式地执行所有必要的内存管理任务。托管代码就是基于.net元数据格式的代码,
运行于.net平台之上,所有的与操作系统的交换有.net来完成,就像是把这些功能委托给.net,所以称之为托管代码。
举个例子l
Vc.net还可以使用mfc,atl来编写程序,他们基于MFC或者ATL,而不是.NET,所有是非托管代码,如果基于.net比如C#,VB.net则是托管代码
非托管代码是指.NET解释不了的
简单的说,托管代码的话,.net可以自动释放资料,非托管代码需要手动释放资料.
对于非托管资源,您在应用程序中使用完这些非托管资源之后,必须显示的释放他们,例如
System.IO.StreamReader的一个文件对象,必须显示的调用对象的Close()方法关闭它,否则会占用系统
的内存和资源,而且可能会出现意想不到的错误。
清楚什么是托管资源,什么是非托管资源
最常见的一类非托管资源就是包装操作系统资源的对象,例如文件,窗口或网络连接,对于这类资源
虽然垃圾回收器可以跟踪封装非托管资源的对象的生存期,但它不了解具体如何清理这些资源。还好.net
Framework提供了Finalize()方法,它允许在垃圾回收器回收该类资源时,适当的清理非托管资源。如果
在MSDN Library 中搜索Finalize将会发现很多类似的主题,这里列举几种常见的非托管资源:
ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Fon
t,Icon,Image,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Time
r,Tooltip 等等资源。可能在使用的时候很多都没有注意到!
关于托管资源,就不用说了撒,像简单的int,string,float,DateTime等等,.net中超过80%的资源都是托
管资源。
非托管资源如何释放,.NET Framework 提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对
象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。默认情况下,
Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您
必须在类中重写 Finalize 方法。然而大家都可以发现在实际的编程中根本无法override方法Finalize
(),在C#中,可以通过析构函数自动生成 Finalize 方法和对基类的 Finalize 方法的调用。
例如:
~MyClass()
{
// Perform some cleanup operations here.
}
该代码隐式翻译为下面的代码。
protected override void Finalize()
{
try
{
// Perform some cleanup operations here.
}
finally
{
base.Finalize();
}
}
但是,在编程中,并不建议进行override方法Finalize(),因为,实现 Finalize 方法或析构函数对性能
可能会有负面影响。一个简单的理由如下:用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收
,当垃圾回收器回收时,它只回收没有终结器(Finalize方法)的不可访问的内存,这时他不能回收具有终
结器(Finalize方法)的不可以访问的内存。它改为将这些对象的项从终止队列中移除并将他们放置在标记
为“准备终止”的对象列表中,该列表中的项指向托管堆中准备被调用其终止代码的对象,下次垃圾回收
器进行回收时,就回收并释放了这些内存。
C#如何直接调用非托管代码,通常有2种方法:
Win32 Types |
Specification |
CLR Type |
char, INT8, SBYTE, CHAR†|
8-bit signed integer |
System.SByte |
short, short int, INT16, SHORT |
16-bit signed integer |
System.Int16 |
int, long, long int, INT32, LONG32, BOOL†, INT |
32-bit signed integer |
System.Int32 |
__int64, INT64, LONGLONG |
64-bit signed integer |
System.Int64 |
unsigned char, UINT8, UCHAR†, BYTE |
8-bit unsigned integer |
System.Byte |
unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR†, __wchar_t |
16-bit unsigned integer |
System.UInt16 |
unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT |
32-bit unsigned integer |
System.UInt32 |
unsigned __int64, UINT64, DWORDLONG, ULONGLONG |
64-bit unsigned integer |
System.UInt64 |
float, FLOAT |
Single-precision floating point |
System.Single |
double, long double, DOUBLE |
Double-precision floating point |
System.Double |
†In Win32 this type is an integer with a specially assigned meaning; in contrast, the CLR provides a specific type devoted to this meaning. |
当“平台调用”调用非托管函数时,它将依次执行以下操作:
[DllImport]
虽等效于 [DllImportAttribute]
,但 DllImportAttribute 才是该属性在 .NET Framework 中的实际名称。将字符串作为 LPStr、LPWStr、LPTStr 或 BStr 封送到非托管代码。某些 UnmanagedType 枚举成员需要附加信息。
Api函数是构筑Windws应用程序的基石,每一种Windows应用程序开发工具,它提供的底层函数都间接或直接地调用了Windows API函数,同时为了实现功能扩展,一般也都提供了调用WindowsAPI函数的接口, 也就是说具备调用动态连接库的能力。Visual C#和其它开发工具一样也能够调用动态链接库的API函数。.NET框架本身提供了这样一种服务,允许受管辖的代码调用动态链接库中实现的非受管辖函数,包括操作系统提供的Windows API函数。它能够定位和调用输出函数,根据需要,组织其各个参数(整型、字符串类型、数组、和结构等等)跨越互操作边界。
下面以C#为例简单介绍调用API的基本过程:
动态链接库函数的声明
动态链接库函数使用前必须声明,相对于VB,C#函数声明显得更加罗嗦,前者通过 Api Viewer粘贴以后,可以直接使用,而后者则需要对参数作些额外的变化工作。
动态链接库函数声明部分一般由下列两部分组成,一是函数名或索引号,二是动态链接库的文件名。
譬如,你想调用User32.DLL中的MessageBox函数,我们必须指明函数的名字MessageBoxA或MessageBoxW,以及库名字User32.dll,我们知道Win32 API对每一个涉及字符串和字符的函数一般都存在两个版本,单字节字符的ANSI版本和双字节字符的UNICODE版本。
下面是一个调用API函数的例子:
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);
其中入口点EntryPoint标识函数在动态链接库的入口位置,在一个受管辖的工程中,目标函数的原始名字和序号入口点不仅标识一个跨越互操作界限的函数。而且,你还可以把这个入口点映射为一个不同的名字,也就是对函数进行重命名。重命名可以给调用函数带来种种便利,通过重命名,一方面我们不用为函数的大小写伤透脑筋,同时它也可以保证与已有的命名规则保持一致,允许带有不同参数类型的函数共存,更重要的是它简化了对ANSI和Unicode版本的调用。CharSet用于标识函数调用所采用的是Unicode或是ANSI版本,ExactSpelling=false将告诉编译器,让编译器决定使用Unicode或者是Ansi版本。其它的参数请参考MSDN在线帮助.
在C#中,你可以在EntryPoint域通过名字和序号声明一个动态链接库函数,如果在方法定义中使用的函数名与DLL入口点相同,你不需要在EntryPoint域显示声明函数。否则,你必须使用下列属性格式指示一个名字和序号。
[DllImport("dllname", EntryPoint="Functionname")]
[DllImport("dllname", EntryPoint="#123")]
值得注意的是,你必须在数字序号前加“#”
下面是一个用MsgBox替换MessageBox名字的例子:
[C#]
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
}
许多受管辖的动态链接库函数期望你能够传递一个复杂的参数类型给函数,譬如一个用户定义的结构类型成员或者受管辖代码定义的一个类成员,这时你必须提供额外的信息格式化这个类型,以保持参数原有的布局和对齐。
C#提供了一个StructLayoutAttribute类,通过它你可以定义自己的格式化类型,在受管辖代码中,格式化类型是一个用StructLayoutAttribute说明的结构或类成员,通过它能够保证其内部成员预期的布局信息。布局的选项共有三种:
布局选项
描述
LayoutKind.Automatic
为了提高效率允许运行态对类型成员重新排序。
注意:永远不要使用这个选项来调用不受管辖的动态链接库函数。
LayoutKind.Explicit
对每个域按照FieldOffset属性对类型成员排序
LayoutKind.Sequential
对出现在受管辖类型定义地方的不受管辖内存中的类型成员进行排序。
传递结构成员
下面的例子说明如何在受管辖代码中定义一个点和矩形类型,并作为一个参数传递给User32.dll库中的PtInRect函数,
函数的不受管辖原型声明如下:
BOOL PtInRect(const RECT *lprc, POINT pt);
注意你必须通过引用传递Rect结构参数,因为函数需要一个Rect的结构指针。
[C#]
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
class Win32API {
[DllImport("User32.dll")]
public static extern Bool PtInRect(ref Rect r, Point p);
}
类似你可以调用GetSystemInfo函数获得系统信息:
? using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO {
public uint dwOemId;
public uint dwPageSize;
public uint lpMinimumApplicationAddress;
public uint lpMaximumApplicationAddress;
public uint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public uint dwProcessorLevel;
public uint dwProcessorRevision;
}
[DllImport("kernel32")]
static extern void GetSystemInfo(ref SYSTEM_INFO pSI);
SYSTEM_INFO pSI = new SYSTEM_INFO();
GetSystemInfo(ref pSI);
类成员的传递
同样只要类具有一个固定的类成员布局,你也可以传递一个类成员给一个不受管辖的动态链接库函数,下面的例子主要说明如何传递一个sequential顺序定义的MySystemTime类给User32.dll的GetSystemTime函数, 函数用C/C++调用规范如下:
void GetSystemTime(SYSTEMTIME* SystemTime);
不像传值类型,类总是通过引用传递参数.
[C#]
[StructLayout(LayoutKind.Sequential)]
public class MySystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
class Win32API {
[DllImport("User32.dll")]
public static extern void GetSystemTime(MySystemTime st);
}
回调函数的传递:
从受管辖的代码中调用大多数动态链接库函数,你只需创建一个受管辖的函数定义,然后调用它即可,这个过程非常直接。
如果一个动态链接库函数需要一个函数指针作为参数,你还需要做以下几步:
首先,你必须参考有关这个函数的文档,确定这个函数是否需要一个回调;第二,你必须在受管辖代码中创建一个回调函数;最后,你可以把指向这个函数的指针作为一个参数创递给DLL函数,.
回调函数及其实现:
回调函数经常用在任务需要重复执行的场合,譬如用于枚举函数,譬如Win32 API 中的EnumFontFamilies(字体枚举), EnumPrinters(打印机), EnumWindows (窗口枚举)函数. 下面以窗口枚举为例,谈谈如何通过调用EnumWindow 函数遍历系统中存在的所有窗口
分下面几个步骤:
1. 在实现调用前先参考函数的声明
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam)
显然这个函数需要一个回调函数地址作为参数.
2. 创建一个受管辖的回调函数,这个例子声明为代表类型(delegate),也就是我们所说的回调,它带有两个参数hwnd和lparam,第一个参数是一个窗口句柄,第二个参数由应用程序定义,两个参数均为整形。
当这个回调函数返回一个非零值时,标示执行成功,零则暗示失败,这个例子总是返回True值,以便持续枚举。
3. 最后创建以代表对象(delegate),并把它作为一个参数传递给EnumWindows 函数,平台会自动地 把代表转化成函数能够识别的回调格式。
[C#]
using System;
using System.Runtime.InteropServices;
public delegate bool CallBack(int hwnd, int lParam);
public class EnumReportApp {
[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);
public static void Main()
{
CallBack myCallBack = new CallBack(EnumReportApp.Report);
EnumWindows(myCallBack, 0);
}
public static bool Report(int hwnd, int lParam) {
Console.Write("窗口句柄为");
Console.WriteLine(hwnd);
return true;
}
}
指针类型参数传递:
在Windows API函数调用时,大部分函数采用指针传递参数,对一个结构变量指针,我们除了使用上面的类和结构方法传递参数之外,我们有时还可以采用数组传递参数。
下面这个函数通过调用GetUserName获得用户名
BOOL GetUserName(
LPTSTR lpBuffer, // 用户名缓冲区
LPDWORD nSize // 存放缓冲区大小的地址指针
);
[DllImport("Advapi32.dll",
EntryPoint="GetComputerName",
ExactSpelling=false,
SetLastError=true)]
static extern bool GetComputerName (
[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,
[MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );
这个函数接受两个参数,char * 和int *,因为你必须分配一个字符串缓冲区以接受字符串指针,
你可以使用String类代替这个参数类型,当然你还可以声明一个字节数组传递ANSI字符串,同样你也可以声明一个只有一个元素的长整型数组,使用数组名作为第二个参数。上面的函数可以调用如下:
byte[] str=new byte[20];
Int32[] len=new Int32[1];
len[0]=20;
GetComputerName (str,len);
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));
最后需要提醒的是,每一种方法使用前必须在文件头加上:
using System.Runtime.InteropServices;