Windows and Linux System Libraries
Where Win32 and POSIX genuinely diverge — process creation, threading, dynamic loading, paths, file semantics, sockets — and how to structure a codebase so platform code is isolated rather than scattered across ifdefs.
Overview
Where cross-platform C++ actually breaks
Cross-platform C++ rarely breaks at the language level: MSVC and GCC/Clang implement the same standard, and since C++17 the library covers paths, threads and time reasonably well. What breaks is the layer beneath, the system libraries. Win32 and POSIX were designed from different premises, and where they differ, they differ structurally.
Process creation: the largest divergence
POSIX configures the child in code between fork() and exec; Windows needs the whole configuration declared up front. A portable spawn abstraction must look like the Windows one, because POSIX can express the Windows shape but not the reverse.
Threading is closer than people expect
- std::thread, std::mutex, std::condition_variable over pthreads or SRWLOCK, CONDITION_VARIABLE and Win32 threads — Stack size, thread names, affinity, priority, cancellation
> [!NOTE] The non-portable list matters in production, and it is exactly what an interviewer probes when you claim your code is portable.
Dynamic loading: the model differs
dlopen and LoadLibrary look alike, but ELF exports all non-static symbols by default while a Windows DLL exports nothing without __declspec(dllexport) or a .def file, and Windows has no RTLD_GLOBAL.
> [!TIP] That asymmetry causes the classic "builds on Linux, unresolved external symbol on Windows". Fix it with an export-macro discipline plus -fvisibility=hidden, so Linux enforces the same contract.
Filesystem semantics: the hardest to reproduce
- Roots — Drive letters, per-drive current directories — One root
- Separator — Backslash — Forward slash
- Length — MAX_PATH 260, two-part opt-in for long paths — Effectively none
- Case — NTFS case-insensitive — ext4 case-sensitive
- Text mode — CRT translates CRLF by default — One mode
- Sharing — Mandatory share modes: delete and rename fail while a handle is open — unlink always succeeds
Nearly every "the build is flaky on Windows only" bug traces to one of these, and share modes are the most disruptive for build tooling.
Sockets: familiar shapes, unfamiliar rules
- Initialization — WSAStartup is mandatory
- Handle type — SOCKET, not int; INVALID_SOCKET is not -1
- Closing — closesocket, not close
- Errors — WSAGetLastError, not errno
The architectural question
The interviewer wants platform code isolated behind narrow interfaces, implemented once per platform in files the build system selects, and a mature portability layer when the surface is wide enough that your own shim becomes the product.
> [!WARNING] The wrong answer is ifdefs woven through business logic: untestable, uncompilable on the platform you are not on, and silently rotting.
Key concepts
- Win32 API
- The C-level Windows system interface: kernel32, advapi32, ws2_32 and friends. Object-handle based (HANDLE), wide-character first (the W suffix functions), and error reporting through GetLastError rather than errno. Documented under learn.microsoft.com/windows/win32.
- POSIX
- The Open Group and IEEE standard that defines the Unix system interface — fork, exec, open, read, pthread_create, dlopen, BSD sockets. Linux, macOS, and the BSDs implement it with local extensions. Errors are reported through errno.
- CreateProcess vs fork/exec
- CreateProcess is a single call that takes the entire child specification declaratively (STARTUPINFO, inheritance flags, environment block, working directory). fork/exec is two calls with arbitrary code between them. The declarative form is the portable common denominator, so design your abstraction that way.
- HANDLE vs file descriptor
- A Windows HANDLE is an opaque kernel-object reference used for files, processes, threads, events, mutexes, and pipes; inheritance is a per-handle attribute set at creation. A POSIX file descriptor is a small integer that is inherited unless FD_CLOEXEC is set. Sockets on Windows are SOCKET values, not descriptors, and do not interoperate with the CRT file API.
- Export visibility asymmetry
- ELF shared objects export every non-static symbol by default; Windows DLLs export nothing unless marked __declspec(dllexport) or listed in a .def file. Building Linux with -fvisibility=hidden and using a generated export macro makes both platforms enforce the same public surface at build time instead of at link time on Windows only.
- MAX_PATH and long-path opt-in
- The Windows API path limit is 260 characters. The \\?\ prefix raises it to roughly 32767 but disables all path normalization (no forward slashes, no dot or dot-dot). Since Windows 10 1607 a process may opt into long paths for many Win32 file functions by setting the LongPathsEnabled REG_DWORD to 1 under Control\FileSystem and adding ws2:longPathAware to the application manifest. Both are required; relative paths remain capped at MAX_PATH.
- Mandatory share modes
- Windows enforces sharing at open time. A handle opened without FILE_SHARE_DELETE prevents deletion and rename of that file for as long as it lives, so an antivirus scanner or a lingering compiler process makes a build step fail with Access is denied. POSIX unlink always succeeds and the inode persists until the last descriptor closes. This single difference accounts for most Windows-only build-tool flakiness.
- Winsock initialization
- Windows sockets require WSAStartup before any socket call and WSACleanup at shutdown. A socket is a SOCKET, the failure value is INVALID_SOCKET rather than -1, the close call is closesocket, and errors come from WSAGetLastError. BSD sockets need none of this, which is why naive ports compile and then fail at the first socket() call.
When to use it
- Porting an existing Linux-only service or tool to Windows, or the reverse, where the language is already portable but the system calls are not
- Designing a build tool, package manager, or agent that manipulates files and spawns processes on both platforms — the layer where locking and path semantics dominate
- Writing a plugin host or extension system that must load shared objects at run time on both platforms
- Choosing between hand-rolling a portability shim and adopting Qt Core, Boost, ASIO, or APR for a long-lived codebase
- Debugging failures that reproduce on exactly one platform: unresolved external symbols, Access is denied on delete, or path-length errors during checkout
Interview questions
- Windows has no fork(). How do you design a process-spawning abstraction that works on both platforms?
- How does std::thread map onto Windows threads and pthreads, and what leaks through the abstraction?
- A build tool works on Linux and intermittently fails on Windows deleting or replacing output files. Diagnose it.
- Walk through the path and filename differences that break cross-platform code.
- How do you structure a codebase so platform code is isolated rather than ifdef-scattered, and when do you adopt a portability layer?
Sign up free to read the full topic
More DevOps topics
Start free on Cariara · Coding problems · System design studio