Skip to content(if available)orjump to list(if available)

I built a native Windows Todo app in pure C (278 KB, no frameworks)

masternight

There is something I like about win32 gui programming. It's a little idiosyncratic, but if you read Raymond Chen's blog you'll see why.

The win32 API has its origins on the 8088 processor and doing things a certain way results in saving 40 bytes of code or uses one less register or something.

I wrote a lot of toy gui apps using mingw and Petzold's book back in the day. Writing custom controls, drawing graphics and text, handling scrolling, hit testing etc was all a lot of fun.

I see in your app you're using strcpy, sprintf. Any kind of serious programming you should be using the length-checked variants. I'm surprised the compiler didn't spew.

You'll also find that the Win32 API has a lot of replacements for what's in the C standard library. If you really want to try and get the executable size down, see if you can write your app using only <Windows.h> and no cstdlib. Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

At some point writing raw C code becomes painful. Still, I think doing your first few attempts in raw C is the best way to learn. Managing all the minutiae gives you a great sense of what's going on while you're learning.

If you want to play more with win32 gui programming, I'd have a look at the WTL (Windows Template Library). It's a C++ wrapper around the win32 API and makes it much easier to reason about what's going on.

BarryGuff

> There is something I like about win32 gui programming

Totally agree with you. I use an excellent PC app called AlomWare Toolbox, and it's the epitome of Win32 design (https://www.alomware.com/images/tab-automation.png), and despite it doing so much it's only about 3 MB in size because of it. No frameworks with it either, just a single executable file. I wish all software were still like this.

bmacho

Is the font-size adjustable? It's too small on my screen

scripturial

At minimum, these days, if you dont use strncpy instead of strcpy, you’ll have to suffer through every man and his dog (or AI tool) forever telling you to do otherwise. (For me this is one of the main arguments of using zig, a lot of these common pitfalls are minimized by using zig, but c is fine as well)

masternight

Heh, and if you use strncpy() you'll have to suffer through me lecturing you on why strncpy() is the wrong function to use as well.

nly

strncpy is more or less perfect in my line of work where a lot of binary protocols have fixed size string fields (char x[32]) etc.

The padding is needed to make packets hashable and not leak uninitialized bytes.

You just never assume a string is null terminated when reading, using strnlen or strncpy when reading as well.

userbinator

Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

I believe MSVC intrinsics will use the rep stos/movs instructions, which are even smaller than calling functions (which includes the size of their import table entries too.)

kevin_thibedeau

The standard allows memset/memcpy to be replaced by inline code. There is no need to use non-standard extensions to get a performance boost.

userbinator

That's how the MSVC intrinsics work. Turn on the option and memset/memcpy, among others, gets replaced automatically:

https://learn.microsoft.com/en-us/cpp/preprocessor/intrinsic...

rcarmo

I spent a lot of time doing that and to be honest, I miss the ability to develop for native UIs with native code.

codebolt

> You'll also find that the Win32 API has a lot of replacements for what's in the C standard library. If you really want to try and get the executable size down, see if you can write your app using only <Windows.h> and no cstdlib. Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

I see he's also using fopen/fread/fclose rather than CreateFile/ReadFile/WriteFile/etc.

donnachangstein

> I see he's also using fopen/fread/fclose rather than CreateFile/ReadFile/WriteFile/etc.

It's a todo list, not a network service. So what if it's using unbounded strcpy's all over the place? It has basically no attack surface. He wrote it for himself, not for criticism from the HN hoi polloi.

For once maybe take someone's work at face value instead of critiquing every mundane detail in order to feel like the smartest person in the room.

Computers are tools to get stuff done. Sometimes those tools are not pretty.

I place much of the criticism being levied here in the same category as the "we must rewrite 'ls' in Rust for security" nonsense that is regularly praised here.

masternight

So what if it's using unbounded strcpy's all over the place? It has basically no attack surface. He wrote it for himself, not for criticism from the HN hoi polloi

I didn't point that out so I could be the smartest person in the room and I certainly don't subscribe to the whole rewrite-the-world in rust.

The sheer amount of time I spent debugging problems caused by buffer overruns and other daft problems is immense. It's literal days of my life that could have been saved had safer APIs been created in the first place.

It's a cool toy program and I encourage the learning but maybe let's try and avoid unnecessary problems.

int_19h

To be fair, CreateFile etc are a lot more verbose than fopen.

null

[deleted]

MortyWaves

> Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

What is or was the purpose of providing these instead of the existing Windows C std?

userbinator

It's worth remembering that Windows 1.x and 2.x predates the C89 standard. This also explains why WINAPI calling convention was inherited from Pascal instead of C. The C standard library was "just another competitor" at the time.

int_19h

The WINAPI calling convention is a cross between C and Pascal - C-style order of arguments on the stack, but Pascal-style callee cleaning the stack before return.

The reason for its use in Windows is that it makes generated code slightly smaller and more efficient, at the cost of not supporting varags easily (which you don't need for most functions anyway). Back when you had 640 Kb of RAM, saving a few bytes here and there adds up quickly.

masternight

Those functions explicitly? I can't find any definitive explanation on why they exist.

It looks like nowdays ZeroMemory() and RtlZeroMemory() are just macros for memset().

Here's an article on some of the RECT helper functions. Relevant for the 8088 CPU but probably not so much today: https://devblogs.microsoft.com/oldnewthing/20200224-00/?p=10...

mike_hearn

Windows didn't standardize on C. It was mostly assembly and some Pascal in the beginning with C and C++ later.

Microsoft have always viewed C as just another language, it's not privileged in the way UNIX privileges C. By implication, the C standard library was provided by your compiler and shipped with your app as a dependency on Windows, it wasn't provided by the operating system.

These days that's been changing, partly because lots of installers dumped the MSVC runtime into c:\windows\system and so whether it was a part of the OS or not became blurred and partly because Microsoft got more willing to privilege languages at the OS level. Even so, the Windows group retains a commitment to language independence that other operating systems just don't have. WinRT comes with lots of metadata for binding it into other languages, for example.

pjmlp

Apple was the one going with Pascal for the OS, originally the Object Pascal linage was started at Apple, in collaboration with Niklaus Wirth that gave feedback on the design.

Narishma

> Windows didn't standardize on C. It was mostly assembly and some Pascal in the beginning with C and C++ later.

No, it was never Pascal. It was always C from the beginning. You may have been confused by them using the Pascal calling convention because it was generally faster on the 16-bit CPUs of the time.

lmz

You could write code without using libc / the C runtime. You still can.

int_19h

Unlike Unix, Windows historically didn't have a standard C runtime at all. Stuff like MSVCRT.DLL etc came later (and are themselves implemented on top of Win32 API, not directly on top of syscalls as is typical in Unix land).

rlkf

I second this, and just want to add that strsafe.h contains replacements for the runtime string routines.

raverbashing

I agree with most of this, but let's be honest, win32 gui programming (like this) is/was a pain

Even MFC barely took the edge out. It's amazing how much better Borland built their "Delphi like" C++ library.

> Instead of memset() you've got ZeroMemory(), instead of memcpy() you've got CopyMemory().

Yes. And your best API for opening (anything but files but maybe files as well) is... CreateFile

Aah the memories :)

int_19h

> It's amazing how much better Borland built their "Delphi like" C++ library.

As I recall, it wasn't "Delphi like", but rather literally the same VCL that Delphi used. That's why C++Builder had all those language extensions - they mapped 1:1 to the corresponding Delphi language features so that you could take any random Delphi unit (like VCL) and just use it from C++. In fact, C++Builder could even compile Delphi source code.

pjmlp

Yes, and it grew out of Object Windows Library, which also add extensions, and was definitly much more pleasant to use than MFC has ever managed to.

No need for the past tense, both products are still on the market with frequent releases and developer conferences, even if no longer at the same adoption level.

electroly

Instead of laboriously calling CreateWindow() for every control, traditionally we would lay out a dialog resource in a .rc file (Visual Studio still has the dialog editor to do it visually) and then use CreateDialog() instead of CreateWindow(). This will create all the controls for you. Add an application manifest and you can get modern UI styling and high-DPI support.

pjmlp

Only UNIX overlaps C standard library with OS library, and back in 1985 (Windows 1.0 release), there was still no standard to speak of.

Sure there was K&R C, which each OS outside UNIX cherry picked what would be available.

Additionally outside UNIX clones, the tradition among vendors has been that the C compiler is responsible for the standard library, not the platform.

Thus the C library was provided by Borland, Watcom, Symantec, Microsoft, Green Hills, Zortech,....

Note it was the same on Mac OS, until MPW came to be.

As it was in IBM and Unisys, micros and mainframes.

VMS before OpenVMS.

And so on.

Since Windows 10, you have the Universal C Runtime as well.

Narishma

I think you replied to the wrong comment.

userbinator

You also get automatic tabbing between controls, and a few other keyboard shortcuts this way. Note that resizing them still needs to be done manually if you want that, but that's usually easy and not more than a few hundred bytes of code.

urbandw311er

Great answer, helpful and not judgemental.

kazinator

However, this approach is easily translatable to a language that has decent FFI, and requires nothing else: no resource compiler and linker to make a resource DLL.

Resource files and their binary format are not a good API.

If you have those CreateWindow calls in a decently high level language, you can probably meta-program some resource-like DSL that fits right in the language.

int_19h

You don't need a "resource DLL"; the compiled .rc file gets linked directly into the binary, and any Win32 C toolchain is capable of doing that, including MinGW.

As API goes, I don't see what's wrong with it (anymore so than Win32 in general). And you do get quite a lot for free, as GP mentioned. Hi-DPI, for example - .rc files use "dialog units" to measure all widgets, which, unlike raw pixel values you pass to CreateWindow, are DPI-independent.

kazinator

What if "the binary" is stock (not built or relinked by you) installation of a language run-time? That's more like the case I'm thinking of.

A program like this nicely translates to that situation.

belter

Look it up in Petzold they used to say...

tonyedgecombe

>Visual Studio still has the dialog editor to do it visually

They are using gcc.

electroly

That doesn't matter; you can still use Visual Studio to create the .rc file. This technique still works great for MinGW-based projects. The important thing is that Visual Studio has a .rc dialog editor.

int_19h

There are several .rc editors for Windows outside of VS that incorporate visual dialog editing. Pelles C would be one example that still gets regular updates.

broken_broken_

I have done something similar for Linux under 2 KiB in assembly some time ago: https://gaultier.github.io/blog/x11_x64.html

As others have said, doing so in pure C and linking dynamically, you can easily remain under 20 KiB, at least on Linux, but Windows should be even simpler since it ships with much more out of the box as part of the OS.

In any event, I salute the effort! You can try the linking options I mentioned at the end of my article, it should help getting the size down.

johnisgood

Well, my somewhat extended TUI (ncurses) TODO program is 15K. Linux. Not statically linked though. I did not get around to build ncurses yet with musl.

eviks

> no frameworks

Checks out: blurry fonts in scaled dpi, no Tab support, can't Ctrl-A select text in text fields and do all the other stuff that pre-modern frameworks offered you, errors on adding a row, ...

> modern

In what way?

Dwedit

Example of setting DPI awareness: https://github.com/Dwedit/GameStretcher/blob/master/Stretche...

This code dynamically checks for and calls one of the following: user32:SetProcessDpiAwarenessContext, shcore:SetProcessDpiAwareness, then user32:SetProcessDPIAware. If the Windows version is extremely old and doesn't implement any of those (Windows XP or earlier), it won't call anything.

scq

You can also set it in the application manifest, which is recommended over setting it programmatically: https://learn.microsoft.com/en-us/windows/win32/hidpi/settin...

Tringi

It's a little more complicated if you are to be using themes, GDI and common controls. Some time ago I put together this example: https://github.com/tringi/win32-dpi

The high DPI support in Windows went through quite an evolution since XP, but mostly to fix what app programmers messed up. You can have nice and crisp XP at 250% dpi if you do things right, e.g.: https://x.com/TheBobPony/status/1733196004881482191/photo/1

sargstuff

Ah isn't the user32:<windows api functions> a framework not related to 'pure' C?

ghewgill

The colons there don't represent C++. That's just a way of referring to a windows API function that exists in a specific DLL (in this case "user32"). Because the functions used here do not exist in older versions of Windows, the linked code dynamically loads user32.dll and tries to get the address of those functions so they can be called. That's why you need to know which Windows DLL they exist in.

jchw

Arguably, Windows itself is an object oriented UI framework.

userbinator

It's "modern" in that it's much bigger than necessary, while missing a lot of functionality.

(A lot of what you mention is missing is trivial to add, especially tabbing between controls.)

card_zero

I think font scaling is fixed (i.e. turned on) with SetThreadDpiAwarenessContext(-4). Or whatever the constant that equates to -4 is called.

bobsmooth

It's modern in that he just released it.

AaronAPU

The 6502 programmer in me is dying inside that 278kb now passes as lightweight.

abbeyj

I tried to reproduce this binary to see what the 278 KB was being taken up by. The first obstacle that I ran into was that the build.bat file doesn't work if you have git configured to use core.autocrlf=false. Changing that to core.autocrlf=true and recloning was sufficient to get me building.

I'm using x86_64-15.1.0-release-win32-seh-msvcrt-rt_v12-rev0.7z from https://github.com/niXman/mingw-builds-binaries/releases/tag... as the toolchain. This produces a 102 KB .exe file. Right off the bat we are doing much better than the claimed 278 KB. Maybe the author is using a different toolchain or different settings? Exact steps to reproduce would be welcome.

We can improve this by passing some switches to GCC.

    gcc -Os => 100 KB
    gcc -Oz => 99 KB
    gcc -flto => 101 KB
    gcc -s => 51 KB
    gcc -s -Oz -flto => 47 KB
If all you are interested in is a small .exe size, there is plenty of room for improvement here.

azhenley

If you had a blog or YouTube channel where you just went around to open source projects optimizing them down, I’d be very interested.

jedimastert

> Maybe the author is using a different toolchain or different settings?

I wonder if they are compiling with debugging symbols? I don't know how much this would change things in vanilla C but that would be my first guess

tecleandor

I think there's a typo somewhere. The repo and the release says 27KB (not 278).

debugnik

The v0.1 release from yesterday, at the time of posting, was 278 KB. The latest release, v0.3 from 9 hours ago, adds -Os -s and UPX to compress down to 27 KB.

tomalbrc

They used mingw, read TFA

abbeyj

I also used mingw and yet I arrived at different results. Maybe it was a different version, or a different distro of MinGW, or a 32-bit vs. 64-bit issue, or I'm linking against a different CRT. Without details from OP, we can't really tell.

null

[deleted]

jcelerier

A lot of it is due to the platform and executable format. Things can be much more lightweight when there's no information for stack traces, no dynamic linking infrastructure, no exception handling tables (necessary even in C in case exceptions traverse a c function,) etc.

userbinator

no dynamic linking infrastructure

You get that for free on Windows.

no exception handling tables (necessary even in C in case exceptions traverse a c function,

Not necessary if you're using pure C. SEH is rarely necessary either.

nottorp

Maybe we could petition the demo scene competitions to have a '64kb TODO app' category.

jackjeff

I’m surprised it’s that big to be honest. I was expecting it to be smaller or half the size to be taken by some app icon. I remember writing this kind of stuff back in the days and it was smaller.

Is it due to MinGw maybe?

mhd

This reminds me of the days when all of a sudden win32 programming in assembly became hip enough, probably as a response to the increasing size of shareware downloads ('twas the dark time of MFC).

Combined with early Palm Pilot 68k programming, those were the last hurrahs of non-retrocomputing asm I can remember.

kgabis

6502? Luxury! In my times you were lucky to have a processor.

pineaux

A processor? Luuuxury! In my time we worked twenty-six hours a day, did all the calculations with pen and paper and would be thrilled to use an abacus!

psychoslave

Ah, back in my early existence, we didn't have time and all these superficial dimensions. Ontological creation out of nothing was all one needed, but it looks like it's all lost art now.

null

[deleted]

p0w3n3d

you had paper? We had to use sand and sticks!

p0w3n3d

Btw. I love how you people refer to this sketch so seamlessly!

kazinator

I remember being thunderstruck in early 1990-something upon seeing that Nethack compiled to a 900kb+ executable.

Borg3

Hehe :) Okey.. I have sth easier to write.. but smaller:

15kB quickrun.exe :) C, pure Win32 API.. No hacks to shrink binary, Mingw32 compiler.

Its GUI app to quickly launch any application via alias.

toxi360

Hello friends, I made this app just to try it out and have some fun, haha, but the comments are right, something like this could have been done more sensibly with C++ or other languages, ahaha.

tomtomtom777

This is exactly how I've learned to create my first Windows programs about 30 years ago, except that I'd use a C++ compiler.

I am not sure why but I believe writing C style code with a C++ compiler was how the windows API was documented to be used. I think Microsoft just went with the idea that C++ was an improved superset of C so should be used even for C-style code.

cesarb

> I think Microsoft just went with the idea that C++ was an improved superset of C so should be used even for C-style code.

And as a consequence, for a long time their official C compiler was stuck on C89, while other platforms already had full C99 support and beyond. I believe their support for newer C standards has gotten better since then, but AFAIK they still don't have full C99 support.

drooopy

Unironically, I would rather use your to-do app over the default Windows 11 one.

toxi360

AHhhahah thanks

Johanx64

It's just the way it should be.

Other language doesn't fundamentally change anything if you want to use win32 API, if anything it would make things more confusing.

People often fall prey to C++isms, and they would have made the whole thing an even more confusing mess (to people not familiar with win32 API).

This is a very cute thing to do and some familiarity with win32 APIs is a nice basic competency thing, regardless of what other people think.

int_19h

C++ actually makes a lot of sense specifically for Win32 API because RAII takes care of releasing all the numerous handles at the right time in the right manner. Also, things like string operations are a pain in pure C (indeed, this app uses stuff like strcpy which is a recipe for buffer overruns etc).

WTL (https://en.wikipedia.org/wiki/Windows_Template_Library) is the oldschool way to do low-level Win32 coding in C++.

toxi360

https://github.com/Efeckc17/YoutubeGO By the way, you can also review or examine this application, I would be very happy :D

kmangutov

This is exactly the sort of project (clean, native UI) that motivated me to learn programming, kudos!

transcriptase

Seeing a lot of chirps in here from people who work on software or websites that load megabytes of JS or C# or in order to send 278kb of telemetry every time the user moves their mouse.

lostmsu

A similar app in C# + WinForms is under 10KB on disk and 6MB RAM. This app takes 1.5MB RAM. Both start instantly.

throwaway2037

Impressive. Can you share a link to the source code?

int_19h

A 10 Kb .exe shouldn't be surprising given that it's fairly high-level (Java-like) bytecode rather than native code, and that WinForms incorporates a lot of the scaffolding that you have to handwrite for a Win32 app, like message dispatch.

phendrenad2

The fact that you can do everything in C when developing a Windows app always makes me feel all warm and fuzzy. Building up from the lowest-level primitives just makes sense.

Meanwhile, on MacOS, everything is an ObjectiveC Object, so if you want to write an app in pure C you can but it's about 1000x more verbose because you've gone an abstraction level deeper than Apple intended, and you essentially have to puppeteer the Objective C class hierarchy to make anything happen. It's incredibly icky.

I don't know why they can't rebase the ObjectiveC class-based API onto a basic win32-style procedural API (technically win32 is also "class-based" but it's minimal). It's part of why I don't see myself porting any of my C code to MacOS any time soon.

int_19h

Interestingly, Windows has its own object-oriented API/ABI with COM (and later WinRT, which builds on COM). However, the MIDL compiler for COM interfaces produces headers that are consumable from C, both to use interfaces and to implement them: https://www.codeproject.com/KB/com/com_in_c1.aspx

As far as I know, this still works today even for WinRT, although the generated C struct and function names get very ugly because they have to include the entire namespace: https://stackoverflow.com/a/7437006/111335

kmeisthax

They did have a procedural API, it was called Carbon; which was a nearly drop-in replacement for the Macintosh Toolbox API that user32.dll blatantly copied.

The thing is, outside of programmer fuzzies, UIs really, really want to be object-oriented. A tree of unrelated objects sharing some common behaviors describes basically 99% of all UI code. And Toolbox / Carbon really strained for lack of having one. That's actually the one original thought Windows added - window classes.

Personally, the weirdness you feel manipulating Obj-C classes directly from C is how I feel any time I have to define a window class or procedure in user32.dll code[0]. OOP wants dedicated language features, just like how UI wants OOP. You can make do without but it's 2000x less ergonomic.

[0] Or anything to do with GTK/GObject.

vparikh

Looks like you are linking to static libraries. You should link to DLL not to static libraries - this is will cut down on the application size dramatically.

TonyTrapp

That seems backwards. If you need to ship the DLLs with the program anyway (they are not part of the operating system, after all), they will contain their full functionality, each one potentially with its own C runtime, etc. If you statically compile everything into a single EXE, there will be only a single C runtime, all unused functions can be trivially removed, etc.

DLLs only reduce size if their code is meant to be shared between different programs.

throwanem

> they are not part of the operating system

Yes they are. Exercising the native Windows API is the entire point of this project, and the only artifact it builds is an executable.

edit: See the thread; I had the wrong end here. I haven't worked with Win32 or C in so long I'd forgotten what balls of fishhooks and hair they both tend to be.

TonyTrapp

The CRT is not part of the operating system, unless you count the UCRT on Windows 10 onwards (yes there is also a MSVCRT copy in the Windows folder that Microsoft strongly discourages you from using, so let's ignore that for now). So unless you link against the system-provided UCRT, you will have to either ship a dynamic or a static copy of the C runtime, and linking it statically will be smaller because that will only contain the few string and time functions used by the program, instead of the whole CRT.

null

[deleted]

90s_dev

You must not have worked with Win32 or C recently, they both tend to be giant balls of fishhooks and hair.

edit: Saw that you just now edited your comment, glad we're on the same page now.

pjmlp

Not really, because this is Windows we are talking about.

A traditonal Windows application would be using the Windows APIs, and not the C standard library, e.g. FillMemory() instead of memset(), thus there is no DLLs to ship with the application.

As can be seen on Petzold books examples.

TonyTrapp

This code is not from the Petzold books. It literally includes and uses string.h, stdlib.h and time.h. It is not using WinAPI equivalents of C functions.

Dwedit

No, linking to a static version of the CRT is a good thing, it cuts out the unused code. If you dynamic link to MSVCRxx/VCRUNTIME, you force the user to download that exact DLL from Microsoft. Dynamic linking to MSVCRT doesn't have that problem, but it's very hard to do in Visual Studio.

The only time you really can't static link to the CRT is LGPL compliance?

int_19h

Windows has been shipping an up-to-date, modern CRT in the box for a decade now (Win10+), and MinGW will even dynamically link to it by default. Even on out-of-support OSes like Vista and Win7, users who have all the security updates installed will have it. So you have to unwind all the way back to WinXP for a version of Windows that doesn't have uCRT out of the box.

There's absolutely no reason to statically link CRT in a Win32 app today. Especially not if your goal is to minimize .exe size.

TonyTrapp

Note that the project is using mingw, so it's not using any Microsoft DLLs for the CRT anyway. mingw brings its own CRT along.

Dwedit

I thought Mingw defaulted to MSVCRT.DLL as the CRT?

dvdkon

If you're going for a small EXE, I'd recommend telling GCC to optimise for size with "-Os".

Link-Time Optimisation with "-flto" might also help, depending on how the libraries were built.

RavSS

I'd suggest `-Oz` instead, as it optimises for size above all else at the cost of performance, unlike `-Os` which is less aggressive (but likely produces similar code anyway). `-Oz` is somewhat new if I remember correctly, so it depends on the GCC version.

userbinator

That's more than 10x bigger than I expected, given that all it seems to do is manipulate a list view. Something like this should be doable in under 10KB.

Koshkin

Incidentally, NASM makes Win32 programming in assembler a breeze.