Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions docfx/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,71 @@ CsWin32 will notice that your project already declares the type and skip generat
Note that if that type is the only thing that references some other generated type, CsWin32 will stop generating that type too.
To keep CsWin32 generating the referred types you need, add them explicitly to `NativeMethods.txt`.

### Owned outputs and scoped native values

`RAIIFree` and compatible `FreeWith` annotations on return values and output parameters
tell CsWin32 which cleanup function owns that particular result. Friendly overloads
can return a SafeHandle for pointer-shaped allocations as well as native handles.
Unannotated pointers and the raw native signatures are unchanged.

CsWin32-generated SafeHandles expose a `DangerousValue` property with the original native type.
For example, `SHGetKnownFolderPath` supplies a `CoTaskMemFreePWSTRSafeHandle` whose
`DangerousValue` is `PWSTR`, although `CoTaskMemFree` accepts `void*`. Reading this
property does not acquire a reference to keep the resource alive. BCL wrappers such as
`SafeFileHandle` retain their existing APIs.

For an owner received from another component or as a parameter, acquire a scoped reference:

```csharp
using Windows.Win32;

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

`Lease()` pairs `DangerousAddRef` and `DangerousRelease` without an additional heap
allocation. Disposal through another alias cannot release the resource until the
lease ends. The lease's `Value` has the same native type as its owner. The lease
uses the `Dispose()` pattern; it does not require `IDisposable`.
Lease helpers require C# 9 or later.

An owner already held in a visible `using` scope can supply `owner.DangerousValue` directly.
This assumes ordinary exclusive ownership: another alias must not explicitly
dispose it during the call. A SafeHandle parameter alone is not such a lifetime
guarantee. Friendly input overloads accept base `SafeHandle` for types with existing
cleanup annotations and keep a reference alive throughout their calls. They also
accept `SafeHandle` for `Windows.Win32.Foundation.HANDLE` even without a type-level
cleanup annotation, preserving this behavior as ownership annotations move to outputs.
This heuristic does not apply to other types such as `HWND`, `PWSTR`, or arbitrary
pointers. Such resources can be passed using their owner's `DangerousValue` or a
lease's `Value`. Set `useSafeHandles` to `false` in `NativeMethods.json` to disable
SafeHandle projection.

The lifetime analyzer reports errors for leases that are not `using` locals, copies,
field storage, parameter passing, returns, and explicit disposal (`PInvoke015`).
It also reports errors for `DangerousValue` or a lease's `Value` without a visible
scope (`PInvoke016`), saving or returning a raw resource (`PInvoke017`), and releasing
an owned resource directly (`PInvoke018`). Use these accessors directly in calls,
or copy their contents into managed data such as a string. String interpolation
such as `$"Path: {path.DangerousValue}"` is allowed inside the owner's or lease's
scope. Interpolation into `FormattableString` or `IFormattable` retains the raw
arguments for later formatting and is not a string copy. C# ref structs remain
copyable: keep these diagnostics enabled to enforce the supported no-copy convention.

Neither a lease nor this analyzer proves that an arbitrary native call does not
retain the resource, transfer ownership, or invalidate it independently. Such
APIs need their own explicit lifetime arrangement.

If an annotated cleanup function needs additional arguments, CsWin32 generates that
function for you to call but cannot automatically construct a SafeHandle: it will
not guess the additional cleanup arguments. A compatible abstract helper, such as
`DeleteTimerQueueTimerSafeHandle`, can be used to supply your own cleanup context
in a derived `ReleaseHandle`. Its typed `DangerousValue` is available inside that
override. You can also request the helper directly in `NativeMethods.txt`.

### Support for trimming, AOT, and/or disabling the runtime marshaler

**NEW!** CsWin32 now has improved support for NativeAOT by generating code from an MSBuild task rather than a source generator so that it can leverage other source generators that support `[LibraryImport]` and others.
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.Windows.CsWin32/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ PInvoke011 | Configuration | Error | SourceGenerator
PInvoke012 | Configuration | Error | SourceGenerator
PInvoke013 | Configuration | Error | SourceGenerator
PInvoke014 | Configuration | Error | SourceGenerator
PInvoke015 | Lifetime | Error | SafeHandleAnalyzer
PInvoke016 | Lifetime | Error | SafeHandleAnalyzer
PInvoke017 | Lifetime | Error | SafeHandleAnalyzer
PInvoke018 | Lifetime | Error | SafeHandleAnalyzer
37 changes: 27 additions & 10 deletions src/Microsoft.Windows.CsWin32/Generator.FriendlyOverloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,26 @@ private static ExpressionSyntax GetIsSpanEmpty(ExpressionSyntax span, bool isRef
private ExpressionSyntax GetIntPtrFromTypeDef(ExpressionSyntax typedefValue, TypeHandleInfo typeDefTypeInfo)
{
ExpressionSyntax intPtrValue = typedefValue;
if (typeDefTypeInfo is PointerTypeHandleInfo)
{
return CastExpression(IntPtrTypeSyntax, typedefValue);
}

if (this.TryGetTypeDefFieldType(typeDefTypeInfo, out TypeHandleInfo? backingType) && backingType is PointerTypeHandleInfo)
{
return CastExpression(IntPtrTypeSyntax, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, typedefValue, IdentifierName("Value")));
}

if (this.TryGetTypeDefFieldType(typeDefTypeInfo, out TypeHandleInfo? returnTypeField) && returnTypeField is PrimitiveTypeHandleInfo primitiveReturnField)
{
switch (primitiveReturnField.PrimitiveTypeCode)
{
case PrimitiveTypeCode.UInt32:
// (IntPtr)result.Value;
intPtrValue = CastExpression(IntPtrTypeSyntax, MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, typedefValue, IdentifierName("Value")));
intPtrValue = UncheckedExpression(CastExpression(
IntPtrTypeSyntax,
CastExpression(IdentifierName("nint"), CastExpression(
IdentifierName("nuint"),
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, typedefValue, IdentifierName("Value"))))));
break;
case PrimitiveTypeCode.UIntPtr:
// unchecked((IntPtr)(long)(ulong)result.Value)
Expand Down Expand Up @@ -100,7 +113,7 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverloads(MethodDefi

if (this.Options.FriendlyOverloads.IncludePointerOverloads && improvePointersToSpansAndRefs && bookkeeping.NumSpanByteParameters > 0)
{
// If we could use Span and _did_ use span Span and the pointer overloads were requested, then Generate overloads that use pointer types instead of Span<byte>/ReadOnlySpan<byte>.
// Also offer pointer-based overloads for byte buffers.
foreach (MethodDeclarationSyntax method in this.DeclareFriendlyOverload(methodDefinition, externMethodDeclaration, declaringTypeName, overloadOf, helperMethodsAdded, avoidWinmdRootAlias, improvePointersToSpansAndRefs: false, omitOptionalParams: false))
{
yield return method;
Expand Down Expand Up @@ -556,16 +569,16 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
bool hasOut = externParam.Modifiers.Any(SyntaxKind.OutKeyword);
arguments[paramIndex] = arguments[paramIndex].WithRefKindKeyword(TokenWithSpace(hasOut ? SyntaxKind.OutKeyword : SyntaxKind.RefKeyword));
}
else if (isOut && !isIn && !isReleaseMethod && parameterTypeInfo is PointerTypeHandleInfo { ElementType: HandleTypeHandleInfo pointedElementInfo } &&
pointedElementInfo.Generator.TryGetHandleReleaseMethod(pointedElementInfo.Handle, paramAttributes, out string? outReleaseMethod) && !this.Reader.StringComparer.Equals(methodDefinition.Name, outReleaseMethod) &&
else if (isOut && !isIn && !isReleaseMethod && parameterTypeInfo is PointerTypeHandleInfo { ElementType: TypeHandleInfo pointedElementInfo } &&
this.TryGetResourceReleaseMethod(pointedElementInfo, paramAttributes, out string? outReleaseMethod) && !this.Reader.StringComparer.Equals(methodDefinition.Name, outReleaseMethod) &&
(memorySize is null) && !isArray)
{
signatureChanged = true;

IdentifierNameSyntax localName = IdentifierName(externParam.Identifier.ValueText + "Local");

// NOTE: We don't handle scenarios where the parameter is [MemorySize] annotated (e.g. EnumProcessModules) or [NativeArrayInfo] (e.g. ITypeInfo.GetNames)
if (this.RequestSafeHandle(outReleaseMethod) is TypeSyntax safeHandleType)
if (this.RequestSafeHandle(outReleaseMethod, pointedElementInfo) is TypeSyntax safeHandleType)
{
// out SafeHandle
parameters[paramIndex] = externParam
Expand Down Expand Up @@ -641,7 +654,12 @@ private IEnumerable<MethodDeclarationSyntax> DeclareFriendlyOverload(
arguments[paramIndex] = Argument(localName);
}
}
else if (this.options.UseSafeHandles && isIn && !isOut && !isReleaseMethod && parameterTypeInfo is HandleTypeHandleInfo parameterHandleTypeInfo && this.TryGetHandleReleaseMethod(parameterHandleTypeInfo.Handle, paramAttributes, out string? releaseMethod) && !this.Reader.StringComparer.Equals(methodDefinition.Name, releaseMethod)
else if (this.options.UseSafeHandles && isIn && !isOut && !isReleaseMethod && parameterTypeInfo is HandleTypeHandleInfo parameterHandleTypeInfo
&& (parameterHandleTypeInfo.Generator.TryGetHandleReleaseMethod(parameterHandleTypeInfo.Handle, paramAttributes, out _)
|| (parameterHandleTypeInfo.Generator.TryGetTypeDefHandle(parameterHandleTypeInfo.Handle, out QualifiedTypeDefinitionHandle inputHandle)
&& inputHandle.Resolve() is QualifiedTypeDefinition inputHandleDefinition
&& inputHandleDefinition.Reader.StringComparer.Equals(inputHandleDefinition.Definition.Namespace, "Windows.Win32.Foundation")
&& inputHandleDefinition.Reader.StringComparer.Equals(inputHandleDefinition.Definition.Name, "HANDLE")))
&& !(this.TryGetTypeDefFieldType(parameterHandleTypeInfo, out TypeHandleInfo? fieldType) && !this.IsSafeHandleCompatibleTypeDefFieldType(fieldType)))
{
IdentifierNameSyntax typeDefHandleName = IdentifierName(externParam.Identifier.ValueText + "Local");
Expand Down Expand Up @@ -1385,9 +1403,8 @@ bool TryHandleCountParam(TypeSyntax elementType, bool nullableSource)
signatureChanged = true;
}

TypeSyntax? returnSafeHandleType = originalSignature.ReturnType is HandleTypeHandleInfo returnTypeHandleInfo
&& returnTypeHandleInfo.Generator.TryGetHandleReleaseMethod(returnTypeHandleInfo.Handle, returnTypeAttributes, out string? returnReleaseMethod)
? this.RequestSafeHandle(returnReleaseMethod) : null;
TypeSyntax? returnSafeHandleType = this.TryGetResourceReleaseMethod(originalSignature.ReturnType, returnTypeAttributes, out string? returnReleaseMethod)
? this.RequestSafeHandle(returnReleaseMethod, originalSignature.ReturnType) : null;

IdentifierNameSyntax resultLocal = IdentifierName("__result");

Expand Down
21 changes: 15 additions & 6 deletions src/Microsoft.Windows.CsWin32/Generator.GeneratedCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private class GeneratedCode

private readonly Dictionary<(string Namespace, string Name), MemberDeclarationSyntax> inlineArrays = new();

private readonly Dictionary<string, TypeSyntax?> releaseMethodsWithSafeHandleTypesGenerating = new();
private readonly Dictionary<string, (TypeSyntax? Type, bool CanConstruct)> releaseMethodsWithSafeHandleTypesGenerating = new();

private readonly List<MethodDeclarationSyntax> inlineArrayIndexerExtensionsMembers = new();

Expand Down Expand Up @@ -380,17 +380,26 @@ internal string GenerateCustomTypeMarshaller(string marshallerName, Func<CustomM
return marshallerType.QualifiedName;
}

internal bool TryGetSafeHandleForReleaseMethod(string releaseMethod, out TypeSyntax? safeHandleType)
internal bool TryGetSafeHandleForReleaseMethod(string releaseMethod, out TypeSyntax? safeHandleType, out bool canConstruct)
{
return this.releaseMethodsWithSafeHandleTypesGenerating.TryGetValue(releaseMethod, out safeHandleType)
|| this.parent?.releaseMethodsWithSafeHandleTypesGenerating.TryGetValue(releaseMethod, out safeHandleType) is true;
if (this.releaseMethodsWithSafeHandleTypesGenerating.TryGetValue(releaseMethod, out var info)
|| this.parent?.releaseMethodsWithSafeHandleTypesGenerating.TryGetValue(releaseMethod, out info) is true)
{
safeHandleType = info.Type;
canConstruct = info.CanConstruct;
return true;
}

safeHandleType = null;
canConstruct = false;
return false;
}

internal void AddSafeHandleNameForReleaseMethod(string releaseMethod, TypeSyntax? safeHandleType)
internal void AddSafeHandleNameForReleaseMethod(string releaseMethod, TypeSyntax? safeHandleType, bool canConstruct)
{
this.ThrowIfNotGenerating();

this.releaseMethodsWithSafeHandleTypesGenerating.Add(releaseMethod, safeHandleType);
this.releaseMethodsWithSafeHandleTypesGenerating.Add(releaseMethod, (safeHandleType, canConstruct));
}

private static void Commit<TKey, TValue>(Dictionary<TKey, TValue> source, Dictionary<TKey, TValue>? target)
Expand Down
Loading
Loading