项目建议理由
我用C++写了一个动态链接库。 动态链接库包含三个文件:.h/.lib/.dll。 然而,许多用户使用 C# 进行编程。 这时候他们就需要提供一个C#可以调用的dll。 C# 调用 dll。 一般有两种方法,即
用来封装函数,但是根据网上提供的相关教程,这种方法只适用于纯C语言编写的dll。 我写的C++动态库中,功能都集中在一个类中,在VS中并没有找到相应的方法来添加它。 dll 参考。 使用此方法添加C++生成的dll时,会报“无法添加对xxx.dll的引用。请确保此文件可访问并且是有效的程序集或COM组件”。
因此,选择CLR()对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创建新项目
在 =>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");
}
为CLR包源代码编写一个新项目。 在VS中创建一个新项目。 C++ => CLR => CLR 空项目。 在属性=>常规=>项目默认值=>配置类型中选择动态库dll。 在属性=>C++=>附加包含目录中选择动态库dll。 添加MYAPI.h所在路径。 在属性=>链接器=>常规=>附加库目录中添加MYAPI.lib所在目录。 在属性=>链接器=>输入=>附加依赖项中添加MYAPI.lib。 创建一个新的 CLR 类。
MYAPI.h 中的类无法通过添加引用来使用,因此需要在 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);
};
}
上述文件中需要注意以下几点:
类的名称必须是ref class,否则在C#中添加引用时无法访问枚举名称。 将枚举的名称替换为枚举类,并将其替换为值。 将指针 * 替换为 %。 如果指针类型保持不变,会导致C#中的代码不安全。 替换为%后,C#中添加引用时,以ref形式传递参数原来的作用是传入数组首地址和数组长度,然后向传入的地址写入数据。 C#对应数组。 数据结构为List和enum,放在命名空间中,主要是为了避免外部访问时每次都添加类前缀。 也可以放在类中来传递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());
}
}
如果类中传递的参数是 MYAPI 定义的同名结构体或枚举,则实际上是 myclr 命名空间中定义的枚举类或值。 不能直接传递,所以需要在函数内部做相应的转换。 方法是:
如果是enum,则在函数内部的MYAPI中定义一个同名的enum临时变量tmp。 如果是传入数据的函数,则使用类型转换,然后将tmp传给MYAPI中的函数。 如果是获取数据的函数,则将MYAPI中的数据传给tmp,然后从tmp中读取数据
如果是,则将值赋值给
如果是读取数据的函数,内部定义一个,从MYAPI读取数据,然后在C#中转成列表
const char* 是 C++ 中的输入字符串类型,在 C#/VB 中被 ::^ 替代。 const char* 通常可以转换为 std::,::^ 到 std:: 的转换方法为:
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);
}
如果是赋值的话,如果两个类型一模一样,而且都是枚举或者通用数据类型(int/char/short//float等),那么就可以使用内存复制
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。 在VS中创建一个新项目。 新建项目C#=>控制台应用程序,选择项目中的引用=>添加引用,选择CLR生成的dll。 使用了.dll。
经过上述步骤后,就可以直接用C#编写程序了,使用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也可以通过添加引用来实现。
创建一个新项目。 在VS中创建一个新项目。 Basic=> ,在项目中选择=>Add ,选择CLR生成的dll使用。
经过上述步骤后,就可以直接用C#编写程序,用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,因此可能会出现以下两种错误:
ion,这个错误是平台不对应造成的。 例如引用了x64平台的dll,但VB/C#项目中选择的平台是x86或者平台选择Any CPU,但属性中勾选了前32。 少量。 同理,如果引用了x86平台的dll,但是VB/C#工程中选择的平台是x64或者平台选择了Any CPU,但是属性中没有勾选32位优先,会报错也会发生。 名词这个错误是由于C++ dll没有复制到VB/C#生成的exe的路径引起的,因为CLR生成的dll中的函数最终实现是在C++ dll中,而VB/C#的路径是通常 bin/Debug 和 bin/ 区域需要优化,在 CLR 中封装 dll 时,需要重写 C++ 中的所有枚举和函数。 如果C++发生变化,手动同步代码的方法就太麻烦了。 您可以编写一个自动生成 CLR 代码的操作来减少手工劳动。 复制CLR生成的dll也依赖于C++生成的dll,所以容易出错。 如果只想生成一个dll,可以选择将C++生成的动态库转为静态库,这样就不用依赖C++的dll了。