Praktik Terbaik Pustaka Dynamic-Link
**Diperbarui:**
- 17 Mei 2006
API penting
Membuat DLL menghadirkan sejumlah tantangan bagi pengembang. DLL tidak memiliki penerapan versi yang diberlakukan sistem. Ketika beberapa versi DLL ada pada sistem, kemudahan ditimpa ditambah dengan kurangnya skema penerapan versi menciptakan dependensi dan konflik API. Kompleksitas di lingkungan pengembangan, implementasi loader, dan dependensi DLL telah menciptakan kerapuhan dalam urutan beban dan perilaku aplikasi. Terakhir, banyak aplikasi mengandalkan DLL dan memiliki serangkaian dependensi kompleks yang harus dihormati agar aplikasi berfungsi dengan baik. Dokumen ini menyediakan panduan bagi pengembang DLL untuk membantu membangun DLL yang lebih kuat, portabel, dan dapat diperluas.
Sinkronisasi yang tidak tepat dalam DllMain dapat menyebabkan aplikasi mengalami kebuntuan atau mengakses data atau kode dalam DLL yang tidak diinisialisasi. Memanggil fungsi tertentu dari dalam DllMain menyebabkan masalah tersebut.
Praktik Terbaik Umum
DllMain dipanggil saat loader-lock ditahan. Oleh karena itu, pembatasan signifikan diberlakukan pada fungsi yang dapat dipanggil dalam DllMain. Dengan demikian, DllMain dirancang untuk melakukan tugas inisialisasi minimal, dengan menggunakan subset kecil Microsoft® Windows® API. Anda tidak dapat memanggil fungsi apa pun di DllMain yang secara langsung atau tidak langsung mencoba memperoleh kunci loader. Jika tidak, Anda akan memperkenalkan kemungkinan aplikasi Anda mengalami kebuntuan atau crash. Kesalahan dalam implementasi DllMain dapat membahmari seluruh proses dan semua utasnya.
DllMain yang ideal hanya akan menjadi stub kosong. Namun, mengingat kompleksitas banyak aplikasi, ini umumnya terlalu ketat. Aturan praktis yang baik untuk DllMain adalah menunda inisialisasi sebanyak mungkin. Inisialisasi malas meningkatkan ketahanan aplikasi karena inisialisasi ini tidak dilakukan saat kunci loader ditahan. Selain itu, inisialisasi malas memungkinkan Anda menggunakan lebih banyak API Windows dengan aman.
Beberapa tugas inisialisasi tidak dapat ditunda. Misalnya, DLL yang bergantung pada file konfigurasi harus gagal dimuat jika file salah bentuk atau berisi sampah. Untuk jenis inisialisasi ini, DLL harus mencoba tindakan dan gagal dengan cepat daripada membuang sumber daya dengan menyelesaikan pekerjaan lain.
Anda tidak boleh melakukan tugas-tugas berikut dari dalam DllMain:
- Panggil LoadLibrary atau LoadLibraryEx (baik secara langsung maupun tidak langsung). Ini dapat menyebabkan kebuntuan atau crash.
- Panggil GetStringTypeA, GetStringTypeEx, atau GetStringTypeW (baik secara langsung maupun tidak langsung). Ini dapat menyebabkan kebuntuan atau crash.
- Sinkronkan dengan utas lain. Ini dapat menyebabkan kebuntuan.
- Dapatkan objek sinkronisasi yang dimiliki oleh kode yang menunggu untuk memperoleh kunci loader. Ini dapat menyebabkan kebuntuan.
- Menginisialisasi utas COM dengan menggunakan CoInitializeEx. Dalam kondisi tertentu, fungsi ini dapat memanggil LoadLibraryEx.
- Panggil fungsi registri.
- Panggil CreateProcess. Membuat proses dapat memuat DLL lain.
- Panggil ExitThread. Keluar dari utas selama pencopotan DLL dapat menyebabkan kunci loader diperoleh lagi, menyebabkan kebuntuan atau crash.
- Panggil CreateThread. Membuat utas dapat berfungsi jika Anda tidak menyinkronkan dengan utas lain, tetapi berisiko.
- Panggil ShGetFolderPathW. Memanggil API folder shell/known dapat mengakibatkan sinkronisasi utas, dan karenanya dapat menyebabkan kebuntuan.
- Buat pipa bernama atau objek bernama lainnya (hanya Windows 2000). Di Windows 2000, objek bernama disediakan oleh DLL Layanan Terminal. Jika DLL ini tidak diinisialisasi, panggilan ke DLL dapat menyebabkan proses crash.
- Gunakan fungsi manajemen memori dari C Run-Time (CRT) dinamis. Jika DLL CRT tidak diinisialisasi, panggilan ke fungsi-fungsi ini dapat menyebabkan proses crash.
- Fungsi panggilan di User32.dll atau Gdi32.dll. Beberapa fungsi memuat DLL lain, yang mungkin tidak diinisialisasi.
- Gunakan kode terkelola.
Tugas-tugas berikut aman untuk dilakukan dalam DllMain:
- Menginisialisasi struktur dan anggota data statis pada waktu kompilasi.
- Membuat dan menginisialisasi objek sinkronisasi.
- Alokasikan memori dan inisialisasi struktur data dinamis (hindari fungsi yang tercantum di atas.)
- Siapkan penyimpanan lokal utas (TLS).
- Buka, baca dari, dan tulis ke file.
- Fungsi panggilan di Kernel32.dll (kecuali fungsi yang tercantum di atas).
- Atur pointer global ke NULL, menunda inisialisasi anggota dinamis. Di Microsoft Windows Vista™, Anda dapat menggunakan fungsi inisialisasi satu kali untuk memastikan bahwa blok kode dijalankan hanya sekali di lingkungan multithreaded.
Kebuntuan Yang Disebabkan oleh Inversi Urutan Penguncian
Saat Anda menerapkan kode yang menggunakan beberapa objek sinkronisasi seperti kunci, sangat penting untuk menghormati urutan kunci. Ketika perlu untuk memperoleh lebih dari satu kunci pada satu waktu, Anda harus menentukan prioritas eksplisit yang disebut hierarki kunci atau urutan kunci. Misalnya, jika kunci A diperoleh sebelum kunci B di suatu tempat dalam kode, dan kunci B diperoleh sebelum kunci C di tempat lain dalam kode, maka urutan kunci adalah A, B, C dan pesanan ini harus diikuti di seluruh kode. Inversi urutan kunci terjadi ketika urutan penguncian tidak diikuti—misalnya, jika kunci B diperoleh sebelum kunci A. Inversi urutan kunci dapat menyebabkan kebuntuan yang sulit di-debug. Untuk menghindari masalah tersebut, semua utas harus memperoleh kunci dalam urutan yang sama.
Penting untuk dicatat bahwa loader memanggil DllMain dengan kunci pemuat yang sudah diperoleh, sehingga kunci loader harus memiliki prioritas tertinggi dalam hierarki penguncian. Perhatikan juga bahwa kode hanya harus memperoleh kunci yang diperlukan untuk sinkronisasi yang tepat; tidak harus memperoleh setiap kunci tunggal yang didefinisikan dalam hierarki. Misalnya, jika bagian kode hanya memerlukan kunci A dan C untuk sinkronisasi yang tepat, maka kode harus memperoleh kunci A sebelum memperoleh kunci C; tidak perlu kode juga memperoleh kunci B. Selain itu, kode DLL tidak dapat secara eksplisit memperoleh kunci loader. Jika kode harus memanggil API seperti GetModuleFileName yang secara tidak langsung dapat memperoleh kunci loader dan kode juga harus memperoleh kunci privat, maka kode harus memanggil GetModuleFileName sebelum memperoleh kunci P, sehingga memastikan bahwa urutan beban dihormati.
Gambar 2 adalah contoh yang menggambarkan inversi urutan kunci. Pertimbangkan DLL yang utas utamanya berisi DllMain. Pemuat pustaka memperoleh kunci pemuat L lalu memanggil ke DllMain. Utas utama membuat objek sinkronisasi A, B, dan G untuk menserialisasikan akses ke struktur datanya dan kemudian mencoba memperoleh kunci G. Utas pekerja yang telah berhasil memperoleh kunci G kemudian memanggil fungsi seperti GetModuleHandle yang mencoba memperoleh kunci loader L. Dengan demikian, utas pekerja diblokir pada L dan utas utama diblokir pada G, mengakibatkan kebuntuan.
Untuk mencegah kebuntuan yang disebabkan oleh inversi urutan kunci, semua utas harus mencoba memperoleh objek sinkronisasi dalam urutan beban yang ditentukan setiap saat.
Praktik Terbaik untuk Sinkronisasi
Pertimbangkan DLL yang membuat utas pekerja sebagai bagian dari inisialisasinya. Setelah pembersihan DLL, perlu disinkronkan dengan semua utas pekerja untuk memastikan bahwa struktur data dalam keadaan konsisten dan kemudian mengakhiri utas pekerja. Saat ini, tidak ada cara mudah untuk menyelesaikan masalah sinkronisasi dan mematikan DLL secara bersih di lingkungan multithreaded. Bagian ini menjelaskan praktik terbaik saat ini untuk sinkronisasi utas selama penonaktifan DLL.
Sinkronisasi Utas di DllMain selama Proses Keluar
- Pada saat DllMain dipanggil pada proses keluar, semua utas proses telah dibersihkan secara paksa dan ada kemungkinan bahwa ruang alamat tidak konsisten. Sinkronisasi tidak diperlukan dalam kasus ini. Dengan kata lain, handler DLL_PROCESS_DETACH yang ideal kosong.
- Windows Vista memastikan bahwa struktur data inti (variabel lingkungan, direktori saat ini, timbunan proses, dan sebagainya) dalam keadaan konsisten. Namun, struktur data lain dapat rusak, sehingga membersihkan memori tidak aman.
- Status persisten yang perlu disimpan harus dibersihkan ke penyimpanan permanen.
Sinkronisasi Utas di DllMain untuk DLL_THREAD_DETACH selama DLL Unload
- Ketika DLL dibongkar, ruang alamat tidak dibuang. Oleh karena itu, DLL diharapkan untuk melakukan pematian yang bersih. Ini termasuk sinkronisasi utas, handel terbuka, status persisten, dan sumber daya yang dialokasikan.
- Sinkronisasi alur sulit karena menunggu utas keluar di DllMain dapat menyebabkan kebuntuan. Misalnya, DLL A menahan kunci loader. Ini memberi sinyal utas T untuk keluar dan menunggu utas keluar. Thread T keluar dan loader mencoba memperoleh kunci loader untuk memanggil dllMain DLL A dengan DLL_THREAD_DETACH. Ini menyebabkan kebuntuan. Untuk meminimalkan risiko kebuntuan:
- DLL A mendapatkan pesan DLL_THREAD_DETACH di DllMain-nya dan mengatur peristiwa untuk utas T, memberi sinyal untuk keluar.
- Thread T menyelesaikan tugasnya saat ini, membawa dirinya ke keadaan yang konsisten, memberi sinyal DLL A, dan menunggu tanpa batas waktu. Perhatikan bahwa rutinitas pemeriksaan konsistensi harus mengikuti pembatasan yang sama dengan DllMain untuk menghindari kebuntuan.
- DLL A mengakhiri T, mengetahui bahwa itu dalam keadaan konsisten.
Jika DLL dibongkar setelah semua utasnya dibuat, tetapi sebelum mulai dieksekusi, utas mungkin mengalami crash. Jika DLL membuat utas di DllMain-nya sebagai bagian dari inisialisasinya, beberapa utas mungkin belum selesai diinisialisasi dan pesan DLL_THREAD_ATTACH mereka masih menunggu untuk dikirimkan ke DLL. Dalam situasi ini, jika DLL dibongkar, dll akan mulai mengakhiri utas. Namun, beberapa utas dapat diblokir di belakang kunci loader. Pesan DLL_THREAD_ATTACH mereka diproses setelah DLL tidak dipetakan, menyebabkan proses crash.
Rekomendasi
Berikut ini adalah panduan yang direkomendasikan:
- Gunakan Pemverifikasi Aplikasi untuk menangkap kesalahan paling umum di DllMain.
- Jika menggunakan kunci privat di dalam DllMain, tentukan hierarki penguncian dan gunakan secara konsisten. Kunci loader harus berada di bagian bawah hierarki ini.
- Verifikasi bahwa tidak ada panggilan yang bergantung pada DLL lain yang mungkin belum dimuat sepenuhnya.
- Lakukan inisialisasi sederhana secara statis pada waktu kompilasi, bukan di DllMain.
- Tunda panggilan apa pun di DllMain yang dapat menunggu hingga nanti.
- Tangguhkan tugas inisialisasi yang dapat menunggu hingga nanti. Kondisi kesalahan tertentu harus dideteksi lebih awal sehingga aplikasi dapat menangani kesalahan dengan anggun. Namun, ada tradeoff antara deteksi dini ini dan hilangnya ketahanan yang dapat diakibatkan olehnya. Menuangkan inisialisasi sering kali yang terbaik.