|
| 1 | +# Design: Pure C# ProjFS Managed API |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +This document describes the design and motivation for replacing the C++/CLI mixed-mode |
| 6 | +`ProjectedFSLib.Managed.dll` with a pure C# P/Invoke implementation that maintains |
| 7 | +100% API compatibility while enabling NativeAOT compilation, trimming, and simplified builds. |
| 8 | + |
| 9 | +## Background |
| 10 | + |
| 11 | +The original ProjFS Managed API is a C++/CLI mixed-mode assembly that wraps the native |
| 12 | +`ProjectedFSLib.dll` APIs. While functional, this approach has several limitations: |
| 13 | + |
| 14 | +| Concern | C++/CLI | Pure C# | |
| 15 | +|---------|---------|---------| |
| 16 | +| Build toolchain | Requires VS C++ workload + C++/CLI support | `dotnet build` only | |
| 17 | +| Runtime dependency | Visual C++ redistributable (Ijwhost.dll) | None | |
| 18 | +| NativeAOT | Not supported (mixed-mode incompatible) | Fully supported | |
| 19 | +| Trimming | Not supported | `IsAotCompatible=true` | |
| 20 | +| Cross-compilation | Requires matching native toolchain | Any machine with .NET SDK | |
| 21 | +| TFMs | net48, netcoreapp3.1 | net8.0, net9.0, net10.0+ | |
| 22 | +| Maintenance | Dual C++/C# expertise needed | C# only | |
| 23 | + |
| 24 | +## Design Goals |
| 25 | + |
| 26 | +1. **API compatibility** — Same `Microsoft.Windows.ProjFS` namespace, same types, same signatures |
| 27 | +2. **Drop-in replacement** — Consumers change only the project reference, no code changes |
| 28 | +3. **AOT-safe** — No reflection, no dynamic code generation, `IsAotCompatible=true` |
| 29 | +4. **Correctness** — Byte-identical struct layouts verified against Windows SDK headers |
| 30 | +5. **Complete feature coverage** — All v1809 APIs + v2004 symlink APIs |
| 31 | + |
| 32 | +## Architecture |
| 33 | + |
| 34 | +### P/Invoke Layer (`ProjFSNative.cs`) |
| 35 | + |
| 36 | +Direct `[DllImport]` declarations for all `ProjectedFSLib.dll` exports: |
| 37 | + |
| 38 | +- Core: `PrjStartVirtualizing`, `PrjStopVirtualizing` |
| 39 | +- Placeholders: `PrjWritePlaceholderInfo`, `PrjWritePlaceholderInfo2`, `PrjUpdateFileIfNeeded` |
| 40 | +- Enumeration: `PrjFillDirEntryBuffer`, `PrjFillDirEntryBuffer2` |
| 41 | +- Data: `PrjWriteFileData`, `PrjAllocateAlignedBuffer`, `PrjFreeAlignedBuffer` |
| 42 | +- Utilities: `PrjFileNameMatch`, `PrjFileNameCompare`, `PrjDoesNameContainWildCards` |
| 43 | + |
| 44 | +All native structs use `[StructLayout(LayoutKind.Sequential)]` with exact field-level |
| 45 | +padding verified against `sizeof()` and `offsetof()` from the Windows SDK: |
| 46 | + |
| 47 | +| Struct | C sizeof | C# Marshal.SizeOf | |
| 48 | +|--------|----------|--------------------| |
| 49 | +| `PRJ_FILE_BASIC_INFO` | 56 | 56 | |
| 50 | +| `PRJ_PLACEHOLDER_INFO` | 344 | 344 | |
| 51 | +| `PRJ_EXTENDED_INFO` | 16 | 16 | |
| 52 | +| `PRJ_CALLBACK_DATA` | 96 | 96 | |
| 53 | +| `PRJ_CALLBACKS` | 64 | 64 | |
| 54 | +| `PRJ_STARTVIRTUALIZING_OPTIONS` | 32 | 32 | |
| 55 | +| `PRJ_NOTIFICATION_MAPPING` | 16 | 16 | |
| 56 | + |
| 57 | +**Key lesson learned:** `PRJ_PLACEHOLDER_INFO` includes a `UINT8 VariableData[1]` flexible |
| 58 | +array member at offset 336. Omitting this field causes `Marshal.SizeOf` to return 336 instead |
| 59 | +of the native 344, and `PrjWritePlaceholderInfo` returns `ERROR_INSUFFICIENT_BUFFER`. |
| 60 | + |
| 61 | +### Callback Routing (`VirtualizationInstance.cs`) |
| 62 | + |
| 63 | +Native ProjFS callbacks (registered via `PRJ_CALLBACKS` function pointers) are routed to |
| 64 | +managed code through `[UnmanagedFunctionPointer(CallingConvention.StdCall)]` delegates. |
| 65 | +The instance context passed to `PrjStartVirtualizing` is a `GCHandle` that allows the |
| 66 | +static callback methods to recover the `VirtualizationInstance` object. |
| 67 | + |
| 68 | +### Constructor Behavior |
| 69 | + |
| 70 | +The constructor matches C++/CLI behavior exactly: |
| 71 | +1. Creates the virtualization root directory if it doesn't exist |
| 72 | +2. Checks for existing ProjFS reparse point via `PrjGetOnDiskFileState` |
| 73 | +3. Marks the directory as a virtualization root via `PrjMarkDirectoryAsPlaceholder` |
| 74 | +4. Throws `Win32Exception` on failure |
| 75 | + |
| 76 | +### String Marshaling |
| 77 | + |
| 78 | +All `PCWSTR` parameters in ProjFS structs are passed as `IntPtr` with |
| 79 | +`Marshal.StringToHGlobalUni()`. Using `GCHandle.Alloc(string, GCHandleType.Pinned)` |
| 80 | +is **incorrect** — `AddrOfPinnedObject()` on a pinned string returns the object header |
| 81 | +address, not the character data pointer. |
| 82 | + |
| 83 | +### Notification Mapping Lifetime |
| 84 | + |
| 85 | +Notification mapping data (the `PRJ_NOTIFICATION_MAPPING` array and the `NotificationRoot` |
| 86 | +string pointers) must remain valid for the lifetime of the virtualization instance. |
| 87 | +ProjFS may cache these pointers internally. They are freed only in `StopVirtualizing`. |
| 88 | + |
| 89 | +## Known Limitations |
| 90 | + |
| 91 | +### ReFS Symlink Restriction |
| 92 | + |
| 93 | +ProjFS symlink placeholders (`WritePlaceholderInfo2`, `PrjFillDirEntryBuffer2` with |
| 94 | +`PRJ_EXT_INFO_TYPE_SYMLINK`) require an **NTFS** volume. The ProjFS kernel minifilter |
| 95 | +(`PrjFlt.sys`) creates symlinks via the NTFS atomic create ECP (`GUID_ECP_ATOMIC_CREATE`), |
| 96 | +which ReFS does not support. On ReFS volumes, `PrjWritePlaceholderInfo2` returns |
| 97 | +`ERROR_NOT_SUPPORTED` (`0x80070032`). |
| 98 | + |
| 99 | +This was diagnosed via WinDbg: the user-mode `ProjectedFSLib.dll` correctly sends a |
| 100 | +`FilterSendMessage` to the kernel, but `PrjFlt.sys` returns `STATUS_NOT_SUPPORTED` |
| 101 | +(`0xC00000BB`) because `FltCreateFileEx2` with the symlink atomic create ECP fails on ReFS. |
| 102 | + |
| 103 | +Non-symlink operations work correctly on both NTFS and ReFS. |
| 104 | + |
| 105 | +### No Windows 10 1803 Beta API Support |
| 106 | + |
| 107 | +The pure C# implementation targets the v1809 (final) ProjFS API only. The v1803 beta API |
| 108 | +(`PrjStartVirtualizationInstance`, `PrjWritePlaceholderInformation`, etc.) is not supported. |
| 109 | +This matches the minimum supported Windows version for ProjFS as a shipped component. |
| 110 | + |
| 111 | +## Test Results |
| 112 | + |
| 113 | +All 16 tests pass on both net8.0 and net10.0: |
| 114 | + |
| 115 | +- 10 core tests: placeholder creation, file hydration, directory enumeration, notifications |
| 116 | +- 6 symlink tests: file symlinks, directory symlinks, relative paths (require NTFS + elevation) |
| 117 | + |
| 118 | +## Migration Guide |
| 119 | + |
| 120 | +To switch from the C++/CLI package to the pure C# implementation: |
| 121 | + |
| 122 | +1. Remove the `Microsoft.Windows.ProjFS` NuGet package reference |
| 123 | +2. Add a project reference to `ProjectedFSLib.Managed.CSharp.csproj` |
| 124 | +3. Remove `<UseIJWHost>True</UseIJWHost>` from your project file (no longer needed) |
| 125 | +4. Remove the Visual C++ redistributable from your installer |
| 126 | +5. No code changes required — same namespace, same API surface |
0 commit comments