写一个系列,主要目的是面向主要是向会用 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 这几个包是不是都已经装上了。 ↩