About
In this code snippet, we will take a look at COM objects, interop using PInvoke and type marshalling in C#.
The Component Object Model or COM defines a binary interoperability standard that enables different applications to interoperate. This means one application can call the functions of another application or send/receive data from it through a COM object. Because COMs are binary defined standards even apps written in different languages can interact.
Note: Here is a very interesting video regarding the security of COM.
Platform invoke or shortly Pinvoke also enables C# interoperability with other software running native unmanaged code by calling exported(we’ll see what this means in the code example later) functions in its DLLs. Here is a real world example, a C# wrapper using pinvoke to call the wkhtmltopdf library.
Finally, this brings us to type marshalling which is the process of transforming or mapping data types from C# to native code and vice versa.
Excel Interop Example:
This is a practical example of using the COM to interact with Excel from C# and generate an .xlsx file. You must have Excel installed on your PC for this to work as we won’t just be using a library that can create/edit Excel files but are actually going to be calling the Excel app from C# via the COM interface. You can find out more about using COM objects here.
Right click on your project and select “Manage NuGet packages”.
Search for the “Microsoft.Office.Interop.Excel” NuGet package and install it.
Here is a very basic example of how interoperability is used to make a call from C# to the Excel application installed on your PC.
using System.Runtime.InteropServices; using Excel = Microsoft.Office.Interop.Excel; namespace ExcelInterop { internal class Program { static void Main(string[] args) { Excel.Application xlApp = null; Excel.Workbook xlWorkbook = null; try { string path = "C:\\Users\\DTPC\\Desktop\\Excel Test\\Test.xlsx"; //Excel "initialization". xlApp = new Excel.Application(); xlWorkbook = xlApp.Workbooks.Open(path); //Iterate through all worksheets and add the following text. foreach (Excel.Worksheet xlWorksheet in xlWorkbook.Sheets) { xlWorksheet.Cells[1, 1] = "First Column"; xlWorksheet.Cells[1, 2] = "Second Column"; xlWorksheet.Cells[1, 3] = "..."; } //Save current file. xlWorkbook.Save(); //Save as new file. //xlWorkbook.SaveAs($"{path}New {fileName}); } catch (Exception ex) { Console.WriteLine($"Something went wrong: {ex.Message} Stacktrace: {ex.StackTrace}"); } finally { //Fully kill excel process from running in the background. xlWorkbook.Close(); xlApp.Quit(); //Release COM objects. Marshal.FinalReleaseComObject(xlWorkbook); Marshal.FinalReleaseComObject(xlApp); } } } }
PInvoke Example:
I will make two projects and put them in a single Visual Studio solution. I will first compile the C++ project into a DLL and then manually take the generated dll and put it into the C# project. You might want to set up the build process so that first the C++ dll gets built then copied into the C# project and finally, the C# project is built/run. As this is just a demo I won’t bother with that and if you are using a pre-existing dll you don’t have to worry about this anyway.
First lets create an empty C++ project.
Next, let’s add a C++ file.
Right click on the project, select settings and set the properties to the same values as in the image below.
Now open the file we added before and add the following code:
//2. And then lets add this line of code. //extern "C" prevents name mangling(https://en.wikipedia.org/wiki/Name_mangling). //__declspec(dllexport) marks the function for DLL export. //int AdditionInterOpFunction() specifies our function. extern "C" __declspec(dllexport) int AdditionInterOpFunction(int a, int b); //1. Lets define a regular function. int AdditionInterOpFunction(int a, int b) { int c = a + b; return c; }
Right click on the project and select build. This will create the DLL which will get placed in the solution directory > c++ project directory > .dll
Now let’s create a C# console app.
You can either manually copy the C++ native code dll we generated before into the debug/release directory(project folder > bin > Debug > net7.0) of the C# project.
Or you can copy the dll into the root directory of your C# project, select the dll in the solution explorer and in the properties set the “Copy to Output Directory” to “Copy if newer”. If you check your C# project properties file you will see a new config was added to copy the file. Now the file will be copied over to the debug/release directory every time the project is compiled.
Now let’s add the C# interop code.
using System.Runtime.InteropServices; /////////////////////////// Main /////////////////////////////// Console.WriteLine("Making interop call."); int interOpResult = MathLib.Add(15, 10); Console.WriteLine("Interop result: " + interOpResult); //25 Console.ReadLine(); //////////////////////////////////////////////////////////////// static class MathLib { //Public endpoint for the user. public static int Add(int a, int b) { //Call the "hidden" method that makes the interop call to the c++ dll. return LibraryAdapter.AdditionInterOpFunction(a, b); } //Let's hide the interop stuff from the end user. private static class LibraryAdapter { //The DllImport attribute specifies where we can find AdditionInterOpFunction. [DllImport("CppLib.dll")] public static extern int AdditionInterOpFunction(int a, int b); } }
Run your C# project and you should be able to call the C++ dll and get back a result like so:
Native Memory And Type Marshalling:
In this example, we’ll modify the previous code to use a struct and we’ll see how to marshal it.
C++ Code:
////////////////// Includes ////////////////// #include<string> #include<comdef.h> ////////////////////////////////////////////// /////////////////// Models /////////////////// struct Operation { BSTR Description; int OperandA; int OperandB; double Result; }; ////////////////////////////////////////////// ///////////////// Functions ////////////////// extern "C" __declspec(dllexport) int AdditionInterOpFunction(Operation operation); int AdditionInterOpFunction(Operation operation) { return operation.OperandA + operation.OperandB; } //////////////////////////////////////////////
C# Code:
using System.Runtime.InteropServices; ///////////////////// Main ////////////////////// Console.WriteLine("Making interop call."); MathLib.Operation operation1 = new MathLib.Operation() { Description = "addition", OperandA = 1, OperandB = 5, }; int result = MathLib.Add(operation1); Console.WriteLine("Interop result: " + result); //6 Console.ReadLine(); ///////////////////////////////////////////////// static class MathLib { //Public endpoint for the user. public static int Add(MathLib.Operation operation) { //Call the "hidden" method that makes the interop call to the c++ dll. return LibraryAdapter.AdditionInterOpFunction(operation); } [StructLayout(LayoutKind.Sequential)] public struct Operation { [MarshalAs(UnmanagedType.BStr)] public string Description; public int OperandA; public int OperandB; } //Let's hide the interop stuff from the end user. private static class LibraryAdapter { //The DllImport attribute specifies where we can find AdditionInterOpFunction. [DllImport("CppLib.dll")] public static extern int AdditionInterOpFunction(MathLib.Operation operation); } }
Working With Native Memory
In this next example, we’ll see how to allocate and work with native memory in C# by using the Marshal class. This knowledge will come in useful in the final example.
using System; using System.Runtime.InteropServices; ///////////////////////////////////////////////////////////////////////////// //Let's get 3 bytes of native memory. nint nativeMemoryPtr = Marshal.AllocHGlobal(3); //Write to the native memory. Marshal.WriteByte(nativeMemoryPtr, 0, 5); //(pointer to begining of memory block, offset, value) Marshal.WriteByte(nativeMemoryPtr, 1, 15); Marshal.WriteByte(nativeMemoryPtr, 2, 78); //You can copy native memory it into an array like so: byte[] fromNativeMemory = new byte[3]; Marshal.Copy(nativeMemoryPtr, fromNativeMemory, 0, 3); //... or you can just stright read data from it like so: for (int i = 0; i < 3; i++) { byte readByte = Marshal.ReadByte(nativeMemoryPtr, i); //(pointer to begining of memory block, offset) Console.WriteLine(readByte.ToString()); } //Clear previously allocated native memory. Marshal.FreeHGlobal(nativeMemoryPtr); ///////////////////////////////////////////////////////////////////////////// Console.WriteLine(""); ///////////////////////////////////////////////////////////////////////////// //Let's get 20 bytes of native memory. nint nativeMemory = Marshal.AllocHGlobal(5); Span<byte> nativeMemorySpan; unsafe { nativeMemorySpan = new Span<byte>(nativeMemory.ToPointer(), 5); } //Write some random numbers. Random random = new Random(); Console.WriteLine(sizeof(byte)); for (int ctr = 0; ctr < nativeMemorySpan.Length; ctr++) nativeMemorySpan[ctr] = (byte)random.Next(0, 255); //a byte can store 256 values from 0 to 255. //Read. foreach (var item in nativeMemorySpan) Console.WriteLine(item.ToString()); //Clear previously allocated native memory. Marshal.FreeHGlobal(nativeMemory); /////////////////////////////////////////////////////////////////////////////
Output:
This final example demonstrates how to allocate memory in C# and send the pointer to that chunk of memory to the C++ native code where it gets used(written/read from).
When the control is returned to C# we can use the pointer to read back the values from that chunk of memory.
C++ Code:
extern "C" __declspec(dllexport) void AdditionInterOpFunction(void* memPtr); void AdditionInterOpFunction(void* memPtr) { char* operandA_intPtr = static_cast<char*>(memPtr); char* operandB_intPtr = static_cast<char*>(operandA_intPtr + 1); char result = *operandA_intPtr + *operandB_intPtr; *reinterpret_cast<char*>(static_cast<char*>(memPtr) + 2) = result; }
C# Code:
///////////////////// Main ////////////////////// Console.WriteLine("Making interop call."); //Allocate 3 bytes of native memory. nint nativeMemoryPtr = Marshal.AllocHGlobal(3); //Save the operands into a memory chunk. //(pointer to beginning of memory block, offset, value) Marshal.WriteByte(nativeMemoryPtr, 0, 3); //1. byte Marshal.WriteByte(nativeMemoryPtr, 1, 2); //2. byte //Make interop call. MathLib.Add(nativeMemoryPtr); //Read the result that the native code saved. byte readByte = Marshal.ReadByte(nativeMemoryPtr, 2); //3. byte Console.WriteLine("Interop result: " + readByte); //5 Console.ReadLine(); //Free memory. Marshal.FreeHGlobal(nativeMemoryPtr); ///////////////////////////////////////////////// static class MathLib { public static void Add(IntPtr ptr) { LibraryAdapter.AdditionInterOpFunction(ptr); } private static class LibraryAdapter { [DllImport("CppLib.dll")] public static extern void AdditionInterOpFunction(IntPtr ptr); } } /////////////////////////////////////////////////////////////////////////////
Output: