处理此字符串问题的优雅方式。 (Unicode-PAnsiString问题)

时间:2009-09-20 22:42:37

标签: delphi unicode

考虑以下情况:

type 
PStructureForSomeCDLL = ^TStructureForSomeCDLL;
TStructureForSomeCDLL = record 
  pName: PAnsiChar;
end

function FillStructureForDLL: PStructureForSomeDLL;
begin
  New(Result);
  // Result.pName := PAnsiChar(SomeObject.SomeString);  // Old D7 code working all right
  Result.pName := Utf8ToAnsi(UTF8Encode(SomeObject.SomeString));  // New problematic unicode version
end;

...code to pass FillStructureForDLL to DLL...

unicode版本中的问题是,现在涉及的字符串转换会在堆栈上返回一个新字符串,并在FillStructureForDLL调用结束时回收,从而使DLL损坏数据。在旧的D7代码中,没有中间转换函数,因此没有问题。

我目前的解决方案是下面的转换器功能,这是IMO太多的黑客攻击。是否有一种更优雅的方式来实现相同的结果?

var gKeepStrings: array of AnsiString;

{ Convert the given Unicode value S to ANSI and increase the ref. count 
  of it so that returned pointer stays valid }
function ConvertToPAnsiChar(const S: string): PAnsiChar;
var temp: AnsiString;
begin
  SetLength(gKeepStrings, Length(gKeepStrings) + 1);
  temp := Utf8ToAnsi(UTF8Encode(S));
  gKeepStrings[High(gKeepStrings)] := temp; // keeps the resulting pointer valid 
                                            // by incresing the ref. count of temp.
  Result := PAnsiChar(temp);
end;

4 个答案:

答案 0 :(得分:3)

一种方法可能是在成为问题之前解决问题,我的意思是通过修改SomeObject的类来维护SomeString的ANSI编码版本(ANSISomeString?)和原始问题SomeString,将两者保持在SomeString属性的“setter”中(使用与您已经在进行的相同的UTF8> ANSI转换)。

在非Unicode版本的编译器中,使ANSISomeString只是SomeString字符串的“副本”,它当然不是副本,只是对SomeString的额外引用计数。在Unicode版本中,它引用了与原始SomeString具有相同“生命周期”的单独ANSI编码。

procedure TSomeObjectClass.SetSomeString(const aValue: String);
begin
  fSomeString := aValue;

{$ifdef UNICODE}
  fANSISomeString := Utf8ToAnsi(UTF8Encode(aValue));
{$else}
  fANSISomeString := fSomeString;
{$endif}
end;

在你的FillStructure ...函数中,只需更改你的代码以引用ANSISomeString属性 - 这完全独立于是否编译Unicode。

function FillStructureForDLL: PStructureForSomeDLL;
begin
  New(Result);
  result.pName := PANSIChar(SomeObject.ANSISomeString);
end;

答案 1 :(得分:2)

至少有三种方法可以做到这一点。

  1. 您可以更改SomeObject的类 定义使用AnsiString 而不是字符串
  2. 你可以 使用转换系统来持有 引用,例如你的例子。
  3. 您可以初始化result.pname 用GetMem并复制结果 转换为result.pname^ Move。只记得FreeMem吧 当你完成了。
  4. 不幸的是,它们都不是完美的解决方案。因此,请查看选项并确定哪一个最适合您。

答案 2 :(得分:2)

希望您的应用程序中已经有代码可以正确处理New() FillStructureForDLL()中所有动态分配的记录。我认为这段代码非常可疑,但我们假设这是减少代码以仅演示问题。无论如何,你传递记录实例的DLL并不关心内存块有多大,它只会得到指向它的指针。因此,您可以自由地增加记录的大小,以便为Pascal字符串提供位置,该字符串现在是Unicode版本中堆栈上的临时实例:

type 
  PStructureForSomeCDLL = ^TStructureForSomeCDLL;
  TStructureForSomeCDLL = record 
    pName: PAnsiChar;
    // ... other parts of the record
    pNameBuffer: string;
  end;

功能:

function FillStructureForDLL: PStructureForSomeDLL;
begin
  New(Result);
  // there may be a bug here, can't test on the Mac... idea should be clear
  Result.pNameBuffer := Utf8ToAnsi(UTF8Encode(SomeObject.SomeString));
  Result.pName := Result.pNameBuffer;
end;

BTW:如果传递给DLL的记录是调用DLL函数的过程或函数中的堆栈变量,那么你甚至不会遇到这个问题。在这种情况下,如果必须传递多个PAnsiChar,则只有Unicode版本才需要临时字符串缓冲区(否则转换调用将重用临时字符串)。请考虑相应地更改代码。

修改

你写评论:

  

如果修改DLL结构是一个选项,这将是最佳解决方案。

您确定无法使用此解决方案吗?关键是从DLL的POV开始,结构根本不会被修改。也许我没有说清楚,但DLL 关心传递给它的结构是否正是声明的结构。它将传递一个指向结构的指针,这个指针需要指向一个至少与结构一样大的内存块,并且需要具有相同的内存布局。但是,它可以是比原始结构更大的内存块,并包含其他数据。

这实际上在Windows API中的很多地方使用过。你有没有想过为什么Windows API中有结构包含第一个给出结构大小的序数值?这是API演进的关键,同时保留了向后兼容性。每当需要新信息使API函数工作时,只需附加到现有结构,并声明结构的新版本。请注意,保留了旧版本结构的内存布局。 DLL的旧客户端仍然可以调用新函数,该函数将使用结构的size成员来确定调用哪个API版本。

在您的情况下,就DLL而言,不存在不同版本的结构。但是,如果真实结构的内存布局得以保留,并且附加数据仅附加,则可以自由地为应用程序声明它大于实际应用程序。唯一不适用的情况是结构的最后一部分是一个大小不一的记录,类似于Windows BITMAP结构 - 一个固定的标题和动态数据。但是,您的记录看起来有固定长度。

答案 3 :(得分:-1)

PChar(AnsiString(SomeObject.SomeString))不会工作吗?