C#和C++ 库的相互引用


https://blog.csdn.net/benbencoco/article/category/2274159

http://developer.51cto.com/art/201105/262722.htm

https://blog.csdn.net/benbencoco/article/details/20464533

https://blog.csdn.net/qq506124204/article/details/8969490

今天在项目中看到C:\WINDOWS\Microsoft.NET\Framework\v...\REGASM.exe /regfile:xxx.reg xxx.dll,注册COM组件。

开始不明白为啥要注册COM组件,c#自己调自己的dll用不着这样啊,网上查看大都是讲如何注册COM组件。

经过不懈网上游荡,原来注册COM组件是给其他语言调用的,因为c#程序是托管代码,所以和其他非托管代码有所差异。

查就查到底,继续在网上查资料,把这块东西系统的整理下:

1.c#引用托管程序集-----直接引用

2.c#引用非托管代码-----通过P/Invoke(Plateform Invoke)机制调用dll函数

IntPtr相当于指针,通过Marshal类操作非托管内存

C代码和C++代码编译方式不同,C语言编译出来的函数入口和函数名一样,但C++编译出来的入口和函数名不一样,这是因为有重载的关系;这时,如果是C++代码,要么在函数名前加extern "C" rvalue function(param...),要么在编译出来的dll用dumpbin类似工具反编译出来函数入口EntryPoint

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
动态调用
    public static class NativeMethod     
    {     
        [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]     
        public static extern int LoadLibrary(     
            [MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);     
    
        [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]     
        public static extern IntPtr GetProcAddress(int hModule,     
            [MarshalAs(UnmanagedType.LPStr)] string lpProcName);     
    
        [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]     
        public static extern bool FreeLibrary(int hModule);     
    }
  static void Main(string[] args)     
        { 
             //1.load c++ dll
             int hModule = NativeMethod.LoadLibrary(@"c:/CppDemo.dll");
             if (hModule == 0)
                 return;
             IntPtr intPtr = NativeMethod.GetProcAddress(hModule, "MyDelegate");
             MyDelegate myDele = (MyDelegate)Marshal.GetDelegateForFunctionPointer(intPtr, typeof(MyDelegate));
             Console.WriteLine(myDele(1,2));
         }

     /// <summary>     
        /// 函数指针     
        /// </summary>     
        /// <param name="a"></param>     
        /// <param name="b"></param>     
        /// <returns></returns>     
        delegate int MyDelegate(int a, int b);     

3. clr c++调用c#dll

   设置项目属性-->配置属性-->常规-->公共语言运行时支持-->公共语言运行时支持(clr)

#using "../debug/xx.dll"
using namespace xxx;

4.非托管c++调用c#dll

1)用c#写dll

写接口和实现类,贴上GUID特性标签(工具->创建GUID->选择5->复制),Properties下的AssemblyInfo.cs中ComVisible为true

2)注册dll为COM组件

如果在本机开发,在属性->生成->为COM互操作注册即能完成注册功能或应用程序->程序集信息

但是要在别的机子上使用那就需要用REGASM.exe来注册

                C:\WINDOWS\Microsoft.NET\Framework\v...\REGASM.exe /regfile:xxx.reg xxx.dll然后运行xxx.reg注册该COM组件

3)使用 

namespace ComInteropDemo     
{     
    //接口声明     
    [Guid("7103C10A-2072-49fc-AD61-475BEE1C5FBB")]       
    public interface ComInteropInterface     
    {     
        [DispId(1)]     
        int Add(int a, int b);     
    
        [DispId(2)]     
        int Minus(int a, int b);     
    }     
       
    //对于实现类的声明     
    [Guid("87796E96-EC28-4570-90C3-A395F4F4A7D6")]     
    [ClassInterface(ClassInterfaceType.None)]     
    public class ComInterop : ServicedComponent, ComInteropInterface     
    {     
        public ComInterop() { }     
    
        public int Add(int a, int b)     
        {     
            return a + b;     
        }     
    
        public int Minus(int a, int b)     
        {     
            return a - b;     
        }     
    }     
}
#include "stdafx.h"     
#include <iostream>     
using namespace std;     
        
#import "..//Debug//ComInteropDemo.tlb"     
//路径一定要正确         
int _tmain(int argc, _TCHAR* argv[])     
{     
    HRESULT hr;         
    //ComInteropDemo::ComInterop *p;     
         
    CoInitialize ( NULL );        
    
    //创建智能指针ComInteropDemo::ComInteropInterface     
    ComInteropDemo::ComInteropInterfacePtr ptr;        
    //创建实例     
    hr = ptr.CreateInstance(__uuidof (ComInteropDemo::ComInterop));         
    if(hr == S_OK)     
    {     
        cout << ptr->Add (1.0, 2.0);     
    }            
    
    CoUninitialize ();     
    return 0;     
}

/*************************************************************************************************************************/

/                                                      https://www.cnblogs.com/neverstop/p/5901652.html                                             /

/************************************************************************************************************************/

最近在开发服务后台的时候,使用c#调用了多个c++编写的dll,期间遇到了一系列的问题,经过一番努力最后都一一解决了,在此做个总结,方便以后参考,毕竟这些问题也都是很常见的,主要有以下问题:

类型对照问题

c#调用c++方法时,首先要在类中定义一个与c++方法对应的外部方法,因为该方法是用C#语言定义的,那么肯定要弄清楚C#类型与c++类型如何对应,否则会导致调用失败,关于这个问题其实不算什么问题,网上有很多类型对照的文章,都有很详细的对应列表,用的时候参考一下就可以了。还可以使用工具,自动根据c++方法签名生成对应的C# import方法签名,参考P/Invoke Interop Assistant。不过有一个问题还是要注意的,在x86模式下c#中的int对应c++中的int,而在x64模式下C#中的int是对应c++中的long,就这么一个小小的变量类型,在不经意间可能就会导致c++代码出错。

还有一个问题是:托管的 PInvoke 签名与非托管的目标签名不匹配,可以在C#代码的方法特性上加上CallingConvention.Cdecl。如下所示:

[DllImport("dllname.dll", CharSet = CharSet.Ansi, EntryPoint = "methodname", CallingConvention = CallingConvention.Cdecl)]

内存释放问题

由于这个问题经常遇到,并且如果不能解决的话肯定不会再考虑使用该dll了,这是一个可用性的问题。所以我在调用c++方法的时候,通常都会先批量跑一边,通过日志记录下每调用一次方法后,当前进程所占用的内存大小,这样在运行一段时间以后,就能很清楚的看到内存是否持续增长,如果是的话就需要和编写该dll的同事进行沟通,给他们提供测试数据,确认产生问题的原因。有时即使C++中的方法进行了内存释放,并且在c++测试代码中已经没有内存增长问题了,但是在C#中调用的时候内存还是会持续增长,该问题可能跟使用的场景有关,我这里是因为调用了一个返回char *类型的c++方法,我直接用C#中的字符串类型的一个变量接收了,结果发现内存总是释放不了,后来让同事把c++的方法更改了一下参数,然后在C#中用StringBuilder类型的变量作为参数传入c++方法中来接收该方法的结果,这样该内存问题就解决了。

C#
// 在C#中声明与C++方法对应的dllimport方法
[DllImport("dllname.dll", CharSet = CharSet.Ansi, EntryPoint = "Handle", CallingConvention = CallingConvention.Cdecl)]
public static extern bool CPPMethod(string content,StringBuilder result);

// 该变量用来接收c++方法的处理结果,作为传出参数传入c++方法,在构造的时候必须明确指定大小
// 如果不指定或者指定的大小不足,会导致c++方法出现空间分配不够的异常
StringBuilder resultSB = new StringBuilder(length);
string cppParam = "some content";
bool isSuccess = CPPMethod(cppParam,resultSB);  // CPPMethod是与C++方法对应的dllimport方法

C++ 
// C++中的DLL函数原型,即:C#中要调用的方法,此处不再返回char *类型的结果,而是将结果放到传出参数result中
extern "C" __declspec(dllexport) bool Handle(char* content, char* result);  // result为传出参数

有的时候内存问题是纯粹由于c++代码导致的,一般遇到内存问题,我会用c++的测试工程再跑一遍,看看是否仍有该问题,如果是说明真是c++的bug了,可以通知同事去修改bug了。

内存问题有时候并不会体现的十分明显,这需要我们更加细心的观察日志并发现导致问题的真正原因。我之前遇到该方面的一个问题,刚开始内存涨幅非常明显,经过多次与开发该dll的同事沟通后,问题已经解决的差不多了,但是大量测试后发现内存还是会有一点上涨,虽然幅度很小,但第六感告诉我此中必有蹊跷,这要是上线跑个几天岂不是还得爆,后来我把每一次调用c++方法后当前进程占用的内存输出到文件中,经过仔细观察,发现绝大部分文件(文件内容要传入c++方法中进行处理)都没问题,内存都很平稳,但是有极小一部分文件在传入c++方法后,会导致内存相比其他文件有一个明显的增长,看来问题是出现在这些文件中,随后把这些文件单独放在一起进行循环调用,内存一下子就大幅增长了,后面就不用说了,问题当然解决了。因此,要保持记日志的良好习惯,哪怕是在测试工程中

版本问题(x86与x64)

版本不匹配的话,在调试时会提示正在加载格式不正确的dll,如果使用的是32位的c++版dll,需要把C#项目的编译平台设置为x86,如果使用的是64位的c++版dll,则设置为any cpu和x64都可以,这个需要自己根据实际情况对应好就可以了。如果程序对内存的使用比较高,最好将程序编译为64位,因为32位程序对单进程的内存大小有限制,经测试最大不超过2G。因为我的程序刚开始使用的是32位的c++版dll,并且在运行时需要调用这些dll加载很多资源,加载完这些资源进程占用的内存就差不多快2G了,所以总会莫名其妙的崩掉,甚至在加载的过程中就直接崩掉了,当时预感到是32位的问题,后来让同事将dll重新编译为64位后就没有这个问题了。可以通过dumpbin命令判断一个dll是32位还是64位,打开vs开发人员命令提示,输入:dumpbin /headers 你的dll路径,例如:dumpbin /headers d:\test.dll,如下图所示:

如果是32位dll,红框那里会显示

这里有一个地方需要注意,默认asp.net项目在调试时会运行在32位下的iisexpress进程中,如果你的项目是64位的,那么需要在VS中将iisexpress配置为64位模式,如下图所示:

编译问题(静态编译与动态编译)

这个问题在运行时有时候会提示dll加载不成功,这个问题在不同的电脑上会有不同的体现,有的存在这个问题,有的就运行正常。而我本机就属于正常的,部署的服务器属于出问题的。出现这个问题后,在确认代码无误后,我用depends.exe这个工具查看了一下导致问题的那个c++版的dll都依赖什么程序集,在出问题的机器上会提示有一些依赖的dll不存在,而这些dll在运行正常的机器上是存在的。下图红色框中的为某些机器上可能会缺少的dll:

如果缺少相关dll,该条目的左边会显示出一个黄色的问号。这个问题可以采用静态编译进行解决,关于什么是静态编译可以自行百度,总之就是将程序所依赖的dll编译到程序集中,这样即使其他机器不存在这些dll也可以正常运行了,静态编译可以在vs的项目属性中进行设置

默认是多线程 DLL(/MD),即:动态编译,这里更改为 多线程(/MT),即:静态编译。

刚才的配置只能解决缺少MSVCP120.DLL和MSVCR120.DLL这一类问题,对于缺少MFC相关的dll,还要经过下面的配置:

默认是使用标准Windows库,这里改为在静态库中使用MFC

资源加载问题(相对路径与绝对路径,dll中又调用其他dll加载资源)

这个问题相对比较隐蔽,出现时不会抛出异常,只能通过c++方法返回的状态码来判断方法执行是否成功,要不是在这里放了一个断点,特意看了一下,可能就遗漏这个问题了。

场景是这样的:
我在webservice中调用c++版dll中的一个初始化方法,该方法会加载一些资源文件,我在vs中调试执行的时候没问题,发布以后居然无法加载资源,貌似是路径问题,我把资源文件放到w3wp.exe的根目录下倒是可以成功加载,放在其他目录中就不行,遇到这个问题首先想到的可能是资源所在的目录权限不够导致iis无法正常加载,因为之前有个同样的问题就是这样,但这次将资源所在的目录更改为Everyone用户的完全控制权限还是不行,并且该问题只出现在b/s项目中,c/s项目没有这个问题。并且该目录中存放了很多资源文件,有好几个c++版的dll都需要从这里加载,其他几个都没问题,就这一个dll不行,看来不是权限的问题。这时候又想是不是相对路径的问题,那我改成绝对路径吧,结果问题依旧,后来在技术群里有个大牛说试试Directory.SetCurrentDirectory,赶紧修改代码,测试了一下确实好使了。代码如下:

// 保存当前工作目录
string currWorkPath = Directory.GetCurrentDirectory();
// 切换当前工作目录
Directory.SetCurrentDirectory(resourcePath);
// 初始化进行资源加载
Init(resourcePath);  // 这里要注意,使用了SetCurrentDirectory方法后,resourcePath要用相对路径
// 还原当前工作目录
Directory.SetCurrentDirectory(currWorkPath);

如注释所示,使用SetCurrentDirectory切换了当前工作目录后,方法中所用的路径要改为相对路径,一开始我用的是绝对路径,居然还是无法加载。

后来发现了该问题的原因,在使用的dll中又调用另外一个dll进行资源加载,可能这样会导致那个间接调用的dll出现路径问题,所以出现资源加载失败。

异常捕获与问题定位

关于异常捕获,虽然在方法中添加了特性HandleProcessCorruptedStateExceptionsSecurityCritical但还是捕获不到c++中的异常,原因可能是c++在遇到某些异常时会造成程序直接退出,这样在C#中就自然捕获不到了,所以还是尽量保证c++代码的健壮性。
如果在c#中调用了多个c++版dll中的方法,因为有时捕获不到异常,很难通过常规方法找到问题的原因,c++方法中一旦出现异常可能会直接导致进程退出了,这时可以借助操作系统中的事件查看器来找出异常是来自哪个dll,同时在原有代码中注释掉那段调用该c++方法的代码,或者mock一个方法调用,保证该段代码无异常,然后再进行测试,如果无异常,那么只要解决了那个c++方法的问题即可,如果还有异常那么就是其他dll的问题,然后可以编写测试代码单独测试曾经出问题的dll中的方法。异常捕获+事件查看器+日志可以帮助开发者发现程序的大部分问题与原因。

其他问题

1、0X1A截断全文的问题

这个是c++代码读取文件时可能会遇到的一个问题,虽然在调试某个问题的过程中发现了这个情况,但后来经开发dll的同事说问题的原因不是这个,这里就仅此记录一下吧,ifstream in("test.txt",'b');这样加上第二个参数就不会截断了。

2、vs实时调试造成iis进程一直等待的问题

两次遇到这个问题都是在下班后出现的,当时也不知道什么原因,后来通过windbg看了一下测试程序和w3wp进程的转储文件,通过!gle -all命令发现每个线程都在等待状态,如下图所示:

iis进程也是如此,本以为是代码死锁了,但是通过!locks命令也没发现有任何异常(关于这个问题,可以参考 应用死锁分析,当时有点懵,不知道是什么造成了这种情况,后来发生一件事情让我弄明白了为什么,那是在快下班的时候,程序正好出现了一个异常(虽是异常,其实不会导致程序崩溃退出),这时服务器上弹出了一个vs实时调试的提示窗口,我注意到iis的cpu使用率突然就降为0,测试程序的控制台也输出了线程等待的消息,联想到之前那些STATUS_WAIT_0的错误信息以及貌似死锁的情况,我感觉到可能是iis终止了所有线程,在等待vs实时调试这个交互窗口的结束,由于平时都是在下班后才会开启测试程序来验证程序的稳定性,所以当弹出这个交互窗口时,一直不会有人去处理,线程不会一直这么等下去,最后测试程序就退出了,iis也无法再继续处理请求了,这个交互窗口也貌似消失了(为什么用貌似,因为我没有专门去留意,只是凭印象觉得之前没见过),想到这我点了一下“取消调试”,程序继续往下运行了,也不再阻塞了。所以在程序运行的时候,最好关闭VS的实时调试功能,以免造成不必要的问题。进入visual studio中,选择【工具】->【选项】,点击【调式】,在【实时】选项卡中把【本机】【脚本】【托管】三个对勾取消掉就可以了。

其实就算实时调试窗口不见了,我们也可以通过系统事件来找到一些蛛丝马迹,如下图所示,只不过很难仅凭这个事件就断定问题的原因,因为服务器上运行了多个w3wp实例,只能说通过这个情况增长一些经验了。

其实还有一些问题,到现在有点记不清了,就不敢贸然凭残存的那点记忆来描述了,以便造成不必要的误解。对于遇到的问题,有些很明显,有些很隐蔽,有些需要仔细分析,有些需要在大量测试的情况下才会发现,这里只想说一句:测试很重要,工作需用心。





  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值