在C#调用C++的DLL简析(一)——生成非托管dll
转自http://joeyliu.blog.51cto.com/3647812/1289614
经过一晚上的折腾,还是下点决心将些许的心得写下来,以免以后重复劳动。
C#与C/C++相比,前者的优势在于UI,后者的优势在于算法,C++下的指针虽然恶心,若使用得当还是相当方便的,最重要的问题是,市面上很多流行的开发工具库,几乎没有不支持C++的,但全面支持C#只能说是难得,在CPU发展到今天,若说C#的执行效率跟C++相比有很大的差距并不是那么靠谱,若非万不得已我还是宁愿用C#来写代码,调试什么的也很方便。
不得已的情况下,要在C#下使用C++的函数或类,最好的方式就是使用动态链接库(dll),至于COM什么的我是至今没弄明白其原理,也许主要是因为使用起来太麻烦了(还要注册什么的),使用dll的话,可以很方便将一个工程细分到成为两部分,协同编程可以加速进展。当然,用C#的代码改写一遍也不是不可能的,可当你有现成的几万行代码那就真头痛得要命,还是安心的使用动态链接库吧。
当你打开VS2012的时候,新建工程项目,也许你可能发现会有一个“CLR类库”的项目类型,直到今天我才知道,CLR原来指的是托管C++,托管C++跟非托管C++虽然有一定的关系,但很多人更愿意将他俩看成为两种不同的程序语言,托管C++是一种很恶搞的存在,它的唯一作用是用披着C++的马甲来写C#的内容,且最终是为C#服务的,使用CLR生成的dll可以直接在C#下引用,要用CLR还不如直接用C#更简单一点(纯粹个人观点)。
这是最常见的技俩,网上的资料是一大票,但为了我这健忘脑袋,我还是逐步贴图讲明吧,据本人一通宵的成果,几经折腾,终于证明了想在native C++下导入类那是不可能的事,所以,以下讲的仅是如何导入函数而已——就如你们在网上看到的文章一样。
(1)建立生成dll的工程
我用的是VS2012,不过好像跟前面的版本没什么太大的差别。打开VS,选择"新建项目"-“VC++”-"Win32"-"Win32项目",工程的名字叫"MyNativeDll",配置如下图所示,因为我有可能用到MFC的类,所以我就勾选了“MFC”的选项,在此需要注意的是,如果你新建时没有勾选MFC,但在后面却想动用MFC的内容,就会遇到“MFC apps must not #include <windows.h>”的Error,只是在工程的配置里修改是根本没有用的,必做要重建工程。
(2)实现dll导出的函数
新建好工程后,在VS的“解决方案资源管理器”中可以看到如下图的目录,其实你完全可以不用管这些默认的文件,如果你要用,可以在看一下MyNativeDll.h里的注释说明,大概能看得懂的。
在工程里添加几个文件,Define.h,CFunction.h,CFunction.cpp,其内容如下所示:
//Define.h 用于导入dll的宏定义。
1
2
3
4
5
6
7
|
//Define.h /////////////////////////////////////////// ////////////////////////////////////////// #ifndef _DEFINE_H_ #define _DEFINE_H_ #define _EXTERN_C_ extern "C" _declspec(dllexport) #endif |
//CFunction.h 函数定义,这里我特意定义了一组结构,我的用意稍后再讲。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
//CFunction.h //////////////////////////////////////////// /////////////////////////////////////////// #ifndef _C_FUNCTION_H_ #define _C_FUNCTION_H_ #include "Define.h" #include <string> #include <istream> struct SystemTime { int year; int month; int day; int hour; int minute; int second; int millsecond; SystemTime & operator= (SystemTime st) { this ->year = st.year; this ->month = st.month; this ->day = st.day; this ->hour = st.hour; this ->minute = st.minute; this ->second = st.second; this ->millsecond = st.millsecond; return * this ; } }; _EXTERN_C_ int add( int x, int y); _EXTERN_C_ int sub( int x, int y); _EXTERN_C_ int testChar( char * src, char * res, int nCount); _EXTERN_C_ int testStruct(SystemTime & stSrc, SystemTime & stRes); #endif //_C_FUNCTION_H_ |
//CFunction.cpp dll函数的实现,简单的赋值而已,大家应该看得明白的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//CFunction.cpp //////////////////////////////////////////// //////////////////////////////////////////// #include "stdafx.h" #include "CFunction.h" #include <stdio.h> int add( int x, int y) { return x + y; } int sub( int x, int y) { return x - y; } int testChar( char * src, char * res, int nCount) { memcpy (res, src, sizeof ( char ) * nCount); return 1; } int testStruct(SystemTime & stSrc, SystemTime & stRes) { stRes = stSrc; return 1; } |
添加好代码之后,选择“生成”的选项,因在工程目录下的Debug文件就已经存在我们所需要的MyNativeDll.dll文件,一起的还有lib的静态库文件(稍后要用到),及其他相关的调试文件,至此,我们已经成功的生成了native C++的动态链接库,我只能说,这是相当简单的第一步在而已。
(3)在C#工程下使用生成的dll
新建一个C#的窗口工程(我个人是很讨厌控制台的程序的),工程命名为“DllTest”,这就不教了。
在新建的窗体工程中添加一个CFunction.cs的类,这个类主要是用于导出上面dll里的函数,废话不多说,直接贴代码:
//CFunction.cs dll的函数接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; namespace DllTest { [StructLayout(LayoutKind.Sequential)] public struct SystemTime { public int year; public int month; public int day; public int hour; public int minute; public int second; public int millsecond; public SystemTime(DateTime dt) { this .year = dt.Year; this .month = dt.Month; this .day = dt.Day; this .hour = dt.Hour; this .minute = dt.Minute; this .second = dt.Second; this .millsecond = dt.Millisecond; } public override string ToString() { return this .year.ToString() + "-" + this .month.ToString() + "-" + this .day.ToString() + " " + this .hour.ToString() + ":" + this .minute.ToString() + "-" + this .second.ToString() + "-" + this .millsecond.ToString(); } }; public class CFunction { [DllImport( "MyNativeDll.dll" , CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int add( int x, int y); [DllImport( "MyNativeDll.dll" , CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int sub( int x, int y); [DllImport( "MyNativeDll.dll" , CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int testChar( ref byte src, ref byte res, int nCount); [DllImport( "MyNativeDll.dll" , CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int testStruct( ref SystemTime stSrc, ref SystemTime stRes); } } |
上面代码的格式看起来不好看,大家自己下附件里的文件看好了,上面的做法相当是作了一个CFunction的静态类而已。然后在Form1.cs窗体里直接写测试代码,我是直接写在Form1的初始化函数里,懒,没办法。
//Form1.cs 在C#的窗体初始化函数添加测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace DllTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); int a = CFunction.add(100, 50); int b = CFunction.sub(100, 50); Debug.WriteLine( "add = " + a.ToString() + " b = " + b.ToString()); Debug.WriteLine( "\r\n" ); string src = "123456" ; byte [] srcBytes = System.Text.Encoding.ASCII.GetBytes(src); byte [] resBytes = new byte [100]; a = CFunction.testChar( ref srcBytes[0], ref resBytes[0], src.Length); string res = (System.Text.Encoding.ASCII.GetString(resBytes, 0, resBytes.Length)).TrimEnd(); Debug.WriteLine(res.ToString()); Debug.WriteLine( "\r\n" ); SystemTime stSrc = new SystemTime(DateTime.Now); SystemTime stRes = new SystemTime(); a = CFunction.testStruct( ref stSrc, ref stRes); Debug.WriteLine(stRes.ToString()); Debug.WriteLine( "\r\n" ); } } } |
在你进行调试之前,务必记得要将在第二步生成的MyNativeDll.dll拷贝至C#工程下的bin\Debug\目录下,然后点击“调试”,看输出窗口,应该会有东西输出的,我就不贴出来了。
(4)总结
1)总体上来讲,生成一个native C++的dll不是很困难的事,重点在于在C#下的dll导出函数那里;
2)个人的经验来看,使用native C++可以导入函数,至于导出C++类,通过指针的方式并非不可能,可是方法过于费解,建议不要那么做;
3)在书写dll导出函数时,变量的传递是关键,建议使用C++的基本类型,如int,float,double等,因为C#下指针的概念很纠结,在C++下的引用符“&”,在C#中则使用ref的标识,需要紧记的一点是,C#与C++的类型并不全然通用(结构对齐问题),注意做变换。像上面的testChar函数,原本string(C#)对应的是char*(C++),但可能由于各种Unicode或多字节的关系,我是没法返回正确的值,于是我采用了byte的传入类型。关于C#与C++混编的类型问题,可以查看下面的文章:C++与C#的类型转换,文章2,文章3,在网上google到好的文章也是不容易的啊。
4)观察我写的结构,在C++下使用的结构体,在C#必须要重新定义一次,使用 [StructLayout(LayoutKind.Sequential)]的标识用于结构的对齐,如果你变量中使用了string这样的类型,还需要使用MarshalAs这样的方法支定义其长度——才可以跟char *相对应;
5)函数的返回值别用什么string了,我是找为到方法取得其正确的返回值,最好使用ref的引用方法回传回来。
6)指针的参数的传递在C#下使用IntPtr类型作转换,这我先不细说,网上相关文章还是不少的。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
转自:http://www.cnblogs.com/Chase/archive/2010/05/31/1748596.html
C#程序调用非托管C++ DLL文件的方法
08年写的一篇文章,当时项目用C#开发,但是有一些希望重用之前的C++代码,于是研究了如何在C#中调用C++的DLL。
C++中的函数声明
1
|
extern "C" __declspec ( dllexport ) int __stdcall testfunc( char * astr, int * a); |
extern ”C”
通常来说,C++编译器可能会改变函数和变量的名字,从而导致严重的链接程序问题。例如,假设使用C++编写一个DLL,当创建DLL时,Microsoft的编译器就会改变函数的名字。函数名将被设置一个前导下划线,再加上一个@符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。例如,下面的函数是作为DLL的输出节中的_MyFunc@8输出的:
1
|
__declspec ( dllexport ) LONG __stdcall MyFunc( int a, int b); |
如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫MyFunc的函数,该函数在Microsoft编译器已有的DLL中并不存在,因此链接将失败。
使用extern “C”关键字可以使编译器按照C语言的方式编译DLL文件,即编译时不改变函数名。
__declspec(dllexport)
在 32 位编译器版本中,可以使用__declspec(dllexport) 关键字从DLL导出数据、函数、类或类成员函数。__declspec(dllexport) 会将导出指令添加到对象文件中,因此不需要使用.def文件。
若要导出函数,__declspec(dllexport) 关键字必须出现在调用约定关键字的左边(如果指定了关键字)。例如:
1
|
__declspec ( dllexport ) void __cdecl Function1( void ); |
__stdcall
表明被调用方清理堆栈。
C#中的函数声明
1
2
3
4
5
6
7
8
|
using System.Runtime.InteropServices; … public class Program { [DllImport( @"E:\Projects\testdll\debug\testdll.dll" )] public static extern int testfunc(StringBuilder abuf, ref int a); } |
using System.Runtime.InteropServices;
System.Runtime.InteropServices 命名空间提供各种各样支持 COM interop 及平台调用服务的成员,使程序可以与非托管代码进行交互操作。
[DllImport(“dllfile path”)]
代码中DllImport关键字作用是告诉编译器入口点在哪里,并将打包函数捆绑在这个类中。在声明的时候还可以添加几个属性:
1
2
3
4
|
[DllImport( "MyDLL.dll" , EntryPoint= "mySum" , CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)] |
EntryPoint: 指定要调用的 DLL 入口点。默认入口点名称是托管方法的名称 。
CharSet: 控制名称重整和封送 String 参数的方式 (默认是UNICODE)
CallingConvention指示入口点的函数调用约定(默认WINAPI)
注意:必须在标记为”static”和”extern”的方法上指定”DllImport”属性。
数据传递方法
1.基本数据类型的传递
函数参数和返回值可以是C#和C++的各种基本数据类型,如int, float, double, char(注意不是char*)等。
示例:
C#代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
using System; using System.Text; using System.Runtime.InteropServices; class Program { [DllImport( @"E:\Projects\testdll\debug\testdll.dll" )] public static extern int testfunc( int a, float b, double c, char d); static void Main( string [] args) { int a = 1; float b = 12; double c = 12.34; char d = 'A' ; testfunc(a,b,c,d); Console.ReadKey(); } } |
C++代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
<pre class = "brush:cpp" >#include <iostream> using namespace std; extern "C" { _declspec( dllexport ) int __stdcall testfunc( int a, float b, double c, char d) { cout<<a<< ", " <<b<< ", " <<c<< ", " <<d<<endl; return 0; } } </pre> |
2.向DLL传入字符串
C#中使用string定义字符串,将字符串对象名传给DLL。
注意:在DLL中更改字符串的值,C#中的值也会改变。
缺点:无法改变字符串的长度,建议使用第3种方法。
C#代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
using System; using System.Text; using System.Runtime.InteropServices; class Program { [DllImport( @"E:\Projects\testdll\debug\testdll.dll" )] public static extern int testfunc( string a); static void Main( string [] args) { string a= "Hello World!" ; testfunc(a); Console.ReadKey(); } } |
C++代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <iostream> using namespace std; extern "C" { _declspec( dllexport ) int __stdcall testfunc( char * astr) { cout<<astr<<endl; *astr= 'A' ; //更改字符串的数据 cout<<astr<<endl; return 0; } } |
3.DLL传出字符串
C#中使用StringBuilder对象创建变长数组,并设置StringBuilder的Capacity为数组最大长度。将此对象名传递给DLL,使用char*接收。
C#代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
using System; using System.Text; using System.Runtime.InteropServices; class Program { [DllImport( @"E:\Projects\testdll\debug\testdll.dll" )] public static extern int testfunc(StringBuilder abuf); static void Main( string [] args) { StringBuilder abuf= new StringBuilder(); abuf.Capacity = 100; //设置字符串最大长度 testfunc(abuf); Console.ReadKey(); } } |
C++代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream> using namespace std; extern "C" { _declspec( dllexport ) int __stdcall testfunc( char * astr) { *astr++= 'a' ; *astr++= 'b' ; //C#中abuf随astr改变 *astr= '\0' ; return 0; } } |
4.DLL传递结构体(需要在C#中重新定义,不推荐使用)
C#中使用StructLayout重新定义需要使用的结构体。
注意:在DLL改变结构体成员的值,C#中随之改变。
C#代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
using System; using System.Text; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] public struct Point { public double x; public double y; } class Program { [DllImport( @"E:\Projects\testdll\debug\testdll.dll" )] public static extern int testfunc(Point p); static void Main( string [] args) { Point p; p.x = 12.34; p.y = 43.21; testfunc(p); Console.ReadKey(); } } |
C++代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include <iostream> using namespace std; struct Point { double x; double y; }; extern "C" { _declspec( dllexport ) int __stdcall testfunc(Point p) { cout<<p.x<< ", " <<p.y<<endl; return 0; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架