Delphi DLL(在XE中)必须处理TStringList(D2007,Ansi)

时间:2021-01-04 00:09:35

The DLL was originally written in D2007 and needed a quick, panic TStringList call (yes, it was one of those “I’m sure to regret”; though all the calls to the DLL, made by several modules, are all made by Delphi code and I wrongly presumed/hoped backwards compatibility when XE came out).

DLL最初是在D2007中编写的,需要一个快速,恐慌的TStringList调用(是的,它是其中一个“我肯定会后悔”;虽然所有对DLL的调用,由几个模块制作,都是由Delphi制作的代码和我错误地假定/希望XE出来时向后兼容)。

So now I’m moving the DLL to XE5 (& thus Unicode) and must maintain the call for compatibility. The worst case is I simply write a new DLL only for XE while keeping the old one for legacy, but feel there should be no reason why XE couldn’t deconstruct/overrride to an {ANSI} TStringList parameter. But my Delphi behind-the-scenes knowledge is not robust and a couple of attempts have not succeeded.

所以现在我将DLL移动到XE5(以及Unicode),并且必须保持调用兼容性。最糟糕的情况是我只是为XE编写一个新的DLL,同时保留旧的旧版本,但是觉得XE无法解析/覆盖到{ANSI} TStringList参数。但是我的Delphi幕后知识并不健全,而且几次尝试都没有成功。

Here is the DLL call – it takes a list of file paths and in this stripped-down code, simply adds each string to an internal list (that is all the DLL does with the parameter, a single read-only reference):

这是DLL调用 - 它采用文件路径列表,在这个精简代码中,只需将每个字符串添加到内部列表(即所有DLL都使用参数,单个只读引用):

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

What I found is that when I compiled this in XE5, that lstPaths.Count is correct, so the basic structure aligns. But the strings were garbage. It seems the mismatch would be two-fold: (a) the string content naturally is being interpreted as two-bytes per character; (b) there is no Element size (at position -10) and code page (at position -12; so yes, garbage strings). I am also vaguely aware of behind-the-scenes memory management, though I only do read-only access. But the actual string pointers themselves should be correct (??) and thus is there a way to coerce my way through?

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

So, regardless of whether I have any of that right, is there any solution? Thanks in advance.

所以,无论我是否拥有这样的权利,有什么解决方案吗?提前致谢。

2 个解决方案

#1


1  

David and Jerry already told you what you should do - re-write the DLL to do the right thing when it comes to passing interop-safe data across module boundaries. However, to answer your actual question:

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

the actual string pointers themselves should be correct (??) and thus is there a way to coerce my way through?

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

So, regardless of whether I have any of that right, is there any solution?

所以,无论我是否拥有这样的权利,有什么解决方案吗?

You can try the following. It is dangerous, but it should work, if a re-write is not an option for you at this time:

您可以尝试以下方法。这很危险,但是如果此时你不能选择重写,它应该可行:

// 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;

#2


2  

What you perhaps don't yet realise is that your code has always been wrong. In general, it is not supported to pass Delphi objects across module boundaries. You can make it work so long as you understand the implementation very well, so long as you don't call virtual methods, so long as you don't do memory allocation, so long as you use the same compiler on both sides, and probably many other reasons. Either use runtime packages (also requires same compiler on both sides), or use interop safe types (integers, floats, null terminated character arrays, pointers, records and arrays of interop safe types, etc.)

你可能还没有意识到你的代码一直都是错的。通常,不支持跨模块边界传递Delphi对象。只要你不调用虚方法,只要你不进行内存分配,只要你在两边都使用相同的编译器,你就可以使它工作得很好。可能有很多其他原因。使用运行时包(两侧也需要相同的编译器),或使用互操作安全类型(整数,浮点数,空终止字符数组,指针,记录和互操作安全类型数组等)

There's really no simple solution here. It should never have worked in the first place and if it did then you have been very unlucky. Unlucky because a much better outcome would have been a failure that would have led you to doing it properly.

这里真的没有简单的解决方案。它应该永远不会起作用,如果它确实那么你就是非常不走运。不幸的是因为一个更好的结果会导致你做得很好。

Perhaps the best thing you can do is make an adapter DLL. The architecture goes like this, from bottom to top:

也许您可以做的最好的事情是制作适配器DLL。从下到上,架构是这样的:

  • Original Delphi 2007 DLL at the bottom, with the bogus export that requires D2007 string list to be supplied.
  • 底部的原始Delphi 2007 DLL,伪造导出需要提供D2007字符串列表。

  • New adapter Delphi 2007 DLL in the middle. It calls the bogus export, and is able to supply a D2007 string list. The adapter DLL exposes a proper interface that does not require Delphi objects to be passed across the module boundary.
  • 新的适配器Delphi 2007 DLL在中间。它调用伪造导出,并能够提供D2007字符串列表。适配器DLL公开了一个不需要Delphi对象通过模块边界传递的适当接口。

  • New XE5 executable at the top. This talks to the adapter, but does so using valid interop types.
  • 顶部有新的XE5可执行文件。这与适配器对话,但使用有效的互操作类型。

#1


1  

David and Jerry already told you what you should do - re-write the DLL to do the right thing when it comes to passing interop-safe data across module boundaries. However, to answer your actual question:

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

the actual string pointers themselves should be correct (??) and thus is there a way to coerce my way through?

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

So, regardless of whether I have any of that right, is there any solution?

所以,无论我是否拥有这样的权利,有什么解决方案吗?

You can try the following. It is dangerous, but it should work, if a re-write is not an option for you at this time:

您可以尝试以下方法。这很危险,但是如果此时你不能选择重写,它应该可行:

// 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;

#2


2  

What you perhaps don't yet realise is that your code has always been wrong. In general, it is not supported to pass Delphi objects across module boundaries. You can make it work so long as you understand the implementation very well, so long as you don't call virtual methods, so long as you don't do memory allocation, so long as you use the same compiler on both sides, and probably many other reasons. Either use runtime packages (also requires same compiler on both sides), or use interop safe types (integers, floats, null terminated character arrays, pointers, records and arrays of interop safe types, etc.)

你可能还没有意识到你的代码一直都是错的。通常,不支持跨模块边界传递Delphi对象。只要你不调用虚方法,只要你不进行内存分配,只要你在两边都使用相同的编译器,你就可以使它工作得很好。可能有很多其他原因。使用运行时包(两侧也需要相同的编译器),或使用互操作安全类型(整数,浮点数,空终止字符数组,指针,记录和互操作安全类型数组等)

There's really no simple solution here. It should never have worked in the first place and if it did then you have been very unlucky. Unlucky because a much better outcome would have been a failure that would have led you to doing it properly.

这里真的没有简单的解决方案。它应该永远不会起作用,如果它确实那么你就是非常不走运。不幸的是因为一个更好的结果会导致你做得很好。

Perhaps the best thing you can do is make an adapter DLL. The architecture goes like this, from bottom to top:

也许您可以做的最好的事情是制作适配器DLL。从下到上,架构是这样的:

  • Original Delphi 2007 DLL at the bottom, with the bogus export that requires D2007 string list to be supplied.
  • 底部的原始Delphi 2007 DLL,伪造导出需要提供D2007字符串列表。

  • New adapter Delphi 2007 DLL in the middle. It calls the bogus export, and is able to supply a D2007 string list. The adapter DLL exposes a proper interface that does not require Delphi objects to be passed across the module boundary.
  • 新的适配器Delphi 2007 DLL在中间。它调用伪造导出,并能够提供D2007字符串列表。适配器DLL公开了一个不需要Delphi对象通过模块边界传递的适当接口。

  • New XE5 executable at the top. This talks to the adapter, but does so using valid interop types.
  • 顶部有新的XE5可执行文件。这与适配器对话,但使用有效的互操作类型。