Repository navigation
Build on Windows: put every system call behind a platform floor - #5
Conversation
libspill had no Windows implementation at all -- not a missing include, but a POSIX-only I/O and threading layer. Psi4 hit it first (issue #4): its clang-cl CI stops at open.c's <unistd.h> while building libspill as a bundled dependency for the libpsio port. Everything the library asks of the operating system now goes through os.h, implemented once per platform in os_posix.c and os_win32.c. No other file names a system call, which is checkable with a grep and is the property worth keeping. Two things the Win32 side had to get right rather than merely compile: * The transfers stay POSITIONAL. §5a promises concurrent ls_read/ls_write on one store need no external locking, and pread/pwrite deliver that by carrying the offset. Win32 gets OVERLAPPED ReadFile/WriteFile, which is positional in the same sense even on a synchronous handle. _lseeki64 + _read would have compiled and quietly broken the promise. * Offsets are 64-bit throughout. off_t is 32 bits in the MSVC CRT, so the (off_t) casts this code used to do would have truncated silently past 2 GB -- inside the range this library exists for, not beyond it. Mapping is the one facility with no cheap equivalent: ls_map reserves the logical span PROT_NONE and maps each extent over its slice with MAP_FIXED, which on Windows needs a placeholder reservation (VirtualAlloc2 + MapViewOfFile3). Rather than carry an untested version of that, LS_MAPPED is refused at ls_open where LS_HAVE_MMAP is absent, with LS_ERR_MODE -- the answer callers already get for the other unsupported mode combinations. Every other mode works, and Psi4's shim never uses mapped mode. Also fixed, because it would have produced a DLL that exports nothing: LS_API was empty on _WIN32. Windows needs the mark to mean dllexport while the library is built and dllimport in a consumer, which is the same intent -fvisibility=hidden already carries elsewhere. Verification. The POSIX build is unchanged and `make check` and ctest pass in full. The no-mapping branches are not left for Windows to discover: LS_NO_MMAP compiles and runs exactly those branches on Linux, where the whole suite exercises them, and test_mapped skips (ctest SKIP_RETURN_CODE) rather than failing when mapping is absent. Both are new CI jobs, along with windows-latest under MSVC and clang-cl. I could not build the Windows path locally -- no MSVC and no mingw cross-compiler here -- so os_win32.c is reviewed, not run. The CI job added here is the first thing that will actually execute it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The job earned its keep on its first run: the port compiled almost entirely
under both MSVC and clang-cl, and then failed at the link.
* strerror_r and posix_memalign were still being called directly. Both are
POSIX-only and both belong in the floor: ls_os_strerror hides the fact
that glibc's strerror_r returns char*, XSI's returns int and the MSVC CRT
spells it strerror_s with the arguments the other way round, and
ls_os_aligned_alloc pairs with ls_os_aligned_free -- which is not
decoration, since memory from _aligned_malloc must never reach free().
* warning C4141: 'dllexport' used more than once. A stray duplicated LS_API
on ls_opts_init, harmless while LS_API expanded to nothing on Windows and
to a repeated visibility attribute on GCC. Making LS_API mean dllexport
is what turned it into a diagnostic.
Found by a sweep for the rest of the POSIX-only surface at the same time, so
this should be the last link round rather than the first of three: strdup is
the only other non-C89 name left and the MSVC CRT provides it (the failing
build reached the link, so it had already resolved).
POSIX is still unaffected: ctest 9/9, the LS_NO_MMAP configuration 8/9 with
test_mapped skipped, and make check green end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The library builds and links on Windows now; this round was entirely the
tests, which the first sweep did not cover -- it looked at src/ and stopped
there.
* pthreads in test_posix and test_surveyed. src/os.h cannot help: it is
internal and its implementation is not exported, so a test linking only
the public library cannot call it. tests/thread_compat.h gives the three
primitives the tests actually use, with the pthread signature on both
sides so the thread bodies are unchanged.
* fork in test_surveyed. §3a(1) is a genuinely multi-process property and
fork is the barrier; a CreateProcess version would be this binary
re-executing itself with an argument, which is a different test rather
than the same one ported. That half is split into its own function and
skipped where there is no fork, saying so. The option refusals that
followed it in the same function are portable and now always run --
previously they would have been lost with it.
* EDQUOT in abi_header_test: POSIX, not C, and absent from the MSVC CRT.
The property is about the errnos that exist, so it is checked for those.
* A real bug, not a portability shim: churn_libspill and test_hdf5 declare
a 64-bit LCG as unsigned long, which is 32 bits on Windows -- the
constants truncate and `rs >> 33` is undefined. MSVC said so (C4293).
uint64_t is what it always meant; on Linux this changes nothing.
unlink -> remove in test_posix, which is C89 and does the same thing here.
Linux unaffected: ctest 9/9 and make check green end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Everything compiles and links on Windows now; this was the first round where the tests ran, and they ran straight into ENOENT from ls_open. build_path resolved its default directory as TMPDIR, then /tmp. Windows has neither: the environment variables are TMP and TEMP, and there is no /tmp to fall back to, so every store opened without an explicit dir tried to create a file in a directory that does not exist. Nothing was wrong with the I/O -- it never got that far. ls_os_tmpdir joins the rest of the floor. POSIX keeps TMPDIR then /tmp exactly as before; Windows asks GetTempPath, which is the documented resolution there (TMP, then TEMP, then the user profile, then the Windows directory) and always ends with a separator, which build_path does not want. A caller that sets ls_opts.dir -- Psi4's shim does, from PSIOManager -- never reached this path in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Review: do not merge yet — both Windows jobs are red, which is your own stated criterion. The design is right and I would take this shape. (Posting as a comment; GitHub will not let this account request changes on its own PR.) What I verified rather than took on trust
Two real bugs found along the way, worth more than the port itself: Why not mergeBoth toolchains fail 6 of 9 tests. It compiles on Windows — real progress — but every failure is the first One root cause, and the report gives nothing to work with. I could not diagnose it from here — no Windows machine — and I will not guess in a review. What would settle it in one run: log the constructed path and Two cheap candidates to eliminate first:
Separately: One correction to the description
Not quite, and Smaller
On the mapping gapRefusing Get the Windows jobs green and I will approve — the structure is not in question. |
Four checks in test_posix and test_cxx stat the file the library created, so the test and the library have to agree on its path. Each side worked it out separately -- the tests as TMPDIR then /tmp, the library as whatever the platform says -- which agreed on Linux by coincidence and stopped agreeing on Windows, where the library now asks GetTempPath. The fix is to say the directory rather than derive it twice: the tests pass ls_opts.dir explicitly and build their paths from the same constant. That removes the duplicated resolution rather than teaching it a third platform, and it is what a consumer inspecting its own scratch files would do anyway -- Psi4's shim passes the directory for exactly this reason. Everything else in that round already passed on Windows, including the one that mattered: "8 threads x 64 distinct keys: no error, no corruption", which is the OVERLAPPED positional-I/O claim actually being exercised. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b41f7cb to
66e12c7
Compare
The whole suite passes on Windows now; the only thing left failing was the
packaging check, and not for a Windows reason:
CMake Error at CMakeLists.txt:10 (project):
No CMAKE_Fortran_COMPILER could be found.
tests/consumer declared `project(consumer LANGUAGES C CXX Fortran)`, so a
consumer with no Fortran compiler could not configure at all -- and it then
linked libspill::spill_f unconditionally, which is not installed unless
LIBSPILL_BUILD_FORTRAN was on. The library's own position is the opposite,
and says so in CMakeLists.txt: the Fortran binding is optional "so that a C
or C++ consumer does not need a Fortran compiler to use the library". The
packaging check was contradicting the thing it exists to verify.
f_use is now built when there is a Fortran compiler AND a Fortran binding to
link, and skipped with a message otherwise; make check runs it only if it was
built. On Linux nothing changes -- all three consumers still build and run.
Verified both ways here: the ordinary build still reports "[PASS] Fortran
consumer via find_package", and an install without the Fortran binding (which
is what the Windows job does) configures, skips f_use, and builds the C and
C++ consumers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Approved — both Windows jobs are green, which was the condition. I re-ran the suite locally on the updated branch and it is clean. The root cause was the class I guessed at, in the place I did notI looked at
|
Review is right that the invariant was stated more strongly than it held: "nothing else in the library names a system call" was not true of mmap/munmap, which stayed in store.c and toc.c behind #ifdef LS_HAVE_MMAP. Of the two ways out offered -- narrow the claim, or make it true -- this takes the second. The claim was the useful thing. ls_os_map_extents and ls_os_unmap now live in the floor. They are declared in internal.h rather than os.h because they speak in ls_extent, and only os_posix.c defines them, under LS_HAVE_MMAP. store.c keeps the policy (which record, the busy count, what goes in the table of contents) and hands the reserve-then-MAP_FIXED sequence to the platform, which is where a Windows implementation would slot in if VirtualAlloc2 + MapViewOfFile3 is ever worth carrying. The reviewer's grep now returns nothing. Also LS_PATH_SEP, for the same reason the review gives: Windows accepts '/' so this fixes no bug, but a floor that abstracts the temp directory and then hardcodes the separator is only half a floor. Not changed, deliberately: LS_ERR_MODE rather than LS_ERR_BACKEND for a build without mapping. The review makes the better argument -- LS_ERR_BACKEND is what LS_HDF5 gets for "not compiled in" -- and also says it is not worth churning without a caller asking. Agreed on both counts; noting it here so the next person finds the reasoning rather than the omission. Verified: ctest 9/9 with test_mapped passing (so the moved mapping code is exercised, not merely compiled), LS_NO_MMAP 9/9 with test_mapped skipped, and make check green end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All seven jobs green at The blocking objection
Of your two candidates: the hardcoded
|
# Conflicts: # src/store.c
The merge resolution had zero_range call ls_os_ftruncate to reach the end of a new extent. That is not safe here: reads and writes run outside toc_lk on purpose, so a truncate is not serialised against a thread that has already grown the file past the length we computed, and it cuts that thread's data off. On Windows it is worse, because _chsize_s zero-fills as it grows, so it clobbers rather than truncates. The Windows CI caught it as a failure in the 8-thread test; the same race exists on Linux and merely did not show. Extend with one zero byte at the last offset instead. That byte is inside the extent being zeroed, so no other thread can own it, and a positional write cannot shrink the file. Skipping the erase entirely when the extent starts past the end of the file also spares the zero-write that platforms without a zeroing primitive were doing over every new tail extent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
libspill main has moved on four commits since b6edf39, two of which matter here. ReLibQC/libspill#5 landed (a19b32a), so libspill builds on Windows: the POSIX-only I/O and threading layer that stopped Psi4's clang-cl CI at open.c's <unistd.h> is now behind a platform floor, with windows-latest under both MSVC and clang-cl in libspill's own CI. That was the blocker for this PR, not a preference. f711d79 stopped fsyncing a kept store on close, behind a new ls_opts.durable_close (opts version 3, source-compatible -- ls_opts_default carries the caller's compile-time version, so the shim needs no change). This is the more interesting one for Psi4: the version this branch pinned yesterday fsynced every keep-close, which would have made the port SLOWER than the code it replaces on close-heavy workloads. Upstream measured an eT coupled-cluster case go from 41.7 s to a 1200 s timeout, ~24 of 28 minutes in fsync. Psi4 cycles scratch files constantly and would have paid the same. The shim leaves durable_close off, which is also the faithful choice: the close.cc this port deletes called close(2) and nothing else, and fsync appears nowhere in libpsio's history. Recorded in PORTING.md, since inheriting a durability guarantee by accident and losing it by accident are both worth a paragraph. Verified against the new pin: libspill's own suite 9/9, and Psi4's libpsio-conformance and roundtrip tests build warning-clean and pass, including the PSIOManager path and psiclean-registration assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #4.
libspill had no Windows implementation at all — not a missing include, but a
POSIX-only I/O and threading layer. Psi4's clang-cl CI stops at
open.c's<unistd.h>while building libspill as a bundled dependency for the libpsioport (psi4/psi4#3540), and
libpsiois core, so there is no "skip it onWindows" option on that side.
The shape
Everything the library asks of the operating system now goes through
src/os.h, implemented once per platform insrc/os_posix.candsrc/os_win32.c. No other file names a system call — that is checkable with agrep, and it is the property worth keeping:
Threading moved the same way:
ls_mutex/ls_cond/ls_thread, which ispthread_*on POSIX andSRWLOCK/CONDITION_VARIABLE/_beginthreadexonWin32. Deliberately not a shim under the
pthread_names — a header thatdefines
pthread_mutex_lockis a trap for anyone who later links a realpthreads on Windows. The rename is mechanical and the suite covers it.
Two things the Win32 side had to get right, not merely compile
ls_read/ls_writeon one store need no external locking, and
pread/pwritedeliver that bycarrying the offset. Win32 gets
OVERLAPPEDReadFile/WriteFile, which ispositional in the same sense even on a synchronous handle.
_lseeki64+_readwould have compiled and quietly broken the promise; there is a commentin
os_win32.csaying so, because the next person will reach for it.off_tis 32 bits in the MSVC CRT, so the(off_t)casts this code used to do would have truncated silently past2 GB — inside the range this library exists for, not beyond it. Nothing below
os.hmentionsoff_tany more.Mapping
The one facility with no cheap equivalent:
ls_mapreserves the logical spanPROT_NONEand maps each extent over its slice withMAP_FIXED, which onWindows needs a placeholder reservation (
VirtualAlloc2+MapViewOfFile3,Win10 1803+). Rather than carry an untested version of that,
LS_MAPPEDisrefused at
ls_openwhereLS_HAVE_MMAPis absent, withLS_ERR_MODE— theanswer callers already get for the other unsupported mode combinations. Every
other mode works, and Psi4's shim never uses mapped mode. Happy to do the real
thing in a follow-up if you would rather not ship the gap.
One bug found on the way
LS_APIwas empty on_WIN32, so a shared build there would have produced aDLL exporting nothing. It now means
dllexportwhile the library is built anddllimportin a consumer — the same intent-fvisibility=hiddenalreadycarries elsewhere, which is why the comment above it still reads true.
Verification
All seven CI jobs green, including both Windows toolchains:
windows (msvc)test_mappedskippedwindows (clang-cl)test_mappedskippedmake check(gcc, clang)cmake + ctest + installno mapping (the Windows path, on Linux)no HDF5, no Fortran, no numpyThe claim that mattered — that
OVERLAPPEDReadFile/WriteFilereally ispositional in the sense §5a promises — is not inferred from "it compiled": it is
test_posix's "8 threads x 64 distinct keys: no error, no corruption",running on Windows. That check passed on Windows even in the round where four of
its siblings were still failing, and
test_posixnow passes in full.LS_NO_MMAPalso compiles and runs the no-mapping branches under the wholesuite on Linux, so that path is not left to be discovered only on Windows. It
earned its place: it caught a regression mid-review that the ordinary Linux
build could not see.
What is still not proven: that a real consumer links this on Windows.
libspill building and passing its own suite there is necessary, not sufficient —
Psi4's psi4/psi4#3540 is the thing that will actually answer it.
Checked downstream
Psi4's libpsio shim builds and passes its conformance harness against this
branch installed from
cmake --install— including the round trip and thePSIOManager path assertions. On Linux; see above.
Six rounds, for the record
Each CI round found a different class of problem, which is the argument for the
Windows job existing rather than for my having been careful:
<unistd.h>, the POSIX-only I/O and threading layerstrerror_r,posix_memalign, and a stray duplicatedLS_APIforkin the tests,EDQUOT, a 64-bit LCGdeclared
unsigned longENOENT: the default scratch directory wasTMPDIRthen/tmpThree of those (the
LS_APIduplicate, the LCG, the Fortran-consumerrequirement) are pre-existing bugs that happen to be invisible on Linux, not
things this port introduced.