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
- 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();
for(int n = 0; n < ilProcessor.Body.Instructions.Count; n++)
{
var instruction = ilProcessor.Body.Instructions[n];
if (instruction.OpCode.Code == Code.Call && instruction.Operand is MethodReference)
{
MethodReference methodDescription = (MethodReference)instruction.Operand;
var attributes = GetAttributes(methodDescription.Resolve());
if (attributes.Contains(_calliAttributeName))
{
var callSite = new CallSite(methodDescription.ReturnType)
{
CallingConvention = MethodCallingConvention.StdCall
};
for (int j = 0; j < methodDescription.Parameters.Count; j++)
{
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);
}
callSite.Parameters.Add(p);
}
var calliInstruction = ilProcessor.Create(OpCodes.Calli, callSite);
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.
if (pdbfile == null)
{
pdbfile = Path.ChangeExtension(filename, "pdb");
}
bool generatePdb = File.Exists(pdbfile);
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 HRESULT STDMETHODCALLTYPE GetCount(
_Out_ UINT *pcDevices) = 0;
virtual HRESULT STDMETHODCALLTYPE Item(
_In_ UINT nDevice,
_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); }
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; 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 HRESULT STDMETHODCALLTYPE GetCount(
_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
{
.maxstack 4
.locals init ([0] native int& pinned pdeviceCount,
[1] int32 CS$1$0000)
.line 32,32 : 9,10 ''
IL_0000: nop
.line 33,33 : 20,53 ''
IL_0001: ldarg.1
IL_0002: stloc.0
.line 34,34 : 13,14 ''
IL_0003: nop
.line 35,35 : 17,33 ''
IL_0004: ldarg.1
IL_0005: ldc.i4.0
IL_0006: stind.i4
.line 36,36 : 17,82 ''
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 ''
IL_001f: ldloc.1
IL_0020: ret
}
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