在DLL中填充TStringList

3

我想在DLL中填充一个TStringList。我的方法似乎与内存管理文档不符,但它可以工作,并且不会导致错误或AV。

有人能告诉我,这段代码是否正确吗?不确定我如何在一般情况下在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 代码:

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;

我不使用BORLNDMM.DLL或任何其他ShareMem单元。
编辑: 我将aList.Add()调用扩展为aList.AddObject()。它也不会崩溃,尽管TObject在DLL中创建并在EXE中释放。
答案: 关于下面已接受答案中的注释,该代码是正确的,因为exe和dll是用相同的Delphi版本编译的,只调用虚拟方法。
结论: 只要使用虚拟方法或接口,就没有内存管理问题。这意味着,对象在哪里创建或释放都无所谓。
4个回答

4
如果你想跨模块边界传递类,那么你需要使用运行时包连接RTL/VCL。这是确保你的DLL中的TStringList类与你的EXE中的完全相同的唯一方法。这是你当前方法的根本问题。另一方面,如果你已经使用运行时包链接到RTL,则没有问题。
如果你不想使用运行时包,则需要彻底重新设计你的接口。你需要停止在模块边界传递类。你可以使用接口,但不能使用类。并且你需要控制内存分配,以确保内存总是在分配它的模块中释放。或者开始使用ShareMem

在上面的代码中,你只需要链接RTL/VCL运行时包。如果你想要在两个模块之间传递你实现的类,那么你需要将你的DLL转换为一个包。 - David Heffernan
@max,我所在的项目已经使用DLL 15年了。由于Unicode的原因,我们无法将其转换为现代Delphi。为了获得对新版Windows的良好支持,我们不得不升级到较新的DevExpress - 现在每个DLL都会增加5-6 MB的大小,因为每个DLL都携带着DevExpress的一个副本。同样的代码可能适用于一个开发人员,但对另一个开发人员来说则失败了,这时候就会发现其中一个人更改了编译器选项。如果您想要无缝集成Delphi,请使用BPLs。如果您想要使用DLL,则请使用类似COM的GUI接口(例如使用BSTR而不是string等类型)。 - Arioch 'The
使用像Spring4D这样的IoC库,并且不要将GUI放入DLL中(而是制作一个单体DLL作为所有行为定义DLL的GUI服务器)。个人而言,我更喜欢设计BPL树 :-) - Arioch 'The
@DavidHeffernan 嗯,他们确实这样做。他们传递 TSQLConnection。不要问我它是如何工作的,我猜因为在 DbExpress 后面的是基于 COM 而不是 Delphi。但尽管使用了缓慢的 BorlndMM.dll,字符串并没有被传递,而是传递 PChars。他们的 C 风格复制几乎总是存在一些问题 *StrLCopy(...,...,SizeOf(...)) 而不是 StrLCopy(...,...,Length(...)-1)*,每次赋值后都会安全地恢复每个字符串缓冲区末尾的零终止符。嗯,我现在有很大的偏见,但我强烈推荐使用 BPL,其中编译器不允许您自己开枪打自己的脚。 - Arioch 'The
@max - 谁阻止你制作自己的 BPL,其中包含所有必需的第三方组件,仅限它们?虽然我会保留标准的第三方 BPL,以便在更新大小上节省空间,因为更新不需要包含已更改重新编译的 BPL。 - Arioch 'The
显示剩余9条评论

2

对于这种类型的功能,并且为了保持无共享内存和无包依赖,我会在dll中使用一个带有枚举器方法的回调函数。例如,这就是您从Windows检索字体的方式。以下是我所指的内容的模拟:

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;

这只是为您提供一个起点... 在DLL方面,您使用回调函数指针调用到您的程序,并逐个传递字符串。


1
最好传递PChar而不是字符串通过边界。 - David Heffernan

1

如果你严肃反对BPLs,那么最好坚持使用DLL和接口的COM约定。

特别是在COM中有类似TStream的接口。而VCL有TStreamAdapter类来转换COM IStream和VCL TStream之间的数据流。

这样你的DLL应该生成一个数据流,将其包装成COM IStream并传递给exe。EXE会进行转换并从TStream中填充stringlist。

更快速和低技术的方法是感觉内存缓冲区,就像Windows API函数所做的那样。它们要么感觉到它,要么返回程序错误,要求更大的缓冲区。好吧,那么你需要调用两次函数-获取缓冲区大小和执行实际工作。如果你混合指针类型,比如PChar可能是PAnsiChar或PWideChar,或者传递错误的缓冲区大小-你没有编译器的安全网,你只是破坏了内存。但这比COM IStream更快。

也许你会创建COM启用的Buffer对象,它具有特殊类型的析构函数,不释放内存,而是将引用传递给DLL后台空闲内存回收线程。因此,当你在主EXe中不再需要它时,它迟早会在DLL本身中被释放。它仍然不像TStream使用舒适,但至少希望不会炸堆管理器。


0
从Dll库中获取字符串列表的最简单方法是: 您必须创建tStringList,然后在Dll内部填充它,然后将文本作为返回值传递。
从Dll库:
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;

来自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;

使用方法:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接