使用引用 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 目录中,名为 bitsversion.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*。 生成的字段(称为字符串)将作为 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 方法的 out 参数。 此外,传入的是作业名称(无需唯一)和下载类型,即下载作业。 还会填充作业标识符的 BITS GUID。

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

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

BITS 版本、强制转换和 QueryInterface

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

例如,在创建作业对象时,即使使用较新的管理器对象和较新的 IBackgroundCopyJob 对象可用,也会获得 IBackgroundCopyJob(BITS 版本 1.0 的一部分)。 由于 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 版本强制转换生成的作业或文件对象。