Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CsWin32 usage sample #7929

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
23 changes: 21 additions & 2 deletions Microsoft.Dotnet.Wpf.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28815.4
# Visual Studio Version 17
VisualStudioVersion = 17.7.33808.371
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Xaml", "src\Microsoft.DotNet.Wpf\src\System.Xaml\System.Xaml.csproj", "{9AC36357-34B7-40A1-95CA-FE9F46D089A7}"
EndProject
Expand Down Expand Up @@ -239,6 +239,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OSVersionHelper", "src\Micr
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PresentationNative", "src\Microsoft.DotNet.Wpf\redist\PresentationNative\PresentationNative.vcxproj", "{AF9084C3-BF37-4A56-A851-89F3BAE731B3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Windows.Primitives", "src\Microsoft.DotNet.Wpf\src\System.Windows.Primitives\System.Windows.Primitives.csproj", "{EAC88D19-F321-4D89-866D-0E7DE46E24EB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1910,6 +1912,22 @@ Global
{AF9084C3-BF37-4A56-A851-89F3BAE731B3}.Release|x86.ActiveCfg = Release|Win32
{AF9084C3-BF37-4A56-A851-89F3BAE731B3}.Release|x86.Build.0 = Release|Win32
{AF9084C3-BF37-4A56-A851-89F3BAE731B3}.Release|x86.Deploy.0 = Release|Win32
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|ARM64.Build.0 = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|x64.ActiveCfg = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|x64.Build.0 = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|x86.ActiveCfg = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Debug|x86.Build.0 = Debug|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|Any CPU.Build.0 = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|ARM64.ActiveCfg = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|ARM64.Build.0 = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|x64.ActiveCfg = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|x64.Build.0 = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|x86.ActiveCfg = Release|Any CPU
{EAC88D19-F321-4D89-866D-0E7DE46E24EB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -2026,6 +2044,7 @@ Global
{7EE0E965-7DA4-4A94-9441-801E8D2CC1CD} = {4557C5C6-10B1-475C-8279-5511955D1C29}
{3801B5AE-6871-4A72-B400-1F6ABCBF9045} = {7EE0E965-7DA4-4A94-9441-801E8D2CC1CD}
{AF9084C3-BF37-4A56-A851-89F3BAE731B3} = {DDED00A7-24FD-4AEF-B264-2150F0E59B4D}
{EAC88D19-F321-4D89-866D-0E7DE46E24EB} = {2EE4A2DA-70B3-4767-9D18-618DA0FE3105}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B4340004-DAC0-497D-B69D-CFA7CD93F567}
Expand Down
2 changes: 2 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<add key="dotnet7-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7-transport/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
<add key="dotnet8-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json" />
<!-- CsWin32 dailies -->
<add key="winsdk" value="https://pkgs.dev.azure.com/azure-public/winsdk/_packaging/CI/nuget/v3/index.json" />
</packageSources>
<disabledPackageSources />
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("WindowsBase, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false,
"useSafeHandles": false,
"comInterop": {
"preserveSigMethods": [
"*"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
AC_SRC_*
BI_COMPRESSION
BITMAPINFO
CLSID_WICImagingFactory
CoCreateInstance
CoInitialize
CreateCompatibleDC
CreateDIBSection
CreateWindowEx
DefWindowProc
DeleteObject
DestroyWindow
DwmIsCompositionEnabled
E_NOINTERFACE
E_POINTER
GetDC
GetSystemMetrics
GUID_WICPixelFormat32bppPBGRA
IsWindow
IWICImagingFactory
RegisterClassEx
ReleaseDC
S_OK
SelectObject
UnregisterClass
UpdateLayeredWindow
WIN32_ERROR
WINCODEC_SDK_VERSION1
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>System.Windows.Primitives</AssemblyName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CLSCompliant>false</CLSCompliant>
<Nullable>enable</Nullable>
<Platforms>AnyCPU</Platforms>

<!--
We align casing and naming with Win32 API. As such some types have all lower case names, which
in theory may conflict with new C# keywords in the future. Our types here are internal so end
users won't be impacted. If some name becomes difficult to adapt to with the @ symbol we can
cross that bridge when we hit it (if ever).
-->
<NoWarn>$(NoWarn);CS8981;CS3016</NoWarn>
<!--
We don't care about CLS compliance since everything here is internal and we want to match native types.
-->
<NoWarn>$(NoWarn);CS3016</NoWarn>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Applying [assembly: CLSCompliant(false)] will disable this warning and disable CLS compliance analysis which significantly improves compiler performance.

<Deterministic>true</Deterministic>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<UsePublicApiAnalyzers>true</UsePublicApiAnalyzers>
<RootNamespace />
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.5-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Comment on lines +28 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.5-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.5-beta" PrivateAssets="all" />

<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Runtime.CompilerServices;
using Windows.Win32.System.Com;

namespace Windows.Win32.Foundation;

/// <summary>
/// Lifetime management struct for a native COM pointer. Meant to be utilized in a <see langword="using"/> statement
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
/// to ensure <see cref="IUnknown.Release"/> is called when going out of scope with the using.
/// </summary>
/// <remarks>
/// <para>
/// This struct has implicit conversions to T** and void** so it can be passed directly to out methods.
/// For example:
/// </para>
/// <code>
/// using ComScope&lt;IUnknown&gt; unknown = new(null);
/// comObject-&gt;QueryInterface(&amp;iid, unknown);
/// </code>
/// <para>
/// Take care to NOT make copies of the struct to avoid accidental over-release.
/// </para>
Comment on lines +23 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Roslyn's [NonCopyable] can be used for this

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide more information on where this attribute is defined and which analyzer package needs to be installed to leverage it?

Copy link
Member

@sharwell sharwell Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of Roslyn.Diagnostics.Analyzers today:
https:/dotnet/roslyn-analyzers/blob/main/src/Roslyn.Diagnostics.Analyzers/Core/AbstractDoNotCopyValue.cs

It's fairly unforgiving. Any use of a non-copyable value which doesn't match a known non-copying value will produce a diagnostic. It also relies on IOperation, so I'm not sure if it handles all cases where a piece of syntax doesn't have IOperation support. In practice, it's worked well for us with minimal hassle.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it, thanks.

/// </remarks>
/// <typeparam name="T">
/// This should be one of the struct COM definitions as generated by CsWin32. Ideally we'd constrain to
/// <see cref="IUnknown.Interface"/> or some other interface tag to enforce that this is being used around
/// a struct that is actually a COM wrapper.
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
/// </typeparam>
internal readonly unsafe ref struct ComScope<T> where T : unmanaged, IComIID
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
{
// Keeping internal as nint allows us to use Unsafe methods to get significantly better generated code.
private readonly nint _value;
public T* Value => (T*)_value;
public IUnknown* AsUnknown => (IUnknown*)_value;

public ComScope(T* value) => _value = (nint)value;

// Can't add an operator for IUnknown* as we have ComScope<IUnknown>.

public static implicit operator T*(in ComScope<T> scope) => (T*)scope._value;

public static implicit operator void*(in ComScope<T> scope) => (void*)scope._value;

public static implicit operator nint(in ComScope<T> scope) => scope._value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator T**(in ComScope<T> scope) => (T**)Unsafe.AsPointer(ref Unsafe.AsRef(scope._value));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator void**(in ComScope<T> scope) => (void**)Unsafe.AsPointer(ref Unsafe.AsRef(scope._value));

public bool IsNull => _value == 0;

public ComScope<TTo> TryQuery<TTo>(out HRESULT hr) where TTo : unmanaged, IComIID
{
ComScope<TTo> scope = new(null);
hr = ((IUnknown*)Value)->QueryInterface(IID.Get<TTo>(), scope);
return scope;
}

/// <summary>
/// Attempt to create a <see cref="ComScope{T}"/> from the given COM interface.
/// </summary>
public static ComScope<T> TryQueryFrom<TFrom>(TFrom* from, out HRESULT hr) where TFrom : unmanaged, IComIID
{
ComScope<T> scope = new(null);
hr = from is null ? HRESULT.E_POINTER : ((IUnknown*)from)->QueryInterface(IID.Get<T>(), scope);
return scope;
}

/// <summary>
/// Create a <see cref="ComScope{T}"/> from the given COM interface. Throws on failure.
/// </summary>
public static ComScope<T> QueryFrom<TFrom>(TFrom* from) where TFrom : unmanaged, IComIID
{
if (from is null)
{
HRESULT.E_POINTER.ThrowOnFailure();
}

ComScope<T> scope = new(null);
((IUnknown*)from)->QueryInterface(IID.Get<T>(), scope).ThrowOnFailure();
return scope;
}

public void Dispose()
{
IUnknown* unknown = (IUnknown*)_value;

// Really want this to be null after disposal to avoid double releases, but we also want
// to maintain the readonly state of the struct to allow passing as `in` without creating implicit
// copies (which would break the T** and void** operators).
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
*(void**)this = null;
if (unknown is not null)
{
unknown->Release();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Windows.Win32.Foundation;

internal static unsafe class IID
{
private static ref readonly Guid IID_NULL
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ReadOnlySpan<byte> data = new byte[]
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

return ref Unsafe.As<byte, Guid>(ref MemoryMarshal.GetReference(data));
}
}

// We cast away the "readonly" here as there is no way to communicate that through a pointer and
// Marshal APIs take the Guid as ref. Even though none of our usages actually change the state.

/// <summary>
/// Gets a pointer to the IID <see cref="Guid"/> for the given <typeparamref name="T"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid* Get<T>() where T : unmanaged, IComIID
=> (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid));

/// <summary>
/// Gets a reference to the IID <see cref="Guid"/> for the given <typeparamref name="T"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref Guid GetRef<T>() where T : unmanaged, IComIID
=> ref Unsafe.AsRef(in T.Guid);

/// <summary>
/// Empty <see cref="Guid"/> (GUID_NULL in docs).
/// </summary>
public static Guid* NULL() => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID_NULL));
}
Loading