使用引用 DLL 从 .NET 和 C# 调用 BITS

从 .NET 程序调用 BITS COM 类的一种方法是使用 MIDLTLBIMP 工具,从 Windows SDK 中的 BITS IDL(接口定义语言)文件开始创建引用 DLL 文件。 参考 DLL 是 BITS COM 类的一组包装类,您可以直接从 .NET 轻松使用这些包装类。

使用自动创建的引用 DLL 的替代方法是使用 GitHubNuGet 中的第三方 .NET BITS 包装器。 这些包装器通常具有更自然的 .NET 编程样式,但它们可能滞后于 BITS 接口中的更改和更新。

创建参考 DLL

BITS IDL 文件

你将从 BITS IDL 文件集开始。 这些文件完全定义 BITS COM 接口。 这些文件位于 Windows Kits 目录中,名为 bits version.idl(例如,bits10_2.idl),但版本 1.0 文件只是 Bits.idl。 创建新版本的 BITS 时,还会创建新的 BITS IDL 文件。

可能还需要修改 SDK BITS IDL 文件的副本,以使用不会自动转换为 .NET 等效项的 BITS 功能。 稍后将讨论可能的 IDL 文件更改。

BITS IDL 文件通过引用包含其他几个 IDL 文件。 它们还会嵌套,因此,如果使用一个版本,则包含所有较低版本。

对于程序中要针对的每个 BITS 版本,需要为该版本准备一个引用 DLL。 例如,如果要编写适用于 BITS 1.5 及更多版本的程序,但在存在 BITS 10.2 时具有其他功能,则需要同时转换 bits1_5.idl 和 bits10_2.idl 文件。

MIDL 和 TLBIMP 实用工具

MIDL(Microsoft接口定义语言)实用工具将描述 BITS COM 接口的 IDL 文件转换为 TLB(类型库)文件。 MIDL 工具依赖于 CL 实用工具(C 预处理器)来正确读取 IDL 语言文件。 CL 实用工具是 Visual Studio 的一部分,在 Visual Studio 安装中包含 C/C++ 功能时安装。

MIDL 实用工具通常会创建一组 C 和 H(C 语言代码和 C 语言标头)文件。 可以通过将输出发送到 NUL 设备来抑制这些额外文件。 例如,设置 /dlldata NUL: 开关将禁止创建 dlldata.c 文件。 下面的示例命令显示应将哪些开关设置为 NUL:。

TLBIMP(类型库导入程序)实用工具在 TLB 文件中读取,并创建相应的引用 DLL 文件。

MIDL 和 TLBIMP 的示例命令

这是生成一组引用文件的完整命令示例。 可能需要根据 Visual Studio 和 Windows SDK 安装以及所面向的 BITS 功能和 OS 版本修改命令。

该示例创建一个目录来放置引用 DLL 文件,并创建一个环境变量 BITSTEMP 以指向该目录。

然后,示例命令运行由 Visual Studio 安装程序创建的 vsdevcmd.bat 文件。 此 BAT 文件将设置路径和一些环境变量,以便 MIDL 和 TLBIMP 命令将运行。 它还将 WindowsSdkDir 和 WindowsSDKLibVersion 变量设置为指向最新的 Windows SDK 目录。

REM Create a working directory
REM You can select a different directory based on your needs.
SET BITSTEMP=C:\BITSTEMPDIR
MKDIR "%BITSTEMP%"

REM Run the VsDevCmd.bat file to locate the Windows
REM SDK directory and the tools directories
REM This will be different for different versions of
REM Visual Studio

CALL "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\Tools\vsdevcmd.bat"

REM Run the MIDL command on the desired BITS IDL file
REM This will generate a TLB file for the TLBIMP command
REM The IDL file will be different depending on which
REM set of BITS interfaces you need to use.
REM Run the MIDL command once per reference file
REM that you will need to explicitly use.
PUSHD .
CD /D "%WindowsSdkDir%Include\%WindowsSDKLibVersion%um"

MIDL  /I ..\shared /out "%BITSTEMP%" bits1_5.idl /dlldata NUL: /header NUL: /iid NUL: /proxy NUL:
MIDL  /I ..\shared /out "%BITSTEMP%" bits4_0.idl /dlldata NUL: /header NUL: /iid NUL: /proxy NUL:
MIDL  /I ..\shared /out "%BITSTEMP%" bits5_0.idl /dlldata NUL: /header NUL: /iid NUL: /proxy NUL:
MIDL  /I ..\shared /out "%BITSTEMP%" bits10_1.idl /dlldata NUL: /header NUL: /iid NUL: /proxy NUL:
MIDL  /I ..\shared /out "%BITSTEMP%" bits10_2.idl /dlldata NUL: /header NUL: /iid NUL: /proxy NUL:

REM Run the TLBIMP command on the resulting TLB file(s)
REM Try to keep a parallel set of names.
TLBIMP "%BITSTEMP%"\bits1_5.tlb /out: "%BITSTEMP%"\BITSReference1_5.dll
TLBIMP "%BITSTEMP%"\bits4_0.tlb /out: "%BITSTEMP%"\BITSReference4_0.dll
TLBIMP "%BITSTEMP%"\bits5_0.tlb /out: "%BITSTEMP%"\BITSReference5_0.dll
TLBIMP "%BITSTEMP%"\bits10_1.tlb /out: "%BITSTEMP%"\BITSReference10_1.dll
TLBIMP "%BITSTEMP%"\bits10_2.tlb /out: "%BITSTEMP%"\BITSReference10_2.dll
DEL "%BITSTEMP%"\bits*.tlb
POPD

运行这些命令后,将在 BITSTEMP 目录中有一组引用 DLL。

将引用的 DLL 文件添加到项目中

若要在 C# 项目中使用引用 DLL,请在 Visual Studio 中打开 C# 项目。 在解决方案资源管理器中,右键单击“引用”,然后单击“添加引用”。 然后单击“浏览”按钮,然后单击“添加”按钮。 导航到包含引用 DLL 的目录,选择它们,然后单击“添加”。 在“引用管理器”窗口中,将检查引用 DLL。 然后,单击“确定”。

BITS 引用 DLL 现在已添加到项目中。

引用 DLL 文件中的信息将嵌入到最终程序中。 无需将引用 DLL 文件寄送到程序;只需寄送.EXE。

可以更改引用 DLL 是否嵌入到最终 EXE 中。 使用 “嵌入互操作类型” 属性来设置引用 DLL 是否将被嵌入。 可以以每个引用为单位来完成。 默认值为 True,用于嵌入 DLL。

修改 IDL 文件以获取更完整的 .NET 代码

BITS IDL(Microsoft接口定义语言)文件可在不作修改的情况下用来制作 BackgroundCopyManager DLL 文件。 但是,生成的 .NET 引用 DLL 将缺少一些不可转换的联合,并且某些结构和枚举具有难以使用的名称。 本部分将介绍一些更改,以提高.NET DLL的完整性和易用性。

更简单的 ENUM 名称

BITS IDL 文件通常定义如下所示的枚举值:

    typedef enum
    {
            BG_AUTH_TARGET_SERVER = 1,
            BG_AUTH_TARGET_PROXY
    } BG_AUTH_TARGET;

BG_AUTH_TARGET是 typedef 的名称;实际枚举未命名。 这通常不会导致 C 代码出现问题,但无法很好地用于 .NET 程序。 将自动创建一个新名称,但它看起来可能类似于_MIDL___MIDL_itf_bits4_0_0005_0001_0001而不是人工可读的值。 可以通过更新 MIDL 文件以包含枚举名称来解决此问题。

    typedef enum BG_AUTH_TARGET
    {
            BG_AUTH_TARGET_SERVER = 1,
            BG_AUTH_TARGET_PROXY
    } BG_AUTH_TARGET;

允许枚举名称与 typedef 名称相同。 某些程序员有一个命名约定,他们会使名称保持不同(例如,在枚举名称前加上下划线),但这只会混淆 .NET 中的转换。

联合类型中的字符串类型

BITS IDL 文件使用 LPWSTR(指向宽字符字符串的长指针)约定传递字符串。 虽然这在传递函数参数(如 Job.GetDisplayName([out] LPWSTR *pVal) 方法时有效,但在字符串是联合的一部分时不起作用。 例如,bits5_0.idl 文件包括 BITS_FILE_PROPERTY_VALUE 联合体:

typedef [switch_type(BITS_FILE_PROPERTY_ID)] union
{
    [case( BITS_FILE_PROPERTY_ID_HTTP_RESPONSE_HEADERS )]
        LPWSTR String;
}
BITS_FILE_PROPERTY_VALUE;

LPWSTR 字段不会包含在联合的 .NET 版本中。 若要解决此问题,请将 LPWSTR 更改为 WCHAR*。 生成的字段(称为 String)将作为 IntPtr 传递。 请使用 System.Runtime.InteropServices.Marshal.PtrToStringAuto(value.String) 方法将此转换为字符串。

结构中的联合

有时,嵌入在结构中的联合根本不包含在结构中。 例如,在 Bits1_5.idl 中,BG_AUTH_CREDENTIALS的定义如下:

    typedef struct
    {
        BG_AUTH_TARGET Target;
        BG_AUTH_SCHEME Scheme;
        [switch_is(Scheme)] BG_AUTH_CREDENTIALS_UNION Credentials;
    }
    BG_AUTH_CREDENTIALS;

BG_AUTH_CREDENTIALS_UNION定义为联合,如下所示:

    typedef [switch_type(BG_AUTH_SCHEME)] union
    {
            [case( BG_AUTH_SCHEME_BASIC, BG_AUTH_SCHEME_DIGEST, BG_AUTH_SCHEME_NTLM,
            BG_AUTH_SCHEME_NEGOTIATE, BG_AUTH_SCHEME_PASSPORT )] BG_BASIC_CREDENTIALS Basic;
            [default] ;
    } BG_AUTH_CREDENTIALS_UNION;

BG_AUTH_CREDENTIALS中的“凭据”字段不会包含在 .NET 类定义中。

请注意,无论BG_AUTH_SCHEME如何,联合体始终定义为BG_BASIC_CREDENTIALS。 由于未将该联合用于联合类型,因此我们可以这样传递一个BG_BASIC_CREDENTIALS:

    typedef struct
    {
        BG_AUTH_TARGET Target;
        BG_AUTH_SCHEME Scheme;
        BG_BASIC_CREDENTIALS Credentials;
    }
    BG_AUTH_CREDENTIALS;

使用来自 C# 的 BITS

通过在 C# 中设置一些 using 语句,可以减少使用不同 BITS 版本时所需键入的字符数。 名称“BITSReference”来自引用 DLL 的名称。

// Set up the BITS namespaces
using BITS = BITSReference1_5;
using BITS4 = BITSReference4_0;
using BITS5 = BITSReference5_0;
using BITS10_2 = BITSReference10_2;

快速示例:下载文件

下面提供了一段简短但完整的 C# 代码片段,用于从 URL 下载文件。

    var mgr = new BITS.BackgroundCopyManager1_5();
    BITS.GUID jobGuid;
    BITS.IBackgroundCopyJob job;
    mgr.CreateJob("Quick download", BITS.BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out jobGuid, out job);
    job.AddFile("https://aka.ms/WinServ16/StndPDF", @"C:\Server2016.pdf");
    job.Resume();
    bool jobIsFinal = false;
    while (!jobIsFinal)
    {
        BITS.BG_JOB_STATE state;
        job.GetState(out state);
        switch (state)
        {
            case BITS.BG_JOB_STATE.BG_JOB_STATE_ERROR:
            case BITS.BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED:
                job.Complete();
                break;

            case BITS.BG_JOB_STATE.BG_JOB_STATE_CANCELLED:
            case BITS.BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED:
                jobIsFinal = true;
                break;
            default:
                Task.Delay(500); // delay a little bit
                break;
        }
    }
    // Job is complete

在此示例代码中,将创建名为 mgr 的 BITS 管理器。 它直接对应于 IBackgroundCopyManager 接口。

由经理创建了一项新工作。 作业是 CreateJob 方法中的输出参数。 此外,传入的是作业名称(无需唯一)和下载类型,即下载作业。 还会填充作业标识符的 BITS GUID。

创建作业后,使用 AddFile 方法向作业添加新的下载文件。 需要传入两个字符串,一个用于远程文件(URL 或文件共享),一个用于本地文件。

添加文件后,对作业调用 Resume 以启动它。 然后,代码会等待作业处于最终状态(ERROR 或 TRANSFERRED),才会完成。

BITS 版本、类型转换和 QueryInterface

你会发现,通常必须使用早期版本的 BITS 对象和程序中的较新版本。

例如,即使有较新的管理器对象和较新的 IBackgroundCopyJob 对象可用,在创建作业对象时,您仍然会获得属于 BITS 版本 1.0 的 IBackgroundCopyJob。 由于 CreateJob 方法不接受较新版本的接口,因此无法直接生成较新版本。

使用 .NET 类型转换将较旧类型对象转换为较新类型对象。 强制转换将根据需要自动调用 COM QueryInterface。

在此示例中,有一个名为“job”的 BITS IBackgroundCopyJob 对象,我们希望将其转换为名为“job5”的 IBackgroundCopyJob5 对象,以便我们可以调用 BITS 5.0 GetProperty 方法。 我们只是转换为 IBackgroundCopyJob5 类型,如下所示:

var job5 = (BITS5.IBackgroundCopyJob5)job;

.NET 将使用正确的 QueryInterface 初始化 job5 变量。

如果代码可能在不支持特定版本的 BITS 系统上运行,可以尝试进行类型转换并捕获 System.InvalidCastException 异常。

BITS5.IBackgroundCopyJob5 job5 = null;
try
{
    job5 = (BITS5.IBackgroundCopyJob5)Job;
}
catch (System.InvalidCastException)
{
    ; // Must be running an earlier version of BITS
}

一个常见问题是尝试转换为不正确的对象类型。 .NET 系统不知道 BITS 接口之间的实际关系。 如果请求的接口类型错误,.NET 会尝试为你创建它,但将抛出 InvalidCastException 异常并显示 HResult 0x80004002(E_NOINTERFACE)。

使用 BITS 版本 10_1 和 10_2

在某些版本的 Windows 10 上,无法使用 10.1 或 10.2 接口直接创建 BITS IBackgroundCopyManager 对象。 相反,必须使用多个版本的 BackgroundCopyManager DLL 引用文件。 例如,可以使用 1.5 版本创建 IBackgroundCopyManager 对象,然后使用 10.1 或 10.2 版本对生成的作业或文件对象进行类型转换。