In the gio/tests directory.
That would potentially work in this case, as you can tailor it to the MSVC linker. In the general case, a linker based approach is very hard to get working across multiple platforms because they all differ in how they arrange linkage.
See, for example, the socket-listener test in GIO, which interposes in loader symbol resolution to override some syscalls. It only works on Linux, though, and would need DYLD_INTERPOSEsupport to be added for macOS. I don’t think anyone’s tried to get it working on Windows.
So yes, it’s less invasive in the code under test (which is important), but it’s hard to get right across multiple platforms. I’d stress that point actually, for the benefit of everyone else: if the code under test has branches which are only used when testing (e.g. something like if (enable_mocks) call_mock() else call_real_function()) then you’re not really testing it at all. So a goal would be to not require any changes to the code under test to make it testable. Sometimes that’s unavoidable, and in those cases it’s preferable to introduce changes which don’t branch, like providing callbacks or an interface (e.g. which are called unconditionally) to abstract the syscalls.
See also glib/tests/getpwuid-preload.c · main · GNOME / GLib · GitLab (and the loading code: glib/tests/meson.build · main · GNOME / GLib · GitLab ), which takes a different approach (basically being loaded via LD_PRELOAD, but of course that differs between platforms) which is equally as complicated.
Yes and no, I think it depends on the situation. If the abstraction changes the form of the code radically from how it would look if it were just open-coded, and makes the form more complex, that can make things overall harder to understand.
Sometimes a helper function (which your test code calls out to, to abstract platform differences) can be clearer than a wrapper abstraction (which wraps all your test code and tries to do everything). I would also suggest it might be simpler to explicitly focus on syscalls (if you aren’t already), rather than arbitrary function calls, as those are already explicitly an API boundary which is quite well defined and amenable to being overridden (i.e. the addresses of syscalls always have to get loaded at runtime).
Ultimately the right shape for test harness code like this can probably only be found by trying something and seeing how it turns out. I’m interested to see how it turns out 