一、序言
在C#开发过程中,我们可能会遇到需要调用Windows API 或是第三方库的场景,然而有时候这些库是由C++编写的,并不能直接应用在C#的程序中,这为开发带来许多阻力。本文介绍两种使用C#调用C++动态库的方式,以及在这个过程中可能遇到的问题,看完之后会对你的困境有所帮助。
二、非托管与托管
很多时候在项目中需要通过C++调用C#的dll,或者反过来调用。首先明白一个前提:C#是托管型代码。C++是非托管型代码。简而言之:
- 托管型代码的对象在托管堆上分配内存,创建的对象由虚拟机托管。(C# )
- 非托管型代码对象有实际的内存地址,创建的对象必须自己来管理和释放。(C++)
所以C#调用C++的动态库可以归类成两种办法:
- 非托管C++创建的dll库,需要用静态方法调用;
- 直接使用CLR,生成托管C++dll库。
接下来详细介绍具体的实现方式。
三、方法一:静态调用
1.加载第三方动态库
在使用C#时,在解决一些问题时会用到Windows API,这时你很容易看到如下的调用:
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left)
{
ReleaseCapture();
SendMessage(Handle, 0x00A1, 2, 0);
}
}
这就是通过DLLImport来静态调用动态库中的方法,比较便捷,适用于一些简单的调用,但是缺点也很明显,想要使用相应的方法就得提前进行静态声明,而且不利于参数传递,也就是说如果参数有在库中声明的结构体,不能直接被静态方式引用,你必须在库中声明一个公共类做为接口来暴露数据。而有时我们使用的DLL是第三方的工具,不方便自己开源修改。
2.自定义动态库
有时候我们需要自己编写动态库来调用,就可以参考如下形式:
例子:新建C++项目,创建动态链接库(DLL),然后添加头文件textdll.h
#pragma once
#ifdef A_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif
extern "C" DLL_API void MessageBoxShow(); //通过extern “C” 使MessageBoxShow方法为一个导出方法,外部可见
这一步是要在头文件中声明接口,之后需要实现相应的函数:
#include "stdafx.h"
#include "textdll.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
void MessageBoxShow()
{
MessageBox(NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK);
}
我们编译之后,在目录下会生成 一个DLL(.dll为后缀的文件),我们复制到C#的debug目录下,使用C#程序静态调用:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; //必须要添加该引用
using System.Text;
using System.Threading.Tasks;
namespace dlltest
{
class Program
{
[DllImport("TESTDLL.dll")] //最关键的,导入该dll
public extern static void MessageBoxShow();
static void Main(string[] args)
{
MessageBoxShow();
}
}
}
但是这样做在很多时候不太方便,如果想要知道库中具体有哪些函数,还需要去翻阅文档或者源码,调用前还需要逐个进行静态的声明,所以接下来引用第二种方法。
四、方法二:托管C++动态库
通过C++/CLI作为中间层,因为C++/CLI既可调用.NET的类库,又可调用C++原生库,
所以可以通过C# 调用 C++/CLI 再调用 C++ DLL这样的三层调用方式完成。
1.动态库
首先需要编写一个DLL,可以是自己编写的动态库也可以是第三方的动态库,但是很重要一点是,在函数中需要有如下声明才能被后续引用:
_declspec(dllexport) int sum(int a, int b);//给c++调用
extern "C" __declspec(dllexport) int sum(int a, int b);//给c#调用
如果是自己编译则在VS中选择创建DLL项目,如何编写动态库这里就不赘述:
2.新建CLR项目
1.创建
打开VS,创建一个CLR的空项目:
从描述中就可以看到CLR本身就是打通.NET与C++代码的桥梁。
2.配置
以实现HID相关的USB功能为例,需要使用到一个hid的动态库,在创建完项目后打开项目属性,先引用第三方的DLL:
这个库目录中需要包含同名的相关文件:
同时引用库:
3.方法
然后新建一个类文件来编写相关方法:Cli.h
#pragma once
#include "hidapi.h"
public ref class DFUCli
{
public:
hid_device* handle;
DFUCli();
int Openhid();
void test();
};
Cli.cpp
#include "DFUCli.h"
DFUCli::DFUCli()
{
}
int DFUCli::Openhid()
{
uint16_t pid = 0x0000;
uint16_t vid = 0x0000;
handle = hid_open(vid, pid, NULL);
if (handle == NULL) {
return -1;
}
return 0;
}
void DFUCli::test()
{
}
编写完成之后进行编译,此处需要更改生成类型为动态库:
这样我们就得到一个CLR的动态库文件。
3.C#引用DLL间接调用方法
新建一个C#工程,引用刚刚生成的DLL:
这时我们就能像使用普通的动态库一样调用相关方法,实例化对象之后,VS还会自动提示相应的函数,无须其他多余操作:
五、问题点汇总
当然由于这种多级的调用,经常会由于错误的操作或者配置导致一些未知的错误,下面整理了一些调试过程中遇到的问题,可能对你有所帮助:
1.fatal error LNK1561: 必须定义入口点
出现这个错误是由于我们创建的DLL是并没有入口函数,也就是main函数,而编译器按常规流程处理引发错误,有两种解决方式。
(1)设置子系统,解决方案上,右键->属性->链接器->系统->子系统,下拉框选择:窗口 (/SUBSYSTEM:WINDOWS)
(2)设置入口点,解决方案上,右键->属性->链接器->高级->入口点,设置成:WinMainCRTStartup
2.未能加载文件或程序集“XXX.dll”或它的某个依赖项的解决方法
这个问题有时候比较隐蔽,正常情况下应用程序查找依赖的dll时,顺序为先查找程序exe的输出路径,如果没有找到,那么会去C:\Windows\System32或System64文件夹中查找。
这种情况下我们只需要添加相关的dll到输出文件夹下就能解决错误,但是当你添加了对应的依赖项之后仍然报相关错误:
这时候你可能觉得无法理解从而陷入误区,然而事实上在这种多级的引用中,有可能你当前直接引用了A.dll,然后A.dll是一个CLR动态库引用了B.dll。如果A.dll找到了,但是A.dll依赖的B.dll没有找到,也会报上述错误,但这时它有可能只会提示没有找到A.dll。所以你需要把相关的依赖项全部都放在一个输出文件夹中来避免这种情况,当然也有可能你不清楚你所使用的动态库是否引用了第三方DLL,此时我们可以用Visual Studio中的dumpbin.exe工具查找A.dll的依赖项。
3.所生成项目的处理器框架“MSIL”与引用“xxx”的处理器架构“AMD64”不匹配
这是由于你的C#程序与动态库的生成配置不同导致的,将两者进行统一即可解决:
4.System.EntryPointNotFoundException: '无法在 DLL“xxx.dll”中找到名为“xx”的入口点。
在系统内的函数名称不对,遇到这个问题可以用工具查看内部名称,发觉函数名称并不是 “xx” 而是类似“?xx@@YAHHH@Z”这种不合法的形式。
原因为dll导出的格式不对,给c#调用时需要增加extern “C”,标准C格式。
_declspec(dllexport) int sum(int a, int b);//给c++调用
extern "C" __declspec(dllexport) int sum(int a, int b);//给c#调用
这一点在前文生成托管动态库中提到过。
5.错误 LNK2019 无法解析的外部符号
这个如果是运行时报错,常见的原因是你引用了某个库的函数,然后也正确添加了它的头文件路径,vs在写代码阶段可以找到这个函数的定义,但是,由于你没有添加或者正确设置这个库的lib或者dll路径的话,那么vs就会在运行时候报错无法解析的外部符号。库目录(lib文件目录)是在项目->属性->配置属性→VC++目录→库目录里进行添加,这一点在前文配置CLR项目中有详细步骤。
第二种可能是类似第4点错误,你没有正确的设置导出格式,需要增加相应的定义。
同样的第3点错误也可能导致这个问题。
除了前几种可能,还有一点就是lib的覆盖问题,引用动态库,在项目生成中如果更改lib生成路径,而正好引用该lib的项目又同时包含了原来的和重新生成的两个lib路径,编译器就有可能链接的还是原来的lib,就会导致这个问题。
综上所述,出现这个问题除了低级的拼写错误外,无非就是没有正确的引用相关的依赖文件,或是程序和他所引用的依赖同时引用了相关依赖但是版本不一致。
6.CS0570 '现用语言不支持xxx
这个比较常见是dll的问题,解决方案中有一个类库项目,原来是引用的dll文件,后来又在本解决方案中建了一个一样的类库项目,有的项目直接引用的是这个类库项目,有的又是引用的dll文件。
后来统一引用类库项目就能编译成功了。
还有一种可能则是函数的参数不兼容,假设你引用了一个MFC项目需要输入一个CString类型的参数,而C#中的String类型不能替代或是强制转换也会出现问题。文章来源:https://www.toymoban.com/news/detail-559095.html
六、总结
使用动态库让我们的程序有了更多可能,同时可以兼容不同的语言,但是在引用过程中容易出现各种问题,特别要遵循一定的规范,否则在复杂操作时会有意想不到的问题。但是使用得当会是不可多得的助力!文章来源地址https://www.toymoban.com/news/detail-559095.html
到了这里,关于C#调用C++动态库的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!