如何从托管代码调用DLL中的非托管代码

最近在公司实习接到一个新的项目需求,大体说来,需要实现C++与C#语言之间的互操作。听起来有点抽象,其实就是能够用C++代码调用.NET平台FCL中的工具类,而C#代码也能够反过来调用C/C++编写好的DLL中的函数。


C++调用DOTNET基础类库很容易,毕竟DOTNET平台就是为实现language independant这个目标而设计的,它通过C++/CLI为传统的C++语言能够在全新平台下工作提供了强有力的支持。我们只需要稍稍改动一下代码语法,再将函数手动包装到一个wrapper里面就可以在DOTNET平台下工作了。这篇文章主要阐述如何从托管的C#模块中去调用非托管的DLL。


因为DLL文件已经存在,要对它用C++/CLI进行改写代价是相当大的。再者,如果该DLL文件是第三方提供的商业产品,我们也不可能也不被允许对其源代码进行修改。于是这里,我们要借助DOTNET平台提供的Platform Invoke Services来达到目的了。网上有很多如何使用PInvoke的例子,MSDN上也有详尽的阐述,但是我在尝试的时候还是遇到了点麻烦。


下面是一个标准的C风格的DLL头文件的写法,我们将它命名为myLib.h

在该文件里声明了一个add函数实现两个浮点数相加的功能。其对应的实现文件myLib.cpp很简单,

用Visual Studio编译动态链接库工程,debug目录下会生成两个重要的文件,其中一个当然就是myLib.dll了,另外一个是名为myLib.lib的文件。myLib.dll就是我们待会将要调用的动态链接库,那么.lib后缀的是什么呢?有了dll还需要它干嘛?不用着急,稍后就会讲到:)

接下来创建一个C# Console Application工程对已经编译好的myLib.dll进行引用。

为了能够使用DllImport属性,引用System.Runtime.InteropServices命名空间是必须的。DllImport属性实际上是DllImportAttribute类,在构造这个类时我们可以根据实际需要对其field值进行指定,这个例子中我们只指定了需要引用的dll文件的名称。除此之外,通常还需要说明的field还有EntryPoint,CharSet和CallingConvention,如果不作显式说明,系统将采用默认值,以上的例子就采用的default value。DllImport可以参见MSDN相关部分:

http://msdn.microsoft.com/en-us/library/e4takf5s%28v=VS.71%29.aspx

需要注意的是DllImport仅仅一次有效,它作用在紧跟其后的Method上。也就是说,如果有多个函数需要声明,我们必须在每个函数开头前重新指定一次DllImport属性。这里只引用了add函数,因而不存在这个问题。在DllImport属性之后我们对要引用的add函数进行了声明,extern向编译器暗示add的定义需要从别的地方寻找。

最后编译测试文件,debug目录下生成exe可执行文件,注意这个时候我们还没有将刚才生成的myLib.dll拷贝到debug目录下。运行生成的exe,和我们预料的一样,程序抛出了运行时异常System.DllNotFoundException。很明显,原因是系统无法定位到myLib.dll这个动态链接库。作为补救,我们将该文件复制到debug目录下,重新运行exe,这下总该行了吧?

Oppps,程序再次crash掉了,这次抛出了另一个异常System.BadImageFormatException。可以确定的是,系统已经找到了myLib.dll这个文件,不然的话运行时还会抱怨找不到dll,那么这次的文件映像损坏是什么意思呢?

最初我能想到的也只能是dll文件出问题了。为了证实一下,我写了一个简单的C++程序去调用myLib.dll,发现可以正常运行。再回头检查一遍C#的测试文件,貌似没可能出错,MSDN的样例也是这么写的。于是就郁闷了,到底哪里出问题了导致DLL不能被正常调用。折腾了一个上午,忽然想到Windows via C++一书中关于dll的一段话,书中是这么说的——

A DLL can export variables, functions, or C++ classes to other modules. In real life, you should avoid exporting variables because this removes a level of abstraction in your code and makes it more difficult to maintain your DLL's code. In addition, C++ classes can be exported only if the modules importing the C++ class are compiled using a compiler from the same vendor. For this reason, you should also avoid exporting C++ classes unless you know that the executable module developers use the same tools as the DLL module developers.

重点在于in addition开头那句,简单但不够准确地说就是:要使得导出的C++类模块可用,导入模块必须用相同厂家的编译器编译。这个限制存在的原因我不清楚,但好在有了点头绪。这里我的dll和测试文件都是在Win7 + Visual Stuidio环境下开发的,书中说的潜在问题对我不成立,但我已经开始怀疑起是编译的问题在捣鬼。于是上网搜搜关键字,C#,dll,BadImage,突然眼前闪过32bit,64bit的字样,让我猛然间醒悟,我立马感到问题就要迎刃而解了!

C#测试文件的目标平台是AnyCPU,这意味着,生成的exe具有在32bit机器上以32bit的方式运行,而在64bit的机器上以64bit的方式运行的智能。我用的机器是64bit,那么好了,exe以默认64bit的方式运行在我的机器上。我们知道,一旦一个程序以32bit或者64bit的方式开始运行,在它的生存周期结束前它就一直是32bit或者64bit,而它加载的其它依赖模块也是对应的版本。换句话说,32bit的applicaition不可能加载64bit版本的依赖模块 (如dll),反之亦然。回过头看看我的dll工程,target platform是x86,也就是编译出来的是32bit的版本。另一方面,由于C#测试文件采用AnyCPU,在我64bit的机器上exe将以64bit的方式运行,运行时它将尝试加载64bit的dll,而我们刚才生成的dll是32bit的,当然就会抱怨文件映像损坏了。功夫不负有心人,问题被揪了出来。

因为非托管代码一定是针对特定平台的,所以不可能用AnyCPU选项进行编译,要么选择x86,要么x64。这里要解决问题自然有两种方法:dll用32bit编译,C#以x86编译;dll用x64编译,C#以在64bit机器上可以用AnyCPU或者x64编译。试着运行一下,程序完美运行!倒腾这么久只能怪万恶的64bit操作系统了~32bit机器上这个问题是不可能存在的,所以很多人可以直接复制MSDN的例子运行,因而这个问题直接被华丽地无视了。

现在回到刚才所讲的随dll一同生成的lib文件。事实上,如果用C++调用C/C++编写的dll,在链接生成exe可执行文件时lib是必不可少的。lib文件是一张清单,它一条条列出了exe中所引用的所有dll模块的名称,以及每个dll文件内所有的导出符号。需要说明,只是名称而已,没有其它任何附属信息。在链接阶段,链接器会对照lib清单进行检查以保证这些dll可以被找到,而且其中的名字引用确实存在,否则链接出错。一旦成功通过链接,lib文件就没有用处了,载入运行时系统不再有针对lib的操作,因此生成exe文件后我们将lib从文件夹中删除也完全不会影响程序的正常执行,但是dll是动态加载,自然要放在特定文件夹中来保证它无论何时都能被系统找到。但有趣的是,DOTNET平台下我们并不需要lib文件就能正常生成exe,回头看看上面的过程,我们有用到lib文件吗?可能这点与DOTNET平台特殊的工作方式有关,有待今后作进一步研究。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Delphi是一种编程语言和开发环境,用于创建各种类型的应用程序。它支持连接到托管内存dll的功能,通过使用一些特定的技术和函数库来实现。 托管内存dll是指由其他语言,如C/C++编写的动态链接库。这些dll通常包含了实现某些特定功能的代码和数据。Delphi可以通过调用这些dll的函数来使用这些功能。 要使用托管内存dll,首先需要在Delphi创建一个包含对应dll函数接口的类型库。这可以通过使用Delphi自带的类型库导入工具来实现,该工具可以从dll文件自动生成相应的接口定义。 一旦类型库被创建,就可以在Delphi使用这些dll函数了。通过使用函数接口,在代码调用dll的函数,并传递所需的参数。这样可以利用dll的功能,让Delphi应用程序具有更多的功能和灵活性。 在使用托管内存dll时,需要注意内存管理的问题。Delphi的垃圾回收机制只能回收托管内存,而不能回收托管内存。所以在与托管内存dll交互时,需要确保正确地分配和释放内存,以防止内存泄漏和访问冲突。 为了管理托管内存,Delphi提供了一些用于分配和释放内存的函数和方法。在调用托管内存dll的函数之前,可以使用这些函数来分配所需的内存空间。在调用完成后,使用相应的函数来释放已分配的内存空间。 总结来说,Delphi可以通过连接到托管内存dll来扩展应用程序的功能。通过创建函数接口,调用dll的函数,并正确管理托管内存,可以实现与dll的交互。这样,就能够利用dll的功能,提供更多的功能和灵活性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值