我正在使用具有自己的自定义界面的第三方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++附带说明。
SomeFoo
的ATL简单对象。同样,没有更改默认值。这将创建一个称为CSomeFoo
的类,该类将添加到您的项目中。 ISomeFoo
并添加了一个名为FooIt
的方法,该方法将struct BarStruct
作为名为 theBar 的[in]参数。 这是我对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#端:
tlbimp SampleATLProject.tlb/out:Foo.dll/namespace:SampleATL.FooStuff
Foo
的“属性”,并通过将其设置为false来关闭嵌入互操作类型。 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:
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
的类型信息,并区分非平凡类型的正确定义。 ITypeInfo
和TYPEATTR
的文档建议这样做是可行的,因为cbAlignment
字段提供了必要的信息。我怀疑大多数都不会改变或违背默认值,因为如果出现问题或在不同版本之间期望的包装改变时,调试起来将很繁琐。而且,许多可以使用IDispatch
的脚本客户端通常不支持结构。人们经常会期望仅支持由IDL oleautomation
关键字控制的类型。IDispatch interface @ MSDN
IDispatch::GetTypeInfo @ MSDN
ITypeInfo interface @ MSDN
TYPEATTR structure @ MSDN
oleautomation keyword @ MSDN