Encapsulate C++ dll with CLR

Reasons for project proposal

A dynamic link library is written in C + +, which contains h/.lib/.dll three files, but many users use C # to program. At this time, it is necessary to provide a dll that C # can call. There are generally two methods for C # to call dll, namely

  1. dllimport is used to encapsulate functions, but according to the relevant tutorials provided on the Internet, this method is only applicable to DLLs written in pure C language. The functions of the C + + dynamic library I wrote are concentrated in one class, and the corresponding dll import method is not found
  2. Add DLL reference in VS. when adding DLL generated by C + +, this method will report "failed to add reference to xxx.dll, please ensure that this file is accessible and is a valid assembly or COM component".

Therefore, CLR(Common Language Runtime) is selected to encapsulate C++ dll again to generate dll that can be referenced in C#/VB and other languages.

Preparation of C++ dll

Generate the sample source code header file of C + +, myapi H as follows

class MYAPI
{
    public:
	static const int kMaxDataLen = 1024;
public:
	/**
	* @brief functional status 
	*/
	enum STATE {
		OFF,///< close
		ON,///< on
	};

	struct IPAddr {
		uint8_t c1;///< bit 1 of IP address
		uint8_t c2;///Bit 2 of IP address
		uint8_t c3;///Bit 3 of IP address
		uint8_t c4;///Bit 4 of IP address
	};
public:
	void setValue(double value);
	void getValue(double* value);
	void setState(STATE state);
	void getState(STATE* state);
	void setAddr(IPAddr addr);
	void getAddr(IPAddr* addr);
};

This class defines an enumerator and structure, and the function interface provided uses the corresponding enumerator and structure.

void MYAPI::setValue(double value)
{

}
void MYAPI::getValue(double* value)
{
}
void MYAPI::setState(STATE state)
{

}
void MYAPI::getState(STATE* state)
{
	
}
void MYAPI::setAddr(IPAddr addr)
{

}

void MYAPI::getAddr(IPAddr* addr)
{
	if (addr)
	{
		addr->c1 = 2;
		addr->c2 = 3;
		addr->c3 = 5;
		addr->c4 = 7;
	}
}

int MYAPI::getData(double *data, int max_length)
{
	if (data == nullptr)return 0;
	for (int i = 0; i < max_length; ++i)
	{
		data[i] = i;
	}
	return max_length;
}

The corresponding cpp implementation file is shown above. Since the file has been encapsulated when the DLL is regenerated, it has no impact on the encapsulation of CLR. If the user wants to use C + + DLL directly, the files provided include myapi h,MYAPI.lib,MYAPI.dll

How to use C++ dll

New project

  1. Add myapi in the attribute = > C + + = > additional include directory H path

  2. Add myapi in attribute = > linker = > General = > additional library directory Lib directory

  3. Add myapi in attribute = > linker = > input = > additional dependencies lib

Write code according to the header file

#include "MYAPI.h"

int main()
{
	MYAPI tmp;
	MYAPI::STATE state = MYAPI::OFF;
	tmp.getState(&state);
	printf("state: %d\n", state);
	MYAPI::IPAddr addr;
	tmp.getAddr(&addr);
	printf("addr: %d.%d.%d.%d\n", addr.c1, addr.c2, addr.c3, addr.c4);
	double value;
	tmp.getValue(&value);
	printf("value: %f\n", value);
	double data[10];
	memset(data, 0, sizeof(data));
	tmp.getData(data, 10);
	printf("data: ");
	for (int i = 0; i < 10; ++i)
	{
		printf("%f ", data[i]);
	}
	printf("\n");
	system("pause");
}

Preparation of CLR packaging source code

New project

  1. Create a new project in VS Visual C + + = > CLR = > CLR empty project
  2. Select dynamic library dll in attribute = > General = > Project default = > configuration type
  3. Add myapi in the attribute = > C + + = > additional include directory H path
  4. Add myapi in attribute = > linker = > General = > additional library directory Lib directory
  5. Add myapi in attribute = > linker = > input = > additional dependencies lib

New CLR class

MYAPI. The classes in H cannot be used by adding references, so create a new class MYAPINET. MYAPINET is located in namespace myclr and provides exactly the same functions as myapi. Its implementation is as follows:

#pragma once
#include <stdint.h>
class MYAPI;

namespace myclr
{
	public enum class STATE {
		OFF,///< close
		ON,///< on
	};

	public value struct IPAddr {
		uint8_t c1;///Bit IP address < 1
		uint8_t c2;///Bit 2 of IP address
		uint8_t c3;///Bit 3 of IP address
		uint8_t c4;///Bit 4 of IP address
	};


	public ref class MYAPINET
	{
	private:
		MYAPI*m_impl;
	public:
		MYAPINET();
		~MYAPINET();
		void setValue(double value);
		void getValue(double% value);
		void setState(STATE state);
		void getState(STATE% state);
		void setAddr(IPAddr addr);
		void getAddr(IPAddr% addr);
		int getData(System::Collections::Generic::List<double>^%data, int max_length);
		int setName(System::String^ name);

	};
}



The following points need to be noted in the above documents:

  1. The name of the class must be ref class, otherwise it cannot be accessed when adding a reference in C#
  2. Replace the name of enum with public enum class
    • If there is no class, enum internal members cannot access it
  3. Replace struct with public value struct
    • If there is no value, the internal members of the structure cannot be accessed
    • If it is ref struct, the function cannot be recognized
  4. Replace the pointer * with%. If the pointer type remains unchanged, it will cause unsafe code in C # after replacement with%. When adding a reference in C # after replacement with%, the parameter will be passed in the form of ref
  5. The original function of getData is to pass in the first address of an array and the length of the array, and then write data to the incoming address. C# the data structure corresponding to the array is List
  6. Putting enum and struct in the namespace is mainly to avoid adding a class prefix every time when accessing externally. It can also be placed in the class

Parameter passing in CLR class

#include "MYAPINET.h"
#include "MYAPI.h"
#include <vector>
#include <string>
namespace
{
	std::string SysStrToStdStr(System::String ^ s)
	{
		using namespace System;
		using namespace Runtime::InteropServices;
		const char* chars =
			(const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
		std::string str = chars;
		Marshal::FreeHGlobal(IntPtr((void*)chars));
		return str;
	}
}
namespace myclr
{
	MYAPINET::MYAPINET()
	{
		m_impl = new MYAPI;
	}  

	MYAPINET::~MYAPINET()
	{
		delete m_impl;
	}

	void MYAPINET::setValue(double value)
	{
		m_impl->setValue(value);
	}

	void MYAPINET::getValue(double% value)
	{
		double value_tmp;
		m_impl->getValue(&value_tmp);
		value = value_tmp;
	}

	void MYAPINET::setState(STATE state)
	{
		MYAPI::STATE state_tmp;
		state_tmp = static_cast<MYAPI::STATE>(state);
		m_impl->setState(state_tmp);
	}

	void MYAPINET::getState(STATE% state)
	{
		MYAPI::STATE state_tmp;
		m_impl->getState(&state_tmp);
		state = static_cast<STATE>(state_tmp);
	}

	void MYAPINET::setAddr(IPAddr addr)
	{
		MYAPI::IPAddr addr_tmp;
		addr_tmp.c1 = addr.c1;
		addr_tmp.c2 = addr.c2;
		addr_tmp.c3 = addr.c3;
		addr_tmp.c4 = addr.c4;
		m_impl->setAddr(addr_tmp);
	}

	void MYAPINET::getAddr(IPAddr% addr)
	{
		MYAPI::IPAddr addr_tmp;
		m_impl->getAddr(&addr_tmp);
		addr.c1 = addr_tmp.c1;
		addr.c2 = addr_tmp.c2;
		addr.c3 = addr_tmp.c3;
		addr.c4 = addr_tmp.c4;
	}

	int MYAPINET::getData(System::Collections::Generic::List<double>^%data, int max_length)
	{
		std::vector<double> data_tmp(max_length);
		int nread = m_impl->getData(data_tmp.data(), data_tmp.size());
		data->Clear();
		for (int i = 0; i < nread; ++i)
		{
			data->Add(data_tmp[i]);
		}
		return nread;
	}
	int MYAPINET::setName(System::String^ name)
	{
		std::string str = SysStrToStdStr(name);
		return m_impl->setName(str.data());
	}

}

If the parameter passed in from MYAPICLR class is in the same name structure or enumeration defined by MYAPI, it is actually enum class or value struct defined in myclr namespace and cannot be passed directly. Therefore, corresponding conversion needs to be made inside the function. The conversion method is:

  1. If it is enum, define a temporary variable tmp of enum with the same name in MYAPI inside the function. If it is a function of incoming data, use static_cast performs type conversion, and then passes tmp into the function in MYAPI. If it is a function to obtain data, it passes the data from MYAPI into tmp, and then reads the data from tmp

  2. If it is struct, the values in struct will be assigned one by one

  3. If it is a function of reading data, first define a vector internally, read the data from MYAPI to vector, and then convert the vector into a list in C#

  4. const char * is an incoming string type in C + +, which is replaced by System::String ^ in C#/VB. const char * can usually be converted to std::string. The conversion method from System::String ^ to std::string is as follows:

    std::string SysStrToStdStr(System::String ^ s)
    	{
    		using namespace System;
    		using namespace Runtime::InteropServices;
    		const char* chars =
    			(const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
    		std::string str = chars;
    		Marshal::FreeHGlobal(IntPtr((void*)chars));
    		return str;
    	}
    

Some simple code skills

If it is the assignment of enum, the following macro can be used for assignment, where x is the amount to be assigned and y is the amount of transmitted value, without handwritten data type

#define CAST_ASSIGN(x,y) x = static_cast<std::remove_reference_t<decltype(x)>>(y);

For variables with%, a suitable method has not been found to remove%, so the following template is used

	template <typename T1, typename T2>
	void simple_cast_assign(T1% dest, const T2&src)
	{
		dest = static_cast<T1>(src);
	}

If it is the assignment of struct, if two struct types are exactly the same and both are enumeration or general data types (int/char/short/double/float, etc.), memcpy can be used for memory copy

template <typename T1, typename T2>
void sameStructMemCopy(T1&dest, const T2&src)
{
void*p1 = (void*)&dest;
void*p2 = (void*)&src;
memcpy(p1, p2, sizeof(T1));
}

dll encapsulated with CLR in C#

New project

  1. Create a new project in VS Visual c# = > console application
  2. Select reference = > Add Reference in the project, and select the dll generated by CLR

dll use

After the above steps, you can directly use MYAPINET to write programs in C # and use C # to write the following code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using myclr;
namespace CSTest
{
    class Program
    {
        static void Main(string[] args)
        {
            MYAPINET tmp = new MYAPINET();
            STATE state = STATE.OFF;
            tmp.getState(ref state);
            Console.Write("state: {0}\n", state);
            IPAddr addr = new IPAddr();
            tmp.getAddr(ref addr);
            Console.Write("addr: {0}.{1}.{2}.{3}\n", addr.c1, addr.c2, addr.c3, addr.c4);
            double value = 0;
            tmp.getValue(ref value);
            Console.Write("value: {0}\n", value);
            List<double> data = new List<double>();
            tmp.getData(ref data, 10);
            Console.Write("data: ");
            for (int i = 0; i < 10; ++i)
            {
                Console.Write("{0} ", data[i]);
            }
            Console.Write("\n");
            Console.ReadKey();
        }
    }
}

dll written with CLR in VB

Like C #, vb can also be implemented by adding references

New project

  1. Create a new project in VS Visual Basic = > console application
  2. Select reference = > Add Reference in the project, and select the dll generated by CLR

dll use

After the above steps, you can directly use MYAPINET to write the program in C# and write the following code in VB

Imports myclr
Module Module1
    Sub Main()
        Dim tmp As MYAPINET = New MYAPINET()
        Dim state As STATE = STATE.OFF
        tmp.getState(state)
        Console.WriteLine("state: {0}", state)
        Dim addr As IPAddr = New IPAddr()
        tmp.getAddr(addr)
        Console.WriteLine("addr: {0}.{1}.{2}.{3}", addr.c1, addr.c2, addr.c3, addr.c4)
        Dim value As Double = 0
        tmp.getValue(value)
        Console.WriteLine("value: {0}", value)
        Dim data As List(Of Double) = New List(Of Double)()
        tmp.getData(data, 10)
        Console.Write("data: ")
        Dim i As Integer = 0
        For i = 0 To 9
            Console.Write("{0} ", data(i))
        Next
        Console.WriteLine()
        Console.ReadKey()
    End Sub
End Module

Possible problems in adding reference dll

Any CPU can be selected when c# directly writing dll, but CLR is used for encapsulation, which depends on the dll generated by C + +. When C + + is generated, there is a platform selection, and the dll generated by CLR depends on the dll generated by C + +. Therefore, the following two errors may occur:

  1. BadImageFormatException: this error is caused by the platform mismatch. For example, it refers to the dll of x64 platform, but the platform selected in the VB/C# project is x86, or the platform selects Any CPU, but the first 32 bits are checked in the property. Similarly, if the dll of X86 platform is referenced, but the platform selected in VB/C# project is x64, or the platform selects Any CPU, but the first 32 bits are not checked in the property, it will also lead to an error.
  2. FileNotFoundException. This error is caused by C++ dll not being copied to the path of VB/C # generated exe, because the final implementation of the function in CLR generated dll is in C++ dll, and the path of VB/C # is usually bin/Debug and bin/Release

Areas to be optimized

  1. When encapsulating dll in CLR, you should rewrite enum, struct and functions in C + +. If there are changes in C + +, the method of manually synchronizing code is too cumbersome. You can write the operation of automatically generating CLR code to reduce manual replication
  2. The dll generated by CLR also depends on the dll generated by C + +, so it is easy to make mistakes. If you want to generate only one dll, you can choose to turn the dynamic library generated by C + + into a static library, so you don't rely on the dll generated by C + +

Keywords: C++ Back-end

Added by Bind on Sun, 20 Feb 2022 03:32:25 +0200