Sebuah fungsi Delphi atau FPC yang mengembalikan sebuah record tidak mendapatkan Result yang baru dan ternolkan pada setiap pemanggilan. Variabel Result tersembunyi itu dimulai dari nol tepat satu kali, dan tidak ada apa pun yang menolkannya ulang secara otomatis di antara pemanggilan, sehingga membersihkannya saat masuk adalah pekerjaan fungsi itu sendiri. Lakukan pembersihan itu dengan FillChar(Result, SizeOf(Result), 0) dan, mulai dari pemanggilan kedua, rutin itu menimpa sebuah referensi string atau dynamic-array yang hidup alih-alih melepaskannya, membuat yatim piatu blok heap apa pun yang ditunjuk referensi itu
Skenario tempat ini menggigit itu remeh. Sebuah proses batch membuka setumpuk PDF pihak ketiga dan menelusuri setiap anotasi pada setiap halaman, menarik teks komentar ke dalam sebuah log audit. Tidak ada apa pun tentang loop itu yang terlihat berbahaya: setiap pemanggilan adalah sebuah fungsi polos yang mengembalikan sebuah record polos, tidak ada pointer terlihat, tidak ada apa pun yang menyerupai manajemen memori manual sama sekali. Reference counting di dalam sebuah record adalah sebuah aturan akuntansi Object Pascal biasa, bukan sebuah keanehan khusus untuk library tertentu, dan codebase Delphi atau FPC mana pun yang mencampur FillChar dengan tipe record yang memegang string atau dynamic array terekspos pada defek yang sama
Mengapa FillChar pada Hasil Record Membocorkan String?
FillChar membocorkan string karena tidak memiliki gambaran apa pun tentang jenis data yang ditimpanya. FillChar(X, Count, Value) bekerja pada variabel apa pun sama sekali: ia mengambil sebuah blok untyped berisi Count byte dan mencap setiap satunya dengan Value, dan itulah seluruh kontraknya. Inilah persis yang membuat FillChar cepat dan serba-guna, karena ia tidak pernah memeriksa tipe X dan tidak pernah bercabang berdasarkan apa arti byte yang mendasarinya. Sebuah field UnicodeString atau WideString di dalam sebuah record bukan karakternya sendiri; ia adalah sebuah pointer ke sebuah blok heap yang membawa sebuah reference count di depan data karakternya. FillChar melihat segelintir byte yang kebetulan memegang sebuah nilai pointer dan menimpanya dengan nol persis seperti ia akan menimpa sebuah field Integer atau Double. Pointer itu menghilang, reference count yang seharusnya diturunkan lebih dulu tidak pernah tersentuh, dan blok yang ditunjuknya duduk teralokasi tanpa apa pun tersisa yang merujuk padanya
Bagaimana Compiler Melacak String dan Dynamic Array di Dalam Sebuah Record
Object Pascal menyebut sebuah tipe terkelola ketika compiler harus menjalankan kode ekstra untuk menjaganya tetap benar melintasi assignment dan keluar-lingkup. Tipe long string seperti AnsiString, UnicodeString, dan WideString memenuhi syarat, begitu pula dynamic array, interface, dan Variant, beserta record atau fixed array apa pun yang mengandung salah satunya sebagai sebuah field. Untuk setiap field terkelola, compiler diam-diam memancarkan pembukuan yang jika tidak akan merepotkan dan mudah salah dilakukan dengan tangan: menaikkan sebuah reference count saat assignment, menurunkannya ketika variabel yang memegangnya ditimpa atau keluar lingkup, dan membebaskan blok yang mendasarinya begitu hitungan itu mencapai nol. Mesin itulah yang membuat kode Pascal biasa tidak pernah secara manual mengalokasikan atau melepaskan sebuah string, dan mengapa menetapkan satu dynamic array ke yang lain adalah operasi murah dan aman alih-alih sebuah loop penyalinan manual. System.Default dan Finalize adalah dua cara terdokumentasi untuk memanggil logika pelepasan yang sama itu sesuai permintaan, dan itulah yang seharusnya dipanggil kode pembersihan sebuah record alih-alih sebuah fill memori mentah
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;
Mengapa Kebocoran Hanya Dimulai pada Pemanggilan Kedua?
Pemanggilan pertama dalam sebuah loop selalu tidak berbahaya, dan itulah persis yang membuat defek ini mudah terlewat dalam pengujian. Sebuah variabel lokal bertipe record terkelola dimulai dari nol, dan tidak ada apa pun yang menolkannya ulang secara otomatis di antara satu langkah loop dan berikutnya, sehingga kali pertama sebuah loop menetapkan nilai kembali sebuah fungsi ke dalam variabel itu, field Description atau ContentsText-nya masih nil. FillChar menimpa nil dengan nol, yang tidak mengubah apa pun sejauh reference count peduli, dan pemanggilan itu kembali terlihat sepenuhnya benar. Pemanggilan kedua berbeda: variabel lokal yang sama itu sudah memegang apa pun yang ditulis pemanggilan pertama ke dalamnya, dan Result pemanggilan baru itu ditulis langsung ke dalam penyimpanan yang sama itu alih-alih ke dalam memori baru dan kosong. FillChar di puncak pemanggilan kedua itu menolkan sebuah field yang tidak lagi nil, dan segala sesuatu di hilir pola byte itu diam-diam salah sejak saat itu. Sebuah test yang memanggil fungsi itu sekali dan memeriksa hasilnya tidak akan pernah melihat masalah tersebut; hanya sebuah loop, atau jalur kode apa pun yang memanggil fungsi itu berulang kali terhadap tujuan yang sama, yang mengungkapkannya
Sebuah Kebocoran Sungguhan: Anotasi, Bookmark, dan Record Link
PDFiumPas mengirimkan persis defek ini sebelum versi 1.56.4, dalam tiga fungsi yang masing-masing mengembalikan sebuah record yang memegang setidaknya satu field terkelola: reader anotasi tingkat-halaman mengembalikan sebuah TPdfAnnotation yang membawa string ContentsText dan AuthorText, reader bookmark mengembalikan sebuah TBookmark yang membawa sebuah string Title, dan reader link-annotation mengembalikan sebuah TLinkAnnotation yang membawa sebuah string ActionPath dan sebuah dynamic array Points. Ketiganya dibuka dengan bentuk yang sama yang ditunjukkan di bawah: bersihkan Result dengan sebuah FillChar mentah, lalu isi field satu per satu dari data halaman yang mendasarinya. Menelusuri setiap anotasi pada sebuah halaman satu per satu, cara biasa membangun sebuah daftar audit atau sebuah panel review, memanggil reader anotasi dalam sebuah loop dan membocorkan teks anotasi sebelumnya pada setiap langkah setelah yang pertama; sebuah PDF yang dirancang dengan jumlah anotasi pembawa-teks yang tidak biasa besar bisa menumbuhkan memori sebuah proses berjalan-lama selama proses itu terus berjalan. Perbaikannya menyentuh satu baris dalam setiap fungsi: mengganti FillChar(Result, SizeOf(Result), 0) dengan Result := Default(TPdfAnnotation) sudah cukup, karena menetapkan Default ke sebuah record terkelola menjalankan urutan pelepasan-lalu-bersihkan biasa milik compiler alih-alih sebuah fill memori mentah
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;
Bahaya yang Sama di Balik Sebuah Parameter var
Reader bookmark menunjukkan sebuah versi lebih halus dari masalah yang sama, karena record yang dibersihkan dengan FillChar bukan Result fungsi itu sendiri tetapi sebuah parameter var satu pemanggilan lebih dalam. SetBookmarkData menerima outputnya sebagai var Data: TBookmark dan dulunya membersihkan Data di puncak body-nya dengan FillChar; GetBookmark, fungsi publik yang benar-benar mengembalikan sebuah TBookmark, memanggil SetBookmarkData dan meneruskan Result-nya sendiri langsung sebagai argumen var itu. Sebuah parameter var diteruskan by reference, sehingga Data di dalam SetBookmarkData dan Result di dalam GetBookmark adalah penyimpanan yang sama di bawah dua nama, dan risiko aliasing apa pun yang berlaku pada Result fungsi itu sendiri berlaku sama langsungnya pada rutin helper mana pun yang menerimanya by reference. Meninjau hanya fungsi yang secara literal mendeklarasikan sebuah tipe kembali record melewatkan bentuk ini; pencariannya harus mengikuti setiap parameter var dan out yang diteruskan sebuah Result ke dalamnya juga
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;
Kapan FillChar Masih Pemanggilan yang Tepat?
FillChar tetap benar, dan seringkali sedikit lebih murah, untuk sebuah record yang sepenuhnya dibangun dari ordinal, field floating-point, fixed-size array dari itu, atau record polos lain yang terbuat dari hal yang sama, karena tidak ada apa pun di dalamnya yang perlu di-finalize compiler. Tipe rectangle milik PDFiumPas sendiri persis kasus itu: TPdfRectangle memegang empat field Double dan tidak ada lagi, dan membersihkan satu dengan FillChar tidak melepaskan apa pun karena tidak ada apa pun yang ter-reference-count untuk dilepaskan. Pemeriksaan yang memisahkan kedua kasus itu sederhana untuk dinyatakan: apakah field mana pun dari record itu, pada kedalaman nesting mana pun, bertipe string, AnsiString, WideString, sebuah dynamic array, sebuah interface, atau sebuah Variant? Sebuah record bisa terlihat sepenuhnya numerik pada tingkat teratas dan tetap gagal tes itu jika salah satu field-nya sendiri sebuah record yang mengubur sebuah string beberapa lapis di bawahnya, sehingga pemeriksaan itu harus mengikuti record bersarang sepenuhnya alih-alih berhenti pada daftar field terluar. Mengaudit sebuah codebase yang ada untuk pola ini bersifat mekanis alih-alih menyeluruh: cari setiap pemanggilan FillChar yang targetnya sebuah variabel record, lalu periksa daftar field record itu terhadap daftar tipe-terkelola di atas. Audit v1.56.4 milik PDFiumPas sendiri menjalankan persis pencarian itu di seluruh library dan menemukan eksposur ini dalam satu unit; setiap titik pemanggilan FillChar lainnya sudah membersihkan sebuah record numerik polos, di mana FillChar dulu, dan tetap, alat yang tepat
Perilaku compiler yang sama yang membuat sebuah Result yang digunakan ulang berbahaya di sini juga mendorong sebuah keluarga terkait perbedaan-pendapat Delphi-versus-FPC di tempat lain dalam codebase ini; sebuah artikel pendamping tentang jebakan lintas-compiler membahas sebuah kasus di mana FPC dan Delphi tidak sepakat persis kapan sebuah temporary hasil-record di-finalize di dalam satu ekspresi tunggal, sebuah gejala berbeda dari fakta mendasar yang sama bahwa Result record sebuah fungsi tidak selalu penyimpanan baru dan privat sebagaimana terlihat. Loop anotasi yang digunakan sebagai contoh berjalan sepanjang artikel ini juga bukan hipotetis: itu adalah penelusuran halaman-demi-halaman yang sama yang akan Anda tulis saat membangun sebuah panel review anotasi, yang persis merupakan bentuk kode yang mengubah satu baris FillChar menjadi kebocoran memori lambat sejak awal
Tak satu pun dari ini membutuhkan penggantian library atau mengejar sebuah bug dalam kode terkompilasi milik orang lain: ini adalah sebuah sifat bahasa Object Pascal itu sendiri, satu yang dikerjakan setiap developer Delphi dan FPC setiap hari, dan perbaikannya adalah satu pemanggilan fungsi tunggal begitu Anda tahu apa yang dicari. API anotasi, bookmark, dan link-annotation yang dijelaskan di sini disertakan sebagai bagian dari PDFium Component untuk Delphi, C++Builder, dan Lazarus/FPC, berdampingan dengan sisa permukaan pembacaan, rendering, dan anotasi PDF yang dibahas di tempat lain di blog ini