C#(020):C#互操作
作者:互联网
一、引言
“为什么我们需要掌握互操作技术的呢?” 对于这个问题的解释就是——掌握了.NET平台下的互操作性技术可以帮助我们在.NET中调用非托管的dll和COM组件。
。.NET 平台下提供了3种互操作性的技术:
- Platform Invoke(P/Invoke),即 平台调用 ,主要用于调用C库函数和Windows API
- C++ Introp, 主要用于Managed C++(托管C++)中调用C++类库
- COM Interop, 主要用于 在.NET中调用COM组件 和在COM中使用.NET程序集。
二、平台调用
使用平台调用的技术可以在托管代码中调用动态链接库(Dll)中实现的非托管函数,如Win32 Dll和C/C++ 创建的dll。
2.1 在托管代码中通过平台调用来调用非托管代码的步骤
(1). 获得非托管函数的信息,即dll的名称,需要调用的非托管函数名等信息
(2). 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性
(3). 在托管代码中直接调用第二步中声明的托管函数
平台调用的过程可以通过下图更好地理解:
2.2、如何使用平台调用Win32 函数——从实例开始
第一步就需要知道非托管函数声明,为了找到需要需要调用的非托管函数,可以借助两个工具——Visual Studio自带的 dumpbin.exe 和
depends.exe.
- dumpbin.exe 是一个命令行工具,可以用于查看从非托管DLL中导出的函数等信息,可以通过打开Visual Studio 2010 Command Prompt(中文版为Visual Studio 命令提示(2010)),然后切换到DLL所在的目录,输入 dummbin.exe/exports dllName, 如 dummbin.exe/exports User32.dll 来查看User32.dll中的函数声明,关于更多命令的参数可以参看MSDN;
- 然而 depends.exe是一个可视化界面工具,大家可以从 “ *VS安装目录\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin* ” 这个路径找到,然后双击 depends.exe 就可以出来一个可视化界面(如果某些人安装的VS没有附带这个工具,也可以从官方网站下载:http://www.dependencywalker.com/),如下图:
上图中 用红色标示出 MessageBox 有两个版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW
代笔的就是Unicode版本,这也是上面所说的依据。下面就看看
MessageBox的C++声明的(更多的函数的定义大家可以从MSDN中找到,这里提供MessageBox的定义在MSDN中的链接:<http://msdn.microsoft.com/en-
us/library/windows/desktop/ms645505(v=vs.85).aspx> ):
$1```
```csharp
$1```
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, String text, String caption, uint type);
```csharp
$1```
[DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
```csharp
$1```
{
// 在托管代码中直接调用声明的托管函数 使用CharSet字段指定的方式,要求在托管代码中声明的函数名必须与非托管函数名一样 否则就会出现找不到入口点的运行时错误
// 下面的调用都可以运行正确
MessageBox(new IntPtr(0), "Learning Hard", "欢迎", 0);
MessageBoxA(new IntPtr(0), "Learning Hard", "欢迎", 0);
MessageBoxW(new IntPtr(0), "Learning Hard", "欢迎", 0);
//使用指定函数入口点的方式调用,OK
MessageBox3(new IntPtr(0), "Learning Hard", "欢迎", 0);
```csharp
$1```
```csharp
$1```
MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0);
}
catch (DllNotFoundException dllNotFoundExc)
{
Console.WriteLine("DllNotFoundException 异常发生,异常信息为: " + dllNotFoundExc.Message);
}
catch (EntryPointNotFoundException entryPointExc)
{
Console.WriteLine("EntryPointNotFoundException 异常发生,异常信息为: " + entryPointExc.Message);
}
捕获由Win32函数本身返回异常的演示代码如下:要想获得在调用Win32函数过程中出现的错误信息,首先必须将DllImport属性的SetLastError字段设置为true,只有这样,平台调用才会将最后一次调用Win32产生的错误码保存起来,然后会在托管代码调用Win32失败后,通过Marshal类的静态方法GetLastWin32Error获得由平台调用保存的错误码,从而对错误进行相应的分析和处理。
class Program
{
// Win32 API
// DWORD WINAPI GetFileAttributes(
// _In_ LPCTSTR lpFileName
//);
// 在托管代码中对非托管函数进行声明
[DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]
public static extern uint GetFileAttributes(string filename);
static void Main(string[] args)
{
// 试图获得一个不存在文件的属性
// 此时调用Win32函数会发生错误
GetFileAttributes("FileNotexist.txt");
// 在应用程序的Bin目录下存在一个test.txt文件,此时调用会成功
//GetFileAttributes("test.txt");
// 获得最后一次获得的错误
int lastErrorCode = Marshal.GetLastWin32Error();
// 将Win32的错误码转换为托管异常
//Win32Exception win32exception = new Win32Exception();
Win32Exception win32exception = new Win32Exception(lastErrorCode);
if (lastErrorCode != 0)
{
Console.WriteLine("调用Win32函数发生错误,错误信息为 : {0}", win32exception.Message);
}
else
{
Console.WriteLine("调用Win32函数成功,返回的信息为: {0}", win32exception.Message);
}
Console.Read();
}
}
### 2.4 数据封送
数据封送是——在托管代码中对非托管函数进行互操作时,需要通过方法的参数和返回值在托管内存和非托管内存之间传递数据的过程,数据封送处理的过程是由CLR(公共语言运行时)的封送处理服务(即封送拆送器)完成的。
封送时需要处理的数据类型分为两种——可直接复制到本机结构中的类型(blittable)和非直接复制到本机结构中的类型(non-bittable)。
#### **2.4.1** 可直接复制到本机结构中的类型
把在托管内存和非托管内存中有相同表现形式的数据类型称为——可直接复制到本机结构中的类型,
**这些数据类型不需要封送拆送器进行任何特殊的处理就可以在托管和非托管代码之间传递** ,
下面列出一些课直接复制到本机结构中的简单数据类型:
Windows 数据类型
|
非托管数据类型
|
托管数据类型
|
托管数据类型解释
---|---|---|---
BYTE/Uchar/UInt8
|
unsigned char
|
**System.Byte**
|
无符号8位整型
Sbyte/Char/Int8
|
char
|
**System.SByte**
|
有符号8位整型
Short/Int16
|
short
|
**System.Int16**
|
有符号16位整型
USHORT/WORD/UInt16/WCHAR
|
unsigned short
|
**System.UInt16**
|
无符号16位整型
Bool/HResult/Int/Long
|
long/int
|
**System.Int32**
|
有符号32位整型
DWORD/ULONG/UINT
|
unsigned long/unsigned int
|```csharp
$1```
_int64
|
**System.Int64**
|
有符号64位整型
UINT64/DWORDLONG/ULONGLONG
|
_uint64
|
**System.UInt64**
|
无符号64位整型
INT_PTR/hANDLE/wPARAM
|
void*/int或_int64
|
**System.IntPtr**
|
有符号指针类型
HANDLE
|
void*
|
**System.UIntPtr**
|
无符号指针类型
FLOAT
|
float
|
**System.Single**
|
单精度浮点数
DOUBLE
|
double
|
**System.Double**
|
双精度浮点数
除了上表列出来的简单类型之外,还有一些复制类型也属于可直接复制到本机结构中的数据类型:
(```csharp
$1```
非托管数据类型
|
托管数据类型
|
```csharp
$1```
|
bool
```csharp
$1```
**System.Char**
|```csharp
$1```
|
**System.StringBuilder**
|
ANSI字符串/Unicode字符串,如果非托管代码需要更新此字符串,然后把更新的字符串传回托管代码中,此时用StringBuilder类型在托管代码中声明字符串
除了上表中列出的类型之外,还有很多其他类型属于非直接复制到本机结构中的类型,例如其他指针类型和句柄类型等。
#### 2.4.3、封送字符串的处理
```csharp
$1```
知道了 **OSVERSIONINFO** 结构体在非托管代码中的定义之后,
现在我们就需要在托管代码中定义一个等价的结构,并且要保证两个结构体在内存中的布局相同。托管代码中的结构体定义如下:
```csharp
$1```
public UInt16 SuiteMask; //标识系统上的程序组
public Byte ProductType; //标识系统类型
public Byte Reserved; //保留,未使用
}
// 获得操作系统信息
private static string GetOSVersion()
{
// 定义一个字符串存储版本信息
string versionName = string.Empty;
// 初始化一个结构体对象
OSVersionInfo osVersionInformation = new OSVersionInfo();
// 调用GetVersionEx 方法前,必须用SizeOf方法设置结构体中OSVersionInfoSize 成员
osVersionInformation.OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo));
// 调用Win32函数
```csharp
$1```
```csharp
$1```
// 对GetVersionEx进行托管定义
// 由于类的定义中CSDVersion为String类型,String是非直接复制到本机结构类型,
// 所以封送拆送器需要进行复制操作。
// 为了是非托管代码能够获得在托管代码中对象设置的初始值(指的是OSVersionInfoSize字段,调用函数前首先初始化该值),
// 所以必须加上[In]属性;函数返回时,为了将结果复制到托管对象中,必须同时加上 [Out]属性
// 这里不能是用ref关键字,因为 OsVersionInfo是类类型,本来就是引用类型,如果加ref 关键字就是传入的为指针的指针了,这样就会导致调用失败
[DllImport("Kernel32", CharSet = CharSet.Unicode, EntryPoint = "GetVersionEx")]
private static extern Boolean GetVersionEx_Struct([In, Out] OSVersionInfo osVersionInfo);
// 获得操作系统信息
private static string GetOSVersion()
{
// 定义一个字符串存储操作系统信息
string versionName = string.Empty;
// 初始化一个类对象
OSVersionInfo osVersionInformation = new OSVersionInfo();
// 调用Win32函数
Boolean result = GetVersionEx_Struct(osVersionInformation);
if (!result)
{
// 如果调用失败,获得最后的错误码
int errorcode = Marshal.GetLastWin32Error();
Win32Exception win32Exc = new Win32Exception(errorcode);
Console.WriteLine("调用失败的错误信息为: " + win32Exc.Message);
// 调用失败时返回为空字符串
return string.Empty;
}
else
{
Console.WriteLine("调用成功");
switch (osVersionInformation.MajorVersion)
{
// 这里仅仅讨论 主版本号为6的情况,其他情况是一样讨论的
case 6:
switch (osVersionInformation.MinorVersion)
{
case 0:
if (osVersionInformation.ProductType == (Byte)0)
{
versionName = " Microsoft Windows Vista";
}
else
{
versionName = "Microsoft Windows Server 2008"; // 服务器版本
}
break;
case 1:
if (osVersionInformation.ProductType == (Byte)0)
{
versionName = " Microsoft Windows 7";
}
else
{
versionName = "Microsoft Windows Server 2008 R2";
}
break;
case 2:
versionName = "Microsoft Windows 8";
break;
}
break;
default:
versionName = "未知的操作系统";
break;
}
return versionName;
}
}
static void Main(string[] args)
{
string OS = GetOSVersion();
Console.WriteLine("当前电脑安装的操作系统为:{0}", OS);
Console.Read();
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public class OSVersionInfo
{
public UInt32 OSVersionInfoSize = (UInt32)Marshal.SizeOf(typeof(OSVersionInfo));
public UInt32 MajorVersion = 0;
public UInt32 MinorVersion = 0;
public UInt32 BuildNumber = 0;
public UInt32 PlatformId = 0;
// 此属性用于表示将其封送成内联数组
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string CSDVersion = null;
public UInt16 ServicePackMajor = 0;
public UInt16 ServicePackMinor = 0;
public UInt16 SuiteMask = 0;
public Byte ProductType = 0;
public Byte Reserved;
}
}
## 三、COM Interop
为了解决在.NET中的托管代码能够调用COM组件的问题,.NET 平台下提供了COM Interop,即COM互操作技术。
在.NET中使用COM对象,主要方法:使用TlbImp工具为COM组件创建一个互操作程序集来绑定早期的COM对象,这样就可以在程序中添加互操作程序集来调用COM对象。
### 在.NET 中使用COM对象的步骤:
1. 找到要使用的COM 组件并注册它。使用 regsvr32.exe 注册或注销 COM DLL。
2. 在项目中添加对 COM 组件或类型库的引用。
3. 添加引用时,Visual Studio 会用到Tlbimp.exe(类型库导入程序),Tlbimp.exe程序将生成一个 .NET Framework 互操作程序集。该程序集又称为运行时可调用包装 (RCW),其中包含了包装COM组件中的类和接口。Visual Studio 将生成组件的引用添加至项目。
4. 创建RCW中类的实例,这样就可以使用托管对象一样来使用COM对象。
在.NET中使用COM组件的过程:
[![13235508_4288d25e62b7490bb4e2916ca0deaef8\[1\]](https://www.icode9.com/i/l/?n=18&i=blog/24244/201810/24244-20181019163910224-1894355041.png)](https://www.icode9.com/i/l/?n=18&i=blog/24244/201810/24244-20181019163909895-1158704904.png)
### 如何在C#中调用COM组件——访问Office 互操作对象
在新建的控制台程序里添加” **Microsoft.Office.Interop.Word 14.0.0.0** “ 这个引用
**Microsoft.Office.Interop.Word.dll**
确实是一个.NET程序集,并且它也叫做COM组件的互操作程序集,这个程序集中包含了COM组件中定义的类型的元数据,
托管代码通过调用互操作程序集中公开的接口或对象来间接地调用COM对象和接口的。
关于通过Tlblmp.exe工具来生成互操作程序集步骤,这里我就不多详细诉说了,大家可以参考MSDN中这个工具详细使用说明
:<http://msdn.microsoft.com/zh-cn/library/tt0cf3sx(v=VS.80).aspx> 。
然而我们也可以使用Visual Studio中内置的支持来完成为COM类型库创建互操作程序集的工作,我们只需要在 **VS中为.NET
项目添加对应的COM组件的引用,此时VS就会自动将COM类型库中的COM类型库转化为程序集中的元数据**
,并在项目的Bin目录下生成对于的互操作程序集,所以在VS中添加COM引用,其实最后程序中引用的是互操作程序集,然后通过RCW来对COM组件进行调用。
然而对于Office中的 **Microsoft.Office.Interop.Wordd.dll,**
这个程序集也是互操作程序集,但是它又是主互操作程序集,即PIA(Primary Interop Assemblies)。主互操作程序集是一个由供应商提供的
**唯一** 的程序集,为了生成主互操作程序集,可以在使用TlbImp命令是打开 /primary 选项。
[![image](https://www.icode9.com/i/l/?n=18&i=blog/24244/201810/24244-20181019163911454-290557665.png)](https://www.icode9.com/i/l/?n=18&i=blog/24244/201810/24244-20181019163910641-1857864888.png)
using System;
using System.Collections.Generic;
using System.Linq;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
namespace OfficeProgramminWalkthruComplete
{
class Walkthrough
{
static void Main(string[] args)
{
// Create a list of accounts.
var bankAccounts = new List<Account>
{
new Account {
ID = 345678,
Balance = 541.27
},
new Account {
ID = 1230221,
Balance = -127.44
}
};
// Display the list in an Excel spreadsheet.
DisplayInExcel(bankAccounts);
// Create a Word document that contains an icon that links to
// the spreadsheet.
CreateIconInWordDoc();
}
static void DisplayInExcel(IEnumerable<Account> accounts)
{
var excelApp = new Excel.Application();
// Make the object visible.
excelApp.Visible = true;
// Create a new, empty workbook and add it to the collection returned
// by property Workbooks. The new workbook becomes the active workbook.
// Add has an optional parameter for specifying a praticular template.
// Because no argument is sent in this example, Add creates a new workbook.
excelApp.Workbooks.Add();
// This example uses a single workSheet.
Excel._Worksheet workSheet = excelApp.ActiveSheet;
// Earlier versions of C# require explicit casting.
//Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet;
// Establish column headings in cells A1 and B1.
workSheet.Cells[1, "A"] = "ID Number";
workSheet.Cells[1, "B"] = "Current Balance";
var row = 1;
foreach (var acct in accounts)
{
row++;
workSheet.Cells[row, "A"] = acct.ID;
workSheet.Cells[row, "B"] = acct.Balance;
}
workSheet.Columns[1].AutoFit();
workSheet.Columns[2].AutoFit();
// Call to AutoFormat in Visual C#. This statement replaces the
// two calls to AutoFit.
workSheet.Range["A1", "B3"].AutoFormat(
Excel.XlRangeAutoFormat.xlRangeAutoFormatClassic2);
// Put the spreadsheet contents on the clipboard. The Copy method has one
// optional parameter for specifying a destination. Because no argument
// is sent, the destination is the Clipboard.
workSheet.Range["A1:B3"].Copy();
}
static void CreateIconInWordDoc()
{
var wordApp = new Word.Application();
wordApp.Visible = true;
// The Add method has four reference parameters, all of which are
// optional. Visual C# allows you to omit arguments for them if
// the default values are what you want.
wordApp.Documents.Add();
// PasteSpecial has seven reference parameters, all of which are
// optional. This example uses named arguments to specify values
// for two of the parameters. Although these are reference
// parameters, you do not need to use the ref keyword, or to create
// variables to send in as arguments. You can send the values directly.
wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true);
}
}
public class Account
{
public int ID { get; set; }
public double Balance { get; set; }
}
}
### 错误处理
try
{
// 如果文档不存在时,就会出现调用COM对象失败的情况
// 打开Word文档
wordDoc = wordApp.Documents.Open(wordPath);
// 向Word中插入文本
Range wordRange = wordDoc.Range(0, 0);
wordRange.Text = "这是插入的文本";
// 保存文档
wordDoc.Save();
}
catch(Exception ex)
{
// 获得异常相对应的HRESULT值
// 因为COM中根据方法返回的HRESULT来判断调用是否成功的
int HResult = Marshal.GetHRForException(ex);
// 设置控制台的前景色,即输出文本的颜色
Console.ForegroundColor = ConsoleColor.Red;
// 下面把HRESULT值以16进制输出
Console.WriteLine("调用抛出异常,异常类型为:{0}, HRESULT= 0x{1:x}", ex.GetType().Name, HResult);
Console.WriteLine("异常信息为:" + ex.Message.Replace('\r', ' '));
}
finally
{
// 关闭文档并
if (wordDoc != null)
{
wordDoc.Close();
}
// 退出Word程序
wordApp.Quit();
}
从上面的结果我们看到了一个 HRESULT值,这个值真是COM代码中返回返回的。在COM中,COM方法通过返回 **HRESULT** 来报告错误;.NET
方法则通过引发异常来报告错误,为了方便地在托管代码中获得COM代码中出现的错误和异常信息,CLR提供了两者之间的转换,每一个代表错误发生的HRESULT都会被映射到.NET
Framework中的一个异常类,对于具体的映射关系可以参考MSDN中 的文章: <http://msdn.microsoft.com/zh-
cn/library/9ztbc5s1(VS.80).aspx> ,
标签:调用,C#,托管,互操作,020,new,COM,public 来源: https://www.cnblogs.com/springsnow/p/16272229.html