Intro to C programming on Windows for POSIX developers
Daniel Andersson
2022-06-18
This blog post assumes the reader is a POSIX developer that wants to start writing C, or C++ on Windows. Windows Subsystem for Linux is not relevant since we are developing native Windows applications. The simplest way to get started is w64devkit. However, w64devkit may cause licensing issues if you intend to distribute your software. Clang for Windows can also be used. MSVC will be the focus of this blog post. There are probably ways to just get compiler, linker, standard library and Windows SDK installed, but for simplicity just install visual studio community edition.
hello.c:
#include <stdio.h>
int main(void) {
printf("Hello Windows!\n");
return 0
}
build.bat:
@echo off
set cflags=/W3 /std:c11 -Zi
cl %c_flags% hello.c
The compiler flags turn on warnings, C11 and debug info, see compiler flag reference for details. Running the program in the developer console will print Hello Windows!
.
It is important to use a developer commmand prompt that sets up the environment, otherwise the compiler and libraries will not be found. Also make sure to use the x64 version of the developer command prompt.
For GUI/Win32 applications, stdout, stderr and stdin work a little different. It seems that Win32/GUI applications are generally not supposed to write to stdout/stderr and it’s easier to just write logfiles to disk. If you are using Visual Studio or any IDE on windows, the easiest way to get some output in a Win32 application is OutputDebugStringA. OutputDebugStringA will end up in the Output tab in the IDE. Using a MessageBox is another quick way to just output some text. MessageBox will block the program until it is closed. In order to get output from printf and friends one has to allocate a console, which will start a new console, regardless if you ran the program from another console, or started it from the file explorer.
if (AllocConsole()) {
FILE *fh = 0;
freopen_s(&fh, "CONOUT$", "w", stdout);
freopen_s(&fh, "CONOUT$", "w", stderr);
}
The good thing is that you don’t have to run your program from a console, but the bad part is that when the program ends, the console is closed and the output is lost. To keep your output, there needs to be a blocking operation before program exit.
The Win32 API uses Hungarian notation for naming variables. Variables are prefixed with its data type, so that you know what kind of data type it is. The Win32 has types with platform dependent length/size, something like typedef long LONG
and typedef unsigned long ULONG
. Parameters could then be LONG lFoo
and ULONG ulBar
. There are also types with fixed length/size, like typedef uint8_t BYTE
.
Data Type | Prefix | Size |
---|---|---|
BYTE | b | 8 |
WORD | w | 16 |
DWORD | dw | 32 |
QWORD | qw | 64 |
A variable named dwBar
would indicate an unsigned 32-bit integer. Additionally, a type or variable prefixed with p, usually indicates that it is a pointer, like typedef uint32_t * PDWORD
. A variable called qwppBaz
would then indicate the type uint_64_t **
.
The HANDLE
data types are important in Win32 programming and they are essentially void *
. The Windows kernel maintains a table of all the different objects that the kernel is responsible for. All windows, icons, buttons have entries in the table with unique addresses, the handles. HWND
is a typedef used for windows and HINSTANCE
is a type used for instances of programs. If you for example want to create a new window, you need to pass the instance of the program to the kernel API.
As an example, referencing MSDN for WinMain() we get the types:
// HINSTANCE
typedef void *PVOID;
typedef PVOID HANDLE;
typedef HANDLE HINSTANCE;
// LPSTR
typedef CHAR *LPSTR;
int WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
);
For WinMain, the first two parameters are thus void *
. The third parameter is char *
to an already allocated string(ASCII). I am unsure why the typedef indicates long *
for lpCmdLine, when MSDN says otherwise. The last parameter is a regular int.
#include <stdio.h>
#include <windows.h>
int WinMain(
HINSTANCE hInst,
HINSTANCE hInstPrev,
PSTR cmdline,
int cmdshell)
{
if (AllocConsole()) {
FILE *fh = 0;
freopen_s(&fh, "CONOUT$", "w", stdout);
freopen_s(&fh, "CONOUT$", "w", stderr);
}
printf("Hello Windows!\n");
MessageBox(NULL, "We are done", "Foo", 0); // Block program before exit
return 0;
}
When using functions from the operating system, you also need to link with some libraries. If you are doing memory management with for example, VirtualAlloc you need to link with kernel32.lib. For native GUI/windows code, like MessageBox, you need to link with user32.lib. When doing socket programming with Winsock2, you need to link with Ws2_32.lib.
@echo off
set cflags=/W3 /std:c11 -Zi
set libs=user32.lib kernel32.lib Ws2_32.lib
cl %c_flags% %libs% hello.c
It is also possible to use
#pragma comment(lib, "kernel32.lib")
in a source file to not have to specify libraries in the batch file.
You can get pretty far by just using a batch file, instead of having to figure out how Visual Studio projects or CMake works on Windows.