Add-ins for Multiple Office Versions without PIAs (Pt2), or _VtblGap

In my last post, I discussed how you could avoid any dependency on the Office PIAs by using ComImport to redefine the host application’s OM interfaces. Someone (A Developer) pointed out that I had actually omitted the trailing 2 members of the IRibbonControl interface – and I mentioned that this wouldn’t stop the code working. I also mentioned that omitting interface members is a valid technique, so today I’ll explain what I mean by this.

I’m attaching the sample solution to this post – and you can compare it with the sample solution I attached to the previous post – you’ll see that the solutions are identical except that in this one I’m omitting selective interface members in my ComImport declarations. The idea is that I don’t want to include definitions of interface members that I’m not using. You might think that I could simply omit these members altogether – and this is true for trailing members, but it’s not true for non-trailing members. Strictly speaking, even for trailing members, it’s not good practice.

The reason is that these are COM interfaces, and the number and position of methods defined in a COM interface is paramount. At runtime, the methods are represented by a virtual function table (vtable), which contains slots that correspond to the methods, in the order of their declaration in the interface.  Because calls into COM interfaces are implemented as calls to offsets from the start of the interface’s vtable (or the vtable for the object that implements the interface), it means that the order/position and number of the members is significant.

This ordering and numbering must be maintained when you redefine a COM interface using ComImport, and the .NET Framework provides member syntax to support this. In order to preserve vtable order, you use the special _VtblGapXX_YY method syntax to indicate missing members, where XX signifies the position in the vtable, and YY signifies the number of vtable members to be omitted from this position. For example, in the _CustomTaskPane interface, from position 1, you can omit 2 vtable members using this declaration:

    void _VtblGap1_2();

This syntax allows me to preserve vtable slots for members I’m not defining, such that the subsequent members are still correctly positioned. Here’s the set of selectively ComImport-ed interfaces for custom task panes. Note that by omitting the DockPosition and DockPositionRestrict members, I can also avoid having to declare the MsoCTPDockPosition and MsoCTPDockPositionRestrict enums:

//public enum MsoCTPDockPosition


//    msoCTPDockPositionLeft,

//    msoCTPDockPositionTop,

//    msoCTPDockPositionRight,

//    msoCTPDockPositionBottom,

//    msoCTPDockPositionFloating



//public enum MsoCTPDockPositionRestrict


//    msoCTPDockPositionRestrictNone,

//    msoCTPDockPositionRestrictNoChange,

//    msoCTPDockPositionRestrictNoHorizontal,

//    msoCTPDockPositionRestrictNoVertical



[ComImport, Guid("000C033B-0000-0000-C000-000000000046"), TypeLibType((short)0x10c0), DefaultMember("Title")]

public interface _CustomTaskPane



    string Title { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(0)] get; }


    // From position 1, we omit 2 vtbl members.

    void _VtblGap1_2();


    //object Application { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)] get; }


    //object Window { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(2)] get; }



    bool Visible { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(3)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(3)] set; }


    object ContentControl { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(4)] get; }


    // From position 5, we omit 1 vtbl member.

    //void _VtblGap5_1();

    void _VtblGap_1();


    //int Height { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(5)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(5)] set; }



    int Width { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(6)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(6)] set; }


    // From position 7, we omit 3 vtbl members.

    void _VtblGap7_3();


    //MsoCTPDockPosition DockPosition { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(7)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(7)] set; }


    //MsoCTPDockPositionRestrict DockPositionRestrict { [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(8)] get; [param: In] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(8)] set; }

    //[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(9)]

    //void Delete();



[ComImport, Guid("000C033B-0000-0000-C000-000000000046")]

public interface CustomTaskPane : _CustomTaskPane




[ComImport, Guid("000C033D-0000-0000-C000-000000000046"), TypeLibType((short)0x10c0)]

public interface ICTPFactory


    [return: MarshalAs(UnmanagedType.Interface)]

    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]

    CustomTaskPane CreateCTP([In, MarshalAs(UnmanagedType.BStr)] string CTPAxID, [In, MarshalAs(UnmanagedType.BStr)] string CTPTitle, [In, Optional, MarshalAs(UnmanagedType.Struct)] object CTPParentWindow);



[ComImport, Guid("000C033E-0000-0000-C000-000000000046"), TypeLibType((short)0x10c0)]

public interface ICustomTaskPaneConsumer


    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]

    void CTPFactoryAvailable([In, MarshalAs(UnmanagedType.Interface)] ICTPFactory CTPFactoryInst);


As you can see from the listing above, from position 5, I used a simpler form of the _VtblGap syntax to omit one vtable member. Note that it's enough to specify the number of vtable slots to reserve - it's not necessary to specify the position, because the position is taken from the position of the _VtblGap_YY declaration itself.

    //void _VtblGap5_1();

    void _VtblGap_1();

Note also that just because I can specify the position in the XX component, this does not allow me to rearrange the position of the _VtblGap declarations themselves. Given this, you might wonder why I bother with the XX in _VtblGapXX_YY, since it’s not necessary for the purposes of reserving correctly ordered vtable slots. The answer is that these _VtblGap entries all count as method declarations, and if I only use the YY component, there’s a good chance I’ll end up with a name conflict and the code will fail to compile. Using the XX component is just a convenience to avoid this problem. The _VtblGap syntax is documented in the Common Language Infrastructure Annotated Standard.


The pattern is similar for the Ribbon interfaces. Note that (as in the example in my previous post), I could simply omit the trailing members, but in this example I’ve chosen to explicitly reserve vtable space even though I’m not using these members and there are no members after these ones in the interface. The reason that preserving overall vtable size is encouraged is because there could theoretically be a consumer of the interface written in such a way that is dependent on the overall size of the vtable. This is pretty far-fetched these days – and in this specific example I’m ComImport-ing these interfaces in my own project which is not designed to be re-used by any other consumer. Nonetheless, it’s probably good practice, and does no harm.

[ComImport, Guid("000C0396-0000-0000-C000-000000000046"), TypeLibType((short)0x1040)]

public interface IRibbonExtensibility


    [return: MarshalAs(UnmanagedType.BStr)]

    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)]

    string GetCustomUI([In, MarshalAs(UnmanagedType.BStr)] string RibbonID);



[ComImport, Guid("000C0395-0000-0000-C000-000000000046"), TypeLibType((short)0x1040)]

public interface IRibbonControl



    string Id { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(1)] get; }


    // From position 2, we omit 2 vtbl members.

    void _VtblGap2_2();


    //object Context { [return: MarshalAs(UnmanagedType.IDispatch)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(2)] get; }


    //string Tag { [return: MarshalAs(UnmanagedType.BStr)] [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime), DispId(3)] get; }


The end-result of all this member omission is smaller code, which means a smaller working set, and likely better performance as a result. It also means less chance for bugs to be introduced and less code maintenance going forward.

Comments (5)

  1. Continuing on from my earlier posts on building add-ins for multiple versions of Office , avoiding the

  2. There are at least 9 different ways to start or connect to an Office app programmatically in managed

  3. NJKA says:

    This is also a realy good way to support multiple versions of Office using a single type library.

    For example. We need our app needs to support Word vesions 10 – 12.

    So we use the version 10 Word Interop and then created Extension members that bolt on the extra functions and interfaces we require for versions 11 & 12.

    We also add extra extension members so we do not need Try Catch blocks for some com calls.

    By using PreserveSig and then checking the HResult inside the an extension method we can make our code much more readable and possibly faster, by stopping the runtime from throwing exceptions. An example the Variables object throw an exception if the variable does not exist and there is no other way to check if the variable exists.

    We have found using Extension methods to be a fantastic solution to supporting multiple versions of Word.


  4. Ken Slovak says:

    Hi Andrew,

    Now that the Office 2010 beta is out there’s a new VTable problem for IRibbonUI. It has 4 new entries in it InvalidateControlMso, ActivateTab, ActivateTabMso and ActivateTabQ) that weren’t there in Office 2007.

    So using the methodology that you’ve outlined in these 2 articles, is it possible to handle the VTable declarations for IRibbonUI in a way that would work with both ribbon supporting versions of Office and still let us start with the Office 2003 PIA’s to handle everything else?

    This would be most welcome as a way to support those versions of Office all from one code base that would either load the appropriate VTable for IRibbonUI depending on the Office version detected at runtime, or would use one set of declarations for the VTable that would work no matter if Office 2007 or 2010 was loaded.


    Ken Slovak

  5. Garry Trinder says:

    Hi Ken – I’m not sure how _VtblGap helps here. When Office adds new interface members, it always does so at the end of the interface. The IID remains the same.

    I suppose you could use ComImport to declare an IRibbonUI interface based on the Office 2010 version, with the 4 additional members.

    Then, if your add-in gets loaded into an Office 2010 host, you would use those members. If, on the other hand, it gets loaded into an Office 2007 host, you’d want to use some other indicator to detect this, and then simply not try to use the non-existent trailing 4 members.

    If you wanted your add-in to work in Office 2003 also, then you’d still implement IRibbonExtensibility (for example), but Office 2003 would never QI for it, so it becomes a no-op.

Skip to main content