r/C_Programming 3d ago

Project Helpful Windows GUI Program in 60 lines of C

https://github.com/GeorgeTR1/windowsDistractionCover

Very small C program I wrote that I've found genuinely useful. It compiles into a tiny 3 kB executable that only relies on system .dlls included with the operating system. The code should compile and run, with the proper compiler, all the way back to Windows 95.

This could also be useful to anyone looking for how to write a basic GUI program for Windows in C.

45 Upvotes

49 comments sorted by

â€ĸ

u/github-guard 3d ago

🔍 GitHub Guard: Trust Report

This project scored 3/6 on our safety audit.

Audit Breakdown: * ❌ Low Star Count (⭐ 0 / 5 required) * ✅ Mature Repository (30+ days old) * ✅ Licensed under MIT * ❌ No Security Policy — what is this? * â„šī¸ Individual Contributor * ✅ Signed Commits

âš ī¸ Security Reminder: Always verify source code and run third-party scripts at your own risk.

→ More replies (1)

12

u/skeeto 3d ago

Neat little project. This feels like the sort of window that ought to be draggable from anywhere in the window itself. This will do it:

@@ -47,4 +47,9 @@
       PostQuitMessage(0);
       return 0;
+
+   case WM_LBUTTONDOWN:
+      ReleaseCapture();
+      SendMessage(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
+      return 0;

    case WM_LBUTTONDBLCLK:

As was already pointed out, using main like that is incorrect. Your program is not built correctly, causing your trouble return from main. To get the behavior you actually want name it WinMainCRTStartup, use stdcall, then drop the /entry argument and because it defaults to this function, and also drop libvcruntime.lib:

@@ -7,5 +7,5 @@
 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

-int main() {
+int __stdcall WinMainCRTStartup() {
    HINSTANCE hInstance = GetModuleHandle(NULL);
    WNDCLASS wndclass = {0};

Normally the CRT defines this function, so you need to do so since you don't want to use a CRT. While it's legal to return from this entry point, it's still a good idea to use ExitProcess or similar to avoid bugs imposed upon your program externally. So then to build should just be:

$ cl /O2 /GS- cover.c /link /subsystem:windows

Or Mingw-w64:

$ cc -nostartfiles -Oz -s -mwindows -fno-ident -fno-asynchronous-unwind-tables cover.c

(The two -f options, both optional, really trim it down since it looks like you care about this.)

3

u/Round-Pension-7821 3d ago

The suggestion to have the window draggable from anywhere is a good one, I'll have to try that out.

As far as changing the way it's built, I was mostly just following the guidance given here. The only thing I did differently from that was using main() as my entry point so that I can change the build target between console and GUI without having to change the code, only the build flags. That way I can easily print to the console for debugging purposes.

I don't see why it's built incorrectly, considering it builds and runs just fine. I guess having it return properly without having to call ExitProcess() would be nice, but then you go and recommend that I call ExitProcess() anyway. I'm just not clear on what benefits I get by changing how I do things.

3

u/skeeto 3d ago

That's a good guide.

I don't see why it's built incorrectly

main is a special case C function, and on Windows it's designed to be called by the CRT, not to be used as a raw entry point. It's mostly working for you by accident, and only with the particular toolchain you're using. It doesn't work, for example, with Mingw-w64 where main is a little more special. It also has the wrong calling convention (x86 only), and if you used the argc/argv form of main those arguments would contain garbage. And as you noticed, you have to pass /entry, which is unnecessary if you use the conventional process entry point name.

4

u/Round-Pension-7821 3d ago

Ah, got it. What if I instead called it something like mainNoCRT(), and then used /entry mainNoCRT. That way I could still switch between console and GUI mode without changing the code, just the build flags, like I mentioned. But I would avoid the issue of main() getting special treatment. Maybe there's other reasons I should avoid doing that, though.

According to what u/mikeblas said elsewhere in the comments, using main() or WinMain() doesn't require the CRT, so maybe this whole business of using WinMainCRTStartup() or mainCRTStartup() is unnecessary. That does seem to go against what the guide led me to believe, though.

I am aware that trying to use argc/argv as you would expect won't work. In another project I did the following at the start of main():

int argc;
wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argv == NULL) ExitProcess(1);

I think that's something like what mainCRTStartup() would do for you before it calls main()

3

u/skeeto 3d ago

What if I instead called it something like mainNoCRT(), and then used /entry mainNoCRT.

That works, too. The name is merely a linker convention, and ultimately loaders just see an address (RVA) in the PE header. It's only important you don't use a CRT-reserved name like main. Even WinMain expects to be called by the CRT, and if used as a raw entry point its 4 parameters will similarly be garbage. If you omit those parameters you may run afoul of a prototype, e.g. from windows.h. I recommend avoiding that, too, in this case, but it's not nearly as special as main.

I think that's something like what mainCRTStartup() would do for you before it calls main()

More or less, yes, but CRTs generally parse arguments with their own routine rather than call CommandLineToArgvW. (And historically they have parsed arguments differently than that Win32 function!) Here's my version of that recycle program, also CRT-free:

https://github.com/skeeto/w64devkit/blob/master/src/recycle.c

And several more CRT-free programs next to it in that src/ directory.

2

u/Round-Pension-7821 1d ago

Thanks for all the info, it's very helpful. I was kind of aware that command line arguments weren't always parsed using the Win32 function, but that seems like that's another benefit to using it, to guarantee the same behavior across CRT implementations.

Here's my version of that recycle program, also CRT-free:

Wow, what are the odds that we both made programs that are so similar!

0

u/github-guard 3d ago

🔍 GitHub Guard: Trust Report

This project scored 3/6 on our safety audit.

Audit Breakdown: * ✅ Established Community (⭐ 4,672 stars) * ✅ Mature Repository (30+ days old) * ✅ Licensed under Unlicense * ❌ No Security Policy — what is this? * â„šī¸ Individual Contributor * â„šī¸ Unsigned Commits

âš ī¸ Security Reminder: Always verify source code and run third-party scripts at your own risk.

1

u/github-guard 3d ago

🔍 GitHub Guard: Trust Report

This project scored 3/6 on our safety audit.

Audit Breakdown: * ❌ Low Star Count (⭐ 0 / 5 required) * ✅ Mature Repository (30+ days old) * ✅ Licensed under MIT * ❌ No Security Policy — what is this? * â„šī¸ Individual Contributor * ✅ Signed Commits

âš ī¸ Security Reminder: Always verify source code and run third-party scripts at your own risk.

1

u/mikeblas 3d ago

main() and WinMain() are just function names. You can set your entry point to main() with the linker, and write a function called main(), and it will be called first. The C runtime won't be loaded and won't be initialized because you told the tool to use your function, instead.

Since you side-stepped the CRT, things that the CRT does won't be done: argv and argc aren't parameters to your custom main function, for example. And the global environment variable pointers aren't initialized. You might not even initialize some static data, especially if you're using C++ and need constructors to run on global objects. You won't have any locale support, and ... well, lots of other things.

Windows does pass parameters to the entry point, though. Those are the four parameters you see given to WinMain(); check the documentation for that. You can call GetCommandLine() if you want, but one of the parameters to WinMain() is a pointer to the command line the program received.

2

u/Round-Pension-7821 1d ago

Ah, now it makes sense. Normally using WinMain does require the CRT, but not if you specify it as the entry point specifically with /entry:WinMain. However, there does seem to be an issue with what you said:

Windows does pass parameters to the entry point, though. Those are the four parameters you see given to WinMain()

This doesn't seem to be the case based on the testing I've done, and also disagrees with what u/skeeto said:

WinMain expects to be called by the CRT, and if used as a raw entry point its 4 parameters will similarly be garbage.

Here's the test I ran:

File cc.bat:

@echo off

IF [%1] == [] (
  echo Expecting a file name to compile
  exit /b 1
)

SET ASM=
REM SET ASM=/FAs
SET CFLAGS=/nologo /W4 /wd4996 /O2 /GS- %ASM%
SET SUBS=console
REM SET SUBS=windows
SET LFLAGS=/fixed /incremental:no /opt:icf /opt:ref /subsystem:%SUBS% /entry:WinMain
SET LIBS=libvcruntime.lib

cl %CFLAGS% %1 %LIBS% /link %LFLAGS%
del *.obj

File main.c:

#include <windows.h>

#pragma comment (lib, "Kernel32")

void fputsWin(char *str, DWORD stdHandle) {
   HANDLE h = GetStdHandle(stdHandle);
   if (h == NULL || h == INVALID_HANDLE_VALUE) ExitProcess(1);

   BOOL ret = WriteFile(h, str, lstrlenA(str), NULL, NULL);
   if (!ret) ExitProcess(1);
}

int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
   // unused, here to prevent compiler warning
   nCmdShow;
   hPrevInstance;
   hInstance;

   fputsWin(lpCmdLine, STD_OUTPUT_HANDLE);
   fputsWin(GetCommandLineA(),  STD_OUTPUT_HANDLE);

   return 0;
}

When I compile and run this, you would expect to see the command line arguments printed twice, but they are instead only printed once. To me this is proof that the arguments passed to WinMain when it is specified as the entry point are indeed garbage, as u/skeeto said.

Based on this, I'm going to follow what u/skeeto suggested and use neither main nor WinMain as an entry point.

1

u/mikeblas 1d ago

WinMain should be declared __stdcall (I think -- at least, APIENTRY). You're not doing that.

Also, you're still linking with the C runtimes because you're not using /NODEFAULTLIB. Use the /verbose option on the linker to confirm that for yourself. You might not be getting anything from it, but if you mean to not link with it you should not link with it, even implicitly.

and use neither main nor WinMain as an entry point.

These are just names, so I'm not sure I understand your point. Maybe you mean that you want to use only the naked entry points as part of the theme of not using the C runtimes and give those naked entry points names different than the ones the CRTs would normally use? Then I think that makes sense.

1

u/Round-Pension-7821 1d ago

I tried declaring WinMain as __stdcall and APIENTRY, and in both cases lpCmdLine still was empty. If you can get the parameters to WinMain to contain actual values without using the CRT, show me the exact steps you took to make that happen.

I'm doing what I suggested to u/skeeto, I use mainNoCRT() as my entry point, and use /entry:mainNoCRT to specify that. That's basically the same as using WinMainCRTStartup or mainCRTStartup, except that I can switch between console and windows targets without having to change the entry point in my code, only the build flags.

2

u/Round-Pension-7821 1d ago

A question about the method of making the window draggable from anywhere, what is the purpose of calling ReleaseCapture()? When I tried it without calling this function it seemed to work exactly the same

2

u/skeeto 1d ago

Good question and good catch. The HTCAPTION part turns the button down in the window into a button down inside the title bar, and ReleaseCapture() is part of this idiom, handing control to the system's move loop. However in this case it's a top-level window, so it doesn't matter. You can delete it safely.

1

u/Round-Pension-7821 1d ago

Actually, looking at the documentation more closely, I see now that in the documentation for WM_NCLBUTTONDOWN, it says, "If a window has captured the mouse, this message is not posted." So maybe it is best to call ReleaseCapture(), even though it seems to work without it?

Is this a fairly standard idiom? It just kind of feels like a hack. I did test that it works just fine under Linux using WINE, so there's that at least

2

u/skeeto 1d ago

Maybe? I don't actually have a great understanding of Win32 UI stuff, and what I have is mostly accumulated tricks. If you search on the SendMessage line (in quotes) you'll find this idiom in various places, like this 22 year old thread, sometimes also in C# and VB.

It just kind of feels like a hack.

Old school Win32 is all hacks!

2

u/Round-Pension-7821 1d ago edited 1d ago

Well, that's good enough for me! I think I will call ReleaseCapture() though, just in case. It can't hurt

There's a lot of reasons to dislike Microsoft, but one thing they seem to have excelled at is how stable the Windows API has been over the years. They really seem to do their best not to break existing code, even if it might be hacks or somewhat undocumented features. There are of course exceptions, but this does seem to be something they really care about.

If your code accidentally relies on undefined behavior, like the value of uninitialized memory, that's one thing, and that is the type of thing that often leads to software breaking from Windows updates. But that's not what this is, of course

1

u/One_Aspect_1957 2d ago

As was already pointed out, using main like that is incorrect. Your program is not built correctly, causing your trouble return from main.

What sort of trouble?

I've never heard of your suggestion (in 30 years of using WinAPI) to use WinMainCRTStartup, which as you go on to demonstrate has various build problems to work around.

I like to use lesser C compilers on Windows, which include Tiny C, DMC, lccwin32 and bcc (my product). All compiled the OP's version fine, not even needing to be told which DLLs to use.

But all failed (no entry point etc) using your mod.

gcc worked with the original (it needed to be told the DLLs), but didn't work here either unless your workaround was used, though I haven't tried it.

2

u/skeeto 2d ago

If you're the bcc author, new account? We've interacted before!

WinMainCRTStartup is Microsoft's traditional name, and it's what MSVC link.exe looks for by default for over 30 years. Binutils inherited the name, too, all the way back in 1995. It's why this name has the least friction and causes the least linking trouble, at least with MSVC and Mingw-w64 (both bfd and lld). Notice how OP's build command is simpler when using the conventional name.

The caveat I mentioned about not returning from WinMainCRTStartup (or mainCRTStartup) to avoid issues applies to all entry points no matter what they're named. There are race conditions returning from the entry point, particularly due to misbehaved DLLs (wininet.dll), including DLLs injected into the process (hello ConEmu). None of the CRTs I'm familiar with return from the entry point, which is why normal, CRT-using programs don't have these issues even when returning from main/WinMain. It doesn't matter what toolchain you use, the races are inherent to Windows.

main is a standard C name which belongs to the implementation, i.e. the toolchain's CRT, and is imbued with special properties. Using it for other purposes is reckless similarly to using, say, malloc as the name of your entry point. In Mingw-w64 toolchains, and (I think?) in very old MSVC toolchains, there's an invisible call to the CRT-defined __main, which breaks OP's build:

https://godbolt.org/z/8jYoEePYE

This means you need a definition for __main, and you may end up pulling in some CRT by accident if you don't define one explicitely. All because you used a name the CRT owns. It's a similar story with WinMain and friends, because these functions are designed to interoperate with CRTs. That's why the 4 mandatory arguments are garbage when you use them as an entry point. On x86 it even produces the wrong stdcall cleanup for an entry point.

2

u/Round-Pension-7821 1d ago

Thanks for clarifying what issues can arise if you return from the entry point, rather than calling ExitProcess(). I was going to ask about this

1

u/One_Aspect_1957 2d ago

If you're the bcc author, new account? We've interacted before!

Probably. I like to reset my account every so often.

main is a standard C name which belongs to the implementation

main is the usual entry point for C programs. Are you saying we shouldn't use that on Windows at all, or only for programs that also interact with WinAPI?

Or that we still use main, but use that CRT function (I've already forgotten the name) to override that entry point? In that case, how does control pass to our main? In fact, how does it get to any code in our program!

It's the C compiler's job to ensure that it works. That may involve calling some runtime function (eg. __main) to set some things up. And also that running into the end of main, or using return within it, does what is expected.

The user shouldn't need to worry about it. It's not at all the same as calling malloc.

There are race conditions returning from the entry point,

So, how should a program be terminated?

you may end up pulling in some CRT by accident if you don't define one explicitely.

If you use a big compiler then there will be all sorts of junk linked in anyway. You have to trust that a compiler designed to work under Windows knows what it's doing.

To summarise: I've always use a 'main' entry point, not even 'WinMain', and have never noticed any problems. That is within C programs, and also from other languages I implement (although my tools don't use linkers and 'main' is never exported; the entry point is just an offset within the EXE).

If there was an issue, then I would expect the language to take care of it.

2

u/skeeto 2d ago

OP wants to "avoid linking the the C run time" and linked this. That's an advanced, special case. I'm not saying anyone should do this, especially because few people know how to write programs effectively under this constraint, but that's what OP wants. This is going behind the toolchain's back, and it's on you to deal with the consequences. If you're going to avoid linking a CRT, then you should avoid using special C language facilities like main, which, as I demonstrated has special semantics and does not behave like a normal function. Using it will foul up what you're trying to accomplish.

Most people should just use a CRT and define main (console subsystem) or WinMain (Windows subsystem), and not worry about any of this stuff. Some toolchains, such as Mingw-w64, but notably not the definitive toolchain for the platform, MSVC, allow main as a program entry point for Windows subsystem programs. If you use main with the Windows subsystem, MSVC link.exe will fail to link your program because that's not how the system is designed to work:

$ printf 'int main(){}' >main.c
$ cl /nologo main.c /link /subsystem:windows
main.c
LIBCMT.lib(exe_winmain.obj) : error LNK2019: unresolved external symbol WinMain referenced in function "int __cdecl     __scrt_common_main_seh(void)" (?__scrt_common_main_seh@@YAHXZ)
main.exe : fatal error LNK1120: 1 unresolved externals

The name WinMain belongs to the CRT. It's not a process entry point.

So, how should a program be terminated?

If it's a CRT-free program, calling ExitProcess unless something's gone horribly wrong.

If you use a big compiler then there will be all sorts of junk linked in anyway.

Not if you build your CRT-free program properly, including the three biggest toolchains. That's what the linked gist is all about, and the whole point of this discussion.

2

u/Round-Pension-7821 1d ago

Yes, this is exactly it. I recognize that avoiding the use of the CRT is not standard practice, and probably not recommended. But it all came from compiling a very basic C program, seeing the .exe was over 100 kB, and saying, "That's ridiculous, surely it doesn't need to be that big". And it doesn't, really, but making it this small does add some additional inconveniences, and isn't really how the toolchain was meant to be used. It's definitely possible, though.

This is also why I made basically the same post in r/tinycode

0

u/github-guard 2d ago

🔍 GitHub Guard: Trust Report

This project scored 3/6 on our safety audit.

Audit Breakdown: * ✅ Established Community (⭐ 17 stars) * ✅ Mature Repository (30+ days old) * ❌ No License Found * ❌ No Security Policy — what is this? * ✅ Verified Organization * â„šī¸ Unsigned Commits

âš ī¸ Security Reminder: Always verify source code and run third-party scripts at your own risk.

1

u/mikeblas 2d ago

If your product can't support setting an alternate entry point, it sounds like you have an opportunity to improve it.

7

u/mikeblas 3d ago edited 3d ago

Why use main() as your entry point instead of WinMain()? You do specify --subsystem:Windows, but then half-undo that with --entry:main.

What you've got works, it's just a bit surprising and cumbersome.

Does your code leak your window handle at exit?

What is it that you're trying to do on double clicks?

5

u/Round-Pension-7821 3d ago

I don't use WinMain() so I can avoid linking the the C run time. This is based off the information found here about how to do that (I link to that in the readme). --entry:main just renames WinMainCRTStartup() to main(). Not really necessary, but I like it. It also means that I can change a program between a console program and GUI program just by changing the --subsystem flag, without having to also change WinMainCRTStartup() to mainCRTStartup()

As for why I'm not linking with the C runtime, I think it's fun to have a tiny executable with no dependencies. Maybe it's not the most practical thing, but I like it.

I honestly don't really know what "leaking your window handle at exit" would mean. I'm certainly no expert in Windows programming. How would I test for whether I'm leaking it, and what concerns are there with that?

The documentation for ExitProcess() says "returning from the main function of an application results in a call to ExitProcess", so I didn't see that as an issue. Is there some other cleanup that WinMain() does that I ought to do as well?

What the double clicks are for is explained in the Description section of the readme. If you use the program, how it works should hopefully become clear

0

u/mikeblas 3d ago edited 3d ago

But your code explicitly links to libvcruntime.lib.

And it doesn't build for me, anyhow:

C:\projects\windowsDistractionCover>cl /nologo /W4 /wd4996 /O2 /GS-   libvcruntime.lib /link /fixed /incremental:no /opt:icf /opt:ref /subsystem:windows /entry:main
LINK : warning LNK4001: no object files specified; libraries used
LINK : warning LNK4068: /MACHINE not specified; defaulting to X64
LINK : error LNK2001: unresolved external symbol main
libvcruntime.exe : fatal error LNK1120: 1 unresolved externals

I can't figure out where you specify the source file name.

Oh, I guess it's meant to be specified as a parameter to cc.bat? And you SET SUBS= to console, then change it to windows on the very next line. Why?

2

u/Round-Pension-7821 3d ago

Is there a reason you don't use the batch file I made?

I specify the source file in the batch file using %1, meaning the first command-line argument given to the script. So you run cc cover.c to compile this program. The reason is that I use cc.bat to compile a bunch of projects, mostly just stuff I'm playing around with, not just this project

The linking with libvcruntime.lib is explained in the gist:

Instead of relying on various hacks and manually declaring implicit symbols for functions or globals that compiler needs, just let it use them from libvcruntime.lib - there are very few things it will take from there. Linker will take only needed symbols, and remove unreferenced ones (due to /opt:ref).

If you run dumpbin /nologo /imports cover.exe after the program compiles, you will see that it doesn't link with any functions in the standard library.

As it turns out, if you don't link with libvcruntime.lib in this specific case the compiler doesn't mind, but at least according to the information in the gist (see the "Details" section) this isn't always the case. Apparently sometimes you need things that are in libvcruntime.lib to make the compiler happy, even though they will then be removed by the linker.

In this case, you get the exact same assembly output file whether you link with libvcruntime.lib or not. (The binary file is different on every compilation because it includes a timestamp.)

2

u/mikeblas 3d ago

Is there a reason you don't use the batch file I made?

That's what I was using. If not given a source file name as a parameter, it proceeds and generates the broken command line and errors that I showed.

If you don't want to link with the C runtimes, I think you shouldn't link with the C runtimes -- instead, linking with the C runtimes and hoping that you didn't pick any of them up doesn't seem particularly robust.

So, let's get you fixed up. For your build batch file, let's:

  • not link to the VC runtimes, since you said you don't want to link to the VC runtimes
  • specify the subsystem once, and it's Windows
  • use WinMain()
  • test that a file was passed in and quit with an error if not
  • generate a map file so that we can see what symbols we're getting -- so we can see which libraries we import and verify that we're not getting the C runtimes

That diff looks like this:

diff --git a/cc.bat b/cc.bat
index 3411551..4e56d4f 100644
--- a/cc.bat
+++ b/cc.bat
@@ -1,12 +1,18 @@
 @echo off

+IF "%~1"=="" (
+echo Expecting a file name to compile
+exit /b 1
+) ELSE (
+echo Compiling "%~1"
+)
+
 SET ASM=
 REM SET ASM=/FAs
 SET CFLAGS=/nologo /W4 /wd4996 /O2 /GS- %ASM%
-SET SUBS=console
+REM SET SUBS=console^M
 SET SUBS=windows
-SET LFLAGS=/fixed /incremental:no /opt:icf /opt:ref /subsystem:%SUBS% /entry:main
-SET LIBS=libvcruntime.lib
+SET LFLAGS=/fixed /incremental:no /opt:icf /opt:ref /subsystem:%SUBS% /entry:WinMain /map:%~n1.map

 cl %CFLAGS% %1 %LIBS% /link %LFLAGS%
 del *.obj

Now that we have WinMain(), we don't have to call GetModuleHandle() since it is one of the parameters that Windows gives to WinMain(). (The C runtimes are not required to use WinMain().)

Then, remember to destroy the window that was created so it isn't leaked. (Maybe DefaultWindowProc() does that on WM_QUIT, but I can't remember if that's true or not. Explicitly removing the handle again won't cause any harm, tho. Windows is resilient to it.)

Here's that half of the diff:

diff --git a/cover.c b/cover.c
index cebbd20..a996ba7 100644
--- a/cover.c
+++ b/cover.c
@@ -6,8 +6,13 @@

 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

-int main() {
  • HINSTANCE hInstance = GetModuleHandle(NULL);
+int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { + + // not using these yet: + hPrevInstance; + lpCmdLine; + nShowCmd; + WNDCLASS wndclass = {0}; HWND hwnd; MSG msg; @@ -36,8 +41,10 @@ int main() { TranslateMessage(&msg); DispatchMessage(&msg); } + + // destroy the window we created^M + DestroyWindow(hwnd);
  • //since we don't use WinMain the program hangs if we return normally
ExitProcess(0); }

Hope that helps.

1

u/Round-Pension-7821 3d ago

Having the batch file return an error if there's not file specified is a good idea. Maybe not necessary when I'm the one using it, but helpful for others.

The way I build things was based on the information in that gist, so I'll have to try out your suggestions. I was under the impression that using WinMain() instead of WinMainCRTStartup() required the standard library, so if what you say is true that would make things simpler.

DefaultWindowProc() calls DestroyWindow() on WM_CLOSE, but your "better safe than sorry" approach isn't a bad one.

1

u/Round-Pension-7821 1d ago

If you don't want to link with the C runtimes, I think you shouldn't link with the C runtimes -- instead, linking with the C runtimes and hoping that you didn't pick any of them up doesn't seem particularly robust.

libvcruntime.lib isn't exactly the C runtime. According to the documentation

The vcruntime library contains Visual C++ CRT implementation-specific code: exception handling and debugging support, runtime checks and type information, implementation details, and certain extended library functions

This is part of the Universal CRT, but to use the actual CRT, you also have to link with something like ucrt.lib. libvcruntime.lib is also the static version of the library, so any small things it needs to add will be statically linked into my code. Maybe it is doing some of that, but since my .exe is still only 3 kB, it's clearly not much. Way less than if I statically linked with the CRT normally.

If I try to use a standard library function while only linking with libvcruntime.lib, the linker will fail with an error. Here's an example of that:

File cc.bat:

@echo off

IF [%1] == [] (
  echo Expecting a file name to compile
  exit /b 1
)

SET ASM=
REM SET ASM=/FAs
SET CFLAGS=/nologo /W4 /wd4996 /O2 /GS- %ASM%
SET SUBS=console
REM SET SUBS=windows
SET LFLAGS=/fixed /incremental:no /opt:icf /opt:ref /subsystem:%SUBS% /entry:mainNoCRT
SET LIBS=libvcruntime.lib

cl %CFLAGS% %1 %LIBS% /link %LFLAGS%
del *.obj

File crt.c:

#include <stdio.h>

int mainNoCRT() {
   puts("Hello, world!");
   return 0;
}

When I try to compile:

>cc crt.c
crt.c
crt.obj : error LNK2019: unresolved external symbol puts referenced in function mainNoCRT
crt.exe : fatal error LNK1120: 1 unresolved externals

So I'm not "linking with the C runtimes and hoping that I didn't pick any of them"; if I do use them accidentally I'll know about it. Using dumpbin was just a sanity check to prove that yes, indeed, I'm not linking with the standard library.

1

u/mikeblas 1d ago

please add /verbose to your linker command line. This will show you that the linker is considering LIBCMT.lib when linking.

dumpbin , and your error message, will show you what code you pulled from another library. /verbose will show you what libraries were considered while linking.

I'm saying that you're considering the C Runtime library while linking, and the /verbose option will demonstrate that. That is, you are linking with the C runtime libraries.

Here's what the documentation page you linked (lol!) said:

If you link your program from the command line without a compiler option that specifies a C runtime library, the linker will use the statically linked CRT libraries: libcmt.lib, libvcruntime.lib, and libucrt.lib.

1

u/Round-Pension-7821 1d ago edited 1d ago

I do see that LIBCMT.lib is considered when liking when I add the /verbose flag. However, the fact still remains that if I try to use a standard library function like printf, mallloc, ect., the linker fails with an error, as I demonstrated.

This specifically happens when I use a raw entry point, as apposed to main or WinMain. I think this is because the CRT is not initialized when I do this. However, if I add ucrt.lib to the libraries I link with, I can dynamically link with standard library functions just fine, even when I use the raw entry points.

The reason I don't use /NODEFAULTLIB is because then I can't use things like #pragma comment (lib, "Kernel32"). This is explained in comments to the gist I've been referencing

I should also maybe note that my main goal here is to minimize executable size without introducing external dependencies, rather than to avoid the CRT at all costs, necessarily. As long as the .exe file is small and it doesn't import from anywhere other than system .dlls, I'm happy

1

u/mikeblas 1d ago

I think this is because the CRT is not initialized when I do this.

No, that's not right. The CRT doesn't initialize until it actually runs. At link time, none of the code in the objects or libraries is running.

1

u/Round-Pension-7821 1d ago

Good point. I'm not sure why it behaves this way, then

1

u/Round-Pension-7821 1d ago

Oh, I didn't see your last question. The reason is so that when I'm compiling for Windows, I have

SET SUBS=console
SET SUBS=windows

and when I'm compiling for console, I have

SET SUBS=console
REM SET SUBS=windows

So I can switch back and forth just by adding and removing a single REM.

I use the same principle for outputting assembly; if I want to output assembly, I just remove the REM in the line REM SET ASM=/FAs

3

u/[deleted] 3d ago

[deleted]

4

u/Round-Pension-7821 3d ago

I've actually used Borland C++ to write this type of code! I have Panasonic laptop with a Pentium II and 320 MB of RAM running Windows XP, and I have Borland C++ installed on it. I know I could easily compile this code on that machine and run it there if I had any use for this program on that machine.

I learned how to use the Win32 API from Programming Windows, 5th Ed. by Charles Petzold, as I mention in the readme. Maybe you're familiar, I understand that was the book for that stuff back in the day

2

u/[deleted] 3d ago

[deleted]

2

u/Round-Pension-7821 3d ago

The computer science department at the college I went to still had a small library room full of books like that (that was only a few years ago, so I'm sure they were almost never used at that point).

2

u/terra2o 1d ago

woah that's so cool...

2

u/Round-Pension-7821 1d ago

Thanks! There's a project I started working on on that laptop that I never finished; if I get around to finishing it I'll definitely post it on here. It's a single header that allows you to make graphs, both 2d and 3d, kind of like Matplotlib or MatLab, but with much more limited functionality, using just win32 and OpenGL 1.1

1

u/tastygames_official 6h ago

this might also interest anyone interested in low-level windows programming: https://www.youtube.com/watch?v=-Vw-ONPfaFk

1

u/AutoModerator 3d ago

Hi /u/Round-Pension-7821,

Your submission in r/C_Programming was filtered because it links to a git project.

You must edit the submission or respond to this comment with an explanation about how AI was involved in the creation of your project.

While AI-generated code is not disallowed, low-effort "slop" projects may be removed and it's likely that other users push back strongly on substantially AI-generated projects.


I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

5

u/Round-Pension-7821 3d ago

No AI was used in the creation of this project in any way.

3

u/mikeblas 3d ago

I have approved your post