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 ):

int WINAPI MessageBox(
  _In_opt_  HWND hWnd,
  _In_opt_  LPCTSTR lpText,
  _In_opt_  LPCTSTR lpCaption,
  _In_      UINT uType
);

现在已经知道了需要调用的Win32 API 函数的定义声明,下面就依据平台调用的步骤,在.NET 中实现对该非托管函数的调用,下面就看看.NET中的代码的:

using System;

// 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间
using System.Runtime.InteropServices;

namespace 平台调用Demo
{
    class Program
    {
        // 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性 在默认情况下,CharSet为CharSet.Ansi
        // 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定 在托管函数中声明注意一定要加上 static 和extern 这两个关键字

        //第一种指定方式,通过CharSet字段指定,在默认情况下CharSet为CharSet.Ansi
        [DllImport("user32.dll")]
        public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

        [DllImport("user32.dll")]
        public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type);

        // [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int
        // MessageBox(IntPtr hWnd, String text, String caption, uint type);

        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern int MessageBoxW(IntPtr hWnd, String text, String caption, uint type);

        // 通过EntryPoint字段指定
        [DllImport("user32.dll", EntryPoint = "MessageBoxA")]
        public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type);

        [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
        public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type);

        static void Main(string[] args)
        {
            // 在托管代码中直接调用声明的托管函数 使用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);
            MessageBox4(new IntPtr(0), "Learning Hard", "欢迎", 0);
        }
    }
}

2.3使用平台调用技术中,还需要注意下面4点

(1). DllImport属性的ExactSpelling字段如果设置为true时,则在托管代码中声明的函数名必须与要调用的非托管函数名完全一致,因为从ExactSpelling字面意思可以看出为 "准确拼写"的意思,当ExactSpelling设置为true时,此时会改变平台调用的行为,此时平台调用只会根据根函数名进行搜索,而找不到的时候不会添加 A或者W来进行再搜索,.

[DllImport("user32.dll", ExactSpelling=true)]
       public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

(2). 如果采用设置CharSet的值来控制调用函数的版本时,则需要在托管代码中声明的函数名必须与根函数名一致,否则也会调用出错

[DllImport("user32.dll")]
        public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type);

(3). 如果通过指定DllImport属性的EntryPoint字段的方式来调用函数版本时,此时必须相应地指定与之匹配的CharSet设置,意思就是——如果指定EntryPoint为 MessageBoxW,那么必须将CharSet指定为CharSet.Unicode,如果指定EntryPoint为 MessageBoxA,那么必须将CharSet指定为CharSet.Ansi或者不指定,因为 CharSet默认值就是Ansi。

 (4). CharSet还有一个可选字段为——CharSet.Auto, 如果把CharSet字段设置为CharSet.Auto,则平台调用会针对目标操作系统适当地自动封送字符串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,默认值为 Unicode;在 Windows 98 和 Windows Me 上,默认值为 Ansi。

2.3、获得Win32函数的错误信息

捕捉由托管定义导致的异常演示代码:

            try
            {
                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


System.UInt32


无符号32位整型


INT64/LONGLONG


_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


双精度浮点数

除了上表列出来的简单类型之外,还有一些复制类型也属于可直接复制到本机结构中的数据类型:

(1) 数据元素都是可直接复制到本机结构中的一元数组,如整数数组,浮点数组等

(2)只包含可直接复制到本机结构中的格式化值类型

(3)成员变量全部都是可复制到本机结构中的类型且作为格式化类型封送的类

上面提到的格式化指的是——在类型定义时,成员的内存布局在声明时就明确指定的类型。在代码中用StructLayout属性修饰被指定的类型,并将StructLayout的LayoutKind属性设置为Sequential或Explicit,例如:

using System.Runtime.InteropServices;

// 下面的结构体也属于可直接复制到本机结构中的类型
[StructLayout(LayoutKind.Sequential)]
public struct Point {
   public int x;
   public int y;
}

2.4.2 非直接复制到本机结构中的类型

对于这种类型,封送器需要对它们进行相应的类型转换之后再复制到被调用的函数中,下面列出一些非直接复制到本机结构中的数据类型:


Windows 数据类型


非托管数据类型


托管数据类型


托管数据类型解释


Bool


bool


System.Boolean


布尔类型


WCHAR/TCHAR


char/ wchar_t


System.Char


ANSI字符/Unicode字符


LPCSTR/LPCWSTR/LPCTSTR/LPSTR/LPWSTR/LPTSTR


const char*/const wchar_t*/char*/wchar_t*


System.String


ANSI字符串/Unicode字符串,如果非托管代码不需要更新此字符串时,此时用String类型在托管代码中声明字符串类型


LPSTR/LPWSTR/LPTSTR


Char*/wchar_t*


System.StringBuilder


ANSI字符串/Unicode字符串,如果非托管代码需要更新此字符串,然后把更新的字符串传回托管代码中,此时用StringBuilder类型在托管代码中声明字符串

除了上表中列出的类型之外,还有很多其他类型属于非直接复制到本机结构中的类型,例如其他指针类型和句柄类型等。

2.4.3、封送字符串的处理

封送作为返回值的字符串,下面是一段演示代码,代码中主要是调用Win32 GetTempPath函数来获得返回临时路径,此时拆送器就需要把返回的字符串封送回托管代码中。使用System.StringBuilder托管数据类型。

    // 托管函数中的返回值封送回托管函数的例子
    class Program
    {

        // Win32 GetTempPath函数的定义如下:
        //DWORD WINAPI GetTempPath(
        //  _In_   DWORD nBufferLength,
        //  _Out_  LPTSTR lpBuffer
        //);  

        // 主要是注意如何在托管代码中定义该函数原型
        [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError=true)]
        public static extern uint GetTempPath(int bufferLength, StringBuilder buffer);
        static void Main(string[] args)
        {
            StringBuilder buffer = new StringBuilder(300);
            uint tempPath=GetTempPath(300, buffer);
            string path = buffer.ToString();
            if (tempPath == 0)
            {
                int errorcode =Marshal.GetLastWin32Error();
                Win32Exception win32expection = new Win32Exception(errorcode);
                Console.WriteLine("调用非托管函数发生异常,异常信息为:" +win32expection.Message);
            }

            Console.WriteLine("调用非托管函数成功。");
            Console.WriteLine("Temp 路径为:" + buffer);
            Console.Read();
        }
    }

2.4.4、封送结构体的处理

在我们实际调用Win32 API函数时,经常需要封送结构体和类等复制类型,下面就以Win32 函数GetVersionEx为例子来演示如何对作为参数的结构体进行封送处理。

下面是GetVersionEx非托管定义(更多关于该函数的信息可以参看MSDN链接:http://msdn.microsoft.com/en-us/library/ms885648.aspx ):

BOOL GetVersionEx(
  LPOSVERSIONINFO lpVersionInformation
);

参数lpVersionInformation是一个指向 OSVERSIONINFO结构体的指针类型,所以我们在托管代码中为函数GetVersionEx函数之前,必须知道 OSVERSIONINFO结构体的非托管定义,然后再在托管代码中定义一个等价的结构体类型作为参数。以下是OSVERSIONINFO结构体的非托管定义:

typedef struct  _OSVERSIONINFO{
    DWORD dwOSVersionInfoSize;       //在使用GetVersionEx之前要将此初始化为结构的大小
    DWORD dwMajorVersion;               //系统主版本号
    DWORD dwMinorVersion;               //系统次版本号
    DWORD dwBuildNumber;               //系统构建号
    DWORD dwPlatformId;                  //系统支持的平台
    TCHAR szCSDVersion[128];          //系统补丁包的名称
    WORD wServicePackMajor;            //系统补丁包的主版本
    WORD wServicePackMinor;            //系统补丁包的次版本
    WORD wSuiteMask;                      //标识系统上的程序组
    BYTE wProductType;                    //标识系统类型
    BYTE wReserved;                         //保留,未使用
} OSVERSIONINFO;

知道了OSVERSIONINFO结构体在非托管代码中的定义之后, 现在我们就需要在托管代码中定义一个等价的结构,并且要保证两个结构体在内存中的布局相同。托管代码中的结构体定义如下:

        // 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构
        // 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数
        [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
        public struct OSVersionInfo
        {
            public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段
            public UInt32 MajorVersion; // 系统主版本号
            public UInt32 MinorVersion; // 系统此版本号
            public UInt32 BuildNumber;  // 系统构建号
            public UInt32 PlatformId;  // 系统支持的平台

            // 此属性用于表示将其封送成内联数组
            [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)]
            public string CSDVersion; // 系统补丁包的名称
            public UInt16 ServicePackMajor; // 系统补丁包的主版本
            public UInt16 ServicePackMinor;  // 系统补丁包的次版本
            public UInt16 SuiteMask;   //标识系统上的程序组
            public Byte ProductType;    //标识系统类型
            public Byte Reserved;  //保留,未使用
        }

从上面的定义可以看出, 托管代码中定义的结构体有以下三个方面与非托管代码中的结构体是相同的:

  • 字段声明的顺序
  • 字段的类型
  • 字段在内存中的大小

并且在上面结构体的定义中,我们使用到了 StructLayout 属性,该属性属于System.Runtime.InteropServices命名空间(所以在使用平台调用技术必须添加这个额外的命名空间)。这个类的作用就是允许开发人员显式指定结构体或类中数据字段的内存布局,为了保证结构体中的数据字段在内存中的顺序与定义时一致,所以指定为 LayoutKind.Sequential(该枚举也是默认值)。

下面就具体看看在托管代码中调用的代码:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace 封送结构体的处理
{
    class Program
    {
        // 对GetVersionEx进行托管定义
        // 为了传递指向结构体的指针并将初始化的信息传递给非托管代码,需要用ref关键字修饰参数
        // 这里不能使用out关键字,如果使用了out关键字,CLR就不会对参数进行初始化操作,这样就会导致调用失败
        [DllImport("Kernel32",CharSet=CharSet.Unicode,EntryPoint="GetVersionEx")]
        private static extern Boolean GetVersionEx_Struct(ref  OSVersionInfo osVersionInfo);

        // 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构
        // 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数
        [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
        public struct OSVersionInfo
        {
            public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段
            public UInt32 MajorVersion; // 系统主版本号
            public UInt32 MinorVersion; // 系统此版本号
            public UInt32 BuildNumber;  // 系统构建号
            public UInt32 PlatformId;  // 系统支持的平台

            // 此属性用于表示将其封送成内联数组
            [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)]
            public string CSDVersion; // 系统补丁包的名称
            public UInt16 ServicePackMajor; // 系统补丁包的主版本
            public UInt16 ServicePackMinor;  // 系统补丁包的次版本
            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函数
            Boolean result = GetVersionEx_Struct(ref 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();
        }
    }
}

2.4.5、封送类的处理

下面直接通过GetVersionEx函数进行封送类的处理的例子,具体代码如下:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace 封送类的处理
{
    class Program
    {
        // 对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对象的步骤:

  • 找到要使用的COM 组件并注册它。使用 regsvr32.exe 注册或注销 COM DLL。
  • 在项目中添加对 COM 组件或类型库的引用。
  • 添加引用时,Visual Studio 会用到Tlbimp.exe(类型库导入程序),Tlbimp.exe程序将生成一个 .NET Framework 互操作程序集。该程序集又称为运行时可调用包装 (RCW),其中包含了包装COM组件中的类和接口。Visual Studio 将生成组件的引用添加至项目。
  • 创建RCW中类的实例,这样就可以使用托管对象一样来使用COM对象。

在.NET中使用COM组件的过程:

如何在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 选项。

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#调用动态库的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • C#调用C++动态库接口函数和回调函数方法

    目录 1. 前言 2. 普通接口函数调用示例 2.1 C++端编写接口 2.2 C#端调用 3. 回调函数调用示例 3.1 C++端编写接口 3.2 C#端调用 1. 前言 需求: 当前C已经写好了一个动态库,完成了产品开发需求,C#需要调用C编写的动态库DLL接口,开发出完整的软件,DLL动态库里包含了普通接口函数,回调函数. 开发环境: win10 64位 .VS2017 2. 普通接口函数调用示例 2.1 C++端编写接口 (1)头文件里声明需要提供的接口,导出接口,方便C#调用 //带返

  • Qt之调用C#的动态库的解决方法

    环境:VS2019+Qt5.12 1. CLR库安装 首先,如果你VS2019没有安装CLR库,那么操作步骤为: 打开 Visual Studio Installer 在已安装中点击修改 将使用C++的桌面开发的对V142(14.25)生成工具的C++/CLI支持 点击右下角的修改,安装完成后重启软件即可 2. 新建类库(.NET Framework) 注意:此处请确认选择用于创建C#类库(.dll)的项目 此时解决方案的视图为: 一个简单的测试直接在Class1.cs文件添加内容即可,此测试中

  • C#调用非托管动态库中的函数方法

    C#如何调用一个非托管动态库中的函数呢,比如用VC6写的动态库,总之C#调用动态库的过程是比Java调用DLL动态库方便快捷多了,下面举例说明这个过程. 1.创建一个非托管动态库 代码如下: 复制代码 代码如下: //这一句是声明动态库输出一个可供外不调用的函数原型.     extern   "C"  __declspec(dllexport)  int  add( int ,  int ); int  add( int  a, int  b)      {          //实

  • C#与C++与互操作实例讲解

    一.C#调用C++库 1.创建C++库 打开VisualStudio,创建一个C++工程,输入项目名称HelloWorldLib 确定,然后下一步.选择应用程序类型为DLL 单击完成,我们就创建好了一个C++库的项目. 这里为了方便,我们直接在HelloWorldLib.cpp里定义函数 C++库导出有两种方式 一.以C语言接口的方式导出 这种方法就是在函数前面加上 extern "C" __declspec(dllexport) 加上extern "C"后,会指示

  • C#中互操作性简介

    一.引言 这个系列是在C#基础知识中遗留下来的一个系列的,因为在C# 4.0中的一个新特性就是对COM互操作改进,然而COM互操作性却是.NET平台下其中一种互操作技术,为了帮助大家更好的了解.NET平台下的互操作技术,所以才有了这个系列.然而有些朋友们可能会有这样的疑问--"为什么我们需要掌握互操作技术的呢?" 对于这个问题的解释就是--掌握了.NET平台下的互操作性技术可以帮助我们在.NET中调用非托管的dll和COM组件..NET是建立在操作系统的之上的一个开发框架,其中.NET

  • 浅谈C#互操作的内存溢出问题

    c#调用C++DLL代码,发现了一个隐藏很深的问题. 危害很大,而且不易察觉. 大概是申明c++的函数时候,有一个long类型的指针.在C#中我的申明成了这样: public extern void Method(ref uint para); 最初怎么也没有发现这里面有什么问题,知道这个隐藏的问题暴露出来,把前面申明的一个变量改变了, 我才恍然大悟. 复制代码 代码如下: uint test = 0;int *p = new IntPtr();Method(ref test); 在调用Meth

  • 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动态库的开发与调用及文件的读写小程序

    详解dll动态库的开发与调用及文件的读写小程序 首先我们先来学习一下动态库的调用,先找到动态库的.dll和.lib文件并将其导入到同源文件相同级别的文件夹下面,然后在添加进其头文件,并右击项目处,然后点击链接,链接我们的lib文件(一定要是全名称包括扩展名),然后我们就可以调用动态库的函数了. Dll是我们具体的函数, lib使我们的函数描述文件. #include <stdio.h> #include <stdlib.h> /* 该代码是对文件读写操作的使用 */ #pragma

  • 如何使用python的ctypes调用医保中心的dll动态库下载医保中心的账单

    需求:根据医保中心的文档和提供的dll动态库调用相关接口下载医保中心的账单. 文档:对调用dll动态库的描述,调用哪个dll文件,同时了解清楚调用这个dll文件中的哪个函数. 分析:结合文档及相关介绍弄清楚相关接口调用流程,从以上可以看出接口调用的是SiInterface.dll文件,然后先调用INIT函数进行初始化,然后再调用BUSINESS_HANDLE函数在医保局签到,然后在次调用BUSINESS_HANDLE函数下载账单,同时根据文档分析出每次调用函数的出入参.(具体的调用流程及每个函数

  • python模块与C和C++动态库相互调用实现过程示例

    目录 Python调用C/C++ 1.Python调用C动态链接库 C语言文件:pycall.c gcc编译生成动态库libpycall.so Python调用动态库的文件:pycall.py 运行结果: 2.Python调用C/C++原生态导出 3.Python调用C/C++通过boost实现 4.Python调用C/C++通过导出类 5.Python调用C/C++通过导出变参函数 6.Python调用C/C++通过导出带Python对象的接口 Python调用C/C++ 1.Python调用

  • Qt动态库调用宿主进程中的对象方法纯虚函数使用

    目录 引言 在运行时加载动态库并获取对象指针(QLibrary) 本贴重点:在动态库中调用宿主进程的对象方法 还是以add方法为例写一个Demo 引言 可执行程序加载动态库并调用动态库导出的函数是比较容易的: 导入库对应的头文件 在CPP文件中调用函数 在链接程序时加上动态库作为参数 假设demo.cpp中需要用到动态库libadd.so中的某个函数,可能是int add(int x, int y),那么我们编译时就需要链接上libadd.so, gcc参数中-L./libs指定了当前目录下的l

  • Rust应用调用C语言动态库的操作方法

    目录 外部功能接口FFI UDP套接字的读超时 Rust调用C语言动态库中的函数 避免重复造轮子,使用Rust官方C语言库 外部功能接口FFI 虽然高级(脚本)编程语言的功能丰富,表达能力强,但对底层的一些特殊操作的支持并不完善,就需要以其他编程语言来实现.调用其他编程语言的接口,被称为Foreign Function Interface,直译为外部功能接口.该接口通常是调用C语言实现的外部功能模块,因为C语言接近于全能,几乎任何功能都能够实现:正如同使用汇编语言也可以实现很多功能一样,但开发效

  • 分析Windows和Linux动态库

    摘要:动态链接库技术实现和设计程序常用的技术,在Windows和Linux系统中都有动态库的概念,采用动态库可以有效的减少程序大小,节省空间,提高效率,增加程序的可扩展性,便于模块化管理.但不同操作系统的动态库由于格式 不同,在需要不同操作系统调用时需要进行动态库程序移植.本文分析和比较了两种操作系统动态库技术,并给出了将Visual C++编制的动态库移植到Linux上的方法和经验. 1.引言 动态库(Dynamic Link Library abbr,DLL)技术是程序设计中经常采用的技术.

  • makefile如何调用静态库的方法实现

    makefile如何调用静态库 1.在windows下 g++环境中,调用一个python的静态链接库,在makefile文件有一段是全局的g++命令: $(PROG): $(OBJS) $(CXX) $(LFLAGS) $^ -o $@ 现在我要链接上一个python的静态库,lib存储在makefile同目录下libs文件夹下: $(PROG): $(OBJS) $(CXX) $(LFLAGS) $^ -o $@ ./libs/libpython37.a 2.在Linux Ubuntu 18

  • Java通过调用C/C++实现的DLL动态库——JNI的方法

    由于项目的需要,最近研究了java 调用DLL的方法,将如何调用的写于此,便于日后查阅: 采用的方法是JNI: Java Native Interface,简称JNI,是Java平台的一部分,可用于让Java和其他语言编写的代码进行交互. 下面是从网上摘取的JNI工作示意图: 总体说明:先在JAVA中建立一个类,通过javac生成.class,再由javah生成.h:然后将.h复制到VC下,由VC实现具体函, 并编译通过后生成DLL,将DLL放入JAVA工程中使用,完毕. 下面说说具体步骤(含实

随机推荐