【问题标题】:Delphi DLL (in XE) must handle TStringList (D2007, Ansi)Delphi DLL(在 XE 中)必须处理 TStringList(D2007,Ansi)
【发布时间】:2015-06-30 21:59:35
【问题描述】:

DLL 最初是在 D2007 中编写的,需要一个快速、恐慌的 TStringList 调用(是的,这是“我一定会后悔的”之一;尽管由几个模块进行的所有对 DLL 的调用都是由 Delphi 代码制作,当 XE 出现时我错误地假设/希望向后兼容)。

所以现在我将 DLL 移动到 XE5(以及因此 Unicode)并且必须保持调用以保持兼容性。最坏的情况是我只是为 XE 编写了一个新的 DLL,而将旧的保留为旧版,但我觉得 XE 没有理由不能解构/覆盖到 {ANSI} TStringList 参数。但是我的 Delphi 幕后知识并不强大,几次尝试都没有成功。

这里是 DLL 调用——它需要一个文件路径列表,并且在这个精简的代码中,只需将每个字符串添加到一个内部列表(这就是 DLL 对参数所做的所有事情,一个只读引用) :

function ViewFileList ( lstPaths: TStringList): Integer; Export; Stdcall;
begin
      for iCount := 0 to lstPaths.Count - 1 do
         lstInternal.Add(lstPaths.strings[iCount]);
end;

我发现,当我在 XE5 中编译时, lstPaths.Count 是正确的,所以基本结构对齐。但是琴弦是垃圾。似乎不匹配有两个方面:(a)字符串内容自然被解释为每个字符两个字节; (b) 没有元素大小(位置-10)和代码页(位置-12;所以是的,垃圾字符串)。我也隐约知道幕后的内存管理,虽然我只做只读访问。但是实际的字符串指针本身应该是正确的(??),因此有没有办法强制我通过?

那么,不管我是否有任何权利,有什么解决办法吗?提前致谢。

【问题讨论】:

  • 它只是靠运气才开始工作的。对象不应跨越 DLL 边界。
  • 只有当 EXE 和 DLL 都使用完全相同的编译器版本以及其他一些编译选项构建时,您才能摆脱这种情况。
  • 感谢 Jerry 强烈反对一开始就这样做的格言 - 完全同意,并且很高兴在这里为任何未来的读者提供。
  • @Jerry:它可以跨不同的版本工作,但它根本不是你可以依赖的东西。如果成功了,那么程序员已经很幸运了。
  • 如果能成功,说明程序员倒霉了

标签: delphi dll unicode ansi tstringlist


【解决方案1】:

您可能还没有意识到您的代码一直都是错误的。一般来说,不支持跨模块边界传递 Delphi 对象。你可以让它工作,只要你非常了解实现,只要你不调用虚方法,只要你不做内存分配,只要两边使用相同的编译器,并且可能还有很多其他原因。要么使用运行时包(双方也需要相同的编译器),要么使用互操作安全类型(整数、浮点数、空终止字符数组、指针、记录和互操作安全类型数组等)

这里真的没有简单的解决方案。它一开始就不应该起作用,如果它起作用了,那么你就很不走运了。不走运,因为如果失败会导致你正确地完成它,那么更好的结果就是失败。

也许您能做的最好的事情就是制作一个适配器 DLL。架构从下到上是这样的:

  • 底部是原始的 Delphi 2007 DLL,需要提供 D2007 字符串列表的虚假导出。
  • 中间有新的适配器Delphi 2007 DLL。它调用虚假导出,并且能够提供 D2007 字符串列表。适配器 DLL 公开了一个适当的接口,不需要跨模块边界传递 Delphi 对象。
  • 顶部有新的 XE5 可执行文件。这会与适配器对话,但会使用有效的互操作类型。

【讨论】:

  • 感谢大卫, - 一个很好的约束列表,基本上为什么不应该将对象传递给 DLL。因此,即使在恐慌的情况下,也可以将我称为短视的“短期组合”,这只是将痛苦转移到线下!
【解决方案2】:

David 和 Jerry 已经告诉过您应该做什么 - 在跨模块边界传递互操作安全数据时,重写 DLL 以做正确的事情 .但是,要回答您的实际问题:

实际的字符串指针本身应该是正确的 (??),因此有没有办法强制我通过?

那么,不管我是否有任何权利,有什么解决办法吗?

您可以尝试以下方法。它是危险的,但它应该起作用,如果此时重写不是你的选择:

// the ASSUMPTION here is that the caller has been compiled in D2007 or earlier,
// and thus is passing an AnsiString-based TStringList object.  When this DLL is
// compiled in Delphi 2009 or later, TStringList is UnicodeString-based instead,
// so we have to re-interpret the data a little.
//
// The basic structure of TStringList itself should be the same, just the string
// content is different.  For backwards compatibility, the refcnt and length
// fields of the StrRec record found in every AnsiString/UnicodeString payload
// are still at the same offsets. Delphi 2009 added some new fields, but we can
// ignore those here.
//
// Of course, XE is the version that removed the RTL support code for the {$STRINGCHECKS}
// compiler directive, which handled all of these details in Delphi 2009 and 2010
// when users were first migrating to Unicode.  But in XE, we'll have to deal with
// it manually.
//
// These assumptions may change in future versions, but lets deal with that if/when
// the time comes...

function ViewFileList ( lstPaths: TStringList): Integer; Export; Stdcall;
{$IFDEF UNICODE}
var
  tmp: AnsiString;
{$ENDIF}
begin
  for iCount := 0 to lstPaths.Count - 1 do
  begin
    {$IFDEF UNICODE}

    // the DLL is being compiled in Delphi 2009 or later...
    //
    // the Length(String) function simply returns the value of the string's
    // StrRec.length field, which fortunately is in the same location in
    // both pre-2009 AnsiString and 2009+ AnsiString/UnicodeString, and in
    // this case will reflect the number of AnsiChar elements in the source
    // AnsiString.  We cannot simply typecast a "UnicodeString" directly to
    // a PAnsiChar, nor can we typecast a PWideChar to a PAnsiChar, but we
    // can typecast a string to a Pointer first and then cast that to a
    // PAnsiChar.  This code is assuming that it can safely get a pointer to
    // the source AnsiString's underlying character data to make a local
    // copy of it that can then be added to the internal list normally.
    //
    // Where this MIGHT fail is if the source AnsiString contains a reference
    // to a string literal (StrRec.refcnt=-1) for its character data, in
    // which case the RTL will try to copy the character data when assigning
    // the source string to a variable, such as the one the compiler is
    // likely to generate for itself to receive the TStringList.Strings[]
    // property value before it can be casted to a Pointer.  If that happens,
    // this is likely to crash when the RTL tries to copy too many bytes from
    // the source AnsiString!  You can use the StringRefCount() function to
    // detect that condition and do something else, if needed.
    //
    // But, if the source AnsiString is a normal allocated string (the usual
    // case), then this should work OK.  Even with the compiler-generated
    // variable in play, the compiler should simply bump the reference count
    // of the source AnsiString, without affecting the underlying character
    // data, just long enough for this code to copy the data and release the
    // reference count...
    //
    SetString(tmp, PAnsiChar(Pointer(lstPaths.strings[iCount])), Length(lstPaths.strings[iCount]) * SizeOf(AnsiChar));
    lstInternal.Add(tmp);

    {$ELSE}

    // the DLL is being compiled in Delphi 2007 or earlier, so just add the
    // source AnsiString as-is and let the RTL do its work normally...
    //
    lstInternal.Add(lstPaths.strings[iCount]);

    {$ENDIF}
  end;
end;

【讨论】:

  • 太好了,Remy - 试过了,效果很好!我很欣赏每个人的“不要在第一名这样做!” - 这也是我当时的想法
  • 哎呀,这里是评论的其余部分:。 . . - 当时我也在脑海中,除了恐慌,需要在这里明确说明。但是非常感谢您采取额外的步骤并为这种特定情况提供答案! - 似乎所有关键元素都在那里,我的确切案例很简单,没有人们正确提到的警告。我实际上是一个经验丰富的程序员,只是在 Delphi 中没有那么多。
  • 在我看来,这只会为未来储存更多的痛苦。下一个痛点是未来的 Delphi 版本会更改字符串列表类的布局。我不知道当你能一劳永逸地解决它时,你为什么要继续储存痛苦。重新编译 dll 或使用适配器。
  • 大卫,请注意(对象结构可能会改变,这会破坏这一点)。我确实看过你的适配器想法,这将是一个有趣的长期策略。在这种特定情况下,我只需要短期或中期。我很久以前写了一个正确的条目,因此只需要等待客户拥有的任何遗留模块(并因此称为这个遗留条目)被升级,此时没有任何东西会调用它 - 所以我只需要进入 XE5这足以弥补这一差距。
猜你喜欢
  • 1970-01-01
  • 2011-11-03
  • 2023-03-18
  • 2011-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多