-1

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).

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.

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):

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?

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

RodD
  • 1
  • 2
  • 2
    It only worked in the first place just by a lot of luck. Objects should never be passed across DLL boundaries. – Jerry Dodge Jun 30 '15 at 22:02
  • 1
    You'll only get away with that if both the EXE and DLL were built using precisely the same compiler version, as well as some other compiling options. – Jerry Dodge Jun 30 '15 at 22:11
  • Thanks Jerry for strong adage against doing this in the 1st place - fully agree and is good to have here for any future readers. – RodD Jul 01 '15 at 19:16
  • @Jerry: It can work across different versions, but it is simply not something you can depend on. If it works, then the programmer has been lucky. – Rudy Velthuis Jul 02 '15 at 15:27
  • If it works, the programmer has been unlucky – David Heffernan Jul 02 '15 at 22:35

2 Answers2

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.)

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:

  • Original Delphi 2007 DLL at the bottom, with the bogus export that requires D2007 string list to be supplied.
  • 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.
  • New XE5 executable at the top. This talks to the adapter, but does so using valid interop types.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks David, - a good list of constraints and basically why one should never pass an object to a DLL. Thus good to call me out on the myopic "short-term kludge", even in a panic bind, that simply shifts the pain down the line! – RodD Jul 01 '15 at 20:18
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:

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;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Great, Remy - tried it and it works! I appreciated everyone's "Don't do this in 1st place!" - It is what was in my head at the time as well – RodD Jul 01 '15 at 20:19
  • Whoops, here is rest of comment: . . . - It was in my head at the time as well, panic aside, and needs to be clearly stated here. But really appreciate you going that extra step and giving an answer to this specific circumstance! - it seemed all the critical elements were there, and my exact case was straightforward with none of the caveats that people rightly mentioned. I am actually a pretty seasoned programmer, just not so much in Delphi. – RodD Jul 01 '15 at 20:39
  • In my view this just stores up more pain for the future. The next pain point is then a future Delphi version changes the layout of the string list class. I'm not sure why you want to continue storing up pain when you can solve it properly once and for all. Either with a recompile of the dll or with an adapter. – David Heffernan Jul 02 '15 at 06:08
  • David, good point to note (the object structure could change which would torpedo this). And I did look at your adapter idea which would have been an interesting long-term strategy. In this specific case, I just need short- or medium-term. I long-ago wrote a proper entry & thus only need to wait for any legacy modules that clients have (& thus call this legacy entry) to be upgraded, at which point nothing will call it - so I only needed to get into XE5 as it will be enough to bridge this gap. – RodD Jul 02 '15 at 20:00