C/C++ 實現動態資原始檔釋放

微軟技術分享發表於2023-12-08

當我們開發Windows應用程式時,通常會涉及到使用資源(Resource)的情況。資源可以包括圖示、點陣圖、字串等,它們以二進位制形式嵌入到可執行檔案中。在某些情況下,我們可能需要從可執行檔案中提取自定義資源並儲存為獨立的檔案。在這篇部落格文章中,我們將討論如何使用C++和WinAPI實現這個目標。

簡介

首先,讓我們考慮一個場景:我們有一個 Windows 應用程式,其中包含了一個自定義的二進位制資源比如預設的配置檔案,我們希望將這個資源提取出來並儲存為一個獨立的檔案以用於初始化程式配置項。為了實現這個目標,我們可以使用Windows API提供的相關函式,來完成對資源的釋放工作。

關鍵函式概述

GetModuleHandle

該函式用於獲取指定模組的控制程式碼。模組可以是一個可執行檔案(例如 .exe 檔案)或一個動態連結庫(例如 .dll 檔案)。該函式返回指定模組的例項控制程式碼,以便在後續的操作中使用。

以下是 GetModuleHandle 函式的一般形式:

HMODULE GetModuleHandle(
  LPCTSTR lpModuleName
);

引數說明:

  • lpModuleName:指定要獲取控制程式碼的模組的名稱。如果為 NULL,則返回撥用執行緒的可執行模組控制程式碼。

在許多情況下,GetModuleHandle 主要用於獲取當前程式的模組控制程式碼,以便在後續的操作中使用該控制程式碼。模組控制程式碼通常用於在程式中查詢資源、定位函式地址等目的。

FindResource

該函式用於定位並返回指定模組(通常是 .exe 或 .dll 檔案)中的資源。資源可以是諸如點陣圖、圖示、對話方塊模板、字串等等的資料。

以下是 FindResource 函式的一般形式:

HRSRC FindResource(
  HMODULE hModule,
  LPCTSTR lpName,
  LPCTSTR lpType
);

引數說明:

  • hModule:指定包含資源的模組的控制程式碼。如果為 NULL,則表示使用當前可執行模組的控制程式碼。
  • lpName:指定資源的名稱或識別符號。可以是字串或整數識別符號。
  • lpType:指定資源的型別。通常是一個字串,如 "RT_BITMAP" 表示點陣圖資源。

如果找到,則返回指向資源的控制程式碼(HRSRC)。這個控制程式碼可以用於後續的資源載入和操作,函式的第二個引數經常配合MAKEINTRESOURCE一起使用,MAKEINTRESOURCE 是一個宏(macro),用於將整數識別符號(ID)轉換為字串指標。在 Windows 程式設計中,通常用於標識資源的 ID。

#define MAKEINTRESOURCE(i) ((LPCTSTR)((DWORD)((WORD)(i))))

這個宏接受一個整數引數 i,然後將其轉換為字串指標。在資源識別符號上下文中,通常將整數識別符號轉換為字串是為了在使用相關資源函式時傳遞正確的引數。

舉個例子,如果有一個字串資源的識別符號是 IDR_MYSTRING,則可以使用 MAKEINTRESOURCE 將其轉換為字串:

LPCTSTR pszResourceName = MAKEINTRESOURCE(IDR_MYSTRING);

在這裡,pszResourceName 將指向字串 "IDR_MYSTRING"。

在前面提到的 FindResource 中,通常將 MAKEINTRESOURCE(IDR_MYSTRING) 作為 lpName 引數傳遞給 FindResource。這是因為 FindResource 函式期望資源名稱是字串型別,而 IDR_MYSTRING 可能是一個整數識別符號。透過使用 MAKEINTRESOURCE,則可以將整數識別符號轉換為字串,以便正確地在資源中查詢。

SizeofResource

該函式用於獲取指定資源的大小。它返回資源的位元組數,可以用於確定載入資源所需的記憶體大小。

以下是 SizeofResource 函式的一般形式:

DWORD SizeofResource(
  HMODULE hModule,
  HRSRC   hResInfo
);

引數說明:

  • hModule:指定包含資源的模組的控制程式碼。如果為 NULL,則表示使用當前可執行模組的控制程式碼。
  • hResInfo:指定資源的控制程式碼,通常由 FindResource 返回。

SizeofResource 返回資源的大小,以位元組為單位。這個函式在載入資源之前可以用來分配足夠的記憶體空間。

LoadResource

該函式用於載入指定資源的資料。該函式返回一個全域性記憶體塊的控制程式碼,該記憶體塊包含了資源的實際資料,你可以透過 LockResource 函式獲取該記憶體塊的指標來訪問資源資料。

以下是 LoadResource 函式的一般形式:

HGLOBAL LoadResource(
  HMODULE hModule,
  HRSRC   hResInfo
);

引數說明:

  • hModule:指定包含資源的模組的控制程式碼。如果為 NULL,則表示使用當前可執行模組的控制程式碼。
  • hResInfo:指定資源的控制程式碼,通常由 FindResource 返回。

LoadResource 用於將資源資料載入到全域性記憶體塊中,並返回該記憶體塊的控制程式碼。在載入資源後,可以使用 LockResource 函式獲取指向資源資料的指標。

LockResource

用於獲取指定資源的資料指標。它接受一個全域性記憶體塊的控制程式碼,該記憶體塊通常由 LoadResource 函式返回,然後返回一個指向資源資料的指標。

以下是 LockResource 函式的一般形式:

LPVOID LockResource(
  HGLOBAL hResData
);

引數說明:

  • hResData:指定資源資料的全域性記憶體塊控制程式碼,通常由 LoadResource 函式返回。

LockResource 用於鎖定指定資源的全域性記憶體塊,並返回指向資源資料的指標。請注意,這個函式實際上並不執行複製,而是返回指向記憶體塊的指標,因此對返回指標的任何修改都會直接影響到記憶體塊本身。

FreeResource

用於釋放由 LoadResource 函式載入的資源。這個函式通常用於釋放不再需要的資源,以防止資源洩漏。

以下是 FreeResource 函式的一般形式:

BOOL FreeResource(
  HGLOBAL hResData
);

引數說明:

  • hResData:指定要釋放的全域性記憶體塊控制程式碼,通常由 LoadResource 函式返回。

FreeResource 用於釋放之前由 LoadResource 載入的資源。請注意,這個函式通常在資源的生命週期結束時呼叫,以確保釋放資源佔用的記憶體。但在實際應用中,現代 Windows 應用通常不需要顯式呼叫 FreeResource,因為 Windows 會在程式退出時自動釋放資源。

在實際的應用程式中,FindResource 可以與 LoadResourceLockResource 等函式一起使用,用於載入和操作資源資料。當資料資源被載入到記憶體之後則可以直接透過fwrite函式將其直接寫出到磁碟中,以此來實現釋放資源的目的。

程式碼功能實現

首先新建一個控制檯程式以作為本次的測試環境,接著準備好我們需要寫出的資料,這裡就準備一個lyshark.ini配置檔案,在專案中右鍵選擇新增並新增資源,此時會彈出如下圖所示的提示資訊;

此時會彈出新增資源選單,透過點選匯入按鈕並輸入資源型別為LYSHARK點選確定儲存這個更改,如下圖所示;

此時我們在主程式中引入#include "resource.h"包含資源標頭檔案,並修改FindResource中的特定位置使其指向我們匯入的配置檔案,在釋放時同樣需要保持fopen("map\\lyshark.ini", "wb+")配置檔案的格式。

這段資源釋放的完整程式碼如下所示;

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <iostream>
#include <WinUser.h>
#include "resource.h"

BOOL UseCustomResource()
{
	// 定位我們的自定義資源
	HMODULE hModule = GetModuleHandle(NULL);
	if (hModule == NULL)
	{
		std::cerr << "錯誤:獲取模組控制程式碼失敗。" << std::endl;
		return FALSE;
	}

	HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(IDR_LYSHARK1), TEXT("LYSHARK"));
	if (hRsrc == NULL)
	{
		std::cerr << "錯誤:無法找到資源。" << std::endl;
		return FALSE;
	}

	// 獲取資源大小
	DWORD dwSize = SizeofResource(hModule, hRsrc);
	if (dwSize == 0)
	{
		std::cerr << "錯誤:無效的資源大小。" << std::endl;
		return FALSE;
	}

	// 載入資源
	HGLOBAL hGlobal = LoadResource(hModule, hRsrc);
	if (hGlobal == NULL)
	{
		std::cerr << "錯誤:無法載入資源。" << std::endl;
		return FALSE;
	}

	// 鎖定資源
	LPVOID lpVoid = LockResource(hGlobal);
	if (lpVoid == NULL)
	{
		std::cerr << "錯誤:無法鎖定資源。" << std::endl;
		FreeResource(hGlobal);  // 在返回前釋放資源
		return FALSE;
	}

	// 如果不存在,建立一個“map”目錄
	if (!CreateDirectory("map", NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
	{
		std::cerr << "錯誤:無法建立目錄。" << std::endl;
		FreeResource(hGlobal);
		return FALSE;
	}

	// 將資源寫入檔案
	FILE* fp = fopen("map\\lyshark.ini", "wb+");
	if (fp == NULL)
	{
		std::cerr << "錯誤:無法建立或開啟檔案。" << std::endl;
		FreeResource(hGlobal);
		return FALSE;
	}

	fwrite(lpVoid, sizeof(char), dwSize, fp);
	fclose(fp);

	// 釋放資源
	FreeResource(hGlobal);

	return TRUE;
}

int main(int argc, char* argv[])
{
	BOOL ref = UseCustomResource();
	std::cout << "釋放狀態: " << ref << std::endl;

	system("pause");
	return 0;
}

以管理員模式執行上述程式,並等待,此時會釋放一個目錄幷包含一個配置檔案,如下圖所示的輸出結果;

結語

透過以上的程式碼實現,我們成功地將自定義資源提取並儲存為一個獨立的檔案。這種技術在一些特殊情況下可能會很有用,例如需要動態載入或替換資源的情況。希望這篇部落格對你理解如何使用 C++ 和 Windows API 進行資源操作有所幫助。