Previous articles have provided a detailed explanation of the data marshaling type issues between C++ and C#, that is, between managed and unmanaged platforms.This article illustrates with data types and simple examples; other more complex business logic can be extended and promoted based on this foundation.There are generally two common methods to call C++ methods: one is to define a header file separately, implement it in cpp, and export as needed in the header file (Windows requires a .def file); the other is to encapsulate the call in a class, wrapping the methods in the member methods of the class, exporting the class, and exposing the method to create a class pointer. When calling back in C#, this class pointer is passed back, and the member methods are called using the class pointer.C# retains the unsafe mode, which supports mixed programming of C# and C++, achieving both development efficiency and software performance.Another mode is C++/CLI, which encapsulates C++ classes through CLI, allowing them to be called in C# in the form of using System;, making it feel similar to direct C# development.Debugging Considerations:
- When debugging, you need to enable the property settings of the C# project: “Debugging of native code”. For C++, set the debugging database format to: /Zi, and optimization settings to: “No”;
- The architecture of the C++ project (x64) must be consistent with the C# test project;
- The architecture (x64) and .NET version of the CLI project must be consistent with the C# test project.
The overall project directory is as follows:
- FunctionLib is the library for defining and implementing native C++ methods and C++ class methods;
- CppClass is the C++/CLI wrapper for the native C++ class library;
- ConsoleApp is the C# project that executes the library calls.
1. Marshal and CallbackThis section includes common basic data types, delegate callbacks, data and pointer operations, etc. Some definitions are as follows:The C++ structure is defined as:
#pragma region Data Structure Definition#pragma pack(1)typedef struct _SimpleStruct {int Osversion;int Subversion;int Mainversion;double Height;double Points[128][2] = {0};int Index[20] = {0};char Szversion[128] = {0};} SimpleStruct, * PSimpleStruct;#pragma pack()#pragma endregion
This structure includes the definitions of basic data types, arrays (two-dimensional), and strings corresponding to C++. The structure definition in C# is as follows:
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)] private struct Versions { public int Osversion; public int Subversion; public int Mainversion; public double Height; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 128 * 2)] public double[] Points; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] public int[] Index; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string Szversion; public override string ToString() { return string.Format("Osversion:{0}\nMainversion:{1}\n" + "Subversion:{2}\nHeight:{3}\nSzversion:{4}", Osversion, Mainversion, Subversion, Height, Szversion); } }
C++ callback definition:
// instead of std::functional<>typedef int(__stdcall* DelegateFunc)(int*, int);
C# delegate definition (the less flexible part is that the array size must be fixed because C++ can only allocate constant sizes, so it cannot be dynamic, and C# also needs to specify the array size):
public delegate int TestDelegate2Cpp([MarshalAs(UnmanagedType.LPArray, SizeConst = 10)] int[] data, int n);
Example running code snippets are as follows:
#region general marshal double sum = FunctionLib.Add(3.2, 2.4); Console.WriteLine(sum); double c = 0; FunctionLib.Minus(3.2, 1.9, out c); Console.WriteLine(c); double[] angles = new double[10] { 355, 322, 0, 2, 368, 359.8, 355.4, 355.1, 348.9, 350 }; double[] data = new double[10]; FunctionLib.GetArray(angles, angles.Length, data); #endregion #region delegate marshal TestDelegate2Cpp del = new TestDelegate2Cpp((int[] dataIn, int n) => { int ret = 0; for (int i = 0; i < n; i++) { ret += dataIn[i]; } return ret; }); int resultII = 0; FunctionLib.DelegateCall(del, ref resultII); Console.WriteLine(resultII); #endregion #region struct array marshal int size = Marshal.SizeOf(typeof(Class)) * 50; byte[] bytes = new byte[size]; IntPtr pbuff = Marshal.AllocHGlobal(size); Class[] pclass = new Class[50]; FunctionLib.GetClass(pbuff); for (int i = 0; i < 50; i++) { IntPtr ppointer = new IntPtr(pbuff.ToInt64() + Marshal.SizeOf(typeof(Class)) * i); pclass[i] = (Class)Marshal.PtrToStructure(ppointer, typeof(Class)); } Marshal.FreeHGlobal(pbuff); Versions version = new Versions(); version.Mainversion = 10; version.Subversion = 1; version.Szversion = "hello world..."; version.Height = 1.67; version.Osversion = 0; GetVersion(ref version); Console.WriteLine("\nbefore get version :\n" + version); size = Marshal.SizeOf(typeof(Versions)); IntPtr pversion = Marshal.AllocHGlobal(size); //Marshal.WriteInt32(pversion, size); Marshal.StructureToPtr(version, pversion, false); FunctionLib.GetVersion(pversion); version.Osversion = Marshal.ReadInt32(pversion, 0); version.Subversion = Marshal.ReadInt32(pversion, 4); version.Mainversion = Marshal.ReadInt32(pversion, 8); version.Height = BitConverter.Int64BitsToDouble(Marshal.ReadInt64(pversion, 12)); version.Points = new double[128 * 2]; Marshal.Copy(pversion + 20, version.Points, 0, 128 * 2); for (int i = 0; i < 128 * 2; i++) { version.Points[i] = BitConverter.Int64BitsToDouble(Marshal.ReadInt64(pversion, 20 + i * 8)); } version.Index = new int[20]; Marshal.Copy(pversion + 20 + 128 * 2 * sizeof(double), version.Index, 0, 20); byte[]szver = new byte[128]; Marshal.Copy(pversion + 20 + 128 * 2 * sizeof(double) + 20 * sizeof(int), szver, 0, 128); version.Szversion = System.Text.Encoding.ASCII.GetString(szver).TrimEnd('\0'); //version.Szversion = Marshal.PtrToStringAnsi((IntPtr)(pversion.ToInt64() + 20)); Marshal.FreeHGlobal(pversion); Console.WriteLine("\nafter get version :\n" + version); IntPtr ptr = FunctionLib.GetStruct(); version = (Versions)Marshal.PtrToStructure(ptr, typeof(Versions)); Marshal.FreeCoTaskMem(ptr); // Non-managed memory allocation method uses CoTaskMemAlloc() so it can directly use Marshal's static method for memory release //FunctionLib.FreeStruct(ptr); Console.WriteLine("\nNon-managed code passed out structure"); Console.WriteLine(version); #endregion #region c# use c++/cli class MShape shape = new MShape(); int agecpp = shape.GetAge(); int volumncpp = shape.GetVolumn(); string result = shape.ReName("lucy"); // call destructor shape.Dispose(); #endregion #region c# use c++ class with IntPtr CSUnmanageWrap cs = new CSUnmanageWrap(); int agec = cs.GetAge(); int volumnc = cs.GetVolumn(); string resultc = cs.ReName("lucy"); cs.Dispose(); #endregion
2. C++/CLIThe C++/CLI class is defined as follows (CppClass.h):
#pragma once
#include "Shape.h"
using namespace System;using namespace System::Runtime::InteropServices;using namespace System::Collections::Generic;using namespace System::Collections;
#pragma managednamespace TestCppClass {public ref class MShape {public:MShape(); ~MShape();public:int GetAge();int GetVolumn(); String^ ReName(String^ oldName);private: Shape* sp = nullptr;};}
Shape.h is the wrapped native C++ class in the FunctionLib library, the code is as follows:
#pragma once#include <iostream>using namespace std;
#ifdef FUNCTIONLIB_EXPORTS#define CPPDLL_EXPORTS __declspec(dllexport)#else#define CPPDLL_EXPORTS __declspec(dllimport)#endif // CPPDLL_EXPORTS
extern "C" class CPPDLL_EXPORTS Shape {public:Shape(); ~Shape();public:int GetAge();int GetVolumn();char* ReName(char* oldName);private:int age;int height;int width;};
The wrapper class part of the code is as follows:
#region Members private IntPtr _pNativeObject; // Variable to hold the C++ class's this pointer #endregion Members
public CSUnmanageWrap() { // We have to Create an instance of this class through an exported function this._pNativeObject = CreateClass(); }
public void Dispose() { Dispose(true); }
protected virtual void Dispose(bool bDisposing) { if (this._pNativeObject != IntPtr.Zero) { // Call the DLL Export to dispose this class DisposeClass(this._pNativeObject); this._pNativeObject = IntPtr.Zero; }
if (bDisposing) { // No need to call the finalizer since we've now cleaned // up the unmanaged memory GC.SuppressFinalize(this); }
}
// This finalizer is called when Garbage collection occurs, but only if // the IDisposable.Dispose method wasn't already called. ~CSUnmanageWrap() { Dispose(false); }
#region Wrapper methods public int GetAge() { int age = CallGetAge(this._pNativeObject); return age; }
public int GetVolumn() { int volumn = CallGetVolumn(this._pNativeObject); return volumn; }
public string ReName(string oldName) { string result = CallReName(this._pNativeObject, oldName); return result; } #endregion Wrapper methods
The above are various example code snippets and experiences of C++/C# interoperability. The overall project code is quite extensive, so it will not be uploaded in full. Friends in need can contact me to obtain it.If you are interested in the above content, feel free to leave a message, like, and follow me.