https://blog.shortround.dev/why-i-think-windows-is-the-best-dev-platform/ Shortround's Dev Blog * Home * Projects Subscribe By Shortround -- Oct 11, 2022 Why I develop on Windows Origin Story In college, I had a weird setup: I got the cheapest Chromebook I could afford (I believe it was an ASUS C201 for $150 at the time). I rented a $5/month Ubuntu VPS from Vultr with a couple hundred MB of RAM and coded everything in vim via the SSH app provided by Google on the Chrome appstore. I mostly did C development when my professors allowed us to choose a language, and when Java was required, we usually didn't have to turn in more than a single .java file, so I was free to use whatever build system I wanted. I was inexperienced in build systems back then, so I used Make (today, I would highly recommend anyone in a similar boat use either Maven or Gradle) and turned in the .java source code, which would be automatically compiled and run by whatever grading program my CS professors used. When I got to my OS and Systems programming class, I really shined. I had already been developing in C for a few years, and was very comfortable with a bash terminal. We turned in all our project files by SSH'ing into a server that was provided by the CS department so we could verify the output of our programs on their Ubuntu server. I learned a lot about linking and used GDB for the first time there. When I dropped out of college for my first job, I had some money to buy a new PC, and I bought a used Thinkpad T420 (later upgraded to a T460). I stuck with Windows for a bit because I was interested in amateur game engine development and was working with SDL2 in C++ with Visual Studio. I could have done the same work on Linux, but I wanted to make Windows my target platform since it had more gamers on it. I have not gone back to Linux since then. Not just a bash terminal A lot of these "Why I switched to Windows" blog posts all say the same thing: "WSL2 made it easy for me to go to Windows without losing all my favorite CLI tools", and that's a good argument: WSL2 provides a better CLI environment than macOS. I find that for many people who like to use macOS as a development platform, the core reason really boils down to the bash (now zsh) terminal, but macOS's programming environment is really one of its weakest features, in my opinion. At my first job, I found that bash scripts from StackOverflow didn't work on the company provided MacBook Pro because Apple had not (and still has not, as far as I know) updated their bash implementation since 2007 (11 years old at that point). Try it: run bash --version in macOS and see what year it puts out. Modern language features didn't work, and so the first thing I do when a company makes me use a MacBook is upgrade the bash binaries to those provided by GNU via homebrew. Similarly, the core utils provided by macOS don't have GNU extensions, like the extended regex support in grep and sed. So, already macOS is shipping a programming environment inferior to literally any Linux distribution compiled in the last 10 years. Those core utils can also be upgraded via brew, but I see why WSL2 makes Windows so much more ergonomic for people to use than macOS. Powershell However, I want to point out some features of Windows as a development environment that I think offer a lot more to a developer than just a glorified bash terminal. In fact, to be honest: I hate bash. The syntax makes no sense to me. People put out bash scripts that just look like a foreign language. I don't think I'm alone in this regard, because I know a lot of developers who will opt to do all of their scripting in python these days, even putting #!/bin/ python3 at the head of a script so that it runs through the shell. I am not joking around when I say that I think PowerShell is a legitimate competitor to bash. A lot of people coming from the Unix-like world of macOS and Linux don't tend to know a lot about PowerShell other than associating it with Windows Server administration. Many people don't know it even exists at all. When I mention the Windows Terminal to people, they think I'm talking about the Windows Command Prompt, a crappy little program whose limited and arcane syntax has annoyed developers for a couple decades now. PowerShell distinguishes itself form the Unix-like world by manipulating structured data instead of streams of bytes (usually in the form of text). Whereas a program on Linux might output some data in JSON format, which you might pipe in jq, run some queries on, and then pipe into sed or something, before finally dumping the payload into your target program, e.g: # Gets some instances, grabs their names, pipes them into another program aws ec2 get-instance | jq '.[].name' -r | myprogram PowerShell allows you to treat JSON data as structured dictionaries, so you have the full scripting environment at your disposal and not just the query language provided by jq or similar utilities (you see this a lot in the bash environment: to avoid having to actually use bash on the result of some program output, programs end up providing their own entire programming environment, such as with awk). A similar query in PowerShell would be roughly the same: aws ec2 get-instance | ConvertFrom-Json | ForEach-Object { $_.name } | myprogram Is this more verbose? Definitely (thought you can alias some of these, if you want.) But notice that the query language is not confined to a single section of the pipeline: PowerShell IS the query language itself, and the output of ForEach-Object isn't a newline separated list of strings (a stream of bytes with the newline character delimiting values), it's an array of strings. This means that you can write cmdlets for PowerShell which have well defined parameter inputs (e.g: the variable must be an array of strings), and you also don't have to do any input parsing. C# cmdlets work natively with PowerShell, and so input parsing and validation is handled by the framework. This is a big deal for me, because I've had to write command line utilities in Java, and there are simply no good command line input parsing libraries for Java. For C/C++, there are conventions, but it's still a diverse field. It took me a while to get the hang of, but so did bash, initially. I now do all my scripting with PowerShell. I use Invoke-WebRequest instead of curl (thought I do like Postman), Select-String instead of grep, and sed becomes irrelevant because PowerShell's String type has built-in sed-like capabilities, e.g: > $xyz = "Hello world!" > $xyz = $xyz -replace "[aeiou]", "_" > echo $xyz H_ll_ w_rld! It probably won't interest most people, but PowerShell also runs on Unix-like systems through PowerShell Core. Terminal Speaking of shells, a lot of people think that the Windows Terminal looks like the old, crappy batch command prompt terminal. It doesn't. I can't even post a screenshot here because Windows 11 won't open the old one anymore. The Windows Terminal looks like this: [image]Blue squares are my name removed by me It has full color support, tabs, color schemes, and allows you to open WSL, PowerShell, Command Prompt, or whatever other shells you want to use. You can even install custom HLSL Shaders on it and get cool effects like the CRT shader: [image-1]Excuse my messy home folder The other thing people seem to think doesn't exist in Windows is the Path environment variable. There is, in fact, a path environment variable, and always has been. It's simply the case that Windows users don't tend to launch software from the Terminal, and so installers don't usually add themselves to the Path (though some will ask). Programs also usually go under a tree of folders in the Program Files folder, with their own relative bin/ folders. You can add your own Paths to the Path variable through powershell: $Env:PATH = $Env:PATH + ";C:\MyPath" Or through the System environment variables dialog: [image-5] I often see people set their environment variables before running a program in bash, like: MYSQL_HOST=MyHost.com \ MYSQL_USER=MyUser \ java -jar myProgram.jar Because PowerShell supports local block scope, you can simply do: { $Env:MYSQL_HOST = "MyHost.com"; $Env:MYSQL_USER = "MyUser"; java -jar myprogram.jar; } The Environment variables will not survive local scope. You can also put them into a .ps1 PowerShell script file and they won't survive after execution of the script. C/C++ This will be a controversial one. I know of few programming communities more opinionated than the C/C++ community. I'm going to be straightforward with you: I'm a pedestrian when it comes to C++. I don't know a lot about the inner workings of the ABI, I don't know the arcane rules about what constitutes Undefined Behavior, and I have not read the C or C++ specifications. The C++ I write looks closer to Java than it does idiomatic C++. That said, I like MSVC because of SAL. SAL lets you put annotations on functions, variables, parameters, classes, etc. to give the compiler hints about how to statically validate input and output. For example, sometimes pointers and references are used to send data in, and sometimes they're used to send data out. Technically, your code should be const-correct to prevent readonly pointers and references from being modified, and allowing writeable pointers and references to be written, but to be very honest with you: I do not remember what const means in every scenario. Sometimes it goes before a variable's name, sometimes before the type, sometimes after a method name. Here's an example of valid C++ const std::vector& const getJointAt(const int const& const i) { /*...*/; } Obviously, this is a facetious example, but I could fit a few more consts in there. My point is that I don't really understand what the code does by looking at it because I'm not a C++ guru (don't worry, I don't write it for a living). SAL allows you to tell the compiler (and the user) what pointers are input and what are output: void doSomething(_In_ Foo* foo, _Out_ Bar* bar){ //... } If the user passed nullptr for an _Out_ marked parameter, the compiler will give you a warning (which you can optionally treat as an error): _Out_ variables must be writeable. But if you're perfectly comfortable with const-correctness, then there's still features for you. Take the Try-X pattern, for example: bool tryGetValue( const std::map& map, const std::string key, std::string& value //output ) { const auto& result = map.find(key); if (result == map.end()) { //output is not set on false return false; } value = result->second; return true; } For more complicated methods, you may forget to set the output variable on a successful return, and that can cause problems at runtime, since there's no requirement by the compiler that you ever write to the output. SAL has the _Success_ annotation for the method: _Success_(return == true) // means "return value == true". Can also just do "return" since it uses C++ truthyness bool tryGetValue( const std::map& map, const std::string key, _Out_ std::string& value ) { //... } If you forget to set the value out variable for any input that results in a successful return, the compiler will warn you about it. SAL can also validate memory reads and writes to make safer code. The C Standard library is notoriously unsafe, but MSVC uses SAL to enable those annoying compiler warnings that you always ignore. Take memcpy for example: void * memcpy( void *dest, void *src, size_t count ); The compiler cannot check if your destination buffer is big enough to hold count bytes, or if src even HAS count bytes. Enter SAL: void * memcpy( _Out_writes_bytes_all_(count) void *dest, _In_reads_bytes_(count) const void *src, size_t count ); I've not seen this work perfectly, but SAL will do it's best to detect if you're using memcpy in a way that violates the rule that src and dst must both have at least count bytes. Usually, this means that you need to manually check the length of your buffers in some way before (though, just use memcpy_s, really). It will also validate that the body of the function doesn't write MORE than count bytes. Package Managers in general, vcpkg in particular macOS doesn't have a package manager, by default. There's brew, which I have not had good experiences with. Sometimes it manages to take a few minutes to update when I run it. No hate to the maintainers, but it just doesn't compare to Debian's aptitude. Windows, however, has a few package managers. Chocolatey is the older community-driven tool. Installing things is as simple as choco install [packageName] (from an administrator shell). Usually it will put CLI tools into the path, and I haven't had any trouble with it, personally. Chocolatey has the biggest catalogue of software for Windows package managers Winget is Microsoft's first party package manager, though I rarely use it. It works in the same way as chocolatey: basically as a CLI frontend for downloading and running .msi and .exe installers. Sometimes these spawn GUI installer, but it's Windows so that's what you gotta be ready for. For C++ integration, I love vcpkg. vcpkg makes it really easy to get library dependencies, and first-party Visual Studio support means that linking them is even easier. With vcpkg, you can create a .json file in your project directory, set your project to use vcpkg and autolink dependencies, and then let er' rip. x64 vs. x86 vs. arm binaries are handled automatically by Visual Studio, and you can often customize feature sets for the libraries you download. Dear ImgUI is a UI library that supports a lot of different operating systems and rendering APIs (Vulkan, OpenGL, DirectX). To download the win32 integration with DirectX rendering, just add this to your vcpkg.json file: { "name": "imgui", "features": [ "dx11-binding", "win32-binding" ] } When you build your project next, it will automatically download and link. You just need to use the headers like any other library Doesn't everything run on Linux these days? A lot of web developers work on projects that are destined to run in a docker container on Linux. So, you might ask, shouldn't I develop on a Unix-like system, or just test in Linux/Docker? If the software is written well, it will be sufficiently cross-platform and modular enough that you can save your resources and run in a host environment when developing. I worked for a company once which developed everything on MacBook Pros. The software was an Apache Tomcat .war file which ran on an Ubuntu server. We tested everything via a virtual machine using vagrant. We also developed an in-house tool in Node.js which generated some frontend files for us. That tool ran only on MacOS and Linux for one simple reason: when generating paths, the code split path strings with a hardcoded / character, rather than a System.delimiter variable. There is no good reason that Java or Node.js code should be OS-dependent, unless you're explicitly interacting with specific OS processes or underlyng Syscalls. That's the entire point of Java. We would have clients who used our development platform to customize their product. Sometimes those clients had PCs at work, and since the software didn't run on their machines, the company's solution was to buy MacBooks for our customers. There is no good technological reason why the software couldn't run on Windows, the company simply had a shitty, elitist attitude about macOS vs. Windows development, and that pride cost them thousands of dollars a year. Relying on vagrant and VMs for debugging a web application meant that our machines would slow to a crawl if we had more than one instance of the application running at a time. But here's where WSL is a pro: docker runs through WSL2 on Windows, and WSL2 takes advantage of hyper-v memory ballooning, so when WSL runs out of memory, it simply gets more. Simple idea, right? I've run into this issue with macOS countless times: you have to set a hardcoded amount of memory for docker to use in its VM, and if you run out, your container kills itself. Then you have to allocate a little more memory (since you don't want to give half of your $1,300 MacBook Pro's measly 8gb of RAM to docker) and hope for a better outcome. Docker is a better experience in WSL than macOS. DirectX This is niche to me, but I really like DirectX. Perhaps it's unfair to compare DirectX 11 to OpenGL (perhaps Vulkan would be a better comparison), but that's where I came from originally, so that's what I'll compare it to. In OpenGL, everything is a GLuint. Creating resources and storing them returns simple resource IDs as integers, which you then need to keep somewhere and give a really good name so that you remember what this particular GLuint does, and where it's supposed to go. DirectX, in Microsoft tradition, has types and enums and structs for frickin everything. A rasterizer state is an ID3D11RasterizerState. The data to create an ID3D11RasterizerState is wrapped in an envelope called a D3D11_RASTERIZER_DESC. You can wrap your Rasterizer in a ComPtr, which is Microsoft's version of a smart pointer. Enabling your rasterizer state is a method on your ID3D11DeviceContext called RSSetState, as in deviceContext->RSSetState(m_shadowMapRasterizer.Get()) where the RS stands for Rasterizer Stage, to let the developer know what stage of the DirectX 11 pipeline they're binding resources to (along with PSSetShaderResources for setting resources in the Pixel Shader stage, OMSetRenderTargets for setting the texture render target in the the Output-Merger stage, or IASetIndexBuffers for setting the index buffer in the Input-Assembler stage. These functions also all have their own types. You cannot pass a ShaderResourceView to OMSetRenderTargets (though you can create a RenderTargetView of the same texture as the ShaderResourceView and use that! They refer to the same underlying texture resource.) In addition to the strong typing, state is a little more explicitly managed in DirectX than in OpenGL. In OpenGL, to set blend modes, you might set it as part of OpenGL's global state, like glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); In DirectX, you would create a D3D11_BLEND_DESC struct, fill it with the options you want, use that to create an ID3D11BlendState, keep a ComPtr to that, and bind it with deviceContext->OMSetBlendState. Samplers, likewise, have their own set of description structures and states which are bound to the Pixel Shader stage with the PSSetShaderResources. Honestly, when I tell people this, a lot of them recoil at the verbosity. Many people prefer dynamic typing and implicitly managed state. It's easier for them, they iterate faster, and they don't give a shit about what stage of the rendering pipeline they've bound a resource to, or if they've properly instantiated a ShaderResourceView for the texture that's also serving as a RenderTarget. They just don't care. And that's ok! I get it. Personally, however, I like when state is explicit and I can detect the underlying machinations of the render pipeline and global state by the names of the functions and types. These types are not perfect. For example, you use a D3D11_SAMPLER_DESC to instantiate both a SamplerState and a SamplerComparisonState object. I had been passing D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT to my Shadow map's texture filtering function when I had meant to pass D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT (note "Comparison"). This is a totally valid way to instantiate a SamplerState, but when you try to bind it as a SamplerComparisonState at runtime, the GPU simply doesn't know what the hell I'm talking about. As a result, PCF shadow filtering doesn't work. I was stuck on this problem for an embarrasingly long time. DirectX (more) Another cool feature of DirectX is its integration with Visual Studio. Usually, I see tutorials telling people to hard code their shaders as strings in their programs, then compile at runtime. That's okay for tutorials, but if your program gets any more complicated, you'll want to start #including headers. If you compile your shaders at runtime, you'll have to create an implementation of ID3DInclude so that the compiler knows where to find the file you're telling it to include. Instead, you can set your HLSL files up to be compiled at runtime. If you add them to your resources filter, you can go to each file's properties and mark them as Pixel Shader files, Vertex Shader files, etc., and set their entrypoint names ( main, vertex, etc.) [image-7] These files will be compiled to .cso files in your output directory, where you can then just load as regular binary files and link them to the GPU withCreateVertexShader or CreatePixelShader. This feature has made HLSL development a lot faster for me. DirectXTK and DirectXMath Whereas OpenGL relies on community support for features like input handling, model loading, and SIMD operations, Microsoft has provided tools called DirectXTK and DirectXMath as first party solutions to these problems. DirectXTK has features like Mouse Input, so that you don't have to write much of your own win32 event handlers and logic. Just integrate the mouse library into your existing top level event handler: LRESULT CALLBACK messageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) { switch (umsg) { case WM_KEYDOWN: onKeyDown((UINT)wparam); return 0; case WM_KEYUP: onKeyUp((UINT)wparam); return 0; case WM_ACTIVATEAPP: case WM_INPUT: case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_RBUTTONDOWN: case WM_RBUTTONUP: case WM_MBUTTONDOWN: case WM_MBUTTONUP: case WM_MOUSEWHEEL: case WM_XBUTTONDOWN: case WM_XBUTTONUP: case WM_MOUSEHOVER: // DirectXTK wndproc function handles all the mouse events // then you just access them through the Mouse class Mouse::ProcessMessage(umsg, wparam, lparam); default: return DefWindowProc(hwnd, umsg, wparam, lparam); } } There's also a texture loading library, WICTextureLoader, which handles all kinds of file formats, as an alternative to stb_image (did you know that guy develops everything in his library suite with Visual C++ 6?) There are other libraries that all seem pretty cool, but I have yet to use them. I suggest checking them out DirectXMath is the SIMD library for DirectX. It's the library that refuses to let you just do operations on your regular XMFLOAT3 vectors, instead forcing your to load and store them into XMVECTORS with XMLoadFloat3 and XMStoreFloat3 first (since the XMVECTOR is just a wrapper for the __m128 SIMD intrinsic). In that regard, glm is a bit more ergonomic. DirectXMath has all the features you would expect from a Graphics-focused SIMD library: matrix multiplication, Perspective/ Orthographic Matrix creation, transformation matrix creation, and lerping for quaternions. My only wish is that the DirectXTK libraries used the DirectXMath vectors so that I didn't have to convert them when deserializing model files Conclusion I like Windows development for more than just WSL2 support: Windows is a good development platform in-itself, and Microsoft likes to support its community. PowerShell, Terminal, MSVC, Visual Studio, DirectX, and the first party integrations between each of these make Windows my personal favorite software development platform Previous issue Dumb phone Subscribe to Shortround's Dev Blog Don't miss out on the latest issues. Sign up now to get access to the library of members-only issues. jamie@example.com Subscribe Shortround's Dev Blog (c) 2023 Powered by Ghost