Ответ 1
Из-за риска слишком много кода, это возможное (плохое) решение моего собственного вопроса. Используя тот факт, что память локального потока хранится в одном блоке для переменных threadvar (как отметил г-н Кеннеди - спасибо), этот код хранит выделенные указатели в TList, а затем освобождает их при отсоединении процесса. Я написал его в основном, просто чтобы посмотреть, будет ли это работать. Я бы, вероятно, не использовал бы это в производственном коде, потому что он делает предположения о времени выполнения Delphi, которые могут меняться с разными версиями и, возможно, не хватает проблем даже с используемой версией (Delphi 7 и 2007).
Эта реализация действительно делает umdh счастливой, она не думает, что утечек памяти больше нет. Тем не менее, если я запускаю тест в цикле (загружать, вызывать точку входа в другой поток, выгружать), использование памяти, как видно в Process Explorer, все еще растет с тревогой. Фактически, я создал полностью пустую DLL с пустым DllMain (который не был вызван, так как я не назначил ему Delphi глобальный указатель DllMain... Дели сам обеспечивает реальную точку входа DllMain). Простой цикл загрузки/выгрузки DLL по-прежнему просочился на 4K за итерацию. Таким образом, все еще может быть что-то еще, что должна включать DLL Delphi (основной вопрос исходного вопроса). Но я не знаю, что это. DLL, написанная на C, не ведет себя так.
Наш код (сервер) может вызывать библиотеки DLL, написанные клиентами, для расширения функциональности. Обычно мы выгружаем DLL после того, как на нем больше нет ссылок. Я думаю, что мое решение проблемы будет заключаться в том, чтобы добавить параметр, чтобы оставить загруженную DLL "навсегда" в памяти. Если клиенты используют Delphi для написания своей DLL, они должны будут включить эту опцию (или, может быть, мы сможем обнаружить, что это DLL Delphi при загрузке... необходимо проверить это). Тем не менее, это было интересное мероприятие.
library Sample;
uses
SysUtils,
Windows,
Classes,
HTTPApp,
SyncObjs;
{$E dll}
var
gListSync : TCriticalSection;
gTLSList : TList;
threadvar
threadint : integer;
// remove all entries from the TLS storage list
procedure RemoveAndFreeTLS();
var
i : integer;
begin
// Only call this at process detach. Those calls are serialized
// so don't get the critical section.
if assigned( gTLSList ) then
for i := 0 to gTLSList.Count - 1 do
// Is this actually safe in DllMain process detach? From reading the MSDN
// docs, it appears that the only safe statement in DllMain is "return;"
LocalFree( Cardinal( gTLSList.Items[i] ));
end;
// Remove this thread entry
procedure RemoveThreadTLSEntry();
var
p : pointer;
begin
// Find the entry for this thread and remove it.
gListSync.enter;
try
if ( SysInit.TlsIndex <> -1 ) and ( assigned( gTLSList )) then
begin
p := TlsGetValue( SysInit.TlsIndex );
// if this thread didn't actually make a call into the DLL and use a threadvar
// then there would be no memory for it
if p <> nil then
gTLSList.Remove( p );
end;
finally
gListSync.leave;
end;
end;
// Add current thread TLS pointer to the global storage list if it is not already
// stored in it.
procedure AddThreadTLSEntry();
var
p : pointer;
begin
gListSync.enter;
try
// Need to create the list if first call
if not assigned( gTLSList ) then
gTLSList := TList.Create;
if SysInit.TlsIndex <> -1 then
begin
p := TlsGetValue( SysInit.TlsIndex );
if p <> nil then
begin
// if it is not stored, add it
if gTLSList.IndexOf( p ) = -1 then
gTLSList.Add( p );
end;
end;
finally
gListSync.leave;
end;
end;
// Some entrypoint that uses threadvar (directly or indirectly)
function MyExportedFunc(): LongWord; stdcall;
begin
threadint := 123;
// Make sure this thread TLS pointer is stored in our global list so
// we can free it at process detach. Do this AFTER using the threadvar.
// Delphi seems to allocate the memory on demand.
AddThreadTLSEntry;
Result := 0;
end;
procedure DllMain(reason: integer) ;
begin
case reason of
DLL_PROCESS_DETACH:
begin
// NOTE - if this is being called due to process termination, then it should
// just return and do nothing. Very dangerous (and against MSDN recommendations)
// otherwise. However, Delphi does not provide that information (the 3rd param of
// the real DlLMain entrypoint). In my test, though, I know this is only called
// as a result of the DLL being unloaded via FreeLibrary
RemoveAndFreeTLS();
gListSync.Free;
if assigned( gTLSList ) then
gTLSList.Free;
end;
DLL_THREAD_DETACH:
begin
// on a thread detach, Delphi will clean up its own TLS, so we just
// need to remove it from the list (otherwise we would get a double free
// on process detach)
RemoveThreadTLSEntry();
end;
end;
end;
exports
DllMain,
MyExportedFunc;
// Initialization
begin
IsMultiThread := TRUE;
// Make sure Delphi calls my DllMain
DllProc := @DllMain;
// sync object for managing TLS pointers. Is it safe to create a critical section?
// This init code is effectively DllMain DLL_PROCESS_ATTACH
gListSync := TCriticalSection.Create;
end.