A guide to Windows application development using w64devkit

There’s a trend of building services where a monolithic application is better suited, or using JavaScript and Python then being stumped by their troublesome deployment story. This leads to solutions like bundling an entire web browser with an application, or using containers to circumscribe a sprawling dependency tree made of mystery meat.

My small development distribution for Windows, w64devkit, is my own little way of pushing back against this trend where it affects me most. Following in the footsteps of projects like Handmade Hero and Making a Video Game from Scratch, this is my guide to no-nonsense software development using my development kit. It’s an overview of the tooling and development workflow, and I’ve tried not to assume too much knowledge of the reader. Being a guide rather than manual, it is incomplete on its own, and I link to substantial external resources to fill in the gaps. The guide is capped with a small game I wrote entirely using my development kit, serving as a demonstration of what sorts of things are not only possible, but quite reasonably attainable.

Initial setup

Of course you cannot use the development kit if you don’t have it yet. Go to the releases section and download the latest release. It will be a .zip file named w64devkit-x.y.z.zip where x.y.z is the version.

You will need to unzip the development kit before using it. Windows has built-in support for .zip files, so you can either right-click to access “Extract All…” or navigate into it as a folder then drag-and-drop the w64devkit directory somewhere outside the .zip file. It doesn’t care where it’s unzipped (aka it’s “portable”), so put it where ever is convenient: your desktop, user profile directory, a thumb drive, etc. You can move it later if you change your mind just so long as you’re not actively running it. If you decide you don’t need it anymore then delete it.

Entering the development environment

There is an activate.bat in the unzipped w64devkit directory. This is the easiest way to enter the development environment, and will not require system configuration changes. This batch file puts the kit’s programs in the PATH environment variable then runs a Bourne shell — the standard unix shell. Aside from the text editor, this is the primary interface for developing software. In time you may even extend this environment with your own tools.

If you want an additional “terminal” window, run activate.bat again. If you use it a lot, you may want to create a shortcut and even pin it to your task bar.

Whether on Windows or unix-like systems, when you type a command into the system shell it uses the PATH environment variable to locate the actual program to run for that command. In practice, the PATH variable is a concatenation of multiple directories, and the shell searches these directories in order. On unix-like systems, PATH elements are separated by colons. However, Windows uses colons to delimit drive letters, so its PATH elements are separated by semicolons.

# Prepending to PATH on unix
PATH="$HOME/bin:$PATH"

# Prepending to PATH on Windows (w64devkit)
PATH="$HOME/bin;$PATH"

For more advanced users: Rather than use activate.bat, you could “Edit environment variables for your account” and manually add w64devkit’s bin directory to your PATH, making the tools generally available everywhere on your system. If you’ve gone this route, you can start a Bourne shell at any time with sh -l. (The -l option requests a login shell.)

Also borrowed from the unix world is the concept of a home directory, specified by the HOME environment variable. By default this will be your user profile directory, typically C:/Users/$USER. Login shells always start in the home directory. This directory is often indicated by tilde (~), and many programs automatically expand a leading tilde to the home directory.

Shell basics

The shell is a command interpreter. It’s named such because it was originally a shell around the operating system kernel — the user interface to the kernel. Your system’s graphical interface — Windows Explorer, or Explorer.exe — is really just a kind of shell, too. That shell is oriented around the mouse and graphics. This is fine for some tasks, but a keyboard-oriented command shell is far better suited for development tasks. It’s more efficient, but more importantly its features are composable: Complex operations and processes can be constructed from simple, easy-to-understand tools. Embrace it!

In the shell you can navigate between directories with cd, make directories with mkdir, remove files with rm, regular expression text searches with grep, etc. Run busybox to see a listing of the available standard commands. Unfortunately there are no manual pages, but you can access basic usage information for any command with busybox CMD --help.

Windows’ standard command shell is cmd.exe. Unfortunately this shell is terrible and exists mostly for legacy compatibility. For users who regularly need a shell, the intended replacement is PowerShell. It’s a significant improvement, but 1) it’s still mediocre, 2) sticking to POSIX shell conventions significantly improves build portability, 3) unix tool knowledge is transferable to basically every other operating system.

Unix’s standard shell was the Bourne shell, sh. The shells in use today are Bourne shell clones with a superset of its features. The most popular interactive shells are Bash and Zsh. On Linux, dash (Debian Almquist shell) has become popular for non-interactive use (scripting). The shell included with w64devkit is the BusyBox fork of the Almquist shell (ash), closely related to dash. The Almquist shell has almost no non-interactive features beyond the standard Bourne shell, and so as far as scripts are concerned can be regarded as a plain Bourne shell clone. That’s why I typically refer to it by the name sh.

However, BusyBox’s Almquist shell has interactive features much like Bash, and Bash users should be quite comfortable. It’s not just tab-completion but a slew of Emacs-like keybindings:

Take special note of Ctrl-r, which is the most important and powerful shortcut of the bunch. Frequent use is a good habit. Don’t mash the up arrow to search through the command history.

Special note for Cygwin and MSYS2 users: the shell is aware of Windows paths and does not present a virtual unix file system scheme. This has important consequences for scripting, both good and bad. The shell even supports backslash as a directory separator, though you should of course prefer forward slashes.

Shell customization

Login shells (-l) evaluate the contents of ~/.profile on startup. This is your chance to customize the shell configuration, such as setting environment variables or defining aliases and functions. For instance, if you wanted the prompt to show the working directory in green you’d set PS1 in your ~/.profile:

PS1="$(printf '\x1b[33;1m\\w\x1b[0m$ ')"

If you find yourself using the same command sequences or set of options again and again, you might consider putting those commands into a script, and then installing that script somewhere on your PATH so that you can run it as a new command. First make a directory to hold your scripts, say in ~/bin:

mkdir ~/bin

In ~/.profile prepend it to your PATH:

PATH="$HOME/bin;$PATH"

If you don’t want to start a fresh shell to try it out, then load the new configuration in your current shell:

source ~/.profile

Suppose you keep getting the tar switches mixed up and you’d like to just have an untar command that does the right thing. Create a file named untar or untar.sh in ~/bin with these contents:

#!/bin/sh
set -e
tar -xaf "$@"

Now a command like untar something.tar.gz will extract the archive contents.

To learn more about Bourne shell scripting, the POSIX shell command language specification is a good reference. All of the features listed in that document are available to your shell scripts.

Text editing

The development kit includes the powerful and popular text editor Vim. It takes effort to learn, but is well worth the investment. It’s packed with features, but since you only need a small number of them on a regular basis it’s not as daunting as it might appear. Using Vim effectively, you will write and edit text so much more quickly than before. That includes not just code, but prose: READMEs, documentation, etc.

(The catch: Non-modal editing will forever feel frustratingly inefficient. That’s not because you will become unpracticed at it, or even have trouble code switching between input styles, but because you’ll now be aware how bad it is. Ignorance is bliss.)

Vim includes its own tutorial for absolute beginners which you can access with the vimtutor command. It will run in the console window and guide you through the basics in about half an hour. Do not be afraid to return to the tutorial at any time since this is the stuff you need to know by heart.

When it comes time to actually use Vim to write code, you can continue writing code via the terminal interface (vim), or you can run the graphical interface (gvim). The latter is recommended since it has some nice quality-of-life features, but it’s not strictly necessary. When starting the GUI, put an ampersand (&) on the command so that it runs in the background. For instance this brings up the editor with two files open but leaves the shell running in the foreground so you can continue using it while you edit:

gvim main.c Makefile &

Vim’s defaults are good but imperfect. Before getting started with actually editing code you should establish at least the following minimal configuration in ~/_vimrc. (To understand these better, use :help to jump the built-in documentation.)

set hidden encoding=utf-8 shellslash
filetype plugin indent on
syntax on

The graphical interface defaults to a white background. Many people (including me) prefer “dark mode” when editing code, so inverting this is simply a matter of choosing a dark color scheme. Vim comes with a handful of color schemes, around half of which have dark backgrounds. Use :colorscheme to change it, and put it in your ~/_vimrc to persist it.

colorscheme slate

The default graphical interface includes a menu bar and tool bar. There are better ways to accomplish all these operations, none of which require touching the mouse, so consider removing all that junk:

set guioptions=ac

Finally, since the development kit is oriented around C and C++, here’s my own entire Vim configuration for C which makes it obey my own style:

set cinoptions+=t0,l1,:0 cinkeys-=0#
let c_no_curly_error=1

Once you’re comfortable with the basics, the best next step is to read Practical Vim: Edit Text at the Speed of Thought by Drew Neil. It’s an opinionated guide to Vim that instills good habits. If you want something cost-free to whet your appetite, check out Seven habits of effective text editing.

Writing an application

We’ve established a shell and text editor. Next is the development workflow for writing an actual application. Ultimately you will invoke a compiler from within Vim, which will parse compiler messages and take you directly to the parts of your source code that need attention. Before we get that far, let’s start with the basics.

The classic example is the “hello world” program, which we’ll suppose is in a file called hello.c:

#include <stdio.h>

int main(void)
{
    puts("Hello, world!");
}

While this development kit provides a version of the GNU compiler, gcc, this guide mostly speaks of it in terms of the generic unix C compiler name, cc. Unix-like systems install cc as an alias for the system’s default C compiler, and w64devkit is no exception.

cc -o hello.exe hello.c

This command creates hello.exe from hello.c. Since this is not (yet?) on your PATH, you must invoke it via a path name (i.e. the command must include a slash), since otherwise the shell will search for it via the PATH variable. Typically this means putting ./ in front of the program name, meaning “run the program in the current directory”. As a convenience you do not need to include the .exe extension:

./hello

Unlike the untar shell script from before, this hello.exe is entirely independent of w64devkit. You can share it with anyone running Windows and they’ll be able to execute it. There’s a little bit of runtime embedded in the executable, but the bulk of the runtime is in the operating system itself. I want to highlight this point because most programming languages don’t work like this, or at least doing so is unnatural with lots of compromises. The users of your software do not need to install a runtime or other supporting software. They just run the executable you give them!

That executable is probably pretty small, less than 50kB — basically a miracle by today’s standards. Sure, it’s hardly doing anything right now, but you can add a whole lot more functionality without that executable getting much bigger. In fact, it’s entirely unoptimized right now and could be even smaller. Passing the -Os flag tells the compiler to optimize for size and -s flag tells the linker to strip out unneeded information.

cc -Os -s -o hello.exe hello.c

That cuts the program down to around a third of its previous size. If necessary you can still do even better than this, but that’s outside the scope of this guide.

So far the program could still be valid enough to compile but contain obvious mistakes. The compiler can warn about many of these mistakes, and so it’s always worth enabling these warnings. This requires two flags: -Wall (“all” warnings) and -Wextra (extra warnings).

cc -Wall -Wextra -o hello.exe hello.c

When you’re working on a program, you often don’t want optimization enabled since it makes it more difficult to debug. However, some warnings aren’t fired unless optimization is enabled. Fortunately there’s an optimization level to resolve this, -Og (optimize for debugging). Combine this with -g to embed debug information in the program. This will be handy later.

cc -Wall -Wextra -Og -g -o hello.exe hello.c

These are the compiler flags you typically want to enable while developing your software. When you distribute it, you’d use either -Os -s (optimize for size) or -O3 -s (optimize for speed).

Makefiles

I mentioned running the compiler from Vim. This isn’t done directly but via special build script called a Makefile. You invoke the make program from Vim, which invokes the compiler as above. The simplest Makefile would look like this, in a file literally named Makefile:

hello.exe: hello.c
    cc -Wall -Wextra -Og -g -o hello.exe hello.c

This tells make that the file named hello.exe is derived from another file called hello.c, and the tab-indented line is the recipe for doing so. Running the make command will run the compiler command if and only if hello.c is newer than hello.exe.

To run make from Vim, use the :make command inside Vim. It will not only run make but also capture its output in an internal buffer called the quickfix list. If there is any warning or error, Vim will jump to it. Use :cn (next) and :cp (prev) to move between issues and correct them, or :cc to re-display the current issue. When you’re done fixing the issues, run :make again to start the cycle over.

Try that now by changing the printed message and recompiling from within Vim. Intentionally create an error (bad syntax, too many arguments, etc.) and see what happens.

Makefiles are a powerful and conventional way to build C and C++ software. Since the development kit includes the standard set of unix utilities, it’s very easy to write portable Makefiles that work across a variety a operating systems and environments. Your software isn’t necessarily tied to Windows just because you’re using a Windows-based development environment. If you want to learn how Makefiles work and how to use them effectively, read A Tutorial on Portable Makefiles. From here on I’ll assume you’ve read that tutorial.

Ultimately I’d probably write my “hello world” Makefile like so:

.POSIX:
CC      = cc
CFLAGS  = -Wall -Wextra -Og -g
LDFLAGS =
LDLIBS  =
EXE     = .exe

hello$(EXE): hello.c
    $(CC) $(CFLAGS) $(LDFLAGS) -o $@ hello.c $(LDLIBS)

When building a release, optimize for size or speed:

make CFLAGS=-Os LDFLAGS=-s

This is very much a Windows-first style of Makefile, but still allows it to be comfortably used on other systems. On Linux this make invocation strips away the .exe extension:

make EXE=

For a Windows-second Makefile, remove the line with EXE = .exe. This allows EXE to come from the environment. So, for instance, I already define the EXE environment variable in my w64devkit ~/.profile:

export EXE=.exe

On Linux running make does the right thing, as does running make on Windows. No special configuration required.

If my software is truly limited to Windows, I’m likely still interested in supporting cross-compilation. A common convention for GNU toolchains is a CROSS Makefile macro. For example:

.POSIX:
CROSS   =
CC      = $(CROSS)gcc
CFLAGS  = -Wall -Wextra -Og -g
LDFLAGS =
LDLIBS  =

hello.exe: hello.c
    $(CC) $(CFLAGS) $(LDFLAGS) -o $@ hello.c $(LDLIBS)

On Windows I just run make, but on Linux I’d set CROSS appropriately.

make CROSS=x86_64-w64-mingw32-

What happens if you’re working on a larger program and you need to jump to the definition of a function, macro, or variable? It would be tedious to use grep all the time to find definitions. The development kit includes a solid implementation of ctags for building a tags database lists the locations for various kinds of definitions, and Vim knows how to read this database. Most often you’ll want to run it recursively like so:

ctags -R

You can of course do this from Vim, too: :!ctags -R

With the cursor over an identifier, press CTRL-] to jump to a definition for that name. Use :tn and :tp to move between different definitions (e.g. when the name is overloaded). Or if you have a tag in mind rather than a name listed in the buffer, use the :tag command to jump by name. Vim maintains a tag stack and jump list for going back and forth, like the backward and forward buttons in a browser.

Debugging

I had mentioned that the -g option embeds extra information in the executable. This is for debuggers, and the development kit includes the GNU Debugger, gdb, to help you debug your programs. The most common way to use the debugger is to invoke your program directly from it:

gdb --args ./hello

From here you can set breakpoints and such, then run the program with start or run, then step through it line by line. See Beej’s Quick Guide to GDB for a guide.

Learning C and C++

So far this guide hasn’t actually assumed any C knowledge. One of the best ways to learn C is by reading the highly-regarded The C Programming Language and doing the exercises. Alternatively, cost-free options are Beej’s Guide to C Programming and Modern C (more advanced). You can use the development kit to go through any of these.

I’ve focused on C, but everything above also applies to C++. To learn C++ A Tour of C++ is a safe bet.

Demonstration

To illustrate how much you can do with nothing beyond than this 76MB development kit, here’s a taste in the form of a weekend project: an Asteroids Clone for Windows. That’s the game in the video at the top of this guide.

The development kit doesn’t include Git so you’d need to install it separately in order to clone the repository, but you could at least skip that and download a .zip snapshot of the source. It has no third-party dependencies yet it includes hardware-accelerated graphics, real-time sound mixing, and gamepad input. Building a larger and more complex game is much less about tooling and more about time and skill. That’s what I mean about w64devkit being (almost) everything you need.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)