Bagikan melalui


TN058: Implementasi Status Modul MFC

Catatan

Catatan teknis berikut belum diperbarui sejak pertama kali disertakan dalam dokumentasi online. Akibatnya, beberapa prosedur dan topik mungkin kedaluarsa atau salah. Untuk informasi terbaru, disarankan agar Anda mencari topik yang menarik dalam indeks dokumentasi online.

Catatan teknis ini menjelaskan implementasi konstruksi "status modul" MFC. Pemahaman tentang implementasi status modul sangat penting untuk menggunakan DLL bersama MFC dari DLL (atau server dalam proses OLE).

Sebelum membaca catatan ini, lihat "Mengelola Data Status Modul MFC" dalam Membuat Dokumen, Windows, dan Tampilan Baru. Artikel ini berisi informasi penggunaan penting dan informasi gambaran umum tentang subjek ini.

Gambaran Umum

Ada tiga jenis informasi status MFC: Status Modul, Status Proses, dan Status Utas. Terkadang jenis status ini dapat digabungkan. Misalnya, peta handel MFC adalah modul lokal dan lokal utas. Ini memungkinkan dua modul yang berbeda untuk memiliki peta yang berbeda di setiap utasnya.

Status Proses dan Status Utas serupa. Item data ini adalah hal-hal yang secara tradisional merupakan variabel global, tetapi harus spesifik untuk proses atau utas tertentu untuk dukungan Win32s yang tepat atau untuk dukungan multithreading yang tepat. Kategori mana yang cocok dengan item data tertentu tergantung pada item tersebut dan semantik yang diinginkan sehubungan dengan batas proses dan utas.

Status Modul unik karena dapat berisi status global yang benar-benar global atau status yang memproses lokal atau utas lokal. Selain itu, dapat dialihkan dengan cepat.

Pengalihan Status Modul

Setiap utas berisi penunjuk ke status modul "saat ini" atau "aktif" (tidak mengherankan, pointer adalah bagian dari status lokal utas MFC). Penunjuk ini diubah ketika utas eksekusi melewati batas modul, seperti aplikasi yang memanggil ke Kontrol OLE atau DLL, atau Kontrol OLE memanggil kembali ke aplikasi.

Status modul saat ini dialihkan dengan memanggil AfxSetModuleState. Sebagian besar, Anda tidak akan pernah berurusan langsung dengan API. MFC, dalam banyak kasus, akan memanggilnya untuk Anda (di WinMain, titik masuk OLE, AfxWndProc, dll.). Ini dilakukan dalam komponen apa pun yang Anda tulis dengan menautkan secara statis dalam khusus WndProc, dan khusus WinMain (atau DllMain) yang mengetahui status modul mana yang harus terkini. Anda dapat melihat kode ini dengan melihat DLLMODUL. CPP atau APPMODUL. CPP di direktori MFC\SRC.

Jarang Anda ingin mengatur status modul dan kemudian tidak mengaturnya kembali. Sebagian besar waktu Anda ingin "mendorong" status modul Anda sendiri sebagai yang saat ini dan kemudian, setelah Anda selesai, "pop" konteks asli kembali. Ini dilakukan oleh AFX_MANAGE_STATE makro dan kelas AFX_MAINTAIN_STATEkhusus .

CCmdTarget memiliki fitur khusus untuk mendukung peralihan status modul. Secara khusus, adalah kelas akar yang CCmdTarget digunakan untuk automasi OLE dan titik masuk OLE COM. Seperti titik masuk lainnya yang terekspos ke sistem, titik masuk ini harus mengatur status modul yang benar. Bagaimana diberikan CCmdTarget tahu apa status modul "benar" harus Jawabannya adalah bahwa ia "mengingat" apa status modul "saat ini" ketika dibangun, sehingga dapat mengatur status modul saat ini ke nilai "diingat" ketika kemudian dipanggil. Akibatnya, status modul yang dikaitkan dengan objek tertentu CCmdTarget adalah status modul yang saat ini saat objek dibuat. Ambil contoh sederhana memuat server INPROC, membuat objek, dan memanggil metodenya.

  1. DLL dimuat oleh OLE menggunakan LoadLibrary.

  2. RawDllMain dipanggil terlebih dahulu. Ini mengatur status modul ke status modul statis yang diketahui untuk DLL. Untuk alasan RawDllMain ini secara statis ditautkan ke DLL.

  3. Konstruktor untuk pabrik kelas yang terkait dengan objek kami dipanggil. COleObjectFactory berasal dari CCmdTarget dan sebagai hasilnya, ia ingat dalam status modul mana itu dibuat. Ini penting — ketika pabrik kelas diminta untuk membuat objek, ia tahu sekarang status modul apa yang akan dibuat saat ini.

  4. DllGetClassObject dipanggil untuk mendapatkan pabrik kelas. MFC mencari daftar pabrik kelas yang terkait dengan modul ini dan mengembalikannya.

  5. COleObjectFactory::XClassFactory2::CreateInstance dipanggil. Sebelum membuat objek dan mengembalikannya, fungsi ini mengatur status modul ke status modul yang saat ini ada di langkah 3 (yang ada saat ini ketika COleObjectFactory dibuat). Ini dilakukan di dalam METHOD_PROLOGUE.

  6. Ketika objek dibuat, objek juga merupakan CCmdTarget turunan dan dengan cara COleObjectFactory yang sama mengingat status modul mana yang aktif, begitu juga objek baru ini. Sekarang objek tahu status modul mana yang akan dialihkan setiap kali dipanggil.

  7. Klien memanggil fungsi pada objek OLE COM yang diterimanya dari panggilannya CoCreateInstance . Ketika objek disebut digunakan METHOD_PROLOGUE untuk mengalihkan status modul seperti COleObjectFactory yang dilakukan.

Seperti yang Anda lihat, status modul disebarkan dari objek ke objek saat dibuat. Penting untuk mengatur status modul dengan tepat. Jika tidak diatur, objek DLL atau COM Anda mungkin berinteraksi dengan buruk dengan aplikasi MFC yang memanggilnya, atau mungkin tidak dapat menemukan sumber dayanya sendiri, atau mungkin gagal dengan cara lain yang menyedihkan.

Perhatikan bahwa jenis DLL tertentu, khususnya DLL "Ekstensi MFC" tidak mengalihkan status modul di dalamnya RawDllMain (sebenarnya, mereka biasanya tidak memiliki RawDllMain). Ini karena mereka dimaksudkan untuk bereaksi "seolah-olah" mereka benar-benar ada dalam aplikasi yang menggunakannya. Mereka sangat merupakan bagian dari aplikasi yang berjalan dan itu adalah niat mereka untuk memodifikasi status global aplikasi tersebut.

Kontrol OLE dan DLL lainnya sangat berbeda. Mereka tidak ingin mengubah status aplikasi panggilan; aplikasi yang memanggilnya bahkan mungkin bukan aplikasi MFC sehingga mungkin tidak ada status untuk dimodifikasi. Ini adalah alasan bahwa pengalihan status modul ditemukan.

Untuk fungsi yang diekspor dari DLL, seperti yang meluncurkan kotak dialog di DLL, Anda perlu menambahkan kode berikut ke awal fungsi:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Ini menukar status modul saat ini dengan status yang dikembalikan dari AfxGetStaticModuleState hingga akhir cakupan saat ini.

Masalah dengan sumber daya di DLL akan terjadi jika makro AFX_MODULE_STATE tidak digunakan. Secara default, MFC menggunakan handel sumber daya aplikasi utama untuk memuat templat sumber daya. Templat ini sebenarnya disimpan di DLL. Akar penyebabnya adalah bahwa informasi status modul MFC belum dialihkan oleh makro AFX_MODULE_STATE. Handel sumber daya dipulihkan dari status modul MFC. Tidak mengalihkan status modul menyebabkan handel sumber daya yang salah digunakan.

AFX_MODULE_STATE tidak perlu dimasukkan ke dalam setiap fungsi di DLL. Misalnya, InitInstance dapat dipanggil oleh kode MFC dalam aplikasi tanpa AFX_MODULE_STATE karena MFC secara otomatis menggeser status modul sebelum InitInstance dan kemudian mengalihkannya kembali setelah InitInstance kembali. Hal yang sama berlaku untuk semua penangan peta pesan. DLL MFC reguler sebenarnya memiliki prosedur jendela master khusus yang secara otomatis mengalihkan status modul sebelum merutekan pesan apa pun.

Memproses Data Lokal

Memproses data lokal tidak akan menjadi perhatian besar jika bukan karena kesulitan model DLL Win32s. Di Win32s semua DLL berbagi data global mereka, bahkan ketika dimuat oleh beberapa aplikasi. Ini sangat berbeda dari model data DLL Win32 "nyata", di mana setiap DLL mendapatkan salinan terpisah dari ruang datanya di setiap proses yang melekat pada DLL. Untuk menambah kompleksitas, data yang dialokasikan pada timbunan dalam DLL Win32s sebenarnya bersifat spesifik proses (setidaknya sejauh kepemilikan berjalan). Pertimbangkan data dan kode berikut:

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

Pertimbangkan apa yang terjadi jika kode di atas terletak di DLL dan DLL tersebut dimuat oleh dua proses A dan B (sebenarnya dapat berupa dua instans dari aplikasi yang sama). Panggilan SetGlobalString("Hello from A"). Akibatnya, memori dialokasikan untuk CString data dalam konteks proses A. Perlu diingat bahwa CString itu sendiri bersifat global dan terlihat oleh A dan B. Sekarang B memanggil GetGlobalString(sz, sizeof(sz)). B akan dapat melihat data yang ditetapkan A. Ini karena Win32s tidak menawarkan perlindungan antara proses seperti win32. Itulah masalah pertama; dalam banyak kasus, tidak diinginkan untuk memiliki satu aplikasi yang memengaruhi data global yang dianggap dimiliki oleh aplikasi yang berbeda.

Ada masalah tambahan juga. Katakanlah A sekarang keluar. Ketika A keluar, memori yang digunakan oleh string 'strGlobal' tersedia untuk sistem — yaitu, semua memori yang dialokasikan oleh proses A dibeberkan secara otomatis oleh sistem operasi. Ini tidak dibeberkan karena CString destruktor sedang dipanggil; belum dipanggil. Ini dibeberkan hanya karena aplikasi yang dialokasikan telah meninggalkan adegan. Sekarang jika B memanggil GetGlobalString(sz, sizeof(sz)), mungkin tidak mendapatkan data yang valid. Beberapa aplikasi lain mungkin telah menggunakan memori tersebut untuk sesuatu yang lain.

Jelas ada masalah. MFC 3.x menggunakan teknik yang disebut thread-local storage (TLS). MFC 3.x akan mengalokasikan indeks TLS yang di bawah Win32s benar-benar bertindak sebagai indeks penyimpanan lokal proses, meskipun tidak disebut dan kemudian akan mereferensikan semua data berdasarkan indeks TLS tersebut. Ini mirip dengan indeks TLS yang digunakan untuk menyimpan data lokal utas di Win32 (lihat di bawah ini untuk informasi selengkapnya tentang subjek tersebut). Hal ini menyebabkan setiap DLL MFC menggunakan setidaknya dua indeks TLS per proses. Ketika Anda memperhitungkan pemuatan banyak DLL Kontrol OLE (OCX), Anda dengan cepat kehabisan indeks TLS (hanya ada 64 yang tersedia). Selain itu, MFC harus menempatkan semua data ini di satu tempat, dalam satu struktur. Itu tidak terlalu luas dan tidak ideal sehubungan dengan penggunaan indeks TLS.

MFC 4.x membahas ini dengan sekumpulan templat kelas yang dapat Anda "bungkus" di sekitar data yang harus diproses secara lokal. Misalnya, masalah yang disebutkan di atas dapat diperbaiki dengan menulis:

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC mengimplementasikan ini dalam dua langkah. Pertama, ada lapisan di atas API Win32 Tls* (TlsAlloc, TlsSetValue, TlsGetValue, dll.) yang hanya menggunakan dua indeks TLS per proses, tidak peduli berapa banyak DLL yang Anda miliki. Kedua, CProcessLocal templat disediakan untuk mengakses data ini. Ini mengambil alih operator -> yang memungkinkan sintaks intuitif yang Anda lihat di atas. Semua objek yang dibungkus oleh CProcessLocal harus berasal dari CNoTrackObject. CNoTrackObjectmenyediakan alokator tingkat bawah (LocalAlloc/LocalFree) dan destruktor virtual sehingga MFC dapat secara otomatis menghancurkan proses objek lokal ketika proses dihentikan. Objek tersebut dapat memiliki destruktor kustom jika pembersihan tambahan diperlukan. Contoh di atas tidak memerlukannya, karena pengkompilasi akan menghasilkan destruktor default untuk menghancurkan objek yang disematkan CString .

Ada keuntungan menarik lainnya dari pendekatan ini. Tidak hanya semua CProcessLocal objek yang dihancurkan secara otomatis, mereka tidak dibangun sampai diperlukan. CProcessLocal::operator-> akan membuat instans objek terkait saat pertama kali dipanggil, dan tidak lebih cepat. Dalam contoh di atas, itu berarti bahwa string 'strGlobal' tidak akan dibangun sampai pertama kali SetGlobalString atau GetGlobalString dipanggil. Dalam beberapa kasus, ini dapat membantu mengurangi waktu startup DLL.

Utas Data Lokal

Mirip dengan memproses data lokal, data lokal utas digunakan ketika data harus lokal ke utas tertentu. Artinya, Anda memerlukan instans data terpisah untuk setiap utas yang mengakses data tersebut. Ini dapat berkali-kali digunakan sebagai pengganti mekanisme sinkronisasi yang luas. Jika data tidak perlu dibagikan oleh beberapa utas, mekanisme tersebut bisa mahal dan tidak perlu. Misalkan kita memiliki CString objek (seperti sampel di atas). Kita dapat membuatnya menjadi utas lokal dengan membungkusnya dengan CThreadLocal templat:

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

Jika MakeRandomString dipanggil dari dua utas yang berbeda, masing-masing akan "mengacak" string dengan cara yang berbeda tanpa mengganggu yang lain. Ini karena sebenarnya strThread ada instans per utas, bukan hanya satu instans global.

Perhatikan bagaimana referensi digunakan untuk mengambil CString alamat sekali alih-alih sekali per iterasi perulangan. Kode perulangan dapat ditulis dengan threadData->strThread di mana-mana 'str' digunakan, tetapi kode akan jauh lebih lambat dalam eksekusi. Yang terbaik adalah menyimpan referensi ke data ketika referensi tersebut terjadi dalam perulangan.

CThreadLocal Templat kelas menggunakan mekanisme yang sama yang CProcessLocal dilakukan dan teknik implementasi yang sama.

Baca juga

Catatan Teknis menurut Angka
Catatan Teknis menurut Kategori