Delphi 2009应用程序中使用Delphi 2007 DLL返回的AnsiString值

4

我有一个使用D2007编译的DLL,其中包含返回AnsiStrings的函数。

我的应用程序是在D2009中编译的。当它调用AnsiString函数时,会得到垃圾数据。

我创建了一个小型的测试应用程序/DLL来进行实验,并发现如果应用程序和DLL都使用相同版本的Delphi(无论是2007还是2009)进行编译,则没有问题。但是当一个编译于2009年,另一个编译于2007年时,我得到了垃圾数据。

我尝试在两个项目中都包含最新版本的FastMM,但即使如此,2009年的应用程序也无法从2007年的DLL中读取AnsiStrings。

这里出了什么问题?有没有办法解决这个问题?


抱歉,我应该发布一些代码。只是为了明确: 接口将返回值声明为“AnsiString”,而不是“string”。据我所知,这与Unicode字符串转换没有直接关系。 - TrespassersW
5
如果你应该发布一些代码,那就放心地发布吧。你可以编辑自己的问题。 - Rob Kennedy
6个回答

11

Delphi 2007与Delphi 2009之间的AnsiStrings内部结构发生了变化。(不用担心,这种可能性自Day 1以来就存在了。) Delphi 2009字符串维护一个数字,指示其数据所在的代码页。

建议你像地球上其他DLL一样做,传递函数可以填充的字符缓冲区。调用者应该传递一个缓冲区指针和一个表示缓冲区大小的数字。(确保你清楚你是按字节还是按字符计算大小。) DLL函数填充缓冲区,写入不超过给定大小的内容,并计算出终止的空字符。

如果调用者不知道缓冲区应该有多少字节,则有两个选项:

  • 当输入缓冲区指针为null时,让DLL表现得特殊一些。在这种情况下,它返回所需的大小,以便调用者可以分配相应的空间并再次调用函数。

  • 让DLL为自己分配空间,并提供预定方法以便调用者稍后释放缓冲区。DLL可以导出一个函数以释放它分配的缓冲区,或者您可以指定一些双方都可用的API函数供调用者使用,例如GlobalFree。您的DLL必须使用相应的分配API,例如GlobalAlloc。(不要使用Delphi内置的内存分配函数,例如GetMemNew;即使是用同一种语言编写的调用方的内存管理器也无法保证知道如何调用FreeDispose,即使它们使用相同的Delphi版本。)

此外,编写只能被单个语言使用的DLL是自私的。以与Windows API相同的风格编写您的DLL,就不会出错。


知道2009年改变了ansistring的定义是好事,但重写DLL会使得移植到2009年更加繁琐,考虑到涉及的代码量,这可能会阻碍我们公司移植到2009年的努力。还有其他解决方法吗? - Zartog
2
@Zartog:这与移植到Delphi 2009无关,而是与在DLL API中使用不当的数据类型有关。尝试从.NET或其他语言使用DLL != Delphi将导致类似的问题。即使没有移植到Delphi 2009,清理代码库也是一个好主意。例如,请参见“技术债务”http://martinfowler.com/bliki/TechnicalDebt.html - mghie

2

好的,所以我还没有尝试过,因此需要在这上面加上免责声明。

在帮助查看器中,查看主题(Unicode in RAD Studio)ms-help://embarcadero.rs2009/devcommon/unicodeinide_xml.html

将Delphi 2007字符串返回到Delphi 2009时,您应该会遇到两个问题。

首先是Rob提到的代码页。您可以通过声明另一个AnsiString并调用StringCodePage来设置它。然后通过调用SetCodePage将其分配给旧的AnsiString。那应该可以解决问题,但如果不行,仍有希望。

第二个问题是元素大小,这将是完全疯狂的东西。它应该为1,所以将其设置为1。这里的问题是没有SetElementSize函数可供借鉴。

尝试这个:

var
  ElemSizeAddr: PWord; // Need a two-byte type
  BrokenAnsiString: AnsiString; // The patient we are trying to cure
...
  ElemSizeAddr := Pointer(PAnsiChar(BrokenAnsiString) - 10);
  ElemSizeAddr^ := 1; // The size of the element

就这样吧!

现在,如果StringCodePage/SetCodePage的事情没有起作用,你可以执行与上面相同的操作,将获取地址的那一行更改为减去12,而不是10。

它被随意地修改了,这就是我喜欢它的原因。

最终要说的是 - 根据你如何返回AnsiString(函数结果、输出参数等),你可能需要先将字符串分配给另一个AnsiString变量,以确保没有内存被覆盖的问题。

你最终需要移植那些DLL,但这使得移植更加可管理。


SetCodePage 的想法行不通。DLL 返回的内存没有分配空间来保存代码页,因此 EXE 无法设置它。DLL 返回的值在 EXE 看来根本不是 AnsiString,因此在其上使用任何 AnsiString 操作都是无效的。从返回的地址中减去 10 将使您进入未分配的内存。它根本不属于字符串。(好吧,它被分配了,但它被内存管理器用于簿记。) - Rob Kennedy
请注意我的回答中的“最后一句话”。从DLL返回值,将其赋值给另一个已分配内存但接收到垃圾数据的AnsiString。然后修复这个第二个AnsiString。这次我表达得更好了吗? - Cobus Kruger
字符串赋值只会增加引用计数,除非它还尝试在源代码页和目标代码页之间进行转换(但请记住,源代码页是垃圾)。如果您将其分配给RawByteString,然后调用UniqueString,则可能能够获得有效的字符串。但您仍然无法释放源字符串。EXE将从指针中减去错误的数量以获取基地址,因此EXE的内存管理器将尝试释放错误的值。 - Rob Kennedy
Rob: 你说得对-我确实错过了那个。但不是一切都完了。像这样做: var MyNewStr:AnsiString; MyNewStr:=UniqueString(BrokenAnsiString);然后修复MyNewStr。我说过未经测试和大免责声明,呵呵?TrespassersW: 是的,越需要它,黑客就变得越糟糕。当然,你可以为导入创建一组重载,并以此减轻痛苦。这有多大帮助取决于函数数量及其定义方式。另一种选择是大量手工劳动,但这可能是正确的方式。 - Cobus Kruger
哦,关于释放DLL返回的字符串,还有一件事情。如果引用计数为1,你可能可以自己做。最终它仍然只是一个内存块。但是,是的,这使得黑客行为更加恶劣。 - Cobus Kruger
显示剩余2条评论

0

这里提供一个快速的解决方案:如果您从dll返回的实际数据在字符串中不超过255个字符,您可以更改in-dll和接口声明以使用ShortString,这将适用于2007/2009版本。由于您已经在2007年使用了AnsiString而没有代码页标识符,因此Unicode不会给您带来任何麻烦。

如果您选择这种方式,您只需要更改声明,例如:

function MyStringReturningFunction : ShortString ; external 'MyLibrary.dll';

对于dll中的函数,返回字符串的函数应该是function MyStringReturningFunction : ShortString;

当然,输入/输出参数也是如此:

procedure MyStringTakingAndReturningFunction(s1:ShortString; var s2:ShortString); external 'MyLibrary.dll';

应该比改变很多代码容易。但是要小心,正如我所说,您的数据不能超过255个字符,因为这是ShortString可以容纳的最大大小。


0
你的 DLL 应该避免返回 AnsiString 值。如果 DLL 和 EXE 都使用 ShareMem 单元进行编译,那么这种方法才能正常工作,即使如此,也仅限于它们使用相同版本的 Delphi 进行编译。据我所知,D2007 的内存管理器与 D2009(或任何其他跨版本使用内存管理器)不兼容。

FastMM已经使ShareMem过时了。自从它被纳入Delphi后,您就可以在DLL之间共享AnsiStrings而无需使用ShareMem。我并没有决定这些DLL的API。因此,所有关于多年前其他人应该做出不同选择的评论并没有真正帮助到我。我感谢他们的意图,但我更关心的是解决方案。 - TrespassersW
需要考虑的一件事是,Delphi集成了一个默认构建的FastMM,并应用了默认设置。用户可以使用完全构建的FastMM来覆盖它,该构建已根据用户的需求进行了定制。如果DLL和EXE没有使用相同的FastMM构建,那么这将如何影响它们之间共享内存的能力? - Remy Lebeau
如果您包含自己的FastMM,那么有一个向后兼容的选项可以设置,以确保它仍然可以与内置的FastMM一起使用。但是,如果您的应用程序或dll使用了自己的FastMM而没有设置该选项,则会出现问题。 - TrespassersW

0

我同意Rob和Remy的观点:通用的DLL应该返回PAnsiChar而不是AnsiStrings。

如果使用D2009编译的DLL可以正常工作,为什么不停止使用D2007编译它,一劳永逸地开始使用D2009编译呢?


你误解了。这些DLL(有数百个)尚未转换。转换它们将是一项巨大的任务。 - TrespassersW

0

你可能只需要将DLL转换为2009版本。根据Embarcadero的说法,转换到2009版本是“容易”的,不应该花费你太多时间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接