Skip to content

Generate SafeHandle for RAIIFree-annotated parameters, and make getting the Value from a SafeHandle easier (but still safe) - #1844

Open
Jevan Saks (jevansaks) wants to merge 6 commits into
mainfrom
jevansaks-safehandle-borrowing
Open

Jevan Saks (jevansaks) wants to merge 6 commits into
mainfrom
jevansaks-safehandle-borrowing

Conversation

@jevansaks

@jevansaks Jevan Saks (jevansaks) commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Summary

Win32metadata is moving cleanup annotations from handle types to the individual return values and output parameters that actually own a resource. This extends CsWin32's support to pointer-shaped resources, accepting both RAIIFree and compatible FreeWith annotations during the transition.

Raw native signatures and existing generation options remain available. An ownership annotation changes the friendly output, not every occurrence of its native type: an owned environment block can become a SafeHandle without turning every void* parameter into one.

Changes

Generated SafeHandles expose a correctly typed DangerousValue. The type comes from the annotated value, not the cleanup function's parameter: HINTERNET for a WinHTTP handle, PWSTR for an allocated path, or void* for an environment block. Reading it does not acquire a reference to keep the resource alive; a lease exposes the same native type through Value after acquiring that reference. Wrapper identity includes the cleanup function and original native type. Existing BCL mappings such as SafeFileHandle retain their APIs. There are no implicit conversions.

Keep HANDLE inputs convenient without treating every native type as an owner. Existing annotation-driven SafeHandle inputs remain supported. Independently of type-level cleanup annotations, friendly inputs of Windows.Win32.Foundation.HANDLE accept SafeHandle and keep a reference alive during the call. This preserves HANDLE input behavior when win32metadata moves ownership annotations to outputs. We do not apply that heuristic to HWND, PWSTR, other typedefs, or arbitrary pointers; callers can pass those through DangerousValue or a lease's Value. There is no additional legacy overload pass. Callers can set useSafeHandles to false in NativeMethods.json to opt out of SafeHandle projection.

A scoped helper makes shared owners easier to use. SafeHandle provides DangerousAddRef and DangerousRelease, but no helper that pairs them in a scope. A generated .Lease() does that without an additional heap allocation:

static bool PathExists(CoTaskMemFreePWSTRSafeHandle path)
{
    using var lease = path.Lease();
    return PInvoke.PathFileExists(lease.Value);
}

The lease keeps the original SafeHandle from releasing its resource until scope exit. Its Value is PWSTR, so it can be passed to a PCWSTR parameter using the existing native string conversion, without casting DangerousGetHandle()'s nint. An exclusively controlled owner already held in a visible using can supply its own DangerousValue directly instead. Leases use pattern-based disposal and require C# 9 or later.

An analyzer catches easy-to-miss lifetime mistakes. Require each lease to be a using local; report errors when it is copied, put in a field, passed to another method, returned, or explicitly disposed. For the owner's DangerousValue or the lease's Value, require a visible lifetime scope and report errors for saving or returning the raw resource, or passing it to its cleanup function. Copying into a string, including $"Path: {path.DangerousValue}", is allowed within the scope; FormattableString and IFormattable retain their raw arguments for later formatting instead. These diagnostics are errors by default. C# ref structs are still copyable: this is an analyzer-enforced usage contract, not a language-level ownership guarantee. It does not prove that an arbitrary callee will not retain the value or consume the resource.

Annotated cleanup functions are generated implicitly. They remain callable even when SafeHandle projection is disabled or the annotated resource cannot be wrapped automatically. If cleanup needs additional arguments, a compatible abstract, typed helper lets callers supply the context in a derived ReleaseHandle. Such helpers can also be requested directly, for example DeleteTimerQueueTimerSafeHandle. CsWin32 does not guess those arguments or automatically construct an abstract owner.

Existing metadata

We looked through the existing return/out annotations and found two cases that need correction:

These recording DCs must first be completed with CloseMetaFile or CloseEnhMetaFile; those functions produce a different resource that then needs deletion. Their raw signatures remain available rather than constructing an incompatible owner.

SHGetKnownFolderPath was the most interesting usable example, so we focused on it for proving this out. Its existing FreeWith("CoTaskMemFree") annotates an allocated PWSTR, while the cleanup method accepts void*. Any returned allocation is wrapped regardless of HRESULT success or failure. Once it is attached to the SafeHandle, finalization remains the fallback if the caller throws before establishing a using.

Coverage

Tests check the signatures users receive, compile realistic call patterns, and verify analyzer diagnostics. Coverage includes both annotation forms, typed handles and pointers, shared cleanup functions, incompatible annotations, implicit cleanup dependencies, caller-defined cleanup helpers, existing options and BCL mappings, older target frameworks, and nullable-enabled projects. Custom metadata is maintained as a hand-authored IL source and an assembled WinMD fixture, not emitted at test runtime. The usage guide is in Getting Started.

Runtime coverage observes reference balance on exceptional exits, owner disposal while leased, rejected acquisition after disposal, cleanup of failure outputs, unsigned native values, and zero allocated bytes for 1,000 warmed-up leases. New tests do not assert literal generated implementation text.

Support compatible RAIIFree and FreeWith return/out annotations, preserve native resource types, and add allocation-free scoped leases with lifetime diagnostics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93
@jevansaks Jevan Saks (jevansaks) changed the title Project contextual native ownership with typed SafeHandle leases Generate SafeHandle for RAIIFree-annotated parameters, and make getting the Value from a SafeHandle easier (while still safe) Oct 8, 2026
@jevansaks Jevan Saks (jevansaks) changed the title Generate SafeHandle for RAIIFree-annotated parameters, and make getting the Value from a SafeHandle easier (while still safe) Generate SafeHandle for RAIIFree-annotated parameters, and make getting the Value from a SafeHandle easier (but still safe) Oct 8, 2026
Jevan Saks (jevansaks) and others added 2 commits October 8, 2026 14:09
Recognize immediate interpolated string copies while rejecting delayed formatting that retains native arguments. Make all generated SafeHandle lifetime diagnostics errors by default.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93
@jevansaks
Jevan Saks (jevansaks) marked this pull request as ready for review October 8, 2026 22:09
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93
Comment thread test/Microsoft.Windows.CsWin32.Tests/HandleTests.cs Outdated
Replace runtime-emitted metadata with an authored IL fixture and move the ownership guide to Getting Started.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93
Resolve cleanup functions in shared signature generation so native and COM outputs expose cleanup even with SafeHandles disabled.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 06478ace-c5d7-418a-9386-f31b4d379d93

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant