我正在使用具有自己的自定义界面的第三方COM服务器,该服务器将结构作为其某些属性进行设置和获取。碰巧的是,我正在为客户端使用C++。我从下面的IDL文件中发布了一些具有代表性的代码,其中名称已更改,GUID已删除。

是确定了结构的打包还是我的客户端代码恰好使用了与COM服务器生成时相同的打包设置,这是否是一个好运?在更改了默认C++编译器打包设置的项目中,是否有可能出错?有没有可以用来确保客户端编译器打包设置正确的实用程序打包设置?

在IDL或从MIDL生成的头文件中,我看不到任何包装说明或语句。如果客户端使用C#或VB,会发生什么情况?如果通过IDispatch机制调用,包装行为是否更清楚地指定?

struct MyStruct
{
    int a, b;
};

[
    object,
    uuid( /* removed */ ),
    dual,
    nonextensible,
    pointer_default(unique)
]
interface IVideoOutputSettings : IDispatch{

    [propget, id(1), HRESULT MyProperty([out, retval] struct MyStruct* pVal);
    [propput, id(1), HRESULT MyProperty([in] struct MyStruct newVal);

    /* other methods */
};

最佳答案

根据此处的MIDL命令行开关引用,默认打包沿8字节边界进行:

/Zp switch @ MSDN (MIDL Language Reference)

如果更改了pack的值,则代码的其他部分更有可能首先中断,因为IDL文件通常是提前预编译的,而且很少有人会故意更改赋予MIDL的命令行开关(但不会如此罕见,有人可以摆弄C-scope的#pragma pack而忘记恢复默认状态)。

如果有充分的理由更改设置,则可以使用pragma pack语句显式设置打包。

pragma Attribute @ MSDN (MIDL Language Reference)

幸运的是,没有任何一方可以更改任何会干扰默认包装的设置。会出错吗?是的,如果有人不愿更改默认设置。

使用IDL文件时,通常会将详细信息编译为typelib(.tlb),并且假定使用相同的typelib时,服务器和客户端的平台均相同。在/Zp开关的脚注中建议这样做,因为某些值将针对某些非x86或16位目标失败。也可能存在32位 64位转换情况,这可能会导致期望值突破。不幸的是,我不知道是否还有更多的案例,但是默认设置确实可以使事情大为改观。

C#和VB没有任何内部行为来处理.tlb中的信息;相反,通常使用tlbimp之类的工具将COM定义转换为可从.NET使用的定义。我无法验证C#/VB.NET与COM客户端和服务器之间是否所有期望都成功;但是,如果您引用从在该设置下编译的IDL创建的.tlb,则可以验证使用8以外的特定杂注设置是否可以工作。虽然我不建议您使用默认的实用程序包,但是如果您希望将一个可用的示例用作引用,可以按照以下步骤进行操作。我创建了一个C++ ATL项目和一个C#项目进行检查。

这是C++附带说明。

  • 我使用Visual Studio 2010中的默认设置创建了一个名为 SampleATLProject 的ATL项目,未更改任何字段。这应该为您创建一个dll项目。
  • 编译了项目,以确保正在创建正确的C侧接口(interface)文件(SampleATLProject_i.c和SampleATLProject_i.h)。
  • 我向项目添加了一个名为SomeFoo的ATL简单对象。同样,没有更改默认值。这将创建一个称为CSomeFoo的类,该类将添加到您的项目中。
  • 编译SampleATLProject。
  • 我右键单击SampleATLProject.idl文件,然后在MIDL设置下,将“结构成员对齐”设置为4个字节(/Zp4)。
  • 编译SampleATLProject。
  • 我更改了IDL以添加一个名为“BarStruct”的结构定义。这需要添加具有MIDL uuid属性的C样式结构定义,并在库部分中引用该结构定义的条目。请参见下面的代码段。
  • 编译SampleATLProject。
  • 在类 View 中,我右键单击ISomeFoo并添加了一个名为FooIt的方法,该方法将struct BarStruct作为名为 theBar 的[in]参数。
  • 编译SampleATLProject。
  • 在SomeFoo.cpp中,我添加了一些代码以打印出结构的大小并抛出一个包含详细信息的消息框。

  • 这是我对ATL项目的IDL。
    import "oaidl.idl";
    import "ocidl.idl";
    
    [uuid(D2240D8B-EB97-4ACD-AC96-21F2EAFFE100)]
    struct BarStruct
    {
      byte a;
      int b;
      byte c;
      byte d;
    };
    
    [
      object,
      uuid(E6C3E82D-4376-41CD-A0DF-CB9371C0C467),
      dual,
      nonextensible,
      pointer_default(unique)
    ]
    interface ISomeFoo : IDispatch{
      [id(1)] HRESULT FooIt([in] struct BarStruct theBar);
    };
    [
      uuid(F15B6312-7C46-4DDC-8D04-9DEA358BD94B),
      version(1.0),
    ]
    library SampleATLProjectLib
    {
      struct BarStruct;
      importlib("stdole2.tlb");
      [
        uuid(930BC9D6-28DF-4851-9703-AFCD1F23CCEF)
      ]
      coclass SomeFoo
      {
        [default] interface ISomeFoo;
      };
    };
    

    CSomeFoo类内部,这是FooIt()的实现。
    STDMETHODIMP CSomeFoo::FooIt(struct BarStruct theBar)
    {
      WCHAR buf[1024];
      swprintf(buf, L"Size: %d, Values: %d %d %d %d", sizeof(struct BarStruct),
               theBar.a, theBar.b, theBar.c, theBar.d);
    
      ::MessageBoxW(0, buf, L"FooIt", MB_OK);
    
      return S_OK;
    }
    

    接下来,在C#端:
  • 转到SampleATLProject的调试目录或所需的输出目录,并在作为C++项目输出的一部分生成的.tlb文件上运行tlbimp.exe。以下为我工作:

    tlbimp SampleATLProject.tlb/out:Foo.dll/namespace:SampleATL.FooStuff
  • 接下来,我创建了一个C#控制台应用程序,并在项目中添加了对Foo.dll的引用。
  • 在“引用”文件夹中,转到Foo的“属性”,并通过将其设置为false来关闭嵌入互操作类型
  • 我添加了一个using语句来引用给tlbimp的命名空间SampleATL.FooStuff,向[STAThread]添加了Main()属性(COM公寓模型必须匹配以进行过程内使用),并添加了一些代码来调用COM组件。

  • Tlbimp.exe (Type Library Importer) @ MSDN

    这是该控制台应用程序的源代码。
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    using SampleATL.FooStuff;
    
    namespace SampleATLProjectConsumer
    {
        class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                BarStruct s;
                s.a = 1;
                s.b = 127;
                s.c = 255;
                s.d = 128;
    
                ISomeFoo handler = new SomeFooClass();
                handler.FooIt(s);
            }
        }
    }
    

    最后,它运行,并且我得到一个模态弹出窗口,其中显示以下字符串:
    Size: 12, Values: 1 127 255 128
    

    为确保可以更改编译指示包(因为最常用的对齐方式是4/8字节打包),我按照以下步骤将其更改为1:
  • 我返回到C++项目,转到SampleATLProject.idl的属性,并将Struct成员对齐方式更改为1(/Zp1)。
  • 重新编译SampleATLProject
  • 使用更新的.tlb文件再次运行tlbimp。
  • .NET文件引用Foo上将出现一个警告图标,但如果单击该引用,该图标可能会消失。如果不是,则可以删除引用并将其重新添加到C#控制台项目中,以确保它使用的是新的更新版本。

  • 我从这里运行它,并得到以下输出:
    Size: 12, Values: 1 1551957760 129 3
    

    那真是怪了。但是,如果我们在SampleATLProject_i.h中强制编辑C级编译指示,则会得到正确的输出。
    #pragma pack(push, 1)
    /* [uuid] */ struct  DECLSPEC_UUID("D2240D8B-EB97-4ACD-AC96-21F2EAFFE100") BarStruct
        {
        byte a;
        int b;
        byte c;
        byte d;
        } ;
    #pragma pack(pop)
    

    在这里重新编译SampleATLProject,.tlb或.NET项目没有更改,我们得到以下信息:
    Size: 7, Values: 1 127 255 128
    

    关于IDispatch,这取决于您的客户端是否后期绑定(bind)。后期绑定(bind)的客户端必须解析IDispatch的类型信息,并区分非平凡类型的正确定义。 ITypeInfoTYPEATTR的文档建议这样做是可行的,因为cbAlignment字段提供了必要的信息。我怀疑大多数都不会改变或违背默认值,因为如果出现问题或在不同版本之间期望的包装改变时,调试起来将很繁琐。而且,许多可以使用IDispatch的脚本客户端通常不支持结构。人们经常会期望仅支持由IDL oleautomation关键字控制的类型。

    IDispatch interface @ MSDN
    IDispatch::GetTypeInfo @ MSDN
    ITypeInfo interface @ MSDN
    TYPEATTR structure @ MSDN

    oleautomation keyword @ MSDN

    10-08 04:15