Типичный подход к этой проблеме - приложение распределяет память, а затем передает ее в DLL для заполнения (даже лучше, если DLL позволяет приложению запрашивать, сколько памяти ему нужно выделить, поэтому у нее нет перераспределить память):
function GetAString(Buffer: PChar; BufLen: Integer): Integer; stdcall;
var
S: String;
begin
S := SomeFuncThatReturnsString;
Result := Min(BufLen, Length(S));
if (Buffer <> nil) and (Result > 0) then
Move(S[1], Buffer^, Result * SizeOf(Char));
end;
Это позволяет приложению решать, когда и как распределять память (стек по сравнению с кучей, повторное использование блоков памяти и т. Д.):
var
S: String;
begin
SetLength(S, 256);
SetLength(S, GetAString(PChar(S), 256));
...
end;
var
S: String;
begin
SetLength(S, GetAString(nil, 0));
if Length(S) > 0 then GetAString(PChar(S), Length(S));
...
end;
var
S: array[0..255] of Char;
Len: Integer;
begin
Len := GetAString(S, 256);
...
end;
Если это не вариант для вас, вам нужно, чтобы DLL выделяла память, возвращала ее в приложение для использования, а затем экспортировала в DLL дополнительную функцию, которую приложение может вызвать, когда это будет сделано передать указатель обратно в DLL для освобождения:
function GetAString: PChar; stdcall;
var
S: String;
begin
S := SomeFuncThatReturnsString;
if S <> '' then
begin
Result := StrAlloc(Length(S)+1);
StrPCopy(Result, S);
end else
Result := nil;
end;
procedure FreeAString(AStr: PChar); stdcall;
begin
StrDispose(AStr);
end;
var
S: PChar;
begin
S := GetAString;
if S <> nil then
try
...
finally
FreeAString(S);
end;
end;