寫一個系列,主要目的是面向主要是向會用 C 語言,會寫一般的 GUI 程序,但不熟悉 Win32 SDK 的開發人員簡單介紹一下 Win32 SDK 開發,同時也是順便把之前沒用到的部分都了解一下。只會涉及到比較常用的幾個部分:
- 常用 Win32 API
- COM 接口調用
- JScript
- GUI
- 密碼學 API
這里約定發行版使用 Fedora ,編譯程序用 MinGW ,運行程序用 WINE1。代碼盡量只用 C 語言。另外,源代碼目錄直接運行 mingw32-make
,就會完成編譯。
先來看一下如何編譯、鏈接。
Hello World
從經典的 Hello World 開始。Hello World 的代碼沒有任何特別之處。
#include <stdio.h>
int main() {
printf("%s\n", "Hello, world!");
return 0;
}
這里用 mingw32-env
,僅僅因為它會設置 CC
等環境變量,這樣輸入的命令看起來可以和 makefile 里的比較接近。輸出文件記得加上后綴 .exe
,Windows 下可執行文件的文件名通常以 .exe
結尾2。
$ mingw32-env
$ ${CC} -o 'hello.exe' 'hello.c'
$ wine 'hello.exe'
Hello, world!
靜態鏈接
靜態鏈接的情況和 Hello World 類似。在 hello.c
中定義 hello
函數。
#include <stdio.h>
#include "hello.h"
void hello() {
printf("%s\n", "Hello, world!");
}
在頭文件里聲明 hello
函數。
#ifndef __HELLO_H__
#define __HELLO_H__
void hello();
#endif /* __HELLO_H__ */
在 main.c
里調用一下。
#include "hello.h"
int main() {
hello();
return 0;
}
除了輸出文件需要加上后綴 .exe
以外,靜態鏈接的時候並無特殊之處。如果你不是經常用靜態鏈接,可以參考 Program Library HOWTO。
$ mingw32-env
$ ${CC} -c -o 'main.o' 'main.c'
$ ${CC} -c -o 'hello.o' 'hello.c'
$ ${AR} r 'libhello.a' 'hello.o'
$ ${CC} -o 'main.exe' -static -L'.' 'main.o' -lhello
$ wine 'main.exe'
Hello, world!
動態鏈接
動態鏈接就有些不同了3。在 main.c
里調用的寫法和靜態鏈接一樣。
#include "hello.h"
int main() {
hello();
return 0;
}
頭文件要根據不同情況,把函數分別聲明為 __declspec(dllexport)
和__declspec(dllimport)
。
#ifndef __HELLO_H__
#define __HELLO_H__
#ifdef __BUILD_DLL__
#define DLLEXPORT __declspec(dllexport)
#else /* __BUILD_DLL__ */
#define DLLEXPORT __declspec(dllimport)
#endif /* __BUILD_DLL__ */
DLLEXPORT void hello();
#endif /* __HELLO_H__ */
hello.c
里的 hello
函數要和頭文件里的聲明保持一致。
#include <stdio.h>
#include "hello.h"
DLLEXPORT void hello() {
printf("%s\n", "Hello, world!");
}
編譯 DLL4時,設置宏 __BUILD_DLL__
。注意 .a
文件要指定-Wl,--out-implib
這個參數才會輸出的,動態鏈接庫的后綴是 .dll
。
$ mingw32-env
$ ${CC} -c -o 'main.o' 'main.c'
$ ${CC} -c -o 'libhello.o' -D__BUILD_DLL__ 'hello.c'
$ ${CC} -shared -o 'libhello.dll' -Wl,--out-implib='libhello.dll.a' 'libhello.o'
$ ${CC} -o 'main.exe' -L'.' 'main.o' -lhello
$ wine 'main.exe'
Hello, world!
運行期調用
運行期調用動態鏈接庫中的函數,使用的函數5有點特別。用 LoadLibrary
來載入動態鏈接庫,用 GetProcAddress
來找到函數地址。
#include <windows.h>
int main() {
HMODULE hModule = LoadLibrary("hello.dll");
if (!hModule) return 1;
FARPROC hello = GetProcAddress(hModule, "hello");
hello();
FreeLibrary(hModule);
return 0;
}
hello.c
和上一個例子一樣。
#include <stdio.h>
__declspec(dllexport)
void hello() {
printf("%s\n", "Hello, world!");
}
除了后綴,編譯的命令沒什么特別的。
$ mingw32-env
$ ${CC} -o 'main.exe' 'main.c'
$ ${CC} -shared -o 'hello.dll' 'hello.c'
$ wine 'main.exe'
Hello, world!
資源文件
Windows 可執行文件格式6中比較特殊的一點是資源文件7。其中,字符串資源處理起來要特別小心8。下面就來看一下字符串資源。以下是資源文件 resource.rc
。
#include <afxres.h>
#include "resource.h"
STRINGTABLE
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
BEGIN
IDS_HELLO L"Hello, world!"
END
STRINGTABLE
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
BEGIN
IDS_HELLO L"\x54c8\x56c9\xff0c\x4e16\x754c\xff01"
END
其中的 IDS_HELLO
是在 resource.h
中定義的。
#ifndef _RESOURCE_H_
#define _RESOURCE_H_
#define ID_BASE_RES 0x100
#define IDS_HELLO (ID_BASE_RES+1)
#endif /* _RESOURCE_H_ */
因為歷史原因,為了載入對應的字符串,你唯一能做的就是逐一跳過在它之前的所有字符串。這里偷懶,就把取出來的字符串直接全存到 hStringHeap
里去了。這里用 WinMain9,僅僅是想讓 hInstance
好看點,即使直接用 NULL
是可以的。
#include <stdio.h>
#include <windows.h>
#include "resource.h"
typedef struct {
UINT uID;
LPTSTR *table;
} STRING_TABLE;
HANDLE hStringHeap = NULL;
STRING_TABLE *pStringTables = NULL;
LPTSTR* FindStringTable(UINT uID) {
SIZE_T size = (pStringTables) ?
HeapSize(hStringHeap, 0, pStringTables)/sizeof(STRING_TABLE):
0;
for (int i=0; i<size; i++)
if (pStringTables[i].uID == uID/16 + 1)
return pStringTables[i].table;
if (pStringTables)
pStringTables = HeapReAlloc(
hStringHeap,
HEAP_ZERO_MEMORY,
pStringTables,
sizeof(STRING_TABLE) * (size+1));
else
pStringTables = HeapAlloc(hStringHeap, HEAP_ZERO_MEMORY, sizeof(STRING_TABLE));
pStringTables[size].uID = uID/16 + 1;
pStringTables[size].table = HeapAlloc(
hStringHeap,
HEAP_ZERO_MEMORY,
sizeof(LPCTSTR) * 16);
return pStringTables[size].table;
}
LPCTSTR LoadResourceString(HINSTANCE hInstance, UINT uID) {
LPTSTR *table = FindStringTable(uID);
if (table[uID&15])
return table[uID&15];
HRSRC hRsrc = FindResource(hInstance, MAKEINTRESOURCE(uID/16+1), RT_STRING);
HGLOBAL hGlobal = LoadResource(hInstance, hRsrc);
LPVOID pResource = LockResource(hGlobal);
LPCWSTR pString = pResource;
for (int i=0; i<(uID&15); i++)
pString += 1 + *((WORD *)pString);
WORD length = *((WORD *)pString);
pString += 1;
int buflen = WideCharToMultiByte(CP_OEMCP, 0, pString, length, NULL, 0, NULL, NULL);
table[uID&15] = HeapAlloc(hStringHeap, HEAP_ZERO_MEMORY, sizeof(CHAR) * (buflen+1));
WideCharToMultiByte(CP_OEMCP, 0, pString, length, table[uID&15], buflen, NULL, NULL);
UnlockResource(pResource);
FreeResource(hGlobal);
return table[uID&15];
}
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow) {
hStringHeap = HeapCreate(0,0,0);
if (!hStringHeap)
return 1;
printf("%s\n", LoadResourceString(hInstance, IDS_HELLO));
HeapDestroy(hStringHeap);
return 0;
}
用 windres
10 命令生成 .res
文件,這樣在鏈接的時候就可以被加到 .exe
里。
$ mingw32-env
$ ${WINDRES} -i 'resource.rc' --input-format=rc -O coff -o 'resource.res'
$ ${CC} -c -o 'hello.o' -std=c99 'hello.c'
$ ${CC} -o 'hello.exe' 'hello.o' 'resource.res'
$ wine 'hello.exe'
Hello, world!
你可以檢查一下,看看 mingw32-binutils、mingw32-cpp、mingw32-filesystem、mingw32-gcc、mingw32-runtime、mingw32-w32api、wine、wine-devel 這幾個包是不是都已經裝上了。 ↩