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

per*_*age 5 com idl visual-c++

我正在使用第三方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 */
};
Run Code Online (Sandbox Code Playgroud)

mek*_*ian 8

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

/ Zp switch @ MSDN(MIDL语言参考)

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

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

pragma Attribute @ MSDN(MIDL语言参考)

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

使用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. 在Class View中,我右键单击ISomeFoo并添加了一个名为的方法FooIt,它接受一个struct BarStruct名为theBar的[in]参数.
  10. 编译SampleATLProject.
  11. 在SomeFoo.cpp中,我添加了一些代码来打印出struct的大小并抛出一个包含详细信息的Message Box.

这是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;
  };
};
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

接下来,在C#端:

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

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

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

  3. 在References文件夹中,转到Properties for Foo并通过将Embed Interop Types设置为false来关闭它.
  4. 我添加了一个using语句来引用SampleATL.FooStufftlbimp给出的命名空间,将[STAThread]属性添加到Main()(COM公寓模型必须匹配进程内消耗),并添加一些代码来调用COM组件.

Tlbimp.exe(类型库导入程序)@ 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,它运行,我得到一个模态弹出窗口,显示以下字符串:

Size: 12, Values: 1 127 255 128
Run Code Online (Sandbox Code Playgroud)

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

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

我从这里运行它得到了这个输出:

Size: 12, Values: 1 1551957760 129 3
Run Code Online (Sandbox Code Playgroud)

那真是怪了.但是,如果我们在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)
Run Code Online (Sandbox Code Playgroud)

这里重新编译SampleATLProject,对.tlb或.NET项目没有任何更改,我们得到以下内容:

Size: 7, Values: 1 127 255 128
Run Code Online (Sandbox Code Playgroud)

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

IDispatch接口@ MSDN
IDispatch :: GetTypeInfo @ MSDN
ITypeInfo接口@ MSDN
TYPEATTR结构@ MSDN

oleautomation keyword @ MSDN


Han*_*ant 6

是的,结构是COM中的一个问题.如果你使用基于IUnknown的接口,那么你将不得不使用适当的编译器设置来掷骰子.更改默认值的理由很少.

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

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