Hacker News new | ask | show | jobs
by gecko 1637 days ago
The title of this article had me do a double-take: C and C++ development on Windows is great. No sanity is needed.

But that's not what the article is about. What the article is about is that the C runtime shim that ships with Visual Studio defaults to using the ANSI API calls without supporting UTF-8, goes on to identify this as "almost certainly political, originally motivated by vendor lock-in" (which it's transparently not), and then talks how Windows makes it impossible to port Unix programs without doing something special.

I half empathize. I'd empathize more if (as the author notes) you couldn't just use MinGW for ports, which has the benefit that you can just use GCC the whole way down and not deal with VC++ differences, but I get that, when porting very small console programs from Unix, this can be annoying. But when it comes to VC++, the accusations of incompetence and whatnot are just odd to me. Microsoft robustly caters to backwards compatibility. This is why the app binaries I wrote for Windows 95 still run on my 2018 laptop. There are heavy trade-offs with that approach which in general have been endlessly debated, one of which is definitely how encodings work, but they're trade-offs. (Just like how Windows won't allow you to delete or move open files by default, which on the one hand often necessitates rebooting on upgrades, and on the other hand avoids entire classes of security issues that the Unix approach has.)

But on the proprietary interface discussion that comes up multiple times in this article? Windows supports file system transactions, supports opting in to a file being accessed by multiple processes rather than advisory opt-out, has different ideas on what's a valid filename than *nix, supports multiple data streams per file, has an entirely different permission model based around ACLs, etc., and that's to say nothing of how the Windows Console is a fundamentally different beast than a terminal. Of course those need APIs different from the Unix-centric C runtime, and it's entirely reasonable that you might need to look at them if you're targeting Windows.

8 comments

A lot of the article is about string handling, I very much agree with that part of the article, having worked on a lot of legacy code built over decades before the introduction of UTF-8 compatibility.

It gets worse with that old code if you try to share modules between windows and Linux applications.

Additional complications come from trying to support TCHAR to allow either type of char for libraries.

Anyway, I have ended up supporting monstrosities of wstring, string, CString, char *, TCHAR mushed together, constantly marshalled converted back and forth.

And more: https://docs.microsoft.com/en-us/cpp/text/how-to-convert-bet...

Agreed! And TCHAR was the dumbest of all of them. “Not all of our libs/tools/editors support Unicode yet so use TCHAR in the meantime and then one day when our stack supports it fully then you can throw the switch and all your char*’s will be wchar_t*’s and I’m sure that’ll go really well in your codebase.”
> And TCHAR was the dumbest of all of them. “Not all of our libs/tools/editors support Unicode yet so use TCHAR

The reason for TCHAR is not "libs/tools/editors" not supporting Unicode, but the operating system itself. With TCHAR and related types, the same source code could target both Windows 95 and Windows NT, you just have to change a single #define (ok, IIRC there are actually three #defines: UNICODE, _UNICODE, and another one I can't recall at the moment) and recompile.

> The title of this article had me do a double-take: C and C++ development on Windows is great. No sanity is needed.

we certainly have a different opinion on what "great" means. It takes less time to rebuild my whole toolchain from scratch on Linux (~15 minutes) than it takes to MSVC to download those friggin debug symbols it seems to require whenever I have to debug something (I sometimes have to wait 30-40 minutes and I'm on friggin 2GB fiber ! and that's seemingly every time I have to do something with that wretched MSVC !)

Thankfully now the clang / lld / libc++ / lldb ... toolchain works pretty well on Windows and allows a lot more sanity but still, it's pretty slow compared to Linux.

My main gripe with writing C++ on Linux is the dependency management. If you need to do stuff that's not covered by the standard library, like interfacing with GTK or X11 you are in a world of pain. You need to probably install a distro-specific package in a distro-specific way to get the headers/symbols, use some build tool to configure those distro-specific include/so locations, and hope to god that the distro maintainers didn't upgrade one of those packages (in a breaking way) between the source commit and the time of build.

If you suffer through this, you have an exe that works on your version of Ubuntu, maybe on other versions of Ubuntu or possibly other Debian-based distros. If you want it to also work on Fedora, it's back to tinkering.

Tbh i think the only sane-ish way of building to dockerize your build env with baked-in versions.

In contrast, you pick and SDK and compiler version for Windows, and as long as you install those versions, things will work.

Versus no dependency management at all? This reasoning falls apart once you need to use a 3rd party library on Windows. There’s no standard way of sharing such a thing so you always wind up packaging the whole thing with your program, and handing the whole mess to your users.

Granted, writing an RPM is a special kind of hell, but at least you don’t have to package everything with your program. But actually you can still do that - I’ve done that plenty of times in embedded. You can always ship your program with its dependant libraries the way you always have to on Windows. In fact it’s a lot easier because most 3rd party libraries were originally coded on Linux and build more sanely on Linux. And RPATHs are pretty easy to figure out.

Linux gives you options.

Yeah, you're right, but you probably need a lot less stuff that's not in the SDK. My go to solution for including dependencies, is just checking in all the dependency .lib, include files into Git LFS (I think this is a rather common approach from what I've seen on Git). For your typical Linux C/C++ project, unless it's made by best in class C++ devs, building can be a pain, most likely because it depends on very particular lib versions, building a largish project from GitHub for Ubuntu 21.10, where the original dev used 20.04 is usually not possible without source/Makefile tweaks. And I don't particularly love the idea of using root access to install packages to just build some random person's project.

IMHO, C++ dependency management kinda stinks, regardless of platform.

> IMHO, C++ dependency management kinda stinks, regardless of platform.

Indeed, and the fact that it's platform-specific in the first place certainly doesn't help!

Writing an rpm isn’t difficult… is that a commonly held belief? Maybe I don’t know what I don’t know, but I’ve found wrapping my head around deb packaging much harder than rpms.
When I last wrote one I was very new to it and rpm.org (where most of the public docs on the format reside I guess) was down/abandoned. It looks like rpm.org and the docs are back now? I had a bunch of special requirements for the lib I was making and not having docs for the format, especially all the funny macros, was pretty frustrating.
> If you need to do stuff that's not covered by the standard library, like interfacing with GTK or X11 you are in a world of pain [...] If you suffer through this, you have an exe that works on your version of Ubuntu, maybe on other versions of Ubuntu or possibly other Debian-based distros. If you want it to also work on Fedora, it's back to tinkering.

GTK is known to break their ABI across major versions (GTK1->GTK2, GTK2->GTK3, GTK3->GTK4) but as a C ABI it should be compatible between minor versions and everything can be assumed to have GTK2 and GTK3 available anyway. X11 as a protocol has always been backwards compatible and Xlib on Linux pretty much never broke the ABI since the 90s. Here is a screenshot with a toolkit i'm working on now and then running the exact same binary (built with dynamic linking - ie. it uses the system's C and X libraries) in 1997 Red Hat in a VM and 2018 Debian (i took that shot some time ago - btw the brown colors is because the VM runs in 4bit VGA mode and i haven't implemented colormap use - it also looks weird in modern X if you run your server at 30bpp/10bpc mode)[0].

Of course that doesn't mean other libraries wont be broken and what you need to do (at least the easy way to do it) is to build on the oldest version of Linux you plan on supporting so that any references are to those versions (there are ways around that), but you can stick with libraries that do not break their ABI. You can use ABI Laboratory's tracker to check that[1]. For example notice how the 3.x branch of Gtk+ was always compatible[2] (there is only a minor change marked as breaking from 3.4.4 to 3.6.0[3] but if you check the actual report it is because two internal functions - that you shouldn't have been using anyway - were removed).

[0] https://i.imgur.com/YxGNB7h.png

[1] https://abi-laboratory.pro/index.php?view=abi-tracker

[2] https://abi-laboratory.pro/index.php?view=timeline&l=gtk%2B

[3] https://abi-laboratory.pro/index.php?view=objects_report&l=g...

Major versions of Gtk are, for all intents and purposes, different toolkits entirely. They are always parallel-installable, they don't conflict with each other.
Well, except for the part that development stops in previous versions and they do not get any real updates while they "hog" the "Gtk" name so any forks that may want to continue development in a backwards compatible way as if the incompatible change never happened cant really be called "Gtk" without being misleading.
The problems you've described are the reason we have tools like CMake, no? CMake's reusable find modules handle the heavy lifting of coping with the annoying differences between Linux distros, and for that matter other OSs.

> you have an exe that works on your version of Ubuntu

This is indeed a downside of the Linux approach, it's the price we pay for the significant flexibility that distros have, and the consequent differences between them. Windows has remarkably good binary compatibility, but it's a huge engineering burden.

> Tbh i think the only sane-ish way of building to dockerize your build env with baked-in versions.

This is an option, but bundling an entire userland for every application has downsides that the various Linux package-management systems aim to avoid: wasted storage, wasted memory, and less robust protection against inadvertently using insecure unpatched dependencies.

The de-facto standard for dependency discovery is pkg-config. CMake being its own little world with its own finding system is annoying and part of the reason why the ecosystem has not migrated from autocrap to CMake en masse. Thankfully Meson came along, which does everything correctly.
pkgconfig is everything but a standard. It barely works on windows and macOS which are the most common platforms.
About the distro-specific include locations, I try to use pkg-config where possible instead of directly specifying the directories and include flags.
> It takes less time to rebuild my whole toolchain from scratch on Linux (~15 minutes) than it takes to MSVC to download those friggin debug symbols it seems to require whenever I have to debug something

Fun fact: you can do the same in gdb and some distributions (e.g. openSUSE) have it enabled by default. Though you also get the source code too.

I was messing around with DRM/KMS the other day and had some weird issue with an error code, so i placed a breakpoint right before the call - gdb downloaded libdrm and libgbm source code (as well as some other stuff) and let me trace the call right into their code, which was super useful to figure out what was going on (and find a tiny bug in libgbm, which i patched and reported).

Yes, it's a relatively recent innovation, but it's pretty awesome. Symbol server has always been one of the things I actually liked about Windows development, which didn't require installing debug packages for every DLL before the bugs happen and you catch them. https://sourceware.org/elfutils/Debuginfod.html

NixOS has had a similar thing for a while called "DwarfFS" where a FUSE filesystem instead resolves filenames back to the package that needs to be installed, which was around for a while before debuginfod, but very NixOS specific. I'm happy this is now so much more widely available as of recently.

Really? I've never had to wait more than 5 minutes, and only the first time since the symbols are cached. On the other hand, the Visual Studio debugger actually works, even on large and complex multi-process systems like Chromium. My experience debugging C/C++ with GDB/LLDB and any frontend using them has been so poor that I've essentially given up trying them in all but the most desperate circumstances.
I agree that downloading symbols can be oddly slow but you can just turn it off, or only turn it on for specific modules. It can be helpful to have symbols for library code to troubleshoot bugs but typically you only need your own symbols and they are already on your computer with your binaries.
Debug symbols are stored locally with MSVC, and booting into debug mode only takes a few seconds longer than non-debug, sounds like you are doing something wrong.
I just use the LLVM toolchain -- on Windows and Linux. You really can't beat clang and lld, the ecosystem and tooling is fantastic.

If something absolutely requires MSVC ABI compatibility, I use "clang-cl".

God bless LLVM developers.

That is why they are now lagging behind everyone else on C++20 support.
"than it takes to MSVC to download those friggin debug symbols it seems to require whenever I have to debug something"

I think you can just unclick the radio button in debug settings that requires that?

> Thankfully now the clang / lld / libc++ / lldb ... toolchain works pretty well on Windows

Off topic, but I wonder if anyone knows whether it's possible to use rustc with lld, instead of link.exe? I tend not to have Visual C++ on my home systems. Is it as simple as the Cargo equivalent of LD=lld-link.exe?

You would need libraries. E.g. the C runtime and system import libraries (msvcrt.lib, vcruntime.lib, kernel32.lib, etc).
Microsoft is certainly evil and all, but I use MSVC on windows and clang on linux and the Windows tools for my project are much smarter and faster when it comes to compiling. Not counting times when the windows machine decides it has more important priorities to attend to rather than doing what I need.
I couldn’t disagree more with your opinion. Symbol servers and pdbs are a tremendous advantage to C and C++ development on Windows. Are you sure you have equivalent experience with both Windows and non-MSVC toolchains?
> I half empathize. I'd empathize more if (as the author notes) you couldn't just use MinGW for ports, which has the benefit that you can just use GCC the whole way down and not deal with VC++ differences

MinGW GCC doesn't make a difference to the article. It uses the same C runtime library as VC++ and has the same problems with defaulting to ANSI codepages and text-mode streams. In fact, as far as I know, there is no native open-source alternative to the VC++ runtime that isn't a full alternative programming environment like Cygwin.

> how the Windows Console is a fundamentally different beast than a terminal

Yes, and almost entirely in ways that are bad.

I think Microsoft have partially recognised that they're tied to compatibility with a set of choices that have lost the popularity wars and now look wrong. That's why they've produced the two different sorts of WSL, each of which has awkward tradeoffs of its own. And Windows Terminal to replace the console. But eventually I think they may be forced to:

- drop \ for /

- switch CRLF to LF as the default

- provide a pty interface

- provide a C environment that uses UTF-8 by default

It's been weird working with dotnet core and seeing the "cross platform, open source" side of Microsoft, who develop in a totally different style. It's like watching a new ecosystem being built in the ruins of the old.

> drop \ for /

API calls accept / as path separator (and interpret it correctly). Shell is a different beast though.

cmd accepts /, but you need to enclose path into quotes, otherwise it tries to interpret it as an option switch.

So you would also need to rewrite all command line utilities to use something like `ipconfig --all` instead of `ipconfig /all`.

Ah, quoting in cmd is its own kind of hell. Not so fun fact and actually the only part of Windows APIs that I truly hate (and I've worked with many of them): command-line arguments are passed to the program as a single flat string. Splitting into the argc,argv array is left to the CRT startup code.
That comes from compatibility with MS-DOS ways of dealing with arguments.
I just tested in PS and found that it eat's / as well
The PTY side has been covered for a couple of years now with the introduction of ConPTY.
The open source side of ASP.NET Core, the rest of .NET tooling has a different agenda regarding cross platform support.
>C and C++ development on Windows is great. No sanity is needed.

C++ is bearable (except the bloated piece of crap that is Visual Studio). but C is almost nonexistent. For many years their compiler lagged in standardized C features (this only somewhat improved recently) and you cannot use vast majority of system APIs, which use COM interfaces.

You can use COM from C... it's just even more painful.
Exposing a COM object from C is even worse than consuming one because you need to manually implement the polymorphism. I did it from Rust once in a toy project; it was certainly interesting making it work, but I would never want to do that in a production application.
> avoids entire classes of security issues that the Unix approach has.

I wonder what these might be. You mean potential race conditions regarding file operations?

When you open a file in Linux, you hold on to the reference of the underlying inode, which means if you load myLib 1.0, then update to myLib 1.0.1 using apt, then all the previously open programs will be stuck on the old version. This is a security issue at best, since unless you restart, there's no way of making sure nobody uses the old lib anymore, but more frequently a source of crashes and bugs, since myLib 1.0 and 1.0.1 might not be perfectly compatible. If I update Ubuntu, Chromium is almost 100% guaranteed to crash, (since the newly started processes use different lib versions), but I've seen other crashes as well.

In summary, I can't recommend you continue using your Linux machine after an update without a restart, since you are open to an entire category of weird bugs.

there are various tools that will tell you if you have old libraries in use by walking /proc

and the Chrome thing sounds rather strange as I thought Chrome forked processes from a "zygote" (a prototype process) rather than re-exec'ing() the binary (which should retain the handle to the deleted library inodes)

not to mention the shared library naming scheme should prevent this sort of incompatible change from occurring

In Windows you have to restart anyway, so your problem seems to be that you have a choice in Linux?
Both have updates in which you do or don't need to restart to apply them. Both also have methods of hotpatching all the way to the kernel level depending how much it's worth it to someone as well.
Windows handles not holding onto the inode is a major, MAJOR pain in the ass.

I hate the fucking "file is busy, try again?" dialog so fucking much.

>Microsoft robustly caters to backwards compatibility.

This includes bugs too. I ran into an undocumented bug in select(1) IIRC that they couldn't fix since it would break backwards compatibility. I spent like a day trying to figure out why my program wouldn't work correctly on Windows.

> C and C++ development on Windows is great. No sanity is needed.

So, you might as well be insane? :D