Spiga

Dllimport时要注意的一些问题,特别是工作在64位IIS7的必看。

2008-12-15 07:25 by 梁逸晨, 3329 visits, 收藏, 编辑

开篇声明:存在必是合理,请不要白费力气和我讨论这个模式的必要性。

最近要在一个项目中把大部分代码都封装到标准C的DLL中,asp.net的部分仅仅做 [Dllimport "xxxxx" ],输入输出接收和传送字符串等这些最简单的动作。


这就需要考虑几个问题:虚拟主机支持、IIS6的兼容性、IIS7-32位模式兼容性、IIS7-64位模式兼容性、32位本地桌面兼容性、64位本地桌面兼容性。

以及:既然是WEB程序,肯定就关系到数据库的使用,既然代码都是用C++(使用标准C++,不是托管C++)编写,那么ADO.NET是肯定不能使用的了,所以选择了SQLITE(原因不在本话题,省略过)。

幸好是选择了SQLITE,才会发现这个问题,要不然如果直接使用ADO加载ACCESS或SQLSERVER的话,将来需要更换数据库时才发现问题就惨了。

一开始,我直接使用SQLITE提供的现成DLL:SQLITE3.DLL,本地桌面测试通过,结构是这样的:

 

可是在IIS中使用的时候,问题出现了:“找不到所需的库文件:SQLITE3.DLL”。怎么会找不到呢? 都放在BIN目录中,我的年龄还不至于到了看眼花的程度,但是本着敬业的态度,还是到医院做了一番检查,医生和仪器都一致同意:“您的视力没有问题。”。确定不是我眼睛有毛病后,想到是不是因为这是试用版WINDOWS做了限制,但是本着敬业的态度,于是我打电话给鲍尔默:“老鲍,试用版WINDOWS会不会在IIS中做了什么限制的手脚?”,老鲍说:“你放心,绝对没有限制,和正式版一模一样。但是我建议你购买正式版WINDOWS,可以享受到7*24小时的电话技术支持服务、免费更新服务……”,老鲍说WINDOWS没有问题,那就肯定没有问题。那么问题出在哪里呢?这时候,有个电话找我:“为什么你昨天给我的那个ISO文件我刻盘了不能安装?”,我问:“你是怎么刻的?详细道来。”,电话那边说:“我把你给我的文件直接拖放到刻录机中,让他自己自动刻录……”,一听就来气了:“镜像文件居然让你这样刻录,亏你还是计算机研究生,你趁早改行回家挑粪去算了。顺便叫你老师和你一起去挑粪,这种弱智问题不要问我了,问你校长去。”。挂了电话。MD,这年头学校都教出什么货色来,还计算机研究生呢,刻个镜像文件都不会……,自己嘟哝到一半,忽然灵光一闪:“镜像文件!”,恍然大悟,想起来了,BIN中的东西实际上都是镜像而已,真正执行的时候,是把这些文件复制到系统专用目录中去执行的,并不是直接执行BIN目录中的文件。我在C#中仅仅是DLLIMPORT  “CPP.DLL”,所以IIS当然就是仅仅只把CPP.DLL复制到系统目录中去了,而C++中的算法是“加载当前目录中的SQLITE3.DLL”,这时候SQLITE3.DLL仍然只是在BIN目录中,当然就找不到了。

知道了原因,马上就闪现了2个解决方案:1 让IIS把SQLITE3.DLL也加载就行了。 2 把SQLITE3.DLL封装到CPP.DLL中。

两个方案应该用哪种呢?直觉告诉我,应该用第二种,因为方案1为系统算法带来了多余的行为,对于C#这一方来说,它仅仅是“中间人”的角色, C#负责做的事情是在逻辑层面(CPP.DLL)和输入输出层面(IIS或者WINFORM)建立一个沟通渠道,它没有理由跨过逻辑层而直接与数据库层(SQLITE.DLL)打交道,即使仅仅是加载,而没有参与真正读取数据库也不行,因为它没有理由去加载一个它“不认识”的家伙。那就这么定了,把SQLITE.DLL集成到CPP.DLL中,让系统只加载一个WIN32的DLL,这样一来也减少了错误发生的机会:要么全部出错,要么就不会出错。

说得轻巧,做起来难,怎么合并?用记事本打开它们两个,然后把内容拼接到一起,再另存为“CPP.DLL”?我可不是研究生,这么高级的操作我不会。还是想别的办法吧,幸好,SQLITE是开源的,并且免费使用在包括商用在内的任何领域。到WWW.SQLITE.ORG去下载了源码,就三个文件,一个sqlite3.c文件,一个sqlite3.h文件,还有一个sqlite3_ext.h(这个文件干什么用的也没有去研究,总之就是多余的,可以删掉了)文件。

把两个文件加载到c++的源码中,去掉原先加载的sqlite3.lib, 编译,得到新的cpp.dll,果然容量增大了380多K,这次肯定行了。把IIS中BIN文件夹里面的东西删掉,再扔这个新的cpp.dll进去,测试运行,成功!此时也不再需要依赖sqlit3.dll了。此时的架构是这样的:

 

 

 

通常,从一个想法的产生到能够真正实现出来,不知道要经过多少次调试、查错、论证……,哪怕它仅仅是一个“Hellow World!”也不能逃过这些劫难,而这次,一切却如此顺利,所以可以说,这种幸运的机会是少之又少的,完全取决于你上辈子积蓄的阴德。

黑暗的旧社会终于被解放了。新中国万岁!

可是,新中国成立10多年后,依然有了一次整体上10多年的社会事件,而那次事件的某些后遗情况也直接影响到我们2008年的今天。

此时,有一些Buger们,正潜藏在DLL的某个阴暗角落里,在等待着反清复明的机会:“下个月乾隆要来纽约,我们先埋伏着,只要他一上到世贸大厦的一半,十四弟的那几架波音787自然会启动,一切等待和大人的指示,别的事情一概不管,以免走漏风声。特别要当心那个叫纪晓岚的。”。

一切都运行得井井有条。dll在windows2003中的测试的过程居然如此风平浪静,似乎要预示着一场毁天灭地的灾难即将淹没庞贝城。

一天的工作结束,不结束也不行了,眼皮都打架了。记录完版本开发进程,备份了代码到网上去,重启电脑,切换到VISTA去,准备洗澡。

说到这里,顺便给大家一个提示:开发工作用的电脑,最好是有两块硬盘,有条件的组RAID1,不想组的也要做个定时同步计划,没有两块硬盘的就应该把项目备份到网上私人空间里或者U盘里,要不然,会在将来某个时刻死得很惨。

我装有两个系统,一个是32位2003,一个是64位VISTA,常用的是VISTA,2003只在必要的时候才使用(例如这次开发这个东西,为了先保证32位平台,就先在2003中测试开发)。因为切换一次系统就要合并一次QQ记录的关系,所以干脆就不在2003里面开QQ了。 洗澡归来,打开VISTA,上QQ看有没有哪个妞来请我出去做秘密郊游,果然有,约好时间,准备睡觉,为明天的郊游做好休息,睡前为了再欣赏一遍今天的劳动成果,打开chrome,在地址栏输入127.0.0.1/using_cpp.aspx,回车。

平时我看电视都是喜欢看新闻频道,一来关心国家大事,二来经常可以看到世界各地的美人。此时传来东央电视台的播报 :"据一位不愿意透露姓名的FBI官员称,当年911事件另有说法:5架波音787客机的64位导航系统未将对象引用设置到对象的实例,导致同时启动了自动寻找地面重点对象的程序"。

 

我的眼前一黑, 完了,明天还怎么和紫嫣妹妹去郊游,想到可能是因为人困了,眼睛出毛病,本着敬业态度,我连夜跑去医院做了眼睛检查,医生和仪器再次确认:“您的视力没有问题”。难道是因为VISTA是30天试用版的关系,我再次接通鲍尔默的电话:“老鲍……”,还没等我问,他就先回答了:“上次你挂电话那么快,我还没有来得及说,不光是WINDOWS2003,就连VISTA的试用版也是和正式版完全一致,不会存在任何使用限制,此外,我强烈建议你购买我们的VISTA系统,你可以享受到7*24小时的……”,算了,不睡了。

我打开IIS7控制台,32位模式、64位模式,经典模式、集成模式、 总共 4*4=16种设置方案,全都无效。重启换回WINDOWS2003,32位模式IIS下运行工作正常,再换回VISTA的64位IIS,工作失败,再试试64位桌面程序加载情况:失败。

可能是编译器问题吧,以前就听说过同是一块cpu,32位模式下编译的东西是不能直接在64位下面运行的这种类似情况,打开VISTA下的VC2008,把cpp.dll重新编译为64位,然后把C#代码先复制到asp.net文件中,让它自动识别工作模式,结果是失败。再在64位环境下编译32位版本cpp.dll,再设置IIS7工作于32位模式,还是失败。死定了!

5小时的详细调试过程省略100万字……

把问题最小化,省略所有的多余代码,事件的起因找到了:

extern "C" _declspec(dllexport) char* ReadData()
{
    std::string str
= "abcd";//实际工作中,这个abcd是由数据库产生的
    
char* temp = strdup(str.c_str());
    
return temp;
    
//delete temp;
}


配对的C#代码是:

[DllImport(@"E:\project\web\test\bin\cpp.dll")]
public static extern string ReadData();

void Page_Load()
{
    Response.Write(ReadData());

}

 

这样的配对在32位环境下工作没有问题,但是在64位下的话,肯定失败,原因出在strdup()上面,可能有的朋友会问:

先 char* temp = new char[str.length()]; 行不行?  回答是32位下面行。64位不行(包括64位下面运行32位模式也不行)。那么传递Stringbuilder作为参数给C++行不行?同样,32位行,64位不行(包括64位下面运行32位模式也不行)。


那么,如果直接返回const char* 不就行了吗? 答案类似,32位下面行,64位下面虽然不出错,但是读取不到字符串。

再次经历3个小时的调试,再次省略100万字……


extern "C" _declspec(dllexport) int Test(char* build)
{
    std::
string str = "abcd";

    memcpy(
build, str.c_str(), str.length()+1);
    
return 0;
}

然后,配对的C#代码是:

[DllImport(@"E:\project\web\test\bin\cpp.dll")]
public static extern int Test(StringBuilder build);

void Page_Load()
{
StringBuilder build 
= new StringBuilder();
    Test(build);

    Response.Write(build.ToString());
}


这次的关键点在于:使用memcpy(需要保存值的指针, 值, 值的长度+1 );  进行内存复制。

搞来搞去,使用这个函数之后,一切都正确了,任何别的32位下面做的那些传统办法尝试都是徒劳的,不是“尝试读取受保护的内存”就是收到空字符串,有时候甚至是刚出炉的dll只有在第一次执行才成功,接下来就永远出错了,然后再回锅编译,再然后第一次执行成功,第2至n次执行失败,如此反复,明显就是64位系统的内存管理机制已经变完了。

把项目实际代码添加回去,终于成功了。各种模式的IIS下面运行正常,32位和64位系统的桌面程序加载也正常、 使用edong网的虚拟主机测试工作正常(他们那里是32位IIS,至今还没发现哪个虚拟主机是64位,不过以后总会有的)。

经过测试,绝对不是.net编译器的x86、x64、anycpu这些编译选项的原因,详细的就不表了。一切都是出在vc上面。

 

最后,还有一个非常重要的注意点:vc2008直接默认配置的话,无论你编译32位还是64位,弄出来的组件都是不能正确在IIS64中加载的,VC2005也一样,为此,我特意安装了VC6来编译,然后再在VC2008中升级项目,才能正确编译通过,这个步骤肯定是设置存在问题,但是目前还没有时间来查找是哪一步的设置出错。想偷懒的朋友用这个土办法就行了。

 

工作终于完成的同时,天亮了,今天是阴天,也正好,苍天蒙蒙才能雾里看花,为秘密郊游添加一份神秘色彩,既不阴暗也不强烈。我用一些色彩颜料修饰一下黑眼圈,等着紫嫣妹妹到来,一路上,听我我描述眼科大夫的人类视力进化论、智能视觉检测仪的工作原理、与微软CEO的商务对话、乾隆下江南的奇遇记、911事件的来龙去脉、调试VISTA系统的源代码工作原理等等这些惊心动魄的午夜经历,紫嫣妹妹露出了无比敬佩的赞扬:“云山苍苍,江水泱泱,先生之风,山高水长”,我羞涩地回道:“此夜曲中闻折柳,何人不起故国情”。

 

 

 

标签: .net
Add your comment

24 条回复

  1. #1楼 haha1[未注册用户]2008-12-15 08:04
    我都不会刻盘,也不会发布IIS。
     回复 引用   
  2. #2楼 haha1[未注册用户]2008-12-15 08:05
    提交出来问题,该不是和你一样的问题吧
     回复 引用   
  3. #3楼 补丁      2008-12-15 08:22
    太罗嗦了...有些东西在文章里作为点缀就可以了,有些刻意追求的感觉
     回复 引用 查看   
  4. #4楼 5yplan      2008-12-15 08:37
    @补丁
    又不是写技术文档,为什么不能这样?呵呵~
     回复 引用 查看   
  5. #5楼 G yc {Son of VB.NET}      2008-12-15 08:43
    有点乱了~~~

    托管代码调用非托管代码本来就会有一些问题。。

    而且。。 听说, X64 下不支持调用X32的程序集
     回复 引用 查看   
  6. #6楼 横刀天笑      2008-12-15 08:45
    楼主的文采可是惊天地、泣鬼神啊
     回复 引用 查看   
  7. #7楼[楼主] 梁逸晨      2008-12-15 08:50
    @G yc {Son of VB.NET}

    X64cpu工作在32位模式时,也就是运行32位系统时,不用管它是什么CPU,照着传统方法做就是了。
    但是工作于64位模式时,要做好调用环境配对:

    运行于64位的.net必须加载专门编译为64位模式的非托管代码。
    运行于32位的.net(例如64位VISTA的IIS7工作于32位模式时)要加载专门编译为32位模式的非托管代码。
     回复 引用 查看   
  8. #8楼 狼Robot      2008-12-15 09:29
    楼主文采翩翩啊.
     回复 引用 查看   
  9. #9楼 代震军      2008-12-15 09:34
    支持楼主的这种到了黄河也心不死的精神!
    对64位模式下的开发我之前还真没过多研究过,这次算是学到了不少。
    另外写做风格也不错,呵呵。
     回复 引用 查看   
  10. #10楼 Kingthy      2008-12-15 09:34
    将SQLite3.dll扔到系统PATH变量下的某个路径下就OK了(IIS有访问权).不一定要合并编译吧?
     回复 引用 查看   
  11. #11楼 上不了岸的鱼{ttzhang}      2008-12-15 09:37
    楼主写的很细心,学习了
     回复 引用 查看   
  12. #12楼 eaglet      2008-12-15 09:46
    “本着敬业态度”
    我想问一下楼主,有没有研究过为什么memcpy就可以呢?原因没找到好像总是不太踏实。我google了一下,没有找到什么理想的答案,好像有人说64 位和 32 位的内存边界不一样,但不太确定。我没有64位环境,不知道楼主能否更深入的研究研究呢?很想知道具体的原因是什么。
    非常感谢楼主的经验分享。
     回复 引用 查看   
  13. #13楼 eaglet      2008-12-15 10:15
    感觉好像和 char* 在32位环境下通常为4字节,而64位环境下为8字节造成。不过不太确定。
    楼主有没有试过用 strcpy 行不行?
     回复 引用 查看   
  14. #14楼[楼主] 梁逸晨      2008-12-15 10:21
    @代震军
    承蒙代老师过奖,这辈子算是没白活了。

    @Kingthy
    设置系统环境变量,IIS还要有访问权的话,那也只有在自己的电脑上面运行了。但是这样又会回到问题的2个本质上面:
    1 不能在虚拟主机上运行的ASP.NET,存在意义不大
    2 如果是给自己的电脑做ASP.NET,那也就完全用.NET就行了。不必要再加载WIN32。


    @eaglet
    至于为什么memcpy就可以,我在调试中尝试过在C#中使用int来接收C++的char* 指针,本来是一样的字符串值,但是每次得到的强制转换过来的数字都不一样,这就表明了每次返回的内存地址是不一样的。这时候,把这些没有规律的、不能预测的内存地址分配给.net类型的话,.net就会认为C++是在“乱搞”,于是就终止了这个行为。抛出“尝试读取受保护的内存”这样的错误。这个错误只有在64位.net上面发生,那么就可以认定:64位.net的保护机制要更保守一些。

    memcpy,注意它的参数里面有一个长度值,顾名思义,这是内存值复制,把一个值赋给另一个对象的值,而.net接受值复制的行为是无可非议的,也是它推荐的。所以,我们可以这样理解,并不是“换了memcpy就可以了",而是“仅剩下memcpy可用了".
     回复 引用 查看   
  15. #15楼[楼主] 梁逸晨      2008-12-15 10:26
    @eaglet
    strcpy试过,不行,一样的问题。还试过一个自己实现的stcdup函数,CSDN上面搜来的,执行原理是一个个字符遍历复制,然后用 std::cout << 可以实现正确屏幕打印,但是到了要让.net接收的时候就不行了,变成0长度字符串。
     回复 引用 查看   
  16. #16楼 eaglet      2008-12-15 10:36
    32位下每次返回的内存地址一样吗?应该也不一样吧?你每次调用 strdup 就会重新分配一次内存,地址怎么会一样呢?C# 是通过内存地址查找到以0结尾的字节来确定字符串的长度,所以按理说返回内存地址,c#依然可以正确读出字符串的长度,并进行类似的内存拷贝,但为什么64位下不可以?楼主认为.net 在 64位下加强了对内存读取的保护,我表示怀疑,如果这样,那不是很多windows API 都不能用了?


     回复 引用 查看   
  17. #17楼 eaglet      2008-12-15 10:38
    我是说如果这样行不行?
    extern "C" _declspec(dllexport) int Test(char* build)
    {
    std::string str = "abcd";
    strcpy(build, str.c_str());

    return 0;
    }
     回复 引用 查看   
  18. #18楼[楼主] 梁逸晨      2008-12-15 10:42
    --引用--------------------------------------------------
    eaglet: 我是说如果这样行不行?
    extern &quot;C&quot; _declspec(dllexport) int Test(char* build)
    {
    std::string str = &quot;abcd&quot;;
    strcpy(build, str.c_str());

    return 0;
    }
    --------------------------------------------------------
    这样的结果:如果那个abcd是写死在代码中的,那么可以正确返回,但是如果它是动态生成值,那么无论值是什么,返回结果是0长度字符串。
     回复 引用 查看   
  19. #19楼 !A.Z[未注册用户]2008-12-15 12:24
    弱弱的问一下,LZ用的是SQLite Data Provider吗...
     回复 引用   
  20. #20楼[楼主] 梁逸晨      2008-12-15 12:34
    @!A.Z
    SQLite Data Provider是.net中使用的Sqlite组件,而我的工作流程是:

    .net 和 win32的dll打交道

    win32的dll使用sqlite源码来和Sqlite打交道

    所以,.net 是“不知道”整个系统中存在任何数据库的。
     回复 引用 查看   
  21. #21楼 毁于随      2008-12-15 14:59
    废话太多.浪费时间.不过对画图的工具感兴趣,是用什么画的?
     回复 引用 查看   
  22. #22楼 forestcell[未注册用户]2008-12-22 17:07
    楼主的确是高手,不过有点儿不负责任。
    费的我研究了很久。都不知道自己的win64C++的DLL有什么问题。
    google一下,竟然只有这篇文章涉及到这项技术。真实佩服微软。

    dllimport这样就能够使用byte了
    [In, Out]byte[]
     回复 引用   
  23. #23楼 海边的风      2008-12-25 08:27
    文采很不错
     回复 引用 查看   
  24. #24楼[楼主] 梁逸晨      2008-12-25 16:08
    @forestcell

    回复这位朋友,如果使用 [In, Out]byte[] ,32位我没试过,但是在64位下面,我传送一个260多K的值时,会出现超出数组长度的问题。
    作为一个HTML页面,260K是很正常的值。
    所以,这种方法在本文的应用范围内是不可取的。
     回复 引用 查看