使用C++编写了一个动态链接库,动态链接库中含有.h/.lib/.dll三个文件,但很多用户都使用C#来进行编程,这个时候需要提供C#可以调用的dll,C#调用dll的方法一般有两种,即
因此选择CLR(Common Language Runtime)对C++ dll再一次进行封装,生成可在C#/VB等语言中添加引用的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++=>附加包含目录中添加MYAPI.h所在路径
在属性=>链接器=>常规=>附加库目录中添加MYAPI.lib所在目录
在属性=>链接器=>输入=>附加依赖项中添加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");
}
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);
};
}
上述文件中需要注意的地方有以下几点:
#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,无法直接传递,因此需要在函数内部做相应转换,转换方式为:
如果是enum,则在函数内部定义一个MYAPI中的同名enum临时变量tmp,如果是传入数据的函数,则用static_cast进行类型转换,再将tmp传入MYAPI中的函数,如果是获取数据的函数,则从MYAPI中的数据传入tmp,再从tmp读取数据
如果是struct,则将struct中的值一一赋值
如果是读取数据的函数,则在内部先定义一个vector,从MYAPI中读取数据到vector,再将vector转换为C#中的list
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#中直接使用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();
}
}
}
和C#类似,vb也可以通过添加引用的方法来实现
经过上述步骤,便可以在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
在使用C#直接编写dll时可以选择Any CPU,但使用CLR进行封装,依赖于C++生成的dll,C++生成的时候是有平台选择的,同时CLR生成dll依赖于C++所生成的dll,因此可能会出现下面两种错误: