3

I want to fill a TStringList inside a DLL. My approach seems to be wrong regarding memory management documentaion, but it works and doesn't cause an error or AV.

Can someone tell me, if that code is OK? Not sure how I can fill a class in general in a DLL.

programm EXE

function MyClass_Create: IMyClass; stdcall; external ...

var
  _myClass_DLL: IMyClass; //shared interface in exe and dll

procedure FillList;
var
  list: TStringList;      
begin
  list := TStringList.Create(true); //memory allocated in EXE
  try
    _myClass_DLL.FillList(list);  //memory allocated in DLL???
    ShowMessage(list.Text);
  finally
    list.Free; //memory freed in EXE, frees also TObject created in DLL
  end;
end;

DLL Code:

library DLL

TMyClass = class(TInterfacedObject, IMyClass)
public
  procedure FillList(aList: TStringList);
end;

procedure TMyClass.FillList(aList: TStringList);
begin
  aList.AddObject('Text1', TObject.Create); //memory allocation in DLL?
  aList.AddObject('Text2', TObject.Create); //memory allocation in DLL?
end;

I don't use BORLNDMM.DLL or any other ShareMem unit.

Edit:
I expanded the aList.Add() call to aList.AddObject(). It also doesn't crash, altough the TObject is created in DLL and freed in EXE.

Answer:
Regarding the comments in the accepted answer below, that code is correct, since exe and dll are compiled with the same delphi version and only virtual methodes are invoked.

Conclusion:
As long as virtual methods or interfaces are used, there is no problem with memory management. That means, it doesn't matter where the object is created or freed.

markus_ja
  • 2,931
  • 2
  • 28
  • 36

4 Answers4

4

If you want to pass classes across module boundaries then you need to link to the RTL/VCL with runtime packages. That's the only way to make sure that the TStringList class in your DLL is the exact same one as in your EXE. That is the fundamental problem with your current approach. On the other hand, if you are already linking to the RTL with runtime packages, then you are fine.

If you don't want to use runtime packages then you need to redesign your interface completely. You would need to stop passing classes across the module boundary. You can use interfaces, but not classes. And you would need to take control of the memory allocation to ensure that memory is always deallocated in the module that allocated it. Or start using ShareMem.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Does it mean, that I have to set the flag "use runtime packages" in the project settings for the exe program? Or is it just enough to convert the dll in a package? – markus_ja Aug 02 '12 at 13:54
  • In the code above, you only need to link to the RTL/VCL runtime packages. If you wanted to pass classes that you implement between your two modules then you would need to convert your DLL into a package too. – David Heffernan Aug 02 '12 at 13:56
  • I am not familiar with packages. Can I then load the package the same way as a dll? like "function MyClass_Create: IMyClass; stdcall; external 'myPackage.bpl'" – markus_ja Aug 02 '12 at 14:13
  • No, once you've got the references set up, you just list the required units from the package in the uses clauses of source files in your exe. So you don't need to declare that function again. It's much simpler than importing from a DLL. – David Heffernan Aug 02 '12 at 14:16
  • Thanks a lot for you help! I am not quite happy with BPLs, since it produces a lot of dependecies, when 3rd party components are used. – markus_ja Aug 02 '12 at 14:40
  • @max Personally I avoid BPLs for the same reason. But I also compile all my code into a single module. That's the best way to avoid using BPLs. – David Heffernan Aug 02 '12 at 14:43
  • @max i am no at project which used DLL for 15 years. Well, transition to modern Delphi is not possible due to Unicode. To get good support for newer Windows we had to upgrade to newer DevExpress - and all our DLLs grow 5-6 MB each, for each DLL now carries a copy of DevExpress. And the same code might work for one developer and fail for another, then it turns out one of them changed compiler options. And all that. If you want seemless Delphi integration - use BPLs. If You want DLLs - then use COM-like GUI Interfaces (with types like BSTR instead of string, etc), – Arioch 'The Aug 02 '12 at 15:09
  • ... IoC libraries like Spring4D, and don't put GUI into DLLs (instead make one monolythic DLL to eb one GUI server for all the behaviour-defining DLLs). PErsonally, i'd prefer designing BPLs tree :-) – Arioch 'The Aug 02 '12 at 15:14
  • @DavidHeffernan Well, they do. They pass TSQLConnection. Don't ask me how it works, i guess because behind a hood DbExpress are COM-based more than Delphi-based. But despite of pervasive using of slow BorlndMM.dll, strings are not passed, but PChars instead. And their C-style copying is almost always off-by-one *StrLCopy(...,...,SizeOf(...)) instead of StrLCopy(...,...,Length(...)-1)* with for-safety reviving zero-terminator at end of each string buffer after each assignment. Well, i am heavily biased now, but i strongly prefer BPL, where ocmpiler would not allow you to shoot your own foot. – Arioch 'The Aug 02 '12 at 15:18
  • @max - who prevents you from making your own BPL containing all neeeded 3rd party components and only them ? Though i'd keep standard 3rd-party BPLs and then saved on update size, for update would not have to need contain changed recompiled BPLs – Arioch 'The Aug 02 '12 at 15:20
  • I found an interesting articel from Mastering Delphi 3. There is a sample, where an object is created in a dll, but freed in the exe. Marco explaned it with the virtual destructor. http://www.marcocantu.com/md3/md3c21.htm (Using a Class from a DLL). Now, I am confused. I thought objects must be destroyed in the module where they are created. Can anyone explaine? – markus_ja Aug 03 '12 at 08:35
  • @max By their nature, virtual methods don't fall foul of module boundary problems, so long as the vtable format used by the compilers is the same for your two modules. The reason is that the vtable points to the code in the module that created the object. Now, if you call a non-virtual method then that's code in the module that makes the call. And that's where the problem is. This also explains why interfaces don't have problems over module boundaries. Again, they have an extra layer of indirection that routes a method call to the code in the module that owns the implementing object. – David Heffernan Aug 03 '12 at 08:43
  • @David: Does it mean, as long as I use virtual functions, I can use classes without memory manager conflicts? That will explain, why my example doesn't crash, since TStringList.Add is a virtual function and is invoked in the exe module? – markus_ja Aug 03 '12 at 09:13
  • That's correct. So long as you use virtual functions, and the vtable layout is the same for both modules (i.e. compiled with the same version of Delphi). – David Heffernan Aug 03 '12 at 09:18
2

For this type of functionality, and to remain share-mem-free, package-free, I would use a callback with an enumerator method in the dll. This is how you retrieve the fonts from windows for example. Here is a mock up of what I'm referring to:

type
  TMyClass = class
  private
    FList: TStringList;  // obv you need to construct this

  public
    function EnumListItem(s: string): integer;
  end;

function TMyClass.EnumListItem(s: string): integer;
begin
  FList.Add(s);
end;

procedure TMyClass.FillList;
begin
  _myClass_DLL.FillList(@EnumListItem);
  ShowMessage(FList.Text);
end;

This is just to give you a starting point.... on the DLL side you use call back into your program using the function pointer and pass in the strings 1 at a time.

GDF
  • 830
  • 5
  • 11
1

If you seriously oppose to BPLs then you'd better stick with COM conventions of DLLs and Interfaces.

In particular there are TStream-like interface in COM. And VCL has TStreamAdapter class to convert between COM IStream and VCL TStream.

That way your DLL should make a data stream, wrap it into COM IStream and pass to exe. EXE would convert back and fill stringlist from TStream.

More fast and lo-tech approach would be to feel memory buffers, like Windows API functions do. They either do feel it, or return program error asking for buffer of larger size. Well, then you would call function twice - to get buffer size and to do actual work. And if you mix the pointer types like PChar may be PAnsiChar or PWideChar, or pass wrong buffer size - you have no safety net from compiler, you just corrupted memory. But that would be faster than COM IStream.

Maybe you'd make COM-enabled Buffer object, which has special kind of destructor not freeing memory, but passing reference to DLL background idle memory recollection thread. So when you no more need it in main EXe it would sooner or later be freed in DLL itself. It is still not so comfort to use as TStream, but at least would hopefully not blow Heap manager.

Arioch 'The
  • 15,799
  • 35
  • 62
0

The easiest way to get a string list from a dll is that way: you have to create tStringList then fill it insside the Dll, then pass the text as return.

From Dll library:

function getDocuments(customer,depot:Pchar):Pchar;export;
var
 s:TstringList;
begin
 S:=TStringList.Create;
 S.Add('Row 1 '+customer);
 S.Add('Row 2 '+depot);
 S.Add('Row 3 ');
 S.Add('Row 4 ');
 S.Add('Row 5 ');
 Result:=pchar(s.Text);
 S.Free;
end;

From the EXE

function GetDLLExternalDocuments(customer,depot:pchar;out fList:TStringList):Word;
var GetDocumentsDLLExport:TGetDocumentsDLLExport;
var s:String;
var HandleDllExport :Thandle;
begin

  HandleDllExport := LoadLibrary('my_dll_library.dll');

  if HandleDllExport <> 0 then
   begin
    @GetDocumentsDLLExport := GetProcAddress(HandleDllExport, 'getDocuments');
    if @GetDocumentsDLLExport <> nil then
      begin
        s:=GetDocumentsDLLExport(cliente,impianto);
        fList.Text:=S;
        result:=0;
      end;


    FreeLibrary(HandleDllExport);
    HandleDllExport:=0;

   end;

end;

Usage:

procedure TfMain.Button1Click(Sender: TObject);
var
 S:tStringList;
begin
  S := tStringList.create;
  GetDLLExternalDocuments('123456','AAAAA',S);
  Showmessage(S.Text);
  s.Free; 
end;
Gianluca Colombo
  • 717
  • 17
  • 38