Teknik Makale

Bir Fonksiyon Sonucunda FillChar, Object Pascal'da Dizeleri Sızdırır

Bir kayıt döndüren bir Delphi veya FPC fonksiyonu, her çağrıda taze, sıfırlanmış bir Result almaz. O gizli Result değişkeni tam olarak bir kez sıfır başlar ve hiçbir şey onu çağrılar arasında otomatik olarak yeniden sıfırlamaz; bu yüzden girişte onu temizlemek fonksiyonun kendi işidir. Bu temizliği FillChar(Result, SizeOf(Result), 0) ile yapın ve ikinci çağrıdan itibaren, rutin canlı bir dize veya dinamik dizi referansını serbest bırakmak yerine üzerine yazar; bu referansın işaret ettiği yığın bloğunu yetim bırakır

Bunun ısırdığı senaryo sıradandır. Bir toplu iş süreci, bir yığın üçüncü taraf PDF'i açar ve her sayfadaki her açıklamayı dolaşarak yorum metnini bir denetim günlüğüne çeker. O döngü hakkında hiçbir şey tehlikeli görünmez: her çağrı düz bir kayıt döndüren düz bir fonksiyondur, görünürde işaretçi yoktur, manuel bellek yönetimine benzeyen hiçbir şey yoktur. Bir kayıt içindeki referans sayımı, herhangi bir kütüphaneye özgü bir tuhaflık değil, düz bir Object Pascal muhasebe kuralıdır ve dizeler veya dinamik diziler tutan kayıt türleriyle FillChar'ı karıştıran herhangi bir Delphi veya FPC kod tabanı aynı kusura açıktır

Bir Kayıt Sonucunda FillChar Dizeleri Neden Sızdırır?

FillChar dizeleri sızdırır çünkü üzerine ne tür veri yazdığı hakkında hiçbir fikri yoktur. FillChar(X, Count, Value) herhangi bir değişken üzerinde çalışır: türsüz bir Count bayt bloğunu alır ve her birini Value ile damgalar ve sözleşmenin tamamı budur. Bu, FillChar'ı hızlı ve genel amaçlı yapan tam olarak şeydir, çünkü X'in türünü hiç incelemez ve altta yatan baytların ne anlama geldiğine hiç dallanmaz. Bir kayıt içindeki bir UnicodeString veya WideString alanı karakterlerin kendisi değildir; karakter verisinin önünde bir referans sayısı taşıyan bir yığın bloğuna bir işaretçidir. FillChar, bir işaretçi değeri tutan birkaç bayt görür ve onları tam olarak bir Integer veya Double alanının üzerine yazacağı gibi sıfırla üzerine yazar. İşaretçi kaybolur, önce azaltması gereken referans sayısına hiç dokunulmaz ve işaret ettiği blok, ona referans veren hiçbir şey kalmadan tahsis edilmiş olarak oturur

Derleyici Bir Kayıt İçindeki Dizeleri ve Dinamik Dizileri Nasıl Takip Eder

Object Pascal, derleyicinin atama ve kapsam çıkışı boyunca doğru kalması için ekstra kod çalıştırması gerektiğinde bir türü yönetilen olarak adlandırır. AnsiString, UnicodeString ve WideString gibi uzun dize türleri buna girer ve dinamik diziler, arayüzler ve Variant'lar da girer; bunlardan birini alan olarak içeren herhangi bir kayıt veya sabit dizi de öyle. Her yönetilen alan için, derleyici sessizce, aksi halde elle yapılması sıkıcı ve yanlış yapılması kolay olacak muhasebeyi yayar: atamada bir referans sayısını artır, tutan değişken üzerine yazıldığında veya kapsamdan çıktığında onu azalt ve o sayı sıfıra ulaştığında altta yatan bloğu serbest bırak. Bu makine, sıradan Pascal kodunun hiçbir zaman bir string'i manuel olarak tahsis etmemesinin veya serbest bırakmamasının ve bir dinamik diziyi diğerine atamanın manuel bir kopyalama döngüsü yerine ucuz, güvenli bir işlem olmasının nedenidir. System.Default ve Finalize, aynı serbest bırakma mantığını talep üzerine çağırmanın belgelenmiş iki yoludur ve bir kaydın temizleme kodunun ham bir bellek doldurması yerine çağırması gereken şey budur

type
  TLineItem = record
    Description: string;  // managed: reference-counted
    Quantity: Integer;    // unmanaged: plain ordinal
  end;

function GetLineItem(Index: Integer): TLineItem;
begin
  FillChar(Result, SizeOf(Result), 0);  // clears bytes, not the reference
  Result.Quantity := Source[Index].Qty;
  Result.Description := Source[Index].Text;
end;

var
  Item: TLineItem;
  I: Integer;
begin
  for I := 0 to High(Source) do
  begin
    Item := GetLineItem(I);  // second pass onward: leaks the prior Description
    Log.Add(Item.Description);
  end;
end;

Sızıntı Neden Yalnızca İkinci Çağrıda Başlar?

Bir döngüdeki ilk çağrı her zaman zararsızdır ki bu, bu kusuru testte gözden kaçırmayı tam olarak kolaylaştıran şeydir. Yönetilen bir kayıt türünün yerel bir değişkeni sıfır başlar ve bir döngü geçişinden diğerine hiçbir şey onu otomatik olarak yeniden sıfırlamaz; bu yüzden bir döngü ilk kez bir fonksiyonun dönüş değerini o değişkene atadığında, Description veya ContentsText alanı hâlâ nil'dir. FillChar nil'in üzerine sıfırla yazar ki bu referans sayısı açısından hiçbir şeyi değiştirmez ve çağrı tamamen doğru görünerek döner. İkinci çağrı farklıdır: aynı yerel değişken zaten ilk çağrının içine yazdığı her neyse onu tutar ve yeni çağrının Result'u taze, boş belleğe değil doğrudan o aynı depolamaya yazılır. O ikinci çağrının başındaki FillChar, artık nil olmayan bir alanı sıfırlar ve o bayt örüntüsünün aşağı akışındaki her şey o andan itibaren sessizce yanlıştır. Fonksiyonu bir kez çağırıp sonucu inceleyen bir test bu sorunu asla görmeyecektir; yalnızca bir döngü veya fonksiyonu aynı hedefe karşı tekrar tekrar çağıran herhangi bir kod yolu bunu açığa çıkarır

Gerçek Bir Sızıntı: Açıklamalar, Yer İmleri ve Bağlantı Kayıtları

PDFiumPas, sürüm 1.56.4'ten önce tam olarak bu kusuru, her biri en az bir yönetilen alan tutan bir kayıt döndüren üç fonksiyonda gönderdi: sayfa düzeyinde açıklama okuyucusu ContentsText ve AuthorText dizelerini taşıyan bir TPdfAnnotation döndürür, yer imi okuyucusu bir Title dizesi taşıyan bir TBookmark döndürür ve bağlantı-açıklaması okuyucusu bir ActionPath dizesi ve bir Points dinamik dizisi taşıyan bir TLinkAnnotation döndürür. Üçü de aşağıda gösterilen aynı şekille açıldı: Result'u ham bir FillChar ile temizle, sonra altta yatan sayfa verisinden alanları birer birer doldur. Bir sayfadaki her açıklamayı birer birer dolaşmak -bir denetim listesi veya bir inceleme paneli kurmanın sıradan yolu- açıklama okuyucusunu bir döngüde çağırdı ve ilkinden sonraki her geçişte önceki açıklamanın metnini sızdırdı; alışılmadık derecede büyük sayıda metin taşıyan açıklamayla hazırlanmış bir PDF, o süreç çalışmaya devam ettiği sürece uzun süre çalışan bir sürecin belleğini büyütebilirdi. Düzeltme her fonksiyonda bir satıra dokundu: FillChar(Result, SizeOf(Result), 0)Result := Default(TPdfAnnotation) ile değiştirmek yeterliydi, çünkü yönetilen bir kayda Default'u atamak, ham bir bellek doldurma yerine derleyicinin sıradan serbest-bırak-sonra-temizle dizisini çalıştırır

function GetPageAnnotation(Page: FPDF_PAGE; Index: Integer): TPdfAnnotation;
var
  Annotation: FPDF_ANNOTATION;
  ContentLength: LongWord;
begin
  Annotation := FPDFPage_GetAnnot(Page, Index);
  FillChar(Result, SizeOf(Result), 0);   // clears bytes, not a live reference
  Result.Subtype := DecodeAnnotationSubtype(FPDFAnnot_GetSubtype(Annotation));
  ContentLength := FPDFAnnot_GetStringValue(Annotation,
    FPDFANNOT_TEXTTYPE_Contents, nil, 0);
  if ContentLength >= 4 then
  begin
    SetLength(Result.ContentsText, ContentLength div 2 - 1);
    FPDFAnnot_GetStringValue(Annotation, FPDFANNOT_TEXTTYPE_Contents,
      Pointer(Result.ContentsText), ContentLength);
  end;
end;

Bir var Parametresinin Arkasındaki Aynı Tehlike

Yer imi okuyucusu, aynı sorunun daha ince bir versiyonunu gösterir, çünkü FillChar ile temizlenen kayıt, fonksiyonun kendi Result'u değil, bir çağrı aşağıda bir var parametresidir. SetBookmarkData, çıktısını var Data: TBookmark olarak alır ve gövdesinin başında Data'yı FillChar ile temizlerdi; gerçekte bir TBookmark döndüren kamuya açık fonksiyon olan GetBookmark, SetBookmarkData'yı çağırır ve kendi Result'unu o var argümanı olarak doğrudan geçirir. Bir var parametresi referans olarak geçirilir; bu yüzden SetBookmarkData içindeki Data ve GetBookmark içindeki Result, iki ad altında aynı depolamadır ve bir fonksiyonun kendi Result'una uygulanan takma ad riski, onu referans olarak alan herhangi bir yardımcı rutine de tam olarak aynı şekilde uygulanır. Yalnızca gerçekten bir kayıt dönüş türü bildiren fonksiyonları gözden geçirmek bu şekli kaçırır; arama, bir Result'un yönlendirildiği her var ve out parametresini de takip etmek zorundadır

procedure TPdf.SetBookmarkData(Bookmark: FPDF_BOOKMARK; var Data: TBookmark);
var
  BufferSize: LongWord;
begin
  Data := Default(TBookmark);   // fixed: was FillChar(Data, SizeOf(Data), 0)
  Data.Handle := Bookmark;
  if Bookmark <> nil then
  begin
    BufferSize := FPDFBookmark_GetTitle(Bookmark, nil, 0);
    if BufferSize >= 4 then
    begin
      SetLength(Data.Title, BufferSize div 2 - 1);
      FPDFBookmark_GetTitle(Bookmark, PWideChar(Data.Title), BufferSize);
    end;
  end;
end;

function TPdf.GetBookmark(const Title: WString): TBookmark;
begin
  CheckActive;
  SetBookmarkData(FPDFBookmark_Find(FDocument, PWideChar(Title)), Result);
end;

FillChar Ne Zaman Hâlâ Doğru Seçimdir?

FillChar, tamamen ordinallerden, kayan noktalı alanlardan, bunların sabit boyutlu dizilerinden veya aynı şeylerden oluşan başka düz kayıtlardan kurulu bir kayıt için hâlâ doğrudur ve genellikle biraz daha ucuzdur, çünkü derleyicinin sonlandıracağı hiçbir şey yoktur. PDFiumPas'ın kendi dikdörtgen türü tam olarak bu durumdur: TPdfRectangle dört Double alanı tutar ve başka hiçbir şey tutmaz; birini FillChar ile temizlemek hiçbir şeyi serbest bırakmaz çünkü serbest bırakılacak referans sayılı hiçbir şey yoktur. İki durumu ayıran kontrol basitçe ifade edilir: kaydın herhangi bir alanı, herhangi bir iç içe geçme derinliğinde, string, AnsiString, WideString, bir dinamik dizi, bir arayüz veya bir Variant türünde mi? Bir kayıt üst düzeyde tamamen sayısal görünebilir ve alanlarından biri birkaç katman aşağıda bir dizeyi gömüyorsa yine de bu testte başarısız olabilir; bu yüzden kontrol, en dıştaki alan listesinde durmak yerine iç içe geçmiş kayıtları sonuna kadar takip etmek zorundadır. Bu örüntü için mevcut bir kod tabanını denetlemek kapsamlı olmaktan çok mekaniktir: hedefi bir kayıt değişkeni olan her FillChar çağrısını arayın, sonra o kaydın alan listesini yukarıdaki yönetilen-tür listesine karşı kontrol edin. PDFiumPas'ın kendi v1.56.4 denetimi, tam olarak bu aramayı tüm kütüphane genelinde çalıştırdı ve bu açığı bir birimde buldu; diğer her FillChar çağrı noktası zaten düz sayısal bir kaydı temizliyordu; burada FillChar doğru araçtı ve öyle kalır

Burada yeniden kullanılan bir Result'u tehlikeli kılan aynı derleyici davranışı, bu kod tabanında başka bir yerdeki ilgili bir Delphi-versus-FPC anlaşmazlıkları ailesini de yönlendirir; çapraz derleyici tuzakları üzerine tamamlayıcı bir makale, FPC ve Delphi'nin tek bir ifade içinde bir kayıt-sonucu geçicisinin tam olarak ne zaman sonlandırıldığı konusunda anlaşamadığı bir durumu ele alır; bu, bir fonksiyonun kayıt Result'unun her zaman göründüğü kadar taze, özel depolama olmadığı gerçeğinin farklı bir belirtisidir. Bu makale boyunca çalışan örnek olarak kullanılan açıklama döngüsü de varsayımsal değildir: bir açıklama inceleme paneli kurarken yazacağınız aynı sayfa-sayfa dolaşımdır ki bu tam olarak tek satırlık bir FillChar'ı en baştan yavaş bir bellek sızıntısına dönüştüren kod şeklidir

Bunların hiçbiri kütüphane değiştirmeyi veya başkasının derlenmiş kodunda bir hata kovalamayı gerektirmez: bu, Object Pascal dilinin kendisinin bir özelliğidir, her Delphi ve FPC geliştiricisinin günlük olarak çalıştığı bir özellik, ve düzeltme, aramayı bildiğinizde tek bir fonksiyon çağrısıdır. Burada açıklanan açıklama, yer imi ve bağlantı-açıklaması API'leri, bu blogda başka yerde ele alınan PDF okuma, render ve açıklama yüzeyinin geri kalanının yanında, Delphi, C++Builder ve Lazarus/FPC için PDFium Bileşeni'nin bir parçası olarak gönderilir