Productive Rage

Dan's techie ramblings

WinDbg Rides Again

I've been trying to get a .Net COM component working in a Classic ASP site and was consistently getting an error the first time it was accessed:

Active Server Pages error 'ASP 0115'

Unexpected error

/engine/default.asp

A trappable error (E0434352) occurred in an external object. The script cannot continue running.

Active Server Pages error 'ASP 0240'

Script Engine Exception

/engine/default.asp

A ScriptEngine threw exception 'C0000005' in 'IActiveScript::Close()' from 'CActiveScriptEngine::FinalRelease()'.

Pretty vague. But at least it's consistently happening..

Since it's not long since I've had to try to use WinDbg to investigate an issue on a live server (see The WinDbg Blues), I thought maybe I could apply it to this problem. So I wanted to go through the motions of attaching to the w3wp.exe process where the exception occured so that I could look into it.

The first problem was that IIS is running in 32 bit mode for this site so I needed to use the 32 bit version of WinDbg. These can be obtained as part of the Windows Software Development Kit (SDK) for Windows 8. It doesn't matter if you're not running Windows 8, it doesn't matter if you don't want anything else in that download, it doesn't matter if you don't have .Net 4.5 installed and it warns you about it when you run the setup executable - select "Debugging Tools for Windows" when you're offered features to install and leave everything else unselected. This will install both the 64 and 32 bit versions of the tool.

The next problem was that ".loadby sos mscorwks" returned the error

Unable to find module 'mscorwks'

The answer to this was found at this MSDN blog post Error loading sos.dll; to use ".loadby sos clr"

Now we're cooking!

WinDbg .loadby sos clr

So now some progress is being made! The next step is to view all of the managed threads in the process with the command "!threads":

WinDbg viewing managed threads

Of these, one reports an exception. It's a bit vague-sounding, a "System.Reflection.TargetInvocationException" but more information can be gleaned with the PrintException command ("!pe"), specifying the address of the exception:

WinDbg PrintException TargetInvocationException

Not that helpful-looking yet, but there's a hint to dig deeper and look at the InnerException:

WinDbg PrintException InnterException

Well now we're getting somewhere! When the component tries to access the System.Web.HttpRuntime.Cache an exception is being raised. This message is a little cryptic and I have no idea why it would only be happening on first access but at least I have something to search for and it's not directly my code that's causing it!

Stack Overflowing..

Google brings me to this Stack Overflow question as the most promising lead: Attempted to read or write protected memory at System.Web.Hosting.UnsafeIISMethods.MgdGetSiteNameFromId. While there are no actual explanations, someone suggests that having encountered this they changed the build parameters to target "x86" specifically and the problem went away. Unfortunately, this was not the case for me. Instead, the HttpRuntime.Cache was only being used if the site didn't provide the COM component with a cache reference that it could stash things in - it was being used as a default cache mechanism. I changed the integration to remove this default and require a cache reference and now the problem has gone. Granted, I didn't strictly-speaking solve the underlying problem.. but I identified it and removed it with a solution I'm happier with overall, so I'm considering this a success! :)

WinDbg dumps in Visual Studio

While I was investigating this, I came across this post from WinDbg guru Tess Ferrandez First look at debugging .NET 4.0 dumps in Visual Studio 2010. Essentially saying that you can debug .Net 4 dumps direct in Visual Studio! Amazing!

There are a couple of caveats:

  1. It must be a .Net 4 dump, 3.5 and earlier won't work
  2. It must be a debug build, release builds won't work
  3. This doesn't appear to be supported by Visual Studio Express

Side note: Because I'm curious, I wanted to know what specifically about a release build it was that prevented it from working. From playing around with the settings, there are two things that appear to make the difference - in the project properties, under the Build tab, "Optimize code" must be unchecked and "Debug Info" must be set to "full" (rather than the default "pdb-only" in the "Advanced Build Settings" (accessed by cliking the "Advanced" button). Obviously, disabling optimisations means you're disabling the benefits of generating a release build..

To try this out, I created the simplest program I could think of investigating:

using System;

namespace WinDbgDumpTest
{
  class Program
  {
    static void Main(string[] args)
    {
      var a = 123;
      Console.WriteLine(a);
      Console.ReadLine();
    }
  }
}

I built this and ran the executable direct from the build location in explorer (if I ran it from within Visual Studio then "WinDbgDumpTest.vshost.exe" appears in the process list, not "WinDbgDumpTest.exe", and this will be VS running the code rather than the code running on its own).

I then attached WinDbg to the process and ran the command

.dump /ma c:\WinDbgDump.dmp

which will "dump complete memory image" (according to the very useful WinDbg / SOS Cheat Sheet). If you don't specify "/ma" then only a "small memory image" will be dumped which will mean that Visual Studio tells you "The value of the local or argument {whatever} is unobtainable at this time" when you try to inspect variables. This caught me out for quite a while and started driving me mad until I realised what I'd done!

Loading the dump file

As described in that post (First look at debugging .NET 4.0 dumps in Visual Studio 2010), the default symbol server can be enabled by going to Tools / Options / Debgging / Symbols and enabling the microsoft symbol server location.

Then open the dump file in Visual Studio (nothing more complicated than going to File / Open and selecting the file).

This should display some summary information but what we want to do from here is click on the "Debug with Mixed" link which will load the state into Visual Studio as if we'd run the code and it had stopped at the point at which the dump was taken. You'll like get a warning at this point such as "Windows has triggered a breakpoint in WinDbgDump.dmp", just click "Break".

If you're examining a dump generated from code such as the example above, you'll want to select the Main Thread from the Threads window and then can jump to the current frame by clicking on the top entry in the Call Stack window.

At this point, you can examine values or jump around the call stack or do pretty much anything (not including clicking continuing execution - you'll get an error "The debugger cannot continue running the process. This operation is not supported when debugging dump files.") you could do if you were in the middle of pausing execution of code executed by Visual Studio - much easier than trying to poke around values in WinDbg! In the example here, I could hover over "a" and see that its value was 123 (similarly, this information is visible in the "Locals" window).

Posted at 16:26

Comments

IDispatch (IWastedTimeOnThis but ILearntLots)

For something I've been working on it looked like I was going to have to interact with COM objects from a legacy system without type libraries and where the internals were written in VBScript. Ouch. It seemed like a restriction of the environment meant that .Net 4 wouldn't be available and so the dynamic keyword wouldn't be available.

It would seem that the COMInteraction code that I wrote in the past would be ideal for this since it should wrap access to generic COM objects but I encountered a problem with that (which I'll touch briefly on later in this post).

So the next step was to find out about the mysterious IDispatch interface that I've heard whispered about in relation to dealings with generic COM objects! Unfortunately, I think in the end I found a way to get .Net 4 into play for my original problem so this might all have been a bit of a waste of time.. but not only was it really interesting but I also found nowhere else on the internet that was doing this with C#. And I read up a lot. (There's articles that touch on most of it, but not all - read on to find out more! :)

What is IDispatch

From IDispatch on Wikipedia:

IDispatch is the interface that exposes the OLE Automation protocol. It is one of the standard interfaces that can be exposed by COM objects .. IDispatch derives from IUnknown and extends its set of three methods (AddRef, Release and QueryInterface) with four more methods - GetTypeInfoCount, GetTypeInfo, GetIDsOfNames and Invoke.

Each property and method implemented by an object that supports the IDispatch interface has what is called a Dispatch ID, which is often abbreviated DISPID. The DISPID is the primary means of identifying a property or method and must be supplied to the Invoke function for a property or method to be invoked, along with an array of Variants containing the parameters. The GetIDsOfNames function can be used to get the appropriate DISPID from a property or method name that is in string format.

It's basically a way to determine what methods can be called on an object and how to call them.

How to use it

I got most of the useful information first from these links:

  1. Calling a member of IDispatch COM interface from C# (Stack Overflow)
  2. Setting a Property by IDispatch Invoke (particularly section 3.4)
  3. Using VARIANTs in Managed Code Part 3 (section 2.4)

The first thing to do is to cast the object reference to the IDispatch interface (this will only work if the object implements IDispatch, for the COM components I was targetting this was the case). The interface isn't available in the framework but can be hooked up with

[ComImport()]
[Guid("00020400-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IDispatch
{
  [PreserveSig]
  int GetTypeInfoCount(out int Count);

  [PreserveSig]
  int GetTypeInfo
  (
    [MarshalAs(UnmanagedType.U4)] int iTInfo,
    [MarshalAs(UnmanagedType.U4)] int lcid,
    out System.Runtime.InteropServices.ComTypes.ITypeInfo typeInfo
  );

  [PreserveSig]
  int GetIDsOfNames
  (
    ref Guid riid,
    [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)]
    string[] rgsNames,
    int cNames,
    int lcid,
    [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId
  );

  [PreserveSig]
  int Invoke
  (
    int dispIdMember,
    ref Guid riid,
    uint lcid,
    ushort wFlags,
    ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams,
    out object pVarResult,
    ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo,
    out UInt32 pArgErr
  );
}

Then the GetIDsofNames is called to determine whether a given method is present:

private const int LOCALE_SYSTEM_DEFAULT = 2048;

// rgDispId will be populated with the DispId of the named member (if available)
var rgDispId = new int[1] { 0 };

// IID_NULL must always be specified for the "riid" argument
// (see http://msdn.microsoft.com/en-gb/library/windows/desktop/ms221306(v=vs.85).aspx)
var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");

var hrRet = ((IDispatch)source).GetIDsOfNames
(
  ref IID_NULL, // 
  new string[1] { name },
  1, // number of names to get ids for
  LOCALE_SYSTEM_DEFAULT,
  rgDispId
);
if (hrRet != 0)
  throw new Exception("Uh-oh!");

return rgDispId[0];

Then the Invoke method is called with the Disp Id, the type of call (eg. execute method, set property, etc..), a "local context" ("applications that do not support multiple national languages can ignore this parameter" - IDispatch::Invoke method (Automation) at MSDN) and the parameters.

Calling a argument-less method

private const int LOCALE_SYSTEM_DEFAULT = 2048;
private const ushort DISPATCH_METHOD = 1;

var dispId = 19; // Or whatever the above code reported

// This DISPPARAMS structure describes zero arguments
var dispParams = new System.Runtime.InteropServices.ComTypes.DISPPARAMS()
{
  cArgs = 0,
  cNamedArgs = 0,
  rgdispidNamedArgs = IntPtr.Zero,
  rgvarg = IntPtr.Zero
};

var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
UInt32 pArgErr = 0;
object varResult;
var excepInfo = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
var hrRet = ((IDispatch)source).Invoke
(
  dispId,
  ref IID_NULL,
  LOCALE_SYSTEM_DEFAULT,
  DISPATCH_METHOD,
  ref dispParams,
  out varResult,
  ref excepInfo,
  out pArgErr
);
if (hrRet != 0)
  throw new Exception("FAIL!");
return varResult;

The DISPPARAMS structure (which is part of the framework) enables the specification of both "named" and "unnamed" arguments. When calling a method, unnamed arguments may be passed in but when setting a property, the value that the property is to be set to must be passed as a named argument with the special constant DISPID_PROPERTYPUT (-3).

The above code could also be used to retrieve a property value (a non-indexed property) by replacing the DISPATCH_METHOD value with DISPATCH_PROPERTYGET (2).

Calling a single-argument method

[DllImport(@"oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
static extern Int32 VariantClear(IntPtr pvarg);

private const int LOCALE_SYSTEM_DEFAULT = 2048;
private const ushort DISPATCH_METHOD = 1;
private const int SizeOfNativeVariant = 16;

var dispId = 19; // Or whatever the above code reported
var arg = "Value";

// This DISPPARAMS describes a single (unnamed) argument
var pVariant = Marshal.AllocCoTaskMem(SizeOfNativeVariant);
Marshal.GetNativeVariantForObject(arg, pVariant);
var dispParams = new System.Runtime.InteropServices.ComTypes.DISPPARAMS()
{
  cArgs = 1,
  cNamedArgs = 0,
  rgdispidNamedArgs = IntPtr.Zero,
  rgvarg = pVariant
};

try
{
  var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
  UInt32 pArgErr = 0;
  object varResult;
  var excepInfo = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
  var hrRet = ((IDispatch)source).Invoke
  (
    dispId,
    ref IID_NULL,
    LOCALE_SYSTEM_DEFAULT,
    DISPATCH_METHOD,
    ref dispParams,
    out varResult,
    ref excepInfo,
    out pArgErr
  );
  if (hrRet != 0)
    throw new Exception("FAIL!");
  return varResult;
}
finally
{
  VariantClear(pVariant);
  Marshal.FreeCoTaskMem(pVariant);
}

As mentioned above, when calling methods there is no need to named arguments so cNamedArgs is still 0 and rgdispidNamedArgs is still IntPtr.Zero (a managed version of a null pointer).

From what I understand (and I'd never used Marshal.AllocCoTaskMem or Marshal.GetNativeVariantForObject before a couple of days ago!), the AllocCoTaskMem call allocates a chunk of unmanaged memory and then GetNativeVariantForObject copies a managed reference into that memory. A variant is always 16 bytes. This is the same variant type used for all VBScript calls, for example, and used for method arguments for IDispatch. More about the VARIANT structure can be found at this MSDN article.

The framework does some sort of clever manipulation to copy the contents of the managed reference into unmanaged memory, the internals of which I'm not going to worry too much about. But there's a couple of things to note; this is a copy operation so if I was getting involved with unmanaged memory for performance reasons then I'd probably want to avoid this. But it does mean that this copied memory is "safe" from the garbage collector doing anything with it. When you peel it back a layer, managed memory can't be expected to work as predictably as unmanaged memory as the garbage collector is free to be doing all manner of clever things to stay on top of memory usage and references and, er.. stuff. Which is a good thing because (for the large part) I don't have to worry about it! But it would be no good if the garbage collector moved memory around that the COM component was in the middle of accessing. Bad things would happen. Bad intermittent things (the worst kind). But this does have one important consequence; since the GC is not in control of this memory, I need to explicitly release it myself when I'm done with it.

Another side note on this: The system also needs to be sure that the GC doesn't do anything interesting with memory contents while it's performing to copy to the variant. The framework uses something called "automatic pinning" to ensure that the reference being considered by the Marshal.GetNativeVariantForObject doesn't move during this operation (ie. it is "pinned" in place in memory). There is also a way to manually pin data where a particular reference can be marked such that its memory not be touched by the GC until it's freed (using GCHandle.Alloc and the GCHandleType.Pinned option, and later calling .Free on the handle returned by Alloc) which may be used in the passing-by-reference approach I alluded to above, but I won't need it here.

Setting a (non-indexed) property

[DllImport(@"oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
static extern Int32 VariantClear(IntPtr pvarg);

private const int LOCALE_SYSTEM_DEFAULT = 2048;
private const ushort DISPATCH_PROPERTYPUT = 4;
private const int DISPID_PROPERTYPUT = -3;
private const int SizeOfNativeVariant = 16;

var dispId = 19; // Or whatever the above code reported
var arg = "Value";

// This DISPPARAMS describes a single named (DISPID_PROPERTYPUT) argument
var pNamedArg = Marshal.AllocCoTaskMem(sizeof(Int64));
Marshal.WriteInt64(pNamedArg, DISPID_PROPERTYPUT);
var pVariant = Marshal.AllocCoTaskMem(SizeOfNativeVariant);
Marshal.GetNativeVariantForObject(arg, pVariant);
var dispParams = new System.Runtime.InteropServices.ComTypes.DISPPARAMS()
{
  cArgs = 1,
  cNamedArgs = 1,
  rgdispidNamedArgs = pNamedArg,
  rgvarg = pVariant
};

try
{
  var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
  UInt32 pArgErr = 0;
  object varResult;
  var excepInfo = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
  var hrRet = ((IDispatch)source).Invoke
  (
    dispId,
    ref IID_NULL,
    LOCALE_SYSTEM_DEFAULT,
    DISPATCH_PROPERTYPUT,
    ref dispParams,
    out varResult,
    ref excepInfo,
    out pArgErr
  );
  if (hrRet != 0)
    throw new Exception("FAIL!");
}
finally
{
  VariantClear(pVariant);
  Marshal.FreeCoTaskMem(pVariant);
  VariantClear(pNamedArg);
  Marshal.FreeCoTaskMem(pNamedArg);
}

The example code in section 3.4 of the Setting a Property by IDispatch Invoke post I linked to earlier uses a manual pinning approach to specifying the named arguments data but as I understand it we can copy the DISPID_PROPERTYPUT value into unmanaged memory instead, in the same way as the property value is passed over the COM boundary.

Specifying multiple arguments

The final step is to support multiple arguments, whether this be for calling methods or for dealing with indexed properties. This is the step that I've been unable to find any examples for in C#.

The problem is that there need to be multiple variant arguments passed to the Invoke call but no built-in way to allocate an array of variants to unmanaged memory. This Stack Overflow question on IntPtr arithmetics looked promising but didn't quite cover it. And it revealed that I didn't know very much about the unsafe and fixed keywords :(

The final code I've ended up with doesn't seem that complicated in and of itself, but I feel like I've gone through the wringer a bit trying to confirm that it's actually correct! The biggest question was how to go allocating a single variant

var rgvarg = Marshal.AllocCoTaskMem(SizeOfNativeVariant);
Marshal.GetNativeVariantForObject(arg, rgvarg);

// Do stuff..

VariantClear(rgvarg);
Marshal.FreeCoTaskMem(rgvarg);

to allocating multiple. I understood that the array of variants should be laid out sequentially in memory but the leap took me some time to get to

var rgvarg = Marshal.AllocCoTaskMem(SizeOfNativeVariant * args.Length);
var variantsToClear = new List<IntPtr>();
for (var index = 0; index < args.Length; index++)
{
  var arg = args[(args.Length - 1) - index]; // Explanation below..
  var pVariant = new IntPtr(
    rgvarg.ToInt64() + (SizeOfNativeVariant * index)
  );
  Marshal.GetNativeVariantForObject(arg, pVariant);
  variantsToClear.Add(pVariant);
}

// Do stuff..

foreach (var variantToClear in variantsToClear)
  VariantClear(variantToClear);
Marshal.FreeCoTaskMem(rgvarg);

Particularly the concerns about the pointer arithmetic which I wasn't sure C# would like, especially after trying to digest all of the Stack Overflow question. But another Add offset to IntPtr did give me some hope thought it led me get thrown by this MSDN page for the .Net 4 IntPtr.Add method, with its usage of unsafe and fixed!

public static void Main()
{
  int[] arr = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
  unsafe {
    fixed(int* parr = arr) {
      IntPtr ptr = new IntPtr(parr);   
      for (int ctr = 0; ctr < arr.Length; ctr++)
      {
        IntPtr newPtr = IntPtr.Add(ptr, ctr * sizeof(Int32));
        Console.Write("{0}   ", Marshal.ReadInt32(newPtr));
      }
    }      
  }
}

So the good news; pointer arithmetic would, dealt with properly, not end the world. Ok, good. And apparently it's safe to always manipulate them using the ToInt64 method

IntPtr ptr = new IntPtr(oldptr.ToInt64() + 2);

whether on a 32 or 64 bit machine. With overhead on 32 bit systems, but I'm not looking for ultimate performance here, I'm looking for functionality! (This last part is one of the answers on Stack Overflow: Add offset to IntPtr.

From what I've learnt about pinning and its effects on the garbage collector, the "fixed" call in the MSDN example is to lock the array in place while it's being iterated over. Since at each insertion into the unmanaged memory I've allocated I'm using Marshal.GetNativeVariantForObject then I don't need to worry about this as that method is copying the data and automatic pinning is holding the data in place while it does so. So I'm all good - I just need to keep track of the variants I've copied so they can be cleared when I'm done and keep tracking of the one area of unmanaged memory I allocated which will need freeing.

One more thing! And this took me a while to track down - I wasn't getting errors but I wasn't getting the results I was expecting. According to the MSDN IDispatch::Invoke method (Automation) page, arguments are stored in the DISPPARAMS structure in reverse order. Reverse order!! Why??! Ah, who cares, I'm over it.

So, without further ado, here's an Invoke method that wraps up all of the above code so that any variety of call - method, indexed-or-not property get, indexed-or-not property set - can be made with all of the complications hidden away. If you don't want it to try to cast the return value then specify "object" as the type param. Anything that has a void return type will return null. This throws the named-argument requirement for property-setting into the mix but should be easy enough to follow if you're fine with everything up til now. (Where an indexed property is set, the last value in the args array should be the value to set it to and the preceeding args elements be the property indices).

public static T Invoke<T>(object source, InvokeFlags invokeFlags, int dispId, params object[] args)
{
  if (source == null)
    throw new ArgumentNullException("source");
  if (!Enum.IsDefined(typeof(InvokeFlags), invokeFlags))
    throw new ArgumentOutOfRangeException("invokeFlags");
  if (args == null)
    throw new ArgumentNullException("args");

  var memoryAllocationsToFree = new List<IntPtr>();
  IntPtr rgdispidNamedArgs;
  int cNamedArgs;
  if (invokeFlags == InvokeFlags.DISPATCH_PROPERTYPUT)
  {
    // There must be at least one argument specified; only one if it is a non-indexed property and
    // multiple if there are index values as well as the value to set to
    if (args.Length < 1)
      throw new ArgumentException("At least one argument must be specified for DISPATCH_PROPERTYPUT");

    var pdPutID = Marshal.AllocCoTaskMem(sizeof(Int64));
    Marshal.WriteInt64(pdPutID, DISPID_PROPERTYPUT);
    memoryAllocationsToFree.Add(pdPutID);

    rgdispidNamedArgs = pdPutID;
    cNamedArgs = 1;
  }
  else
  {
    rgdispidNamedArgs = IntPtr.Zero;
    cNamedArgs = 0;
  }

  var variantsToClear = new List<IntPtr>();
  IntPtr rgvarg;
  if (args.Length == 0)
    rgvarg = IntPtr.Zero;
  else
  {
    // We need to allocate enough memory to store a variant for each argument (and then populate this
    // memory)
    rgvarg = Marshal.AllocCoTaskMem(SizeOfNativeVariant * args.Length);
    memoryAllocationsToFree.Add(rgvarg);
    for (var index = 0; index < args.Length; index++)
    {
      // Note: The "IDispatch::Invoke method (Automation)" page
      // (http://msdn.microsoft.com/en-us/library/windows/desktop/ms221479(v=vs.85).aspx) states that
      // "Arguments are stored in pDispParams->rgvarg in reverse order" so we'll reverse them here
      var arg = args[(args.Length - 1) - index];

      // According to http://stackoverflow.com/a/1866268 it seems like using ToInt64 here will be valid
      // for both 32 and 64 bit machines. While this may apparently not be the most performant approach,
      // it should do the job.
      // Don't think we have to worry about pinning any references when we do this manipulation here
      // since we are allocating the array in unmanaged memory and so the garbage collector won't be
      // moving anything around (and GetNativeVariantForObject copies the reference and automatic
      // pinning will prevent the GC from interfering while this is happening).
      var pVariant = new IntPtr(
        rgvarg.ToInt64() + (SizeOfNativeVariant * index)
      );
      Marshal.GetNativeVariantForObject(arg, pVariant);
      variantsToClear.Add(pVariant);
    }
  }

  var dispParams = new ComTypes.DISPPARAMS()
  {
    cArgs = args.Length,
    rgvarg = rgvarg,
    cNamedArgs = cNamedArgs,
    rgdispidNamedArgs = rgdispidNamedArgs
  };

  try
  {
    var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
    UInt32 pArgErr = 0;
    object varResult;
    var excepInfo = new ComTypes.EXCEPINFO();
    var hrRet = ((IDispatch)source).Invoke
    (
      dispId,
      ref IID_NULL,
      LOCALE_SYSTEM_DEFAULT,
      (ushort)invokeFlags,
      ref dispParams,
      out varResult,
      ref excepInfo,
      out pArgErr
    );
    if (hrRet != 0)
    {
      var message = "Failing attempting to invoke method with DispId " + dispId + ": ";
      if ((excepInfo.bstrDescription ?? "").Trim() == "")
        message += "Unspecified error";
      else
        message += excepInfo.bstrDescription;
      var errorType = GetErrorMessageForHResult(hrRet);
      if (errorType != CommonErrors.Unknown)
        message += " [" + errorType.ToString() + "]";
      throw new ArgumentException(message);
    }
    return (T)varResult;
  }
  finally
  {
    foreach (var variantToClear in variantsToClear)
      VariantClear(variantToClear);

    foreach (var memoryAllocationToFree in memoryAllocationsToFree)
      Marshal.FreeCoTaskMem(memoryAllocationToFree);
  }
}

public static int GetDispId(object source, string name)
{
  if (source == null)
    throw new ArgumentNullException("source");
  if (string.IsNullOrEmpty(name))
    throw new ArgumentNullException("Null/blank name specified");

  // This will be populated with a the DispId of the named member (if available)
  var rgDispId = new int[1] { 0 };
  var IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
  var hrRet = ((IDispatch)source).GetIDsOfNames
  (
    ref IID_NULL,
    new string[1] { name },
    1, // number of names to get ids for
    LOCALE_SYSTEM_DEFAULT,
    rgDispId
  );
  if (hrRet != 0)
  {
    var message = "Invalid member \"" + name + "\"";
    var errorType = GetErrorMessageForHResult(hrRet);
    if (errorType != CommonErrors.Unknown)
      message += " [" + errorType.ToString() + "]";
    throw new ArgumentException(message);
  }
  return rgDispId[0];
}

public enum InvokeFlags : ushort
{
  DISPATCH_METHOD = 1,
  DISPATCH_PROPERTYGET = 2,
  DISPATCH_PROPERTYPUT = 4
}

private static CommonErrors GetErrorMessageForHResult(int hrRet)
{
  if (Enum.IsDefined(typeof(CommonErrors), hrRet))
    return (CommonErrors)hrRet;

  return CommonErrors.Unknown;
}

public enum CommonErrors
{
  Unknown = 0,

  // A load of values from http://blogs.msdn.com/b/eldar/archive/2007/04/03/a-lot-of-hresult-codes.aspx
}

Included is a GetDispId method and an "InvokeFlags" enum to wrap up those values. If an error is encountered, it will try to look up the hresult value in an enum that I've trimmed out here but you can find the values at http://blogs.msdn.com/b/eldar/archive/2007/04/03/a-lot-of-hresult-codes.aspx.

A waste of my time?

It's looking like the environment restriction against using .Net 4 is going to go away (I think it was just me being a bit dense with configuration to be honest but I'm not quite convinced yet!) so I should be able to replace all of this code I was thinking of using with the "dynamic" keyword again.

But it's certainly been interesting getting to the bottom of this, and it's given me a greater appreciation of the "dynamic" implementation! Until now I was under the impression that it did much of what it does with fairly straight forward reflection and some sort of caching for performance. But after looking into this I've looked into it more and realised that it does a lot more, varying its integration method depending upon what it's talking to (like if it's a .Net object, a IDispatch-implementing reference, an Iron Python object and whatever else). I have a much greater respect for it now! :)

The COMInteraction Project

One thing it has got me thinking about, though, is the COMInteraction code I wrote. The current code uses reflection and IL generation to sort of force method and property calls onto COM objects, which worked great for the components I was targetting at the time (VBScript WSC components) but which failed when I tried to use it with a Classic ASP Server reference that got passed through the chain. It didn't like the possibly hacky approach I used at all. But it is happy with being called by the Invoke method above since it implements IDispatch. So I'm contemplating now whether I can extend the work to generate different IL depending upon the source type; leaving it using reflection where possible and alternately using IDispatch where reflection won't work but IDispatch may. Sort of like "dynamic" much on a more conservative scale :)

A little bit more about dynamic's magic

Now that I understand more about how IDispatch enables the implementing type to be queried it answers a question I've wondered about before: how can the debugger show properties and data for a dynamic reference that's pointing at a COM object? The GetTypeInfo and GetIDsOfNames of the IDispatch interface can expose this information.

There's some example code on this blog post (by the same guy who wrote some of the other posts I linked earlier): Obtain Type Information of IDispatch-Based COM Objects from Managed Code.. I've played with it a bit and it looks interesting, but I've not gone any further than his method querying code (he retrieves a list of methods but doesn't examine the arguments that the methods take, for example).

Posted at 20:54

Comments