Files
nucleic-windows/spikes

M1 spikes (docs/WINDOWS_PORT.md §13)

Programs that answer questions the port is currently guessing at. They are checked in and runnable on purpose: a spike whose answer nobody can reproduce six months later is a rumour.

They are deliberately not in windows/Nucleic.sln. The solution's broker contract tests must keep running on any machine against FakeWslc; these need the Microsoft.WSL.Containers preview NuGet and a real WSL stack, so they are built by path.


WslcApiDump — what does the wslc API actually look like?

Status: it has served its original purpose (docs/WINDOWS_PORT.md §13.1). The real API is known, and it was obtained without a Windows machine at all — the package is public, so the .nupkg was downloaded, its projection assembly extracted, and its metadata read with MetadataLoadContext. What this tool is for has therefore changed: its assumption list now describes the surface actually observed in 2.9.3, so running it says what the next package version moved, not what we guessed wrong.

Why it is still reflection. Same reason as before: a typed check fails to compile on the first rename and reports one problem, where this reports all of them at once. That property is worth keeping for a preview API whose next version can break anything.

Two facts it discovered that anything referencing this package needs:

  • the assembly is wslcsdkcs.dll, not Microsoft.WSL.Containers.dll — loading it by package name fails;
  • the only published version is 2.9.3, targeting net8.0-windows10.0.19041.0. The 0.1.0-preview.1 pin this repo carried could never have restored.
cd windows/spikes/WslcApiDump
dotnet run                      # dump the API + check every facade assumption
dotnet run -- --probe           # + GetMissingComponents / GetVersion
dotnet run -- --session         # + create a session, a SECOND with the same name, identity-test, tear down
dotnet run -- --session --keep  # …and leave the sessions running afterwards
dotnet run -- --all-types       # include the ABI/marshalling plumbing in the dump

If it reports missing components, the machine cannot run wslc yet, and the two components are NOT fixed the same way — the tool prints the specific remedy for each:

Missing Fix
VirtualMachinePlatform wsl --install, then reboot (an OS optional feature).
WslPackage wsl --update --pre-release, then wsl --shutdown. WSL is installed but older than the SDK, and 2.9.3 is pre-release-only — a plain wsl --update will not get there. Confirm with wsl --version.
SdkNeedsUpdate The NuGet pin is ahead of the installed service: update WSL further, or pin the package back.

The distinction is worth knowing because the failures look alike but mean opposite things: REGDB_E_CLASSNOTREG (0x80040154) is nothing installed, ERROR_NOT_SUPPORTED (0x80070032) is installed but too old. The tool decodes both, along with the WSLC_E_* range from wslc.idl, because these COM exceptions often carry an empty message and leave nothing but a hex code. --session is skipped while components are missing rather than failing the same way.

Note that the assumption check reads the Microsoft.WSL.Containers namespace only. That is correctness, not tidiness: a C#/WinRT projection also exports ABI.Microsoft.WSL.Containers.* marshalling types with the same short names, and matching on short name alone checks every member against the marshalling struct — which reported 42 false MISSINGs, with CreateMarshaler helpfully offered as the nearest name.

It writes the full public object model to wslc-api-dump.txt (--out to relocate) and prints an ok / MISSING line per assumption, each with why that member matters and a nearest-name hint. It exits 0 even when assumptions fail — a mismatch is the product, not an error. Only a genuinely broken run (the assembly won't load) exits non-zero.

--probe has been run clean on real hardware (Windows 11 amd64, WSL 2.9.3): 54/54 ok, no missing components. Every other path is exercised against a stand-in assembly carrying the observed 2.9.3 shape — the ABI. shadow types, a service reporting missing components, an 0x80070032 with an empty message, a ServiceVersion with no ToString() override, and a duplicate-name Session throwing 0x80040607 — so a failure on your machine is a finding about wslc, not about this tool.

Note the ToString() one, because it bit: a WinRT projection class does not override ToString(), so printing a returned object gives you its type name. Values are rendered by their properties instead — ServiceVersion { Major=2, Minor=9, Revision=3 }, not Microsoft.WSL.Containers.ServiceVersion.

What to send back: the console output, and wslc-api-dump.txt if anything is MISSING — which now means the package moved under us, not that we guessed wrong.

Why --session earns its risk

--session creates a real wslc session named nucleic-spike under %LOCALAPPDATA%\Nucleic\spike\wslc, starts it, and then creates a second one with the same name. That second construction is the whole point, and it is the last open question D13 turns on: the compat SDK exposes a Session constructor and no attach, so does constructing over an existing name re-adopt it, or refuse?

Answered on 2.9.4: it cannot. The constructor is lazy and always succeeds — judging by it is what made the first reading of this probe wrong. Start() is where the service is consulted, and a second Start() on a running name fails with ERROR_ALREADY_EXISTS (0x800700B7). Note it is not WSLC_E_SESSION_RESERVED, which exists in wslc.idl but evidently means something narrower — don't key on it.

So the probe judges on Start(). If a future version lets the second Start() through, it then runs an identity test: terminate the FIRST session and read from the SECOND. A read that worked before and fails after is one underlying session answering both handles; a read that keeps working means two independent VMs.

The identity test deliberately uses only the compat SDK. The obvious check would be wslc session ls — but wslc.exe is not on PATH by default, so a spike that depends on it answers nothing on a stock machine. (It ships beside wsl.exe; try C:\Program Files\WSL\wslc.exe. That it isn't on PATH is one more small argument for D13's no-CLI stance.)

Sessions are torn down at the end by default — an earlier version left a WSL VM running and told you to clean it up with a command that doesn't exist. Pass --keep to leave them, which is how you check the other half of the §2.3 question: whether session state outlives the process that created it. With --keep, wsl --shutdown clears everything.

The gateway address is not what this probe is for any more — §13.1 established that no API surfaces one, and D13 moves the control plane to hvsocket via IWSLCVirtualMachine::GetId.

The three answers that change the design

Most mismatches are a one-line edit in WslcFacade.cs — that is exactly what the IWslc seam is for, and nothing above it should move. These three are different:

Finding Consequence
No container enumeration, stats, pty or attach in the SDK Settled by D13. wslcsdk.dll wraps WSLCCompat.idl (the stable SDK surface), which genuinely lacks them; they all exist on wslc.idl, the service-internal COM interface wslc.exe calls — ListContainers, Stats, ResizeTty, OpenContainer/Attach, OpenSessionByName, and IWSLCVirtualMachine::GetId. The broker binds both surfaces; no CLI.
No create-or-attach on Session Answered: IWSLCSessionManager::OpenSessionByName / EnterSession / ListSessions on the internal interface. §2.3 reattach has its mechanism.
No gateway address anywhere on the API Skip TCP: IWSLCVirtualMachine::GetId returns the VM GUID, so §5's AF_HYPERV/AF_VSOCK path (true vsock parity with macOS) should become primary at M1 (b). Gateway TCP stays as fallback, its address from GetAdaptersAddresses over vEthernet (WSL).
No uid on ProcessSettings Recoverable, and already planned for: exec wraps argv in setpriv/su agent -c. Interceptors and nash don't care about the numeric uid (§3.2).

Not yet written

  • WslcSpike — the typed happy path: session → GHCR pull of naros-agent → container with an NTFS ContainerVolume → exec git status in the bind-mounted worktree → stdio round-trip → SIGTERM, plus the 9P latency numbers §15 wants (git status and npm install on a real repo, mounted vs. in-VM). Deliberately held back until WslcApiDump has run: written now, against guessed names, it would not compile, and fixing it blind is the mistake this whole approach exists to avoid.
  • HvSocketSpike — M1 (b), and now the primary control-plane spike rather than the fallback one (D13): bind an AF_HYPERV listener on the VM GUID from IWSLCVirtualMachine::GetId and dial AF_VSOCK from inside a wslc container. If vsock traverses the container's namespaces, the Windows control plane gets true parity with macOS and §5's firewall/NAT variance stops mattering. Measure gateway-TCP reachability in the same run so the fallback stays evidenced.