Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET COM Interop using Postbuild

0.00/5 (No votes)
30 Sep 2013 1  
COM Interop using a little post compiler. As a result, it is possible to use multithreading in combination with COM interfaces.

Contents 

  • Introduction  
  • "post compiler" 
  • Using the Code   
    • Baseclass for all COM Objects
    • Custom Attributes for the "post compiler" 
    • Use the "post compiler" 
    • Example (CoreAudio-API import)
  •  Running the sample
    • Debugging
  • Points of Interest
  • History
  • License  

Introduction

I wrote a DirectSound-wrapper for my audio library CSCore some time ago. To access the DirectSound-Cominterfaces, I had to import them. I used the traditional way to import the COM interfaces, using interfaces in combination with GuidAttribute, ComImportAttribute, and InterfaceTypeAttribute. When I was done with importing the interfaces, I recognized that I could not use the interfaces in combination with multiple threads. I tried nearly everything but it kept returning E_NOINTERFACE as errorcode.

I searched for an alternative way to access COM interfaces in C#. Alexandre Mutel told me about the Calli-instruction. That instruction uses a pointer to a method entry point and calls the method directly through the pointer. You can read more about it here.

Since the Calli-instruction is not part of C#, I had to place it manually in the assembly. To do that, I had to extend the build process by executing a little “post compiler” in the post build.

The “post compiler” has to search for calls to dummy methods which are marked with a custom attribute. If such a method-call is found, it gets replaced by a calli-instruction (including the parameters of the method-call). To find the mentioned call, the post compiler has to process every type, every method, etc.

Writing a “post compiler” to Access the Calli-instruction

The post compiler is nothing other than a simple console application. To edit the assembly, you can use many different libraries. I am using Mono.Cecil, which is available through nuget.

The class AssemblyPatcher will process every method in the assembly (including property getters and setters) with this method:

private void ProcessMethod(MethodDefinition method)
{
    if (method.HasBody)
    {
        ILProcessor ilProcessor = method.Body.GetILProcessor();
 
        //process every instruction of the methods body
        for(int n = 0; n < ilProcessor.Body.Instructions.Count; n++)
        //foreach won't work because iterator length is bigger than count??
        {
            var instruction = ilProcessor.Body.Instructions[n];
 
            //check whether the instruction is a call to a method
            if (instruction.OpCode.Code == Code.Call && instruction.Operand is MethodReference)
            {
                //get the called method
                MethodReference methodDescription = (MethodReference)instruction.Operand;
                //get the attributes of the called method
                var attributes = GetAttributes(methodDescription.Resolve());
 
                //check whether the called method is marked with the given calli attribute
                if (attributes.Contains(_calliAttributeName))
                {
                    //create a callsite for the calli instruction using stdcall calling convention
                    var callSite = new CallSite(methodDescription.ReturnType)
                    {
                        CallingConvention = MethodCallingConvention.StdCall
                    };
 
                    //iterate through every parameter of the original method-call
                    for (int j = 0; j < methodDescription.Parameters.Count; j++)
                    //foreach won't work because iterator length is bigger than count??
                    {
                        var p = methodDescription.Parameters[j];
                        if (p.ParameterType.FullName == "System.Boolean")
                        {
                            MessageIntegra-tion.WriteWarning("Native bool has a size of 4 bytes." + 
                              " Use any type which as a size of 32bit instead of System.Boolean.",
                                methodDescription.FullName);
                        }
 
                        //append every parameter of the method-call 
                        //to the callsite of the calli instruction
                        callSite.Parameters.Add(p);
                    }
 
                    //create a calli-instruction including the just built callSite
                    var calliInstruction = ilProcessor.Create(OpCodes.Calli, callSite);
                    //replace the method-call by the calli-instruction
                    ilProcessor.Replace(instruction, calliInstruction);
 
                    _replacedCallsCount++;
                }
            }
        }
    }
}

Since the post compiler modifies the assembly, the PDB-file also has to be rewritten. You can pass the original pdb file as a parameter or the post compiler searches automatically for it changing the extension of the processed assembly to .pdb.

//check whether the pdbfile has been passed through application parameters
if (pdbfile == null)
{
    //if not use the default pdbfilepath by changing the extension of the assembly to .pdb
    pdbfile = Path.ChangeExtension(filename, "pdb");
}
 
//check whether the original pdb-file exists
bool generatePdb = File.Exists(pdbfile);
 
//if the original pdb-file exists -> prepare for rewriting the symbols file
wp.WriteSymbols = generatePdb;
rp.ReadSymbols = generatePdb;
if (rp.ReadSymbols)
{
    rp.SymbolReaderProvider = new PdbReaderProvider();
}

If any errors occur, the MessageIntegration class will print out the error in a format which is compatible with the Microsoft Build process. So the errors will be shown in the Visual Studio error window:

public static void WriteError(string message, string location)
{
    Console.Error.WriteLine("{0}:error:[CSCli]{1}", location, message);
}
 
public static void WriteWarning(string message, string location)
{
    Console.Error.WriteLine("{0}:warning:[CSCli]{1}", location, message);
}

Using the Code

Creating a Base Class for all COM Objects

The ComObject base class stores the native COM pointer and provides access to the IUnknown-interface. It also provides access to the GetMethodPtr-method which calculates the method pointer based on the native COM pointer and the method index. To determine the index, we need the header file or an idl file. For example, we can take a look at the IMMDeviceCollection interface. It is part of the mmdeviceapi.h header file:

MIDL_INTERFACE("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")
IMMDeviceCollection : public IUnknown
{
public:
    virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE GetCount( 
        /* [annotation][out] */ 
        _Out_  UINT *pcDevices) = 0;
        
    virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Item( 
        /* [annotation][in] */ 
        _In_  UINT nDevice,
        /* [annotation][out] */ 
        _Out_  IMMDevice **ppDevice) = 0;
        
};

You can see that IMMDeviceCollection is derived from IUnknown. So if we want to know the index of the GetCount-method, we first have to determine how many methods the IUnknown-interface has. We will find out that IUnknown-interface has three methods. So the index of GetCount-method is 3. All in all, you have to count every method in every base class/interface and add it to the index of the method you want the pointer for. In the example above, the Item-method would have the index 4. The implementation of GetMethodPtr looks like this:

public unsafe void* GetMethodPtr(int methodIndex)
{
    return GetMethodPtr(methodIndex, 3); //default start index of 3
}
 
public unsafe void* GetMethodPtr(int methodIndex, int startIndex)
{
    return ((void**)(*(void**)_comPtr))[methodIndex + startIndex];
}

Creating the Calli- and the RemoveTypeAttribute

The “post compiler” takes usage of the attributes. CalliAttribute is used to identify dummy methods which have to be replaced by a calli-instruction. RemoveTypeAttribute is used to identify dummy classes, which contain dummy methods. All classes which are marked with RemoveTypeAttribute will be automatically removed after processing the assembly (including the Calli- and RemoveTypeAttribute).

[RemoveType]
[AttributeUsage(AttributeTargets.Method)]
public class CalliAttribute : Attribute
{
}

[RemoveType]
[AttributeUsage(AttributeTargets.Class)]
public class RemoveTypeAttribute : Attribute
{
}

Add the post compiler to the Post Build

To run the post compiler automatically, we have to create an entry in the post build event commandline. Open the properties of your application. Open the tab “Buildevents” and add the following commandline to the post build event:

call "$(SolutionDir)CSCli\$(OutDir)CSCli.exe" -file:"$(TargetPath)"

Now select a project in your Solution Explorer. On the project menu, choose Project Dependencies. Select the Dependencies tab and select your application from the drop-down menu. Now tick the CSCli project in the “depends on” field.

Using the post compiler to Import Parts of the CoreAudio-API

To import a new COM-interface, create a new class which derives from the previously created ComObject class and the GUID attribute. You can find the GUID of a class by taking a look at the interface definition (see the previous example).

[Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")]
public class MMDeviceCollection : ComObject
{
    public MMDeviceCollection(IntPtr comPtr)
        : base(comPtr)
    {
    }
}

Now we have to add the methods of the interface. The first method is GetCount with the index 3 (because of IUnknown):

public unsafe int GetCountNative(out int deviceCount)
{
    fixed (void* pdeviceCount = &deviceCount)
    {
        deviceCount = 0; //to avoid compiler errors
        return InteropCalls.CallI(ComPtr, pdeviceCount, GetMethodPtr(0));
    }
}

Now let’s take a look at the implementation: First of all, we need to take a look at the definition in the header file:

virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE GetCount( 
    /* [annotation][out] */ 
    _Out_  UINT *pcDevices) = 0;

We can see that the GetCount-method has one parameter of the type UINT*. We can also see that it will receive its value after calling the GetCount-method (see [out] or _Out_). So we have a new parameter of the type int (or uint) and the out keyword to our GetCountNative-method.

Since we need a pointer to the UINT value, we can use the fixed keyword to pin the deviceCount-parameter in memory and receive a pointer to its memory location. After that, we need to create a dummy class called InteropCalls (you can choose your own name). That dummy class will contain all dummy calls which will be replaced by the post compiler. Don’t worry. After compiling and running the post compiler, the dummy class will be removed. After creating the dummy class InteropCalls (you just need to do that once), we can call our dummy method with the desired parameters. First of all, you always have to pass the Com-interface as the first parameter and the method-ptr as the last parameter. The first parameter you can always get through the ComPtr property of the ComObject base class. The last parameter you can always get through the GetMethodPtr method. Note that you can pass the index 0 instead of 3, because the first overloading of GetMethodPtr, adds the startIndex 3 automatically. Between the first and the last parameter, you have to place the parameters of the called com method. Bringing all together, the code should look like the example code above.

Because there is still a compiler error, which tells us, that there is no method called CallI defined, we can choose generate and Visual Studio will generate the method automatically.

Now choose navigate to the generated method CallI and add the CSCalli-Attribute to it:

[CSCalli]
internal static unsafe int CallI(void* ComPtr, void* ppc, void* p)
{
    throw new NotImplementedException();
}

That’s it. Now you can compile your project and we can take a look at the result:

.method public hidebysig instance int32 
          GetCountNative([out] int32& deviceCount) cil managed
{
    // Code size       33 (0x21)
    .maxstack  4
    .locals init ([0] native int& pinned pdeviceCount,
             [1] int32 CS$1$0000)
    .line 32,32 : 9,10 ''
//000030: 
//000031:         public unsafe int GetCountNative(out int deviceCount)
//000032:         {
    IL_0000:  nop
    .line 33,33 : 20,53 ''
//000033:             fixed (void* pdeviceCount = &deviceCount)
    IL_0001:  ldarg.1
    IL_0002:  stloc.0
    .line 34,34 : 13,14 ''
//000034:             {
    IL_0003:  nop
    .line 35,35 : 17,33 ''
//000035:                 deviceCount = 0; //to avoid compiler errors
    IL_0004:  ldarg.1
    IL_0005:  ldc.i4.0
    IL_0006:  stind.i4
    .line 36,36 : 17,82 ''
//000036:                 return InteropCalls.CallI(ComPtr, pdeviceCount, GetMethodPtr(0));
    IL_0007:  ldarg.0
    IL_0008:  call       instance void* System.Runtime.InteropServices.ComObject::get_ComPtr()
    IL_000d:  ldloc.0
    IL_000e:  conv.i
    IL_000f:  ldarg.0
    IL_0010:  ldc.i4.0
    IL_0011:  call       instance void* System.Runtime.InteropServices.ComObject::GetMethodPtr(int32)
    IL_0016:  calli      unmanaged stdcall int32(void*,void*)				
    IL_001b:  stloc.1
    IL_001c:  leave.s    IL_001e

    IL_001e:  nop
    .line 38,38 : 9,10 ''
//000037:             }
//000038:         }
    IL_001f:  ldloc.1
    IL_0020:  ret
} // end of method MMDeviceCollection::GetCountNative

As you can see, the "post compiler" replaced the call to the InteropCalls.CallI-method with the calli-instruction (see IL_0016). Now let’s try it out and run the sample application.

ComInteropTest Sample

For a sample project, you can see the ComInteropTest project. It provides access to a few interfaces of the CoreAudio API. Run the sample and you’ll see all available audio devices:

Debugging

Since the post compiler rewrites the symbols file, you can still debug the whole application.

Since the dummy calls were removed during the build process, you can’t step into them. They were replaced by the calli-instruction which means that you now call the COM method instead of the dummy method.

Points of Interest

If you import any com interfaces, always remind yourself, that you have to do all marshalling yourself. That means that you have to pass exactly the same values, as the C function expects. For example, you cannot replace the unmanaged type BOOL with the managed type System.Boolean. BOOL = 1byte, System.Boolean = 4 Byte.

A sample implementation of a com compatible bool type would be something like this:

[Serializable]
[StructLayout(LayoutKind.Sequential, Size = 4)]
public struct NativeBool : IEquatable<nativebool>
{
	public static readonly NativeBool True = new NativeBool(true);
	public static readonly NativeBool False = new NativeBool(false);

	private int _value;

	public NativeBool(bool value)
	{
		_value = value ? 1 : 0;
	}

	public bool Equals(NativeBool other)
	{
		return this._value == other._value;
	}

	public override bool Equals(object obj)
	{
		if (obj is NativeBool || obj is Boolean)
			return Equals((NativeBool)obj);

		return false;
	}

	public override int GetHashCode()
	{
		return _value.GetHashCode();
	}

	public static bool operator ==(NativeBool left, NativeBool right)
	{
		return left.Equals(right);
	}

	public static bool operator !=(NativeBool left, NativeBool right)
	{
		return !(left == right);
	}

	public static implicit operator bool(NativeBool value)
	{
		return value._value != 0;
	}

	public static implicit operator NativeBool(bool value)
	{
		return new NativeBool(value);
	}

	public override string ToString()
	{
		return this ? "True" : "False";
	}
}

History

  • 07.09.2013 - Added NativeBool-code
  • 06.09.2013 - Fixed spelling mistakes
  • 05.09.2013 - Added history
  • 02.09.2013 - Added points of interest
  • 27.08.2013 - Added tags
  • 27.08.2013 - Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here