Repository navigation
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
Conversation
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
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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
RAIIFreeand compatibleFreeWithannotations 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:HINTERNETfor a WinHTTP handle,PWSTRfor an allocated path, orvoid*for an environment block. Reading it does not acquire a reference to keep the resource alive; a lease exposes the same native type throughValueafter acquiring that reference. Wrapper identity includes the cleanup function and original native type. Existing BCL mappings such asSafeFileHandleretain 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.HANDLEaccept 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 throughDangerousValueor a lease'sValue. There is no additional legacy overload pass. Callers can setuseSafeHandlestofalseinNativeMethods.jsonto opt out of SafeHandle projection.A scoped helper makes shared owners easier to use.
SafeHandleprovidesDangerousAddRefandDangerousRelease, but no helper that pairs them in a scope. A generated.Lease()does that without an additional heap allocation:The lease keeps the original SafeHandle from releasing its resource until scope exit. Its
ValueisPWSTR, so it can be passed to aPCWSTRparameter using the existing native string conversion, without castingDangerousGetHandle()'snint. An exclusively controlled owner already held in a visibleusingcan supply its ownDangerousValuedirectly 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
usinglocal; report errors when it is copied, put in a field, passed to another method, returned, or explicitly disposed. For the owner'sDangerousValueor the lease'sValue, 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;FormattableStringandIFormattableretain 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 exampleDeleteTimerQueueTimerSafeHandle. 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:
CreateMetaFileA/WreturnHDC, but nameDeleteMetaFile, which takesHMETAFILE.CreateEnhMetaFileA/WreturnHDC, but nameDeleteEnhMetaFile, which takesHENHMETAFILE.These recording DCs must first be completed with
CloseMetaFileorCloseEnhMetaFile; those functions produce a different resource that then needs deletion. Their raw signatures remain available rather than constructing an incompatible owner.SHGetKnownFolderPathwas the most interesting usable example, so we focused on it for proving this out. Its existingFreeWith("CoTaskMemFree")annotates an allocatedPWSTR, while the cleanup method acceptsvoid*. 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 ausing.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.