Cet article a été initialement publié dans le courant de l'année 2008.

Partage de la mémoire entre applications et DLL sous Windows

Sous Windows, une DLL et une application ne partagent pas le même tas (heap), et c’est parfois bien dommage... Lorsque qu’une application passe un pointeur sur un bloc de mémoire, la DLL ne peut pas en changer sa taille ni le libérer, puisque le pointeur a été alloué par l’autre module. Ainsi, lorsqu’une DLL doit retourner un nombre variable de structures de données, il est ainsi d’usage d’allouer un tampon "suffisamment grand" pour en contenir assez.... ce qui n’est pas vraiment optimal en terme de consommation mémoire et franchement corsé quand ce "assez" n’est pas borné.

Une solution est de passer à la DLL un jeu de pointeurs de fonction, qui référencent des procédures exposées dans l’application permettant d’allouer ou de désallouer de la mémoire :

type IMalloc = packed record Malloc: function(const Size: Integer): Pointer; stdcall; Realloc: function (Ptr: Pointer; const Size: Integer): Pointer; stdcall; Free: procedure(Ptr: Pointer); stdcall; end;

Autrement dit, on définir une structure dont les trois membres sont des pointeurs sur fonction. Ici le “packed” sert à empêcher le compilateur d’introduire des octets d’alignement, permettant la "traduction" de cette structure en C/C++ et son exploitation par des DLL écrites dans ce langage; et le "stdcall" permet l’appel interprocessus sous Win32 (c’est la convention d’appel par défaut des API Windows).

Où est l’astuce ? Lorsque la DLL alloue/libère de la mémoire via le jeu de fonctions, c’est le tas de l’application qui est utilisé dans tous les cas, puisque les fonctions pointées sont exportées depuis celle-ci.

Côté application, il faut implémenter ces fonctions. Par exemple :

function DefaultMalloc(const Size: Integer): Pointer; stdcall; begin try GetMem(Result, Size); if Result <> nil then FillChar(PChar(Result)[0], Size, #0); except Result := nil; end; end; function DefaultRealloc(Ptr: Pointer; const Size: Integer): Pointer; stdcall; begin try ReallocMem(Ptr, Size); Result := Ptr; except Result := nil; end; end; procedure DefaultFree(Ptr: Pointer); stdcall; begin FreeMem(Ptr); end;

...et créer une fonction initialisant les champs de la structure :

function GetDefaultMalloc : IMalloc; begin Result.Malloc := @DefaultMalloc; Result.Realloc := @DefaultRealloc; Result.Free := @DefaultFree; end;

On peut créer des fonctions "utilitaires" pour allouer plus facilement de la mémoire à partir de notre structure de données :

function SharedGetMem(AMalloc: IMalloc; const Size: Integer): Pointer; begin Result := AMalloc.Malloc(Size); end; function SharedReallocMem(AMalloc: IMalloc; Ptr: Pointer; const Size: Integer): Pointer; begin Result := AMalloc.Realloc(Ptr, Size); end; procedure SharedFreeMem(AMalloc: IMalloc; Ptr: Pointer); begin AMalloc.Free(Ptr); end;

C’est fini ! L’application n’a plus qu’à plus appeller GetDefaultMalloc(), passer une structure de type IMalloc à la DLL, qui utilisera SharedGetMem() pour allouer de la mémoire sur un pointeur passé en paramètre. Pour un code orthogonal, l’application appellera SharedFreeMem() sur le pointeur retourné par la DLL une fois les résultats renvoyés.

Côté application (un exemple tiré d’UltraBackup) :

function LoadUserEventsHistory(... out AEvents: TThActionsHistoryInfosArray): Integer; var P, LList: PThActionHistoryInfos; LMalloc: _TThMalloc; //...// begin SetLength(AEvents, 0); //Obtentir l'allocateur LMalloc := GetDefaultMalloc(); //Appel de la fonction dans la DLL if STATUS_SUCCESS(thbGetUserEventsHistory(..., LMalloc, @LList, ...)) then begin //traitement, masqué ici //libération de la mémoire SharedFreeMem(LMalloc, LList); end else Result := 0; end;

Côté DLL :

function thbGetUserEventsHistory(...AMalloc: _TThMalloc;...): TThRemoteCallStatus; stdcall; var LList: TThActionsHistoryInfosArray; I, L: Integer; begin //Code passé if STATUS_SUCCESS(Result) then begin //On alloue le pointeur via notre structure PPEventsList^ := AMalloc.Malloc(L * SizeOf(TThActionHistoryInfos)); //Code passé end; end;

Voilà...La même chose a été mis en place avec IMalloc de COM, mais cette solution a l’avantage d’être légère et multiplateforme.