From 8c8add3c407624fc6b5b133684c40a26614c3107 Mon Sep 17 00:00:00 2001 From: Nucleic Date: Wed, 29 Jul 2026 03:26:09 -0700 Subject: [PATCH] Merge nucleic/lucid-river-toad-6efj into dev --- spikes/README.md | 10 +++ spikes/WslcApiDump/InternalComProbe.cs | 94 +++++++++++++++++++++++++- spikes/WslcApiDump/Program.cs | 9 ++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/spikes/README.md b/spikes/README.md index ea796bb..95aaa33 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -128,6 +128,16 @@ service proper (`LxssUserSession`, `LxssUserSessionInBox`). for `IWSLCSessionManager`. One object, two faces. `IWSLCVirtualMachine` was refused by every class, confirming §13.1's retraction against the machine rather than against an IDL. +`--internal-call` then confirmed the **vtable** matches the IDL: `GetVersion()` (slot 3) returned +2.9.4, agreeing with the compat `WslcService.GetVersion()` in the same run — the same service +answering through both faces — and `ListSessions()` (slot 6) returned S_OK. So hand-written +`ComImport` is sufficient and the C++/WinRT shim §13.1 mused about is not needed. + +It returned **0 sessions**, though, so the entry-struct layout (inline `wchar_t` buffers, the same +shape `ListContainers` uses) is still untested. Run `--session --keep --internal-call` to create a +session, re-adopt it through the internal interface, and exercise it — that combination is §2.3's +reattach story end to end, and it also answers the handoff question below. `wsl --shutdown` after. + The hypothesis was that one of those objects also implements the internal interface — COM objects routinely expose several — so `--internal` activates each and QIs for the internal IIDs. It **never calls a method**, so a vtable mismatch cannot crash it, and QI alone answers the diff --git a/spikes/WslcApiDump/InternalComProbe.cs b/spikes/WslcApiDump/InternalComProbe.cs index a8d9738..b628d6e 100644 --- a/spikes/WslcApiDump/InternalComProbe.cs +++ b/spikes/WslcApiDump/InternalComProbe.cs @@ -62,8 +62,9 @@ internal static class InternalComProbe + "because §13.2 retracted a decision on that reading and it deserves confirming"), ]; - internal static void Run(bool callThrough) + internal static void Run(bool callThrough, string? sessionName = null) { + InternalComProbe.sessionName = sessionName; Console.WriteLine(); Console.WriteLine("internal COM probe (docs/WINDOWS_PORT.md §13.2):"); Console.WriteLine(" wslc.idl declares no coclass, so the question is whether an EXISTING"); @@ -218,14 +219,98 @@ internal static class InternalComProbe } // The callee allocated it; nobody else will free it. if (sessions != IntPtr.Zero) Marshal.FreeCoTaskMem(sessions); - Console.WriteLine(" → enumeration and reattach are both reachable from here."); + if (count == 0) + // Worth saying, because "it returned S_OK" is weaker evidence than it looks: with + // no entries, the WSLCSessionListEntry layout — two DWORDs then two INLINE + // wchar_t buffers — was never actually unmarshalled. ListContainers uses the same + // `size_is(, *Count)` double-pointer shape, so that layout is still unproven. + Console.WriteLine(" (0 entries, so the ENTRY STRUCT layout is still " + + "untested — run with a session up: --session --keep --internal-call)"); } catch (Exception e) { Console.WriteLine($" ListSessions() threw {e.GetType().Name}: {e.Message}"); + return; + } + + ProbeSessionHandoff(proxy); + } + + /// + /// The question that decides the SHAPE of D13's internal arm. + /// + /// `IWSLCSession` (internal, `EF0661E4-…`) and `IWSLCCompatSession` (SDK-facing, + /// `DD7B2EF9-…`) are different interfaces with different IIDs. `WSLCCompatSessionManager` + /// already proved one object can wear both faces — so if the session `OpenSessionByName` + /// returns ALSO answers a QI for the compat interface, then a re-adopted session can be + /// handed to `Session.FromAbi(ptr)` and driven by the **existing** facade code. + /// + /// * **YES** → internal COM is used only to *find* things. Reattach costs one QI, and + /// nothing else in `WslcFacade` changes. + /// * **NO** → every re-adopted handle is internal-only, and start/stop/delete/exec each need + /// a second, hand-marshalled implementation. That is a much bigger arm, and it is the point + /// at which the C++/WinRT shim §13.1 mused about earns its keep. + /// + private static void ProbeSessionHandoff(IWSLCSessionManager proxy) + { + var name = sessionName; + if (string.IsNullOrEmpty(name)) + { + Console.WriteLine(" (no --session-name given; skipping the reattach handoff test)"); + return; + } + + Console.WriteLine(); + Console.WriteLine($" reattach handoff test — OpenSessionByName(\"{name}\"):"); + int hr; + IntPtr session; + try + { + hr = proxy.OpenSessionByName(name, out session); + } + catch (Exception e) + { + Console.WriteLine($" threw {e.GetType().Name}: {e.Message}"); + return; + } + + if (hr < 0) + { + Console.WriteLine($" failed: {Hresult(hr)}"); + Console.WriteLine(" (expected if no session of that name is running — create " + + "one first: --session --keep --internal-call)"); + return; + } + + Console.WriteLine(" opened — the session half of reattach WORKS"); + try + { + var compat = IidCompatSession; + var qi = Marshal.QueryInterface(session, in compat, out var projected); + if (qi >= 0) + { + Console.WriteLine(" QI IWSLCCompatSession: **YES**"); + Console.WriteLine(" → a re-adopted session can be handed to " + + "Session.FromAbi() and driven by the EXISTING compat facade code."); + Console.WriteLine(" → D13's internal arm shrinks to: find, then hand off."); + Marshal.Release(projected); + } + else + { + Console.WriteLine($" QI IWSLCCompatSession: no ({Hresult(qi)})"); + Console.WriteLine(" → a re-adopted session is internal-only, so every " + + "operation on it needs a second hand-marshalled path."); + Console.WriteLine(" → that is the case where a C++/WinRT shim is worth it."); + } + } + finally + { + Marshal.Release(session); } } + private static string? sessionName; + private static IEnumerable Wrap(string text) { var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -264,6 +349,11 @@ internal static class InternalComProbe /// private static readonly Guid IidIUnknown = new("00000000-0000-0000-C000-000000000046"); + /// `IWSLCCompatSession` from WSLCCompat.idl — the SDK-facing session interface, and the one + /// the C#/WinRT `Session` projection wraps. Distinct from the internal `IWSLCSession` + /// (`EF0661E4-…`); whether one object answers both is the reattach-handoff question. + private static readonly Guid IidCompatSession = new("DD7B2EF9-AA01-4F21-8A3A-29D394CBB579"); + private const uint ClsctxAll = 0x17; // INPROC_SERVER|HANDLER|LOCAL_SERVER|REMOTE_SERVER private const uint CoinitMultithreaded = 0; private const uint RpcEChangedMode = 0x80010106; diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs index e9461ec..b608569 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -104,7 +104,14 @@ internal static class Program // interface can be reached at all, which is what D13's enumeration/reattach/pty arm // hangs on (docs/WINDOWS_PORT.md §13.2). if (args.Contains("--internal") || args.Contains("--internal-call")) - InternalComProbe.Run(callThrough: args.Contains("--internal-call")); + InternalComProbe.Run( + callThrough: args.Contains("--internal-call"), + // Shares --session's name so `--session --keep --internal-call` creates a session + // and then re-adopts it through the internal interface in one run — which is the + // only way to test the handoff, and the whole §2.3 reattach story in miniature. + sessionName: args.Contains("--session") || args.Contains("--session-name") + ? ArgValue(args, "--session-name") ?? "nucleic-spike" + : null); Console.WriteLine(); if (componentsMissing is { Count: > 0 })