是否定义了在COM接口中传递的结构的打包?

时间:2012-04-13 17:29:45

标签: visual-c++ com idl

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

是否定义了结构的包装或者我的客户端代码是否恰好使用与COM服务器构建的相同打包设置?在默认C ++编译器打包设置已更改的项目中是否可能出错?是否有可用于确保客户端编译器打包设置正确的pragma包设置?

我无法在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 */
};

2 个答案:

答案 0 :(得分:7)

默认打包是沿着8字节边界,根据这里的MIDL命令行开关引用:

/Zp switch @ MSDN (MIDL Language Reference)

如果更改了包值,代码的其他部分更可能先破坏,因为IDL文件通常是提前预编译的,并且很少有人会故意改变提供给MIDL的命令行开关(但并非罕见,有人可以摆弄C范围#pragma pack并忘记恢复默认状态。)

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

pragma Attribute @ MSDN (MIDL Language Reference)

非常幸运的是,没有任何一方改变任何会干扰默认包装的设置。它会出错吗?是的,如果有人不顾一切地改变默认值。

使用IDL文件时,通常会将详细信息编译为类型库(.tlb),并且假设使用相同的类型库时,服务器和客户端的平台相同。这在/Zp开关的脚注中建议,因为某些值将针对某些非x86或16位目标失败。还可以有32位< - >。 64位转换案例可能导致预期中断。不幸的是,我不知道是否还有更多的案例,但默认情况下的工作量很小。

C#和VB没有任何内在行为来处理.tlb中的信息;相反,像tlbimp这样的工具通常用于将COM定义转换为可从.NET使用的定义。我无法验证C#/ VB.NET与COM客户端和服务器之间的所有期望是否成功;但是,我可以验证,如果引用从在该设置下编译的IDL创建的.tlb,则使用8以外的特定编译指示设置将起作用。虽然我不建议违反默认的pragma包,但如果您想要一个工作示例作为参考,请执行以下步骤。我创建了一个C ++ ATL项目和一个C#项目来检查。

以下是C ++方面的说明。

  1. 我使用Visual Studio 2010中的默认设置创建了一个名为 SampleATLProject 的ATL项目,没有更改任何字段。这应该为你创建一个dll项目。
  2. 编译项​​目以确保正在创建正确的C端接口文件(SampleATLProject_i.c和SampleATLProject_i.h)。
  3. 我在项目中添加了一个名为SomeFoo的ATL简单对象。同样,没有更改默认值。这将创建一个名为CSomeFoo的类,该类将添加到您的项目中。
  4. 编译SampleATLProject。
  5. 我右键单击SampleATLProject.idl文件,然后在MIDL设置下,将Struct Member Alignment设置为4个字节(/ Zp4)。
  6. 编译SampleATLProject。
  7. 我更改了IDL以添加名为' BarStruct'的结构定义。这需要添加带有MIDL uuid属性的C风格结构定义,以及引用结构定义的库部分中的条目。请参阅下面的代码段。
  8. 编译SampleATLProject。
  9. 在课堂视图中,我右键点击ISomeFoo并添加了一个名为FooIt的方法,该方法将struct BarStruct作为[in]参数,名为 theBar
  10. 编译SampleATLProject。
  11. 在SomeFoo.cpp中,我添加了一些代码来打印出结构的大小,并抛出一个包含详细信息的消息框。
  12. 这是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#方面:

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

      tlbimp SampleATLProject.tlb /out:Foo.dll /namespace:SampleATL.FooStuff

    2. 接下来,我创建了一个C#控制台应用程序,并在项目中添加了对Foo.dll的引用。

    3. 在References文件夹中,转到Foo的属性,并通过将其设置为false来关闭嵌入互操作类型
    4. 我添加了一个using语句来引用tlbimp给出的命名空间SampleATL.FooStuff,将[STAThread]属性添加到Main()(COM公寓模型必须匹配进程内消耗) ,并添加了一些代码来调用COM组件。
    5. 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
      

      为了确保可以更改pragma包(因为4/8字节打包是最常用的对齐方式),我按照以下步骤将其更改为1:

      1. 我返回到C ++项目,转到SampleATLProject.idl的属性并将Struct Member Alignment更改为1(/ Zp1)。
      2. 重新编译SampleATLProject
      3. 使用更新的.tlb文件再次运行tlbimp。
      4. 警告图标将显示在Foo的.NET文件参考上,但如果单击参考,可能会消失。如果没有,您可以删除并重新添加对C#控制台项目的引用,以确保它使用新的更新版本。
      5. 我从这里开始运行并得到了这个输出:

        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,这取决于您的客户是否是后期限制。后期绑定客户端必须解析IDispatch的类型信息方面,并识别非平凡类型的正确定义。 ITypeInfoTYPEATTR的文档表明,只要cbAlignment字段提供了必要的信息,就有可能。我怀疑大多数人永远不会改变或违反默认值,因为如果出现问题或者版本之间的包装期望必须改变,这将是繁琐的调试。此外,许多可以使用IDispatch的脚本客户端通常不支持结构。人们经常可以预期只支持由IDL oleautomation关键字管理的类型。

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

        oleautomation keyword @ MSDN

答案 1 :(得分:6)

是的,结构是COM中的问题。如果您使用基于IUnknown的接口,那么您将必须使用适当的编译器设置来掷骰子。改变默认值的理由很少。

如果使用COM Automation,则必须在.IDL中使用typedef声明结构。这样客户端代码就可以使用IRecordInfo在类型库信息的指导下正确访问结构。您所要做的就是确保编译器的/ Zp设置与midl.exe的/ Zp设置相匹配。不难做到。

通过认识到任何结构都可以通过具有属性的接口来描述,您可以完全解决问题。现在没关系。