I'm working with a third party COM server with its own custom interface that sets and gets structs as some of its properties. As it happens I'm using C++ for the client. I've posted some representative code from the IDL file below with names changed and GUIDs removed.
Is the packing of the structure defined or is it just good fortune that my client code happens to use the same packing settings that the COM server was built with? Would it be likely to go wrong in projects where the default C++ compiler packing settings had been changed? Is there a pragma pack setting that I could use to make sure the client compiler packing settings are correct?
I can't see any packing pragmas or statements in either the IDL or the header file generated from MIDL. What would happen if the client was in C# or VB instead? Is the packing behaviour more clearly specified if called via the IDispatch mechanism?
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 */
};
When a structure is packed, these paddings are not inserted. The compiler has to generate more code (which runs slower) to extract the non-aligned data members, and also to write to them.
The #pragma pack directive modifies the current alignment rule for only the members of structures whose declarations follow the directive. It does not affect the alignment of the structure directly, but by affecting the alignment of the members of the structure, it may affect the alignment of the overall structure.
When you use #pragma pack(1) , this changes the default structure packing to byte packing, removing all padding bytes normally inserted to preserve alignment.
The structure padding is automatically done by the compiler to make sure all its members are byte aligned. Here 'char' is only 1 byte but after 3 byte padding, the number starts at 4 byte boundary. For 'int' and 'double', it takes up 4 and 8 bytes respectively.
The default packing is along 8-byte boundaries, according to the MIDL command line switch reference here:
/Zp switch @ MSDN (MIDL Language Reference)
Other parts of your code are more likely to break first if the pack value is changed, as the IDL file is usually pre-compiled ahead of time, and it is rare that someone will deliberately alter the command line switches given to MIDL (but not so rare that someone could fiddle with the C-scope #pragma pack
and forget to restore the default state).
If you have a good reason to alter the setting, you can explicitly set the packing with a pragma pack
statement.
pragma Attribute @ MSDN (MIDL Language Reference)
It is pretty good fortune that no party has changed any setting that would interfere with the default packing. Can it go wrong? Yes, if someone goes out of their way to change the defaults.
When using an IDL file, the details are typically compiled into a typelib (.tlb), and it is assumed that the platform are the same for both servers and clients when using the same typelib. This is suggested in the footnotes for the /Zp
switch, as certain values will fail against certain non-x86 or 16-bit targets. There can also be 32bit <-> 64bit conversion cases that could cause expectations to break. Unfortunately I don't know if there are even more cases out there, but the defaults do work with minimal fuss.
C# and VB do not have any intrinsic behavior to handle information in a .tlb; instead, a tool like tlbimp is typically used to convert COM definitions into definitions usable from .NET. I can't verify whether all expectations succeed between C#/VB.NET and COM clients and servers; However, I can verify that using a specific pragma setting other than 8 will work if you reference a .tlb that was created from an IDL compiled under that setting. While I wouldn't recommend going against the default pragma pack, here are the steps to perform if you'd like a working example to use as a reference. I created a C++ ATL project and a C# project to check.
Here are the C++ side instructions.
SomeFoo
to the project. Again, no defaults were altered. This creates a class called CSomeFoo
that is added to your project.ISomeFoo
and added a method called FooIt
, that takes a struct BarStruct
as an [in] parameter called theBar.Here is my IDL for the ATL project.
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;
};
};
Inside the CSomeFoo
class, here is the implementation for 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;
}
Next, on the C# side:
Go to the debug or desired output directory for SampleATLProject and run tlbimp.exe on the .tlb file generated as part of the C++ project output. The following worked for me:
tlbimp SampleATLProject.tlb /out:Foo.dll /namespace:SampleATL.FooStuff
Next, I created a C# console application, and added a reference to Foo.dll to the project.
Foo
and turn off Embed Interop Types by setting it to false.SampleATL.FooStuff
as given to tlbimp, added the [STAThread]
attribute to Main()
(the COM apartment models have to match for in-proc consumption), and added some code to call the COM component.Tlbimp.exe (Type Library Importer) @ MSDN
Here is the source code for that console app.
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);
}
}
}
Finally, it runs and I get a modal popup with the following string displayed:
Size: 12, Values: 1 127 255 128
To be sure that a pragma pack change can be made (as 4/8 byte packing are the most common alignments used), I followed these steps to change it to 1:
Foo
, but may disappear if you click on the reference. If it doesn't, you can remove and re-add the reference to the C# console project to be sure it is using the new updated version.I ran it from here and got this output:
Size: 12, Values: 1 1551957760 129 3
That's weird. But, if we forcefully edit the C-level pragma in SampleATLProject_i.h, we get the correct output.
#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 is recompiled here, no changes to the .tlb or .NET project, and we get the following:
Size: 7, Values: 1 127 255 128
Regarding IDispatch
, it depends on whether your client is late-bound. Late-bound clients have to parse the type information side of IDispatch
and discern the proper definitions for non-trivial types. The documentation for ITypeInfo
and TYPEATTR
suggests that it is possible, given that the cbAlignment
field provides the information necessary. I suspect most will never alter or go against the defaults, as this would be tedious to debug if things went wrong or if the pack expectations had to change between versions. Also, structures are not typically supported by many scripting clients that can consume IDispatch
. One can frequently expect that only the types governed by the IDL oleautomation
keyword are supported.
IDispatch interface @ MSDN
IDispatch::GetTypeInfo @ MSDN
ITypeInfo interface @ MSDN
TYPEATTR structure @ MSDN
oleautomation keyword @ MSDN
Yes, structs are a problem in COM. If you use IUnknown based interfaces then you'll have to roll the dice with proper compiler settings. Few reasons to change the default.
If you use COM Automation then you have to declare the struct with a typedef in the .IDL. So that the client code can use IRecordInfo to access the structure properly, guided by the type library info. All you have to do is ensure that your compiler's /Zp setting matches midl.exe's /Zp setting. Not hard to do.
You sail around the problem entirely by realizing that any structure can be described by an interface with properties. Now it doesn't matter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With