Windows and POSIX abstraction

Daniel Andersson
2024-08-12

In order to get your software to work on multiple platforms, without using electron etc, the supported OS’s differences need to be abstracted away. There may be better ways to do the following examples for Windows and if so, let me know at info@irrbloss.dev. The point of this post is more to show some differences that I encountered while abstracting a platform layer for Windows, Linux and OpenBSD.

Shared library differences between platforms

Symbol visibility caught me off guard when I started porting a project from linux to windows. By default symbols are hidden on Windows, and visible on Linux. This means in a project that uses a shared library, on POSIX platforms, symbols from the binary/application can be called from the dll. Not so on the windows platform. I now prefer hidden by default and that can be achieved on Linux by compiling with -fvisibility=hidden. Then you ‘mark’ every function or symbol that should be exported with a macro

#if defined(_WIN32)
#define MY_API __declspec(dllexport)
#if defined(__linux__)
#define MY_API __attribute__((visibility("default")))
#else
#error "Unknown platform"
#endif

Technically the defines should be for compilers and not platforms, since clang can be used on Windows too.

Monitoring for file changes

Another difference between Linux and Windows is the way file watching works. On Linux using inotify files that need to be monitored have to be added manually with inotify_add_watch. On Windows it is a little bit more involved. You can use ReadDirectoryChangesW and then poll for changes. If you are only interested in files being changed and not added for example, you need to keep track of which files to monitor separately, since ReadDirectoryChangesW will give events for all files in the specified directory.

Platform file paths

My first pass of abstracting paths is using Linux/POSIX paths in UTF-8 as normalized paths stored in str8. What that means is that the Windows layer translates to and from UTF-16 and ‘Windows paths’ to normalized path. For example C:\path\to\file turns into /C:/path/to/file/. The normalized path is then passed around and processed in some way. Then it’s translated back to UTF-16 and passed along to whatever Windows API that uses it. That seems to be the simplest thing to start with since I don’t have to escape backslashes.

The first pass paths only support the regular Windows file paths, not UNC etc.

Mutex, futex, Critical Section

The impression I had when I started writing this blog post was that Window’s mutex implementation was slower, when compared to pthread mutexes, and that they were ‘global’ between processes by default. I saw somewhere that Critical Section should be used instead of mutexes. I read up on it a little bit and it seems they are a hybrid of user mode and kernel mode, like a futex. But then there is also WaitOnAddress, which according to the futex wikipedia is Windows’ version of a futex. It is a little confusing, but Microsoft is very good with supporting old APIs etc. That is probably why there are multiple versions of similar things.

With my os abstraction layer, I have yet to make a project that utilizes mulithreading and the first pass for ‘mutex’ that I made was with with pthread mutex on Linux and Critical Section on Windows.

Time and sleeping

Windows does not have very granular ‘time slices’ for scheduling and that is likely why Sleep only has millisecond resolution, there is no support for micro- or nanosecond sleep.

One option is to use a busy loop in conjuction with QueryPerformanceCounter or _rdtsc/_rdtscp. The Windows scheduler could however schedule the application away just before it was supposed to get going and you do not get what you want anyway.

For something that uses a GPU to draw stuff, it is better to rely on VSYNC, rather than sleeping. For non interactive applications blocking on function calls, like poll is an alternative to sleeping. Multi-threading applications can also use condition variables instead of sleeping where appropriate.

GetTickCount only has 10+ms resolution and is not as good as clock_gettime(MONOTONIC) on POSIX, QueryPerformanceCounter has higher resolution and is preferable for checking elapsed time.