Compiling with Clang without Visual Studio

(Edited on: )
Tags: programming


Lately, I have been trying to code in C. When coding on personal projects, I don't want to feel like I am working. I don’t want to deal with C++, annoying build systems, slow compile times, or any kind of "standard" library. I just want something simple, fast, and minimalistic! I went down the rabbit hole: C++, Rust, ++ again, C, and finally freestanding C.

While reinstalling Windows on my home desktop computer, I decided not to install Visual Studio. Now that RAD Debugger is a thing, I can install the Windows SDK, LLVM, RAD Debugger, and my editor of choice for a perfect minimalist setup.

But it was not that simple…​
Instead of showing my ugly code, I will take a sample from the Win32 documentation as an example:

#ifndef UNICODE
#define UNICODE
#endif

#include <Windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
{
    // Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";

    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create the window.

    HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Learn to Program Windows",    // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

        NULL,       // Parent window
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
        );

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);

    // Run the message loop.

    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

            // All painting occurs here, between BeginPaint and EndPaint.

            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

            EndPaint(hwnd, &ps);
        }
        return 0;

    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

If we naively try to compile it with Clang, we get this error:

> clang -o win32_sample win32_sample.c
clang: warning: unable to find a Visual Studio installation; try running Clang from a developer command prompt [-Wmsvc-not-found]
win32_sample.c:5:10: fatal error: 'Windows.h' file not found
    5 | #include <Windows.h>
      |          ^~~~~~~~~~~
1 error generated.

We will ignore the warning about Clang not finding Visual Studio. As for Windows.h, let's find it using Everything.

Search box indicating Windows.h path

So Windows.h is located in the Windows SDK C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\um\, fair enough.
Let's add it to the include list as a system path with the -isystem flag.

> clang -o win32_sample win32_sample.c -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um/"
clang: warning: unable to find a Visual Studio installation; try running Clang from a developer command prompt [-Wmsvc-not-found]
In file included from win32_sample.c:5:
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\Windows.h:1:10: fatal error: 'winapifamily.h' file not found
    1 | #include <winapifamily.h>
      |          ^~~~~~~~~~~~~~~~
1 error generated.

Now we need winapifamily.h, which is located in C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\shared\.
Let's try again.

> clang -o win32_sample win32_sample.c -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um/" -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared/"
clang: warning: unable to find a Visual Studio installation; try running Clang from a developer command prompt [-Wmsvc-not-found]
In file included from win32_sample.c:5:
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\Windows.h:171:10: fatal error: 'excpt.h' file not found
  171 | #include <excpt.h>
      |          ^~~~~~~~~
1 error generated.

And we are hitting the first wall: there is no excpt.h on my machine. Apparently, this file comes with Visual Studio installation. I mean, why can't the Windows SDK be self-contained? It's ridiculous that Visual Studio relies on the Windows SDK, and the Windows SDK needs Visual Studio…​

Let's try to make our way through and brute-force it. The idea is to make our own libc stub by creating some empty header files and seeing what compile errors we get.

A few headers later (excpt.h, ctype.h, string.h, stdlib.h), we are hitting these errors:

> clang -o win32_sample win32_sample.c -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um/" -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared/" -I "./"
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\winnt.h:480:9: error: unknown type name 'wchar_t'
  480 | typedef wchar_t WCHAR;    // wc,   16-bit UNICODE character
      |         ^
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\winnt.h:1441:1: error: unknown type name 'EXCEPTION_DISPOSITION'
 1441 | EXCEPTION_DISPOSITION
      | ^
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\winnt.h:22463:5: error: call to undeclared library function 'memset' with type 'void *(void *, int, unsigned long long)'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
 22463 |     RtlZeroMemory(Config, sizeof(CUSTOM_SYSTEM_EVENT_TRIGGER_CONFIG));
       |     ^
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um\winioctl.h:4754:5: error: call to undeclared library function 'memcpy' with type 'void *(void *, const void *, unsigned long long)'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
 4754 |     RtlCopyMemory(DeviceDsmParameterBlock(Input),
      |     ^
C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared\stralign.h:483:16: error: call to undeclared function '_wcsicmp'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
  483 |         return _wcsicmp( (LPCWSTR)String1, (LPCWSTR)String2 );
      |                ^
5 errors generated.

Declaring memcpy and memset is common for CRT-free programs. Even if not used in code, the compiler can still generate calls to these functions to copy or zero-initialize structs.
Next are wchar_t related symbols. Windows implements Unicode with UCS encoding. We need to define wchar_t 16 bits type, and we should be fine. Depending on which Windows headers are included, there will probably be different missing functions related to strings.
In this sample, we just need to stub _wcsicmp, hoping that it will not get called and that, if it is the case, it is good enough to fool Windows, and ourselves…​
EXCEPTION_DISPOSITION is an enum returned by structured exception handlers. As all enums in C are int, we can safely typedef it to an int.

// CRT-free stub
// ctype.h
#define wchar_t unsigned short
#define __ascii_towlower(c)  ( (((c) >= L'A') && ((c) <= L'Z')) ? ((c) - L'A' + L'a') : (c) )
// string.h
void memcpy(void *dst, const void* src, size_t len)
{
  for (size_t i = 0; i < len; ++i) ((char*)dst)[i] = ((const char*)src)[i];
}
void memset(void *dst, int val, size_t len)
{
  for (size_t i = 0; i < len; ++i) ((char*)dst)[i] = (char)val;
}
//
int _wcsicmp (
        const wchar_t * dst,
        const wchar_t * src
        )
{
        wchar_t f,l;
        do  {
            f = __ascii_towlower(*dst);
            l = __ascii_towlower(*src);
            dst++;
            src++;
        } while ( (f) && (f == l) );
        return (int)(f - l);
}
// exception handling
typedef int EXCEPTION_DISPOSITION;

The sample is now compiling (correctly?). Clang is still not happy: error: unable to execute command: program not executable.
By default, Clang relies on link.exe to link. Fortunately, LLVM includes lld on Windows; we can enable it by passing this flag to clang: -fuse-ld=lld.

> clang -o win32_sample win32_sample.c -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/um/" -I "C:/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared/" -I "./" -fuse-ld=lld
lld-link: error: could not open 'libcmt.lib': no such file or directory
lld-link: error: could not open 'oldnames.lib': no such file or directory
clang: error: linker command failed with exit code 1 (use -v to see invocation)

And it does not link.

libcmt is the C runtime, and oldnames is …​ old. We don't need them, so we'll add the -nostdlib flag to tell Clang that we don't need any library by default, as we would much rather specify them manually. We also need link to user32.lib, which contains the Window API, by adding -luser32.

lld-link: error: <root>: undefined symbol: wWinMainCRTStartup

We are almost done!

The entrypoint for regular win32 programs is either main for command line applications, or [w]WinMain for graphical apps. The wWinMain version takes the command line as a wide string, and the WinMain as an ANSI string. The actual entrypoints are mainCRTStartup and [w]WinMainCRTStartup. They are usually implemented by the CRT, they will first initialize all static variables, then call your entrypoint (without the CRTStartup). Let's our entrypoint that will call wWinMain:

int WINAPI wWinMainCRTStartup()
{
  wWinMain((HINSTANCE)0x400000, NULL, NULL, SW_SHOWDEFAULT);
  ExitProcess(0);
  return 0;
}

The CRT entrypoint is reponsible for preparing command line arguments for the application entrypoint. You can implement it using the win32 function `CommandLineToArgvW `, but it is actually a big mess. It is so bad that UCRT, the latest C runtime from Microsoft, does not even use it.

Screenshot of the sample running

Success!