Skip to content

Commit 52d8380

Browse files
committed
Add design document and Marp presentation for pure C# migration
- doc/design-pure-csharp.md: Detailed technical design covering architecture, struct layout verification, string marshaling pitfalls, ReFS limitation, and migration guide for consumers - doc/projfs-pure-csharp-overview.md: Marp slide deck for presenting to the ProjFS team — covers motivation, architecture, ReFS discovery, results, and the ask to accept the PR and publish an updated NuGet package
1 parent e2d7970 commit 52d8380

2 files changed

Lines changed: 275 additions & 0 deletions

File tree

doc/design-pure-csharp.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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

doc/projfs-pure-csharp-overview.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
marp: true
3+
theme: default
4+
class: invert
5+
paginate: true
6+
header: "ProjFS Managed API — Pure C# Migration"
7+
footer: "github.com/miniksa/ProjFS-Managed-API"
8+
style: |
9+
section { font-family: 'Segoe UI', sans-serif; }
10+
h1 { color: #60a5fa; }
11+
h2 { color: #93c5fd; }
12+
code { background: #1e293b; color: #e2e8f0; }
13+
table { font-size: 0.8em; }
14+
---
15+
16+
# ProjFS Managed API
17+
## From C++/CLI to Pure C#
18+
19+
Drop the C++ toolchain. Keep the API.
20+
Enable NativeAOT for all ProjFS consumers.
21+
22+
---
23+
24+
# Why Change?
25+
26+
The current `ProjectedFSLib.Managed.dll` is a **C++/CLI mixed-mode assembly**.
27+
28+
| Problem | Impact |
29+
|---------|--------|
30+
| Requires VS C++ workload + C++/CLI support | Build infrastructure complexity |
31+
| Requires Visual C++ redistributable | Deployment/installer overhead |
32+
| Incompatible with NativeAOT | Blocks modern .NET optimization |
33+
| Incompatible with trimming | Larger deployment sizes |
34+
| Targets netcoreapp3.1 / net48 | Stuck on legacy TFMs |
35+
| Two languages in one project | Higher maintenance bar |
36+
37+
---
38+
39+
# What We Did
40+
41+
**Pure C# P/Invoke replacement** — same namespace, same API, zero C++.
42+
43+
```xml
44+
<!-- Before -->
45+
<PackageReference Include="Microsoft.Windows.ProjFS" Version="1.2.19351.1" />
46+
47+
<!-- After -->
48+
<ProjectReference Include="ProjectedFSLib.Managed.CSharp.csproj" />
49+
<!-- No other code changes needed -->
50+
```
51+
52+
- `Microsoft.Windows.ProjFS` namespace preserved
53+
- All types, interfaces, delegates, enums — identical signatures
54+
- 16/16 existing tests pass (including symlink tests)
55+
56+
---
57+
58+
# Architecture
59+
60+
```
61+
┌──────────────────────────────────────────┐
62+
│ Your ProjFS Provider (C#) │
63+
│ (VFSForGit, SimpleProvider, etc.) │
64+
├──────────────────────────────────────────┤
65+
│ ProjectedFSLib.Managed.dll (C#) │ ← NEW: Pure C# P/Invoke
66+
│ VirtualizationInstance · Callbacks · │
67+
│ DirectoryEnumerationResults · Utils │
68+
├──────────────────────────────────────────┤
69+
│ ProjectedFSLib.dll (native) │ Windows SDK (unchanged)
70+
├──────────────────────────────────────────┤
71+
│ PrjFlt.sys (kernel) │ Minifilter (unchanged)
72+
└──────────────────────────────────────────┘
73+
```
74+
75+
We replaced **one layer** — the managed wrapper. Everything above and below is unchanged.
76+
77+
---
78+
79+
# Key Technical Decisions
80+
81+
### Struct Layout Verification
82+
Every P/Invoke struct verified against Windows SDK with native `sizeof()`/`offsetof()`:
83+
- `PRJ_PLACEHOLDER_INFO` = **344 bytes** (includes `VariableData[1]` flexible array)
84+
- Missing 8 bytes caused `ERROR_INSUFFICIENT_BUFFER` for all placeholder writes
85+
86+
### String Marshaling
87+
`PCWSTR` fields use `Marshal.StringToHGlobalUni()`, **not** `GCHandle.Alloc + AddrOfPinnedObject`.
88+
Pinning a .NET string gives the object header address, not the character data.
89+
90+
### Notification Mapping Lifetime
91+
ProjFS caches notification mapping pointers — must keep alive for instance lifetime.
92+
93+
---
94+
95+
# What We Learned
96+
97+
## ReFS Does Not Support ProjFS Symlinks
98+
99+
`WritePlaceholderInfo2` (symlink placeholders) fails with `ERROR_NOT_SUPPORTED`
100+
on ReFS volumes.
101+
102+
**Root cause** (via WinDbg + kernel source):
103+
- `PrjFlt.sys``PrjfCreateSymbolicLink``FltCreateFileEx2` with atomic create ECP
104+
- NTFS: ✅ Supports `GUID_ECP_ATOMIC_CREATE` with `IO_REPARSE_TAG_SYMLINK`
105+
- ReFS: ❌ Returns `STATUS_NOT_SUPPORTED` (`0xC00000BB`)
106+
107+
Non-symlink operations work on both NTFS and ReFS.
108+
109+
---
110+
111+
# Results
112+
113+
| Metric | C++/CLI | Pure C# |
114+
|--------|---------|---------|
115+
| Build toolchain | VS 2022 + C++ + C++/CLI | `dotnet build` |
116+
| Runtime deps | VC++ Redist + Ijwhost.dll | None |
117+
| NativeAOT |||
118+
| Trimming || ✅ (`IsAotCompatible=true`) |
119+
| TFMs | net48, netcoreapp3.1 | net8.0, net9.0, net10.0 |
120+
| Tests passing | 16/16 | 16/16 |
121+
| Lines of code | ~4,000 (C++/CLI) | ~1,700 (C#) |
122+
| Source files | 30+ (.h, .cpp, .vcxproj) | 5 (.cs, .csproj) |
123+
124+
---
125+
126+
# Migration Path for Consumers
127+
128+
1. Replace NuGet package reference with project reference
129+
2. Remove `<UseIJWHost>True</UseIJWHost>`
130+
3. Remove VC++ redistributable from installer
131+
4. **No code changes** — same namespace, same API
132+
133+
### For VFSForGit specifically:
134+
This unblocks the complete .NET 10 + NativeAOT migration:
135+
- 3.5x faster startup (53ms → 15ms)
136+
- 2x faster pipe roundtrips (341ms → 179ms)
137+
- 78% faster file reads
138+
- Zero regressions across all benchmarks
139+
140+
---
141+
142+
# Ask
143+
144+
1. **Accept the PR** to replace C++/CLI with pure C# in ProjFS-Managed-API
145+
2. **Publish updated NuGet** targeting modern .NET TFMs
146+
3. **VFSForGit** can then depend on the upstream package instead of vendoring
147+
148+
This is **incremental** — the old C++/CLI package continues to work for existing consumers.
149+
New consumers get NativeAOT support, simpler builds, and fewer dependencies.

0 commit comments

Comments
 (0)