如何在c++中使用 .net dll

简介

c++编写的程序为非托管代码,c#写的为托管代码。CLR提供了一些机制,允许应用程序同时包含托管与非托管代码。具体可以分为以下3种:

  1. 托管代码能调用DLL中的非托管函数。通过P/Invoke(Platform Invoke)机制调用DLL中的函数,如Kernel32.dll等。

  2. 托管代码可以使用现有COM组件(服务器)。许多公司都已经实现了大量非托管COM组件。利用来自这些组件的类型库,可创建一个托管程序集来描述COM组件。托管代码可像访问其他任何类型一样访问托管程序集中的类型。

  3. 非托管代码可以使用托管类型(服务器)。许多现有的非托管代码要求提供COM组件来确保代码正确工作。使用托管代码可以更简单地实现这些组件,避免所有代码都不得不和引用计数和接口打交道。比如C++调用C#开发的dll。

对于c++/cli,网上比较常见的是用它来包装非托管代码来供托管代码调用。但目前遇到一个需要在c++中调用c#写的dll的问题,需要用到c++/cli作为桥梁,在此做个记录。

编写c#dll

具体的代码没有必要贴出来,以一个函数声明为例子。这个函数将在c++/cli中被包装为c可以调用的函数。而在c++/cli中的包装,最主要的就是托管数据类型和非托管数据类型的一个转换。

public T caget<T>(string pvname, int setGateWay, string epicsCaAddrList)

我们可以看一下这个函数。它的参数有system::string,和int基本类型。返回是一个泛型。int基本类型不需要转换。system::string需要转换成非托管。对于c++调用的dll能不能有泛型接口,我其实不确定。但感觉泛型是在调用时确定类型,产生的执行代码是有明确类型的。而动态链接库它是可以复用的可执行代码,对应的类型应该明确。我的判断是导出函数不能有泛型接口,而且经过网上搜索也没有找到可以的例子。(如果有找到可以使用的方法,欢迎补充)。因而T的转换我采用的是switch case,因为此处T的类型根据需求已经确定只有有限的几种,所以可以采用这种一一匹配的方式。

通过c++/cli封装

可以在c#dll同一解决方案下创建clr class library项目(依次选择 visual c++ → clr → clr class library)。然后对clr项目添加dll的引用。 然后进行对c#dll的封装。具体的语法可以在网上查找教程。在这里主要记录一下类型转换,也是我觉得c++/cli封装最主要的部分。

extern "C" __declspec(dllexport) int caget(const char* pvname, void* pdata, DATA_TYPE dataType, int setGateWay, const char* epicsCaAddrList)

这是我对caget函数的封装声明。system::string我转换成了char*(原本想转换为std::string,后来调用时发现会报错)。泛型类型的返回被改成了通过改变void*的值来返回。 这样函数参数就已经转换成了非托管代码。在这个封装函数内部我们需要调用c#对应的caget。所以这里需要一个步骤,即将非托管转换成托管来作为caget的参数。

String^ strPvname = gcnew String(pvname);

上述代码就是把char转换成了system::string。 而将system::string转换成char可以看这个:

using namespace System::Runtime::InteropServices;
char* ManagedString2UnmanagedStringA(String^ strIn)
{
IntPtr ip = Marshal::StringToHGlobalAnsi(strIn);
const char* pTemp = static_cast<const char*>(ip.ToPointer());
char *pOut = new char[strlen(pTemp)+1];
strcpy_s(pOut, strlen(pTemp)+1, pTemp);
Marshal::FreeHGlobal(ip);
return pOut;
}

我的代码里还涉及到了如何将int[]转换成int*。

cli::array<int>^ managedIntListResult  = generator->caget<cli::array<int>^>(strPvname, setGateWay, strEpicsCaAddrList);
cli::pin_ptr<int> pIntArrayElement = &managedIntListResult[0];

pIntArrayElement就可以当普通的指针用。这个转换同样适用于各种数组转换成指针。

c++调用

将c++/cli工程中编译生成的debug文件夹里的文件拷贝到调用工程的exe同目录下。也可以在dll工程中直接把输出路径设置成调用工程的debug目录,方便调试。具体代码如下:

#include "pch.h"
#include <iostream>
#include "tchar.h"
#include <string>
#include <windows.h>
using namespace std;
typedef enum
{
    TYPE_STRING,
    TYPE_INT_LIST,
    TYPE_DOUBLE_LIST,
    TYPE_FLOAT_LIST,
    TYPE_SHORT_LIST,
    TYPE_BYTE_LIST
}DATA_TYPE;
// 设置函数指针,对应caget和caput
//char* caget(char* pvname, int setGateWay, char* epicsCaAddrList)
//void caput(char* pvname, char* newvalue, int setGateWay, char* epicsCaAddrList)
typedef void(*pCAget)(string, void*, DATA_TYPE,int,string);
//typedef void(*pCAput)(char*, char*, int, char*);
void main()
{
    //获取dll
    HMODULE hModule = LoadLibrary(_T("catoolwrap.dll"));
    if (hModule)
    {
        pCAget caget = (pCAget)GetProcAddress(hModule, "caget");
        DWORD err = GetLastError();
       // pCAput caput = (pCAput)GetProcAddress(hModule, "caput");
        //err = GetLastError();
        if (caget)
        {
            string pvname = "10293:aiExample";
            //char value[50] = "3";
            char result[1000] = {};
            //caput(pvname, value, 0, NULL);
            caget(pvname, result, TYPE_STRING,0,NULL);
        }
        else
        {
            ::MessageBoxA(NULL, "Get function fail.", "Fail", MB_OK);
        }
        getchar();
        //free library
        FreeLibrary(hModule);
        hModule = nullptr;
    }
    else
    {
        ::MessageBoxA(NULL, "Load dll fail.", "Fail", MB_OK);
    }
}

本文章使用limfx的vsocde插件快速发布