使用CLR对C++ dll进行封装

项目提出原因

使用C++编写了一个动态链接库,动态链接库中含有.h/.lib/.dll三个文件,但很多用户都使用C#来进行编程,这个时候需要提供C#可以调用的dll,C#调用dll的方法一般有两种,即

  1. 使用dllimport进行封装函数,但根据网上提供的相关教程,这种方法只适用于纯C语言编写的dll,我所编写C++动态库,函数都集中在一个类里面,没有找到对应的dllimport方法
  2. 在VS中添加dll引用,这种方法在添加C++生成的dll时,会报“未能添加对xxx.dll的引用,请确保此文件可访问并且是一个有效的程序集或COM组件。

因此选择CLR(Common Language Runtime)对C++ dll再一次进行封装,生成可在C#/VB等语言中添加引用的dll。

C++ dll的编写

生成C++ 的样例源代码头文件MYAPI.h如下

class MYAPI
{
    public:
	static const int kMaxDataLen = 1024;
public:
	/**
	* @brief 功能状态
	*/
	enum STATE {
		OFF,///<关闭
		ON,///<开启
	};

	struct IPAddr {
		uint8_t c1;///

该类自定义了一个枚举量和结构体,同时再提供的函数接口使用了相应的枚举量和结构体。

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;
}

对应的cpp实现文件如上所示,由于再生成dll时,该文件已被封装,因此对CLR的封装无影响。假如用户要直接使用C++的dll,这时提供的文件包括MYAPI.h,MYAPI.lib,MYAPI.dll

C++ dll的使用方法

新建工程

  1. 在属性=>C++=>附加包含目录中添加MYAPI.h所在路径

  2. 在属性=>链接器=>常规=>附加库目录中添加MYAPI.lib所在目录

  3. 在属性=>链接器=>输入=>附加依赖项中添加MYAPI.lib

根据头文件编写代码

#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");
}

CLR封装源码的编写

新建工程

  1. 在VS中新建项目Visual C++ => CLR => CLR空项目
  2. 在属性=>常规=>项目默认值=>配置类型中选择动态库dll
  3. 在属性=>C++=>附加包含目录中添加MYAPI.h所在路径
  4. 在属性=>链接器=>常规=>附加库目录中添加MYAPI.lib所在目录
  5. 在属性=>链接器=>输入=>附加依赖项中添加MYAPI.lib

新建CLR类

MYAPI.h中的类无法通过添加引用的形式使用,因此新建一个类MYAPINET,MYAPINET位于namespace myclr中,同时提供与MYAPI完全相同的函数,其实现如下:

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

namespace myclr
{
	public enum class STATE {
		OFF,///<关闭
		ON,///<开启
	};

	public value struct IPAddr {
		uint8_t c1;///
		uint8_t c2;///IP地址第2位
		uint8_t c3;///IP地址第3位
		uint8_t c4;///IP地址第4位
	};


	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);

	};
}



上述文件中需要注意的地方有以下几点:

  1. 类的名称一定是ref class,否则在C#中添加引用时无法访问
  2. enum的名称替换成public enum class
    • 如果没有class,则enum内部成员无法访问
  3. struct替换成public value struct
    • 假如没有value,则结构体内部成员无法访问
    • 假如假如为ref struct,则函数无法识别
  4. 将指针*替换成%,如果保持指针类型不变,会在C#造成不安全的代码,替换成%后,在C#中添加引用时,参数将以ref的形式传递
  5. getData的原有作用时,传入一个数组首地址和数组的长度,然后往传入的地址写入数据,C#与数组对应的数据结构为List
  6. 将enum和struct放在命名空间中,主要是为了不在外部访问时每次都加一个类的前缀,也可放在类中

CLR类中参数的传递

#include "MYAPINET.h"
#include "MYAPI.h"
#include 
#include 
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());
	}

}

MYAPICLR类中传入的参数,如果是与MYAPI定义的同名结构体或枚举里,此时实际为myclr命名空间中定义的enum class或value struct,无法直接传递,因此需要在函数内部做相应转换,转换方式为:

  1. 如果是enum,则在函数内部定义一个MYAPI中的同名enum临时变量tmp,如果是传入数据的函数,则用static_cast进行类型转换,再将tmp传入MYAPI中的函数,如果是获取数据的函数,则从MYAPI中的数据传入tmp,再从tmp读取数据

  2. 如果是struct,则将struct中的值一一赋值

  3. 如果是读取数据的函数,则在内部先定义一个vector,从MYAPI中读取数据到vector,再将vector转换为C#中的list

  4. const char*为C++中传入字符串类型,在C#/VB中用System::String^来替代,const char*通常又可以转换为std::string,System::String^向std::string的转换方式为:

    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;
    	}
    

一些简单代码的技巧

如果是enum的赋值,可以使用如下宏进行赋值,其中x为待赋值的量,y为传递值的量,不用手写数据类型

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

对于带%的变量,还没有找到一个合适的方法将%去掉,因此使用如下模板

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

如果是struct的赋值,如果两个struct类型完全一样,且都是枚举或通用数据类型(int/char/short/double/float等),则完全可以用memcpy进行内存拷贝

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));
}

在C#中使用CLR封装的dll

新建工程

  1. 在VS中新建项目Visual C# => 控制台应用程序
  2. 在项目中选择引用=>添加引用,选择CLR所生成的dll

dll使用

经过上述步骤,便可以在C#中直接使用MYAPINET编写程序,使用C#编写如下代码

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();
        }
    }
}

在VB中使用CLR编写的dll

和C#类似,vb也可以通过添加引用的方法来实现

新建工程

  1. 在VS中新建项目Visual Basic=> 控制台应用程序
  2. 在项目中选择引用=>添加引用,选择CLR所生成的dll

dll使用

经过上述步骤,便可以在C#中直接使用MYAPINET编写程序,使用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

添加引用dll可能遇到的问题

在使用C#直接编写dll时可以选择Any CPU,但使用CLR进行封装,依赖于C++生成的dll,C++生成的时候是有平台选择的,同时CLR生成dll依赖于C++所生成的dll,因此可能会出现下面两种错误:

  1. BadImageFormatException,这个错误是平台对应不上导致的,例如,引用的是x64平台的dll,但在VB/C#项目中选择的平台是x86或者是平台选择Any CPU,但在属性中勾选了首先32位。同理,如果引用的是x86平台的dll,但在VB/C#项目中选择的平台是x64或者是平台选择Any CPU,但在属性中未勾选首先32位,也会导致错误。
  2. FileNotFoundException,这个错误是C++ dll未拷贝到VB/C#生成的exe所在路径导致的,因为CLR生成dll中函数的最终实现是在C++ dll中的,VB/C#的路径通常为bin/Debug和bin/Release

有待优化的地方

  1. CLR中封装dll时,要将C++中的enum、struct以及函数全都重写一遍,如果C++中发生了更改,手动同步代码的方法过于繁琐,可以编写自动生成CLR代码的操作,减少人工复制
  2. CLR生成的dll还要依赖于C++ 生成的dll,因此容易出错,如果想仅仅生成一个dll,可以选择将C++生成动态库变成生成静态库,这样就不依赖C++ 的dll

你可能感兴趣的:(C++,c++,开发语言,后端)