DLL, interfejs, FreeLibrary i AccessViolation - potrzebna pomoc

0

Cześć, siedzę nad tym już drugi dzień i nic. Środowisko XE2 Update 4
Generalnie zasada jest taka - mam interfejs. Klasa w DLL implementuje ten interfejs, a potem zwracam ten obiekt do aplikacji głównej.
Na koniec zwalniam bibliotekę DLL.

Może to trochę chaotycznie brzmi, więc trochę kodu. Jak jest interfejs skonstruowany, to chyba nie ma znaczenia.
Generalnie w DLL mam klasę:

TPlugin = class(TInterfacedObject, IPlugInterface)
//deklaracje metod
end;

W dll mam też jedną funkcję, którą eksportuję:

function CoCreatePlugin: IPlugInterface; stdcall;
begin
  Result := TPlugin.Create;
end;

(zasada działania pochodzi z jakiegoś artykułu w necie, który czytałem dawno temu)

Teraz w aplikacji głównej robię coś takiego:
(plugin to zmienna typu TPlugin, który jest rekordem i wygląda mniej więcej tak:

type
  TPlugin = record
    Active: boolean; //czy używać, czy nie
    {$IF DEFINED(SERVICE) or DEFINED(TEST_APP)} //nie sądzę, żeby dyrektywy miały wpływ na błąd, ale są tutaj
    Handle: THandle;
    IFace: IPlugInterface;
    {$IFEND}
end;

I teraz ładuję bibliotekę i wywołuję CoCreatePlugin:

Plugin.Handle:=LoadLibrary('plik.dll');

if Plugin.Handle <> 0 then
begin
  @CreatePluginProc:=GetProcAddress(Plugin.Handle, 'CoCreatePlugin');
  if @CreatePluginProc = nil then //generalnie wtedy wywalam błąd

  Plugin.IFace:=CreatePluginProc;
  @CreatePluginProc:=nil; //zawsze tutaj jest wszystko ok

I od tego momentu mogę wywoływać wszystkie metody z dll poprzez: Plugin.IFace.Metoda();
Wszystkie metody w interfejsie są oznaczone jako safecall.

Problem pojawia się podczas zwalniania biblioteki. O dziwo, wcześniej to działało.

res:=FPlugins[PluginIndex].IFace.Disconnect; 
if res<>S_OK then exit;

FreeLibrary(FPlugins[PluginIndex].Handle);
FPlugins[PluginIndex].Handle:=0;
FPlugins[PluginIndex].IFace:=nil;

I teraz tak. Błąd powstaje zawsze w ostatniej linijce, tam gdzie jest przyrównanie do NIL.

Project Project1.exe raised exception class $C0000005 with message 'access violation at 0x0040ba2e: read of address 0x04dcad8c'.

Przy czym adres: 0x04dcad8c to adres, pod którym znajduje się FPlugins[PluginIndex].IFace. (czyli sam obiekt)
Jeśli natomiast nie ma tego przyrównania do nil, to access violation pokazuje się jakby w losowych miejscach. Np. jak kliknę gdziekolwiek poza okno programu. Albo po pewnym czasie(w programie nie ma żadnego działania).

Project Project1.exe raised exception class $C0000005 with message 'access violation at 0x02770fef: read of address 0x02770fef

Adres 0x02770fef MOŻE BYĆ wskaźnikiem na tamten obiekt, tzn: @FPlugins[PluginIndex].IFace
Czasami jednak wskazuje na adresy 0x00000000. Przy czym zawsze jest class $C0000005

I ja już nie wiem, co robić. Czy potrzebujecie jeszcze jakiejś informacji? Czy coś jest niepokojącego w tym kodzie?
Zaznaczam, że jeśli nie mam wywołania FreeLibrary, to nie ma też Access Violation.
Proszę o pomoc.

2

najpierw przypisz nil a potem zwolnij dll

0

Tak robiłem na początku. Było to samo.
Teraz jest jeszcze gorzej. Przypisanie IFace:=nil wywołuje metodę _Release interfejsu, więc w tym momencie biblioteka NIE MOŻE być zwolniona.

Być może trzymasz gdzieś jeszcze kopię IFace w jakiejś zmiennej. Musisz się pozbyć wszystkich referencji do interfejsu, żebyś mógł wyrzucić bibliotekę.

0

No właśnie nigdzie indziej nie ma. Jest tylko przypisanie na początku - wywołaniem funkcji "CreatePluginProc" i tyle. Potem tylko w kilku miejscach sprawdzam, czy ten interfejs jest nil:

if Plugin.IFace <> nil then...
0

if @CreatePluginProc = nil then
Tak się nie sprawdza pointerów. assigned

Plugin.IFace:=CreatePluginProc;
Plugin.IFace:=CreatePluginProc();. Ale to kosmetyka.

@CreatePluginProc:=nil;
Już nie będę się pastwił nad niezrozumiałą dla mnie składnią Delphi, ale na cholerę to nilujesz? Tego nigdzie w pamięci się nie trzyma, to powinna być zmienna lokalna procedury "LoadMyDllLib" etc.

FPlugins[PluginIndex].IFace:=nil;
Zrozum, że najpierw niszczysz interfejs, potem zwalniasz bibliotekę. Z tego co wiem to inaczej niszczy się interfejsy niż tak, ale lecę z pamięci a interfejsów z dwa razy w swojej karierze programistycznej użyłem.

FPlugins[PluginIndex].Handle:=0;
A to na cholerę? Gdy biblioteka jest zwalniana to jej miejsce w tablicy powinno być usuwane poprzez skrócenie tablicy.

Czy coś jest niepokojącego w tym kodzie?

Tak, to jak piszesz kod jest niepokojące.

Teraz jest jeszcze gorzej. Przypisanie IFace:=nil wywołuje metodę _Release interfejsu, więc w tym momencie biblioteka NIE MOŻE być zwolniona.

Cóż, biblioteka powinna być zwolniona po tym gdy interfejs się zamknie. Sekwencyjność kodu powinna tak zrobić.

No właśnie nigdzie indziej nie ma. Jest tylko przypisanie na początku - wywołaniem funkcji "CreatePluginProc" i tyle. Potem tylko w kilku miejscach sprawdzam, czy ten interfejs jest nil:

Co wy macie z tym nil?! jest assigned.

Sprawdzaj czy interfejs został zwolniony zanim wywołasz FreeLibrary. Jak nie to assertuj czy raisuj.

0

Zdecyduj się TPlugin to albo rekord albo klasa nie może robić za to i to !

0

@Juhas

Plugin.Handle:=LoadLibrary('plik.dll'); 
if Plugin.Handle <> 0 then
begin
  @CreatePluginProc:=GetProcAddress(Plugin.Handle, 'CoCreatePlugin');
  if @CreatePluginProc = nil then //generalnie wtedy wywalam błąd
 
  Plugin.IFace:=CreatePluginProc;
  @CreatePluginProc:=nil; //zawsze tutaj jest wszystko ok

CreatePluginProc powinno być zmienną lokalną i nie potrzebne jest @CreatePluginProc:=nil;

Co do twojego błędu, to może być on spowodowany tym że, któryś wątek wykorzystuje (próbuje) już nie istniejący interfejs.
Powinieneś mieć jakąś funkcję, która zwalnia pamięć twojej DLL'ki (zakładam że Disconnect tego nie robi), choć tą kwestie powinno rozwiązywać FreeLibrary
Debuger wywala cię tam bo to ostatnia linijka z kodu, który dokonuje błędu na prawidłowo pracującej metodzie. Spróbuj zakomentować

FreeLibrary(FPlugins[PluginIndex].Handle);
FPlugins[PluginIndex].Handle:=0;
FPlugins[PluginIndex].IFace:=nil;

I zobacz czy wywala błąd.

Nie podoba mi się zapis

res:=FPlugins[PluginIndex].IFace.Disconnect; 
if res<>S_OK then exit;

raczej tak wygląda lepiej i nie alokujesz niepotrzebnie zmiennej

 FPlugins[PluginIndex].IFace.Disconnect <> S_OK then exit;

@3g5iue
Masz racje, w kodzie jest bajzel xD

if @CreatePluginProc = nil then

Tak też można i nie jest to błędne

Zrozum, że najpierw niszczysz interfejs, potem zwalniasz bibliotekę. Z tego co wiem to inaczej niszczy się interfejsy niż tak, ale lecę z pamięci a interfejsów z dwa razy w swojej karierze programistycznej użyłem.

FPlugins[PluginIndex].Handle:=0;

A to na cholerę? Gdy biblioteka jest zwalniana to jej miejsce w tablicy powinno być usuwane poprzez skrócenie tablicy.

O ile się nie mylę to nie ma czegoś takiego jak usuwanie elementu z tablicy, tylko zmniejszasz jej rozmiar i przesuwasz górę o -1, cały ten proces trwa długo (w zależności od rozmiarów tablicy) ponadto łatwiej jest oznaczyć handle zerem i wykorzystać już istniejący index przy ładowaniu następnej biblioteki (może to jednak tworzyć problem z zwalnianiem pamięci)

@babubabu

Chyba nie zrozumiałeś co przeczytałeś

Zrozumiałem co przeczytałem!
Tak czy srak musisz zamieścić strukturę interfejsu w głównej aplikacji (nazwana TPlugin) a dodatkowo deklarujesz że typ TPlugin jest rekordem.

Ale my sobie możemy gdybać jak to wygląda skoro nie masz całego kodu przed sobą tylko skrawki i to nie kompletne :D

0

to ja jeszcze cos dorzuce od siebie.

  1. Tworzysz dll (bez zadnego tam sharemem i innych badziewi). Mam nadzieje ze dll dziedziczy poTInterfacedObject i implementuje dodatkowo Twoj interface
   IMyInterface = interface(IUnknown) // to dla c#
   [tutaj GUID = ctrl + shift + g]
     procedure MyMethod(var AValue: OleVariant); safecall;
   end;

a w dll

  TMyObject = class(TInterfacedObject, IMyInterface)
    procedure MyMethod(var AValue: OleVariant); safecall; 
  end;
  1. Deklarujesz metode eksportujaca interface jesli z dll okreslasz konwencje wolania metody
  procedure CreateObject(var AObject: IMyInterface); safecall;
  begin
     // do your stuff here
  end;

  exports
    CreateObject;  // lub CreateObject name 'XYZ'
  1. W hoscie wywolujacym dll deklarujesz typ woladnej metody
type
  TCreateObejct = procedure(var AObject: IMyInterface); safecall;
  1. Okreslasz strukture w ktorej trzymasz informacje o dll, nie jest prawda ze powinna to byc zmienna lokalna np jesli dll exportuje formularz ktory pozniej przez winapi gdzies dokujesz np na pagecontrol to mozesz miec kilka obiktow utworzonych z dll btw kazdy obiekt takiego formularza moze miec rozne stany view, edit, new
    Ja tak mam to zrobione u siebie - u mnie nie tylko dll z delphi sa obslugiwane tak np z c# tez
    Zalecalbyc rekord do trzymania informacji + record helper (masz w koncu xe2)
type
  TPluginInformation = record
    Plugin: HMODULE;
    CreateObject: TCreateObjct;
    MyInterface: IMyInterface;
  end;
  1. W metodzie w ktorej tworzysz obiekt z dll zakladam ze rekord juz jest gdzies utworzony (zalozmy ze sie nazywa Info)
procedure ObjectFromDll(Sender: TObject);
begin
  Info.Plugin := SafeLoadLibrary('C:\ala ma kota\My.dll');
 
 if Info.Plugin = 0 then
    raise Exception.Create('blad ladowania dll');

  Info.CreateObject := GetProccAdress(Info.Plugin, 'CreateObject'); // lub XYZ w zaleznosci od sekcji export z dll

  if not Assigned(Info.CreateObject)
     raise Exception.Create('boom');   

  Info.CreateObject(MyInterface);
  MyInterface.MyMethod(.......);

  Info.MyInterface := nil;
  FreeLibrary(Info.Plugin);
end;

I to wszystko. Pamietaj tylko zeby przekazywac do interface przez OleVariant;

0

Tak też można i nie jest to błędne

Błędne czy nie, to sprawa dyskusyjna. Natomiast ja powiedziałem że używa się assigned

O ile się nie mylę to nie ma czegoś takiego jak usuwanie elementu z tablicy, tylko zmniejszasz jej rozmiar i przesuwasz górę o -1

No tak, nie ma czegoś takiego jak pisanie, tylko jest przyłożenie ręki do kartki i naciskanie na nią odpowiednio długopisem.

cały ten proces trwa długo

Niech zgadnę. Masz Pentium II 100mhz z Windowsem 95, tak?
Cóż, jeżeli ty swoje programy piszesz po to żeby tak często wykonywana i krytyczna funkcja jak zwalnianie biblioteki co zapewne się robi tylko przy wychodzeniu była maksymalnie optymalna, to dobrze że tych funkcji które są mało istotne jak procedury zajmujące najwięcej czasu nie optymalizujesz. Możesz czas wykonywania 90% programu zmniejszyć do 0 i uzyskać 10% przyrostu prędkości albo zmniejszyć czas wykonywania tych ważnych 10% do zera i uzyskać 90% przyrostu prędkości. Widzę że jesteś za pierwszą opcją.

ponadto łatwiej jest oznaczyć handle zerem i wykorzystać już istniejący index przy ładowaniu następnej biblioteki

No właśnie nie łatwiej jest się bawić w szukanie, tylko usuwać przy wywalaniu i dodawać przy dodawaniu. Proste...

(może to jednak tworzyć problem z zwalnianiem pamięci)

Nie wiem jakim cudem, skoro o zwalnianie tablic dba kompilator.

BTW. @OMG, masz doublepost bo jakiś (l)admin niechlujnie usunął mój post. Nice try, monk.

Z tego co wiem, to Assigned sprawdza czy parametr jej przekazany nie jest równy nil. Wynika to nawet z: Assigned - patrz też przykład kodu, w tym artykule. - olesio wczoraj, 19:58
@3g5iue: skoro składnia jest dla ciebie niezrozumiała, to może się powstrzymaj od wypowiedzi, bo mieszasz prawdę z fałszem i zamęt wprowadzasz. - Azarien wczoraj, 21:35

Cóż, raz już na to odpowiedziałem, drugi raz nie będę.

0

btw lista uchwytow do dll?
ok pod warunkiem ze to sa inne dll bo jesli ta sama to bedzie ten sam handle (jesli wczesniej wczytano juz dll do pamieci).

2

Błędne czy nie, to sprawa dyskusyjna. Natomiast ja powiedziałem że używa się assigned

ten twój ulubiony Assigned() robi tylko tyle co

function Assigned(p:pointer):boolean;
begin
  Result := p<>nil
end;

i choć użycie tej funkcji może być zalecane, to naprawdę, są lepsze tematy do zaczepienia flejma.

0

Zauważyłem, że błąd AV powstaje jedynie w środowisku. Natomiast po odpaleniu execa, błędu nie ma. Co o tym myślicie?

0

@Juhas - taka sytuacja może zaistnieć np. wtedy, gdy instrukcja powodująca wyjątek znajduje się wewnątrz bloku try .. except, który go nie obsługuje (tzn. po wystąpieniu wyjątku nie informujesz użytkownika o zaistniałej sytuacji); Jeśli program kompilujesz i uruchamiasz przez IDE to wtedy debuger wychwytuje wyjątek (pod warunkiem, że mu tego nie zabronisz w ustawieniach projektu), a jeśli aplikację uruchamiasz z dysku (bez debugera) to nie dostajesz żadnej informacji; Inna sytuacja nie przychodzi mi do głowy.

1 użytkowników online, w tym zalogowanych: 0, gości: 1