Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the packing of structs passed in COM interfaces defined?

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 */
};
like image 304
persiflage Avatar asked Apr 13 '12 17:04

persiflage


People also ask

What is packed structure in C?

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.

How does pragma pack work in C?

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.

What does #pragma pack 1 mean?

When you use #pragma pack(1) , this changes the default structure packing to byte packing, removing all padding bytes normally inserted to preserve alignment.

Why are structs padded?

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.


2 Answers

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.

  1. I created an ATL project called SampleATLProject with the default settings in Visual Studio 2010, no fields changed. This should create a dll project for you.
  2. Compiled the project to assure that the proper C-side interface files are being created (SampleATLProject_i.c and SampleATLProject_i.h).
  3. I added an ATL Simple Object called SomeFoo to the project. Again, no defaults were altered. This creates a class called CSomeFoo that is added to your project.
  4. Compile SampleATLProject.
  5. I right-clicked the SampleATLProject.idl file, then under the MIDL settings, set the Struct Member Alignment to 4 bytes (/Zp4).
  6. Compile SampleATLProject.
  7. I altered the IDL to add a struct definition called 'BarStruct'. This entailed adding a C-style struct definition with the MIDL uuid attribute, and an entry in library section referencing the struct definition. See snippet below.
  8. Compile SampleATLProject.
  9. From the Class View, I right-clicked on ISomeFoo and added a method called FooIt, that takes a struct BarStruct as an [in] parameter called theBar.
  10. Compile SampleATLProject.
  11. In SomeFoo.cpp, I added some code to print out the size of the struct and throw up a Message Box containing the details.

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:

  1. 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

  2. Next, I created a C# console application, and added a reference to Foo.dll to the project.

  3. In the References folder, go to the Properties for Foo and turn off Embed Interop Types by setting it to false.
  4. I added a using statement to reference the namespace 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:

  1. I returned to the C++ project, went to the properties for SampleATLProject.idl and changed the Struct Member Alignment to 1 (/Zp1).
  2. Recompile SampleATLProject
  3. Run tlbimp again with the updated .tlb file.
  4. A warning icon will appear on the .NET File Reference to 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

like image 53
meklarian Avatar answered Nov 03 '22 01:11

meklarian


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.

like image 32
Hans Passant Avatar answered Nov 03 '22 01:11

Hans Passant