当结果来自 DLL 时,如何确定 DLL 调用的缓冲区大小

How to determine the size of a buffer for a DLL call when the result comes from the DLL

同时使用 Delphi 10.2 Tokyo 和 Delphi XE2。

我有一个 DLL,它 post 将 XML 数据发送到站点。 DLL 使用 Delphi 10 构建,以便使用 TLS 1.2,Delphi XE2 不可用。

对 DLL 的调用来自 Delphi XE2 EXE,但我认为这无关紧要,但我还是注意到了。

对站点post数据的调用往往会return文本数据。有时是非常大量的文本数据。超过 150K 个字符。

我原来的 DLL 约定基本上是不正确的,因为我 return 将 return 文本数据的内容编辑为 PChar。在我在这里和其他地方的阅读中,这是一个很大的禁忌。

在我开始获取大量数据之前,"bad" 方法一直很有效 returned。我对其进行了测试,但它在任何超过 132,365 个字符的情况下都失败了。

我重构了我的 DLL 并调用代码将缓冲区作为 PChar 传递给填充,但我在尝试填充输出值时遇到错误!

其次,由于我不知道 returned 数据有多大,我如何指定从我的调用方法中填充多大的缓冲区?

出现错误的 DLL 代码:

library TestDLL;

uses
  SysUtils,
  Classes,
  Windows,
  Messages,
  vcl.Dialogs,
  IdSSLOpenSSL, IdHTTP, IdIOHandlerStack, IdURI,
  IdCompressorZLib;

{$R *.res}

function PostAdminDataViaDll(body, method, url: PChar; OutData : PChar; OutLen : integer): integer; stdcall
var HTTPReq : TIdHTTP;
var Response: TStringStream;
var SendStream : TStringStream;
var IdSSLIOHandler : TIdSSLIOHandlerSocketOpenSSL;
var Uri : TIdURI;
var s : string;
begin
  Result := -1;
  try
    HTTPReq := TIdHTTP.Create(nil);
    IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
    IdSSLIOHandler.SSLOptions.Mode := sslmClient;
    IdSSLIOHandler.SSLOptions.SSLVersions := [sslvTLSv1_2, sslvTLSv1_1];
    if Assigned(HTTPReq) then begin
      HTTPReq.Compressor := TIdCompressorZLib.Create(HTTPReq);
      HTTPReq.IOHandler := IdSSLIOHandler;
      HTTPReq.ReadTimeout := 180000;//set read timeout to 3 minutes
      HTTPReq.Request.ContentType := 'text/xml;charset=UTF-8';
      HTTPReq.Request.Accept := 'text/xml';
      HTTPReq.Request.CustomHeaders.AddValue('SOAPAction', 'http://tempuri.org/Administration/' + method);
      HTTPReq.HTTPOptions := [];
    end;
    SendStream := TStringStream.Create(Body);
    Response := TStringStream.Create(EmptyStr);
    try
      HTTPReq.Request.ContentLength := Length(Body);

      Uri := TiDUri.Create(url);
      try
        HTTPReq.Request.Host := Uri.Host;
      finally
        Uri.Free;
      end;

      HTTPReq.Post(url + 'admin.asmx', SendStream,Response);

      if Response.Size > 0 then begin
        if assigned(OutData) then begin
          s := Response.DataString;// Redundant? Probably can just use Response.DataString?
          StrPLCopy(OutData, s, OutLen);// <- ACCESS VIOLATION HERE
          //StrPLCopy(OutData, s, Response.Size);// <- ACCESS VIOLATION HERE
          Result := 0;
        end;
      end
      else begin
        Result := -2;
      end;
    finally
      Response.Free;
      SendStream.Free;
      IdSSLIOHandler.Free;
      HTTPReq.Free;
    end;
  except
    on E:Exception do begin
      ShowMessage(E.Message);
      Result := 1;
    end;
  end;
end;

exports
  PostAdminDataViaDll;

begin
end.

我的调用方法代码:

function PostAdminData(body, method, url : string): IXMLDOMDocument;
type
   TMyPost = function (body, method, url: PChar; OutData : PChar; OutLen : integer): integer; stdcall;
var Handle : THandle;
var MyPost : TMyPost;
var dataString : string;
var returnData : string;
begin
  if not (FileExists(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL')) then begin
    Application.MessageBox(pchar('Unable to find TestDLL.DLL.'), pchar('Error posting'),MB_ICONERROR + MB_OK);
    Exit;
  end;

  dataString := EmptyStr;
  returnData := '';

  Handle := LoadLibrary(PChar(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL'));
  if Handle <> 0 then begin
    try
      try
        MyPost := GetProcAddress(Handle, 'PostAdminDataViaDll');
        if @MyPost <> nil then begin
          // NOTE 32767 is not big enough for the returned data! Help!
          if MyPost(PChar(body), PChar(method), PChar(url), PChar(returnData), 32767) = 0 then begin
            dataString := returnData;
          end;
        end;
      except
      end;
    finally
      FreeLibrary(Handle);
    end;
  end
  else begin
    Application.MessageBox(pchar('Unable to find TestDLL.DLL.'), pchar('Error posting'),MB_ICONERROR + MB_OK);
  end;

  if not sametext(dataString, EmptyStr) then begin
    try
      Result := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
      Result.async := False;
      Result.loadXML(dataString);
    except
    end;
  end;
end;

I have a DLL that posts XML data to a site. The DLL is built with Delphi 10 in order to use TLS 1.2, which is not available with Delphi XE2.

为什么不直接将 XE2 中的 Indy 更新为支持 TLS 1.2 的更新版本?那么你根本不需要DLL。

My original DLL convention was basically not correct, as I returned the contents of the returned text data as a PChar. In my readings here and elsewhere, that's a big no-no.

它不是 "big no-no",尤其是在响应数据本质上是动态的情况下。返回一个指向动态分配数据的指针是完全没问题的。当调用者使用完它时,您只需导出一个额外的函数来释放数据,仅此而已。 "big no-no" 如果调用者忘记调用第二个函数,这确实会引入潜在的内存泄漏。但这就是 try..finally 的好处。

That "bad" methodology worked well until I started to get very large amounts of data returned. I tested it, and it failed on anything greater than 132,365 characters.

那不是很多内存。您遇到的任何失败都可能是因为您滥用了内存。

I restructured my DLL and calling code to pass in a buffer as a PChar to fill in, but I get an error trying to fill the output value!

那是你没有正确填写记忆

Secondly, since I never know how big the returned data will be, how to I specify how big a buffer to fill from my calling method?

使用 POST 时不能。您必须将响应数据缓存到旁边的某个地方,然后公开让调用者查询该缓存的大小和数据的方法。

My DLL code where I get the error:

My Calling method code:

我发现该代码中存在许多逻辑错误。

但是,访问冲突错误的最重要原因是您的 EXE 根本没有为其 returnData 变量分配 任何 内存。

string 转换为 PChar 从不 生成 nil 指针。如果输入 string 不为空,则返回指向字符串第一个 Char 的指针。否则,将返回指向静态 #0 Char 的指针。这确保 string 强制转换为 PChar always 导致 non-nil, null-terminated, C风格字符串.

您的 EXE 告诉 DLL returnData 最多可以容纳 32767 个字符,但实际上它根本不能容纳任何字符!在DLL中,OutData不是nilOutLen是错误的

另外,StrPLCopy() 总是 null-terminates输出,但是MaxLen参数不包含null-terminator,所以调用者必须为 MaxLen+1 个字符分配空间。 StrPLCopy() documentation.

中说明了这一点

说了这么多,试试这样的:

library TestDLL;

uses
  SysUtils,
  Classes,
  Windows,
  Messages,
  Vcl.Dialogs,
  IdIOHandlerStack, IdSSLOpenSSL, IdHTTP, IdCompressorZLib;

{$R *.res}

function PostAdminDataViaDll(body, method, url: PChar;
  var OutData : PChar): integer; stdcall;
var
  HTTPReq : TIdHTTP;
  SendStream : TStringStream;
  IdSSLIOHandler : TIdSSLIOHandlerSocketOpenSSL;
  s : string;
begin
  OutData := nil;

  try
    HTTPReq := TIdHTTP.Create(nil);
    try
      IdSSLIOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(HTTPReq);
      IdSSLIOHandler.SSLOptions.Mode := sslmClient;
      IdSSLIOHandler.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
      HTTPReq.IOHandler := IdSSLIOHandler;

      HTTPReq.Compressor := TIdCompressorZLib.Create(HTTPReq);
      HTTPReq.ReadTimeout := 180000;//set read timeout to 3 minutes
      HTTPReq.HTTPOptions := [];

      HTTPReq.Request.ContentType := 'text/xml';
      HTTPReq.Request.Charset := 'UTF-8';
      HTTPReq.Request.Accept := 'text/xml';
      HTTPReq.Request.CustomHeaders.AddValue('SOAPAction', 'http://tempuri.org/Administration/' + method);

      SendStream := TStringStream.Create(Body, TEncoding.UTF8);
      try
        s := HTTPReq.Post(string(url) + 'admin.asmx', SendStream);
      finally
        SendStream.Free;
      end;

      Result := Length(s);
      if Result > 0 then begin
        GetMem(OutData, (Result + 1) * Sizeof(Char));
        Move(PChar(s)^, OutData^, (Result + 1) * Sizeof(Char));
      end;
    finally
      HTTPReq.Free;
    end;
  except
    on E: Exception do begin
      ShowMessage(E.Message);
      Result := -1;
    end;
  end;
end;

function FreeDataViaDll(Data : Pointer): integer; stdcall;
begin
  try
    FreeMem(Data);
    Result := 0;
  except
    on E: Exception do begin
      ShowMessage(E.Message);
      Result := -1;
    end;
  end;
end;

exports
  PostAdminDataToCenPosViaDll,
  FreeDataViaDll;

begin
end.

function PostAdminData(body, method, url : string): IXMLDOMDocument;
type
   TMyPost = function (body, method, url: PChar; var OutData : PChar): integer; stdcall;
   TMyFree = function (Data  Pointer): integer; stdcall;
var
  hDll : THandle;
  MyPost : TMyPost;
  MyFree : TMyFree;
  dataString : string;
  returnData : PChar;
  returnLen : Integer;
begin
  hDll := LoadLibrary(PChar(ExtractFilePath(Application.ExeName) + 'TestDLL.DLL'));
  if hDll = 0 then begin
    Application.MessageBox('Unable to load TestDLL.DLL.', 'Error posting', MB_ICONERROR or MB_OK);
    Exit;
  end;
  try
    try
      MyPost := GetProcAddress(hDll, 'PostAdminDataViaDll');
      MyFree := GetProcAddress(hDll, 'FreeDataViaDll');
      if Assigned(MyPost) and Assigned(MyFree) then begin
        returnLen := MyPost(PChar(body), PChar(method), PChar(url), returnData);
        if returnLen > 0 then begin
          try
            SetString(dataString, returnData, returnLen);
          finally
            MyFree(returnData);
          end;
        end;
      end;
    finally
      FreeLibrary(hDll);
    end;
  except
  end;

  if dataString <> '' then begin
    try
      Result := CreateOleObject('Microsoft.XMLDOM') as IXMLDOMDocument;
      Result.async := False;
      Result.loadXML(dataString);
    except
    end;
  end;
end;