C#/Win32

repository·main·Indexed 25 days ago

https://github.com/microsoft/cswin32

A source generator that provides strongly-typed P/Invoke and COM Interop bindings for C# by projecting metadata from .winmd files. It generates bindings at compilation time, including support for SafeHandle-types and XML documentation linking to Microsoft Learn. CsWin32 supports standard Win32 APIs via NativeMethods.txt, 3rd party native libraries through MSBuild items, and layered composition using C# 14 extension members via the extensionReceiver configuration.

Tokens
10.2K
Snippets
22
Records
37
Agent score
82%

What's inside CsWin32

  1. Overview of C#/Win32 Interop Projection

    main

    C#/Win32 is a source generator that provides strongly-typed P/Invoke and COM Interop projection support for C#. It works by generating bindings at compilation time from .winmd metadata files (specifically supporting Microsoft.Windows.SDK.Win32Metadata).

    Key benefits include:

    • Fast Generation: Interop code is generated quickly during compilation.
    • Developer Friendly: Generates friendly overloads and extensions, including support for SafeHandle-types.
    • Rich Documentation: Generates XML documentation that links directly to official Microsoft Learn documentation.
    • Lightweight: Does not ship bulky assemblies with your application, as the code is source-generated.
  2. Key features of C#/Win32 Interop Projection

    main

    C#/Win32 Interop Projection provides a high-performance, lightweight way to consume Win32 APIs in C#. Key capabilities include:

    • Fast Interop Generation: Generates interop code during the compilation process.
    • Developer-Friendly API: Automatically generates friendly overloads and extensions, including support for SafeHandle-based types to improve memory safety.
    • Integrated Documentation: Generates XML documentation that includes direct links back to official Microsoft Learn documentation.
    • Zero Runtime Overhead: Does not require shipping bulky companion assemblies with your application.
    • Layered Composition: Supports extending a single shared PInvoke static class across multiple assemblies. This allows developers to access a vast range of native APIs through a single unified symbol/class structure.
  3. Use layered composition with `extensionReceiver`

    main

    CsWin32 allows multiple assemblies to contribute to a single, unified static class for native API discovery using C# 14 extension members. This is useful for layered codebases (e.g., a low-level library, a helper library, and an application) where you want all APIs to be accessible through a single symbol like PInvoke.X() regardless of which assembly declared them.

    Roles

    Every assembly using CsWin32 in a layered setup must play one of two roles:

    RoleNativeMethods.json configurationBehavior
    OwnerextensionReceiver is not setEmits a plain partial class <className> with members declared directly on it. This is typically your lowest-level assembly.
    ExtenderextensionReceiver: "<OwnerClassName>" is setEmits a partial class <className> where members are wrapped in an extension(<OwnerClassName>) { ... } block.

    Requirements

    • C# 14 or later: Every project consuming the generated extension members must use C# 14. This is default for .NET 10+. For older versions, set <LangVersion>14</LangVersion> (or Preview/Latest) in your .csproj.
    • Unique Class Names: Each extender must use a className that is different from the owner and all other extenders.
    // Example Owner (MyApp.Core)
    {
      "$schema": "https://aka.ms/CsWin32.schema.json",
      "className": "PInvoke",
      "public": true
    }
    
    // Example Extender (MyApp.Helpers)
    {
      "$schema": "https://aka.ms/CsWin32.schema.json",
      "className": "PInvokeHelpers",
      "extensionReceiver": "PInvoke",
      "public": true
    }
  4. Handle architecture-specific Win32 APIs

    main

    Most Win32 APIs are compatible with any CPU architecture and can be generated in an AnyCPU C# project. However, some APIs vary by architecture or are exclusive to specific architectures.

    If your NativeMethods.txt file contains an architecture-specific API but your project targets AnyCPU, CsWin32 will emit the following warning:

    warning PInvoke005: This API is only available when targeting a specific CPU architecture. AnyCPU cannot generate this API.

    Key Behaviors:

    • Wildcards: If using wildcards (e.g., Kernel32.*), CsWin32 will only generate the APIs compatible with your selected architecture. No warning is emitted if a wildcard causes some APIs to be omitted.
    • Dependencies: An API might be architecture-neutral but depend on an architecture-specific struct (e.g., VirtualQuery depends on MEMORY_BASIC_INFORMATION). In these cases, the method cannot be declared in an AnyCPU target because the required struct cannot be defined for all architectures simultaneously.
  5. Understand constant forwarding in extension blocks

    main

    C# 14 extension blocks cannot contain const or static readonly fields. To support both constant contexts and runtime discovery, CsWin32 emits constants in two forms:

    1. The real const: Located on the extender's host class. Use this for enum initializers, attribute arguments, and switch cases.
    2. A forwarder property: Located on the receiver type. This allows PInvoke.X to work in standard runtime code.

    Constants for typedef structs

    If a constant is typed as a typedef struct (like HWND or HRESULT) and is being added by an extender, CsWin32 attaches the constant to the struct itself using an extension(<Struct>) block. This ensures you can still access it via <Struct>.<Name> (e.g., HRESULT.S_OK), maintaining consistency with how the owner assembly handles that type.

  6. Handle optional out/ref parameters

    main

    For Win32 APIs with [optional, out] or [optional, in, out] parameters, CsWin32 generates two versions of the method:

    1. A version including all ref or out parameters.
    2. A version omitting those optional parameters.

    This allows you to call the API without providing a variable for the optional output if it is not needed.

    // Omitting the optional parameter:
    IsTextUnicode(buffer);
    
    // Passing ref for optional parameter:
    IS_TEXT_UNICODE_RESULT result = default;
    IsTextUnicode(buffer, ref result);
  7. Target a single specific architecture

    main

    To generate architecture-specific APIs, you must change your C# project target from AnyCPU to a specific CPU architecture. This produces an architecture-specific assembly that will only load in processes of that same architecture.

    Add the <PlatformTarget> property to your .csproj file to target a specific architecture, such as x64.

    <PlatformTarget>x64</PlatformTarget>
  8. Activate and use COM classes (e.g., NetFwMgr)

    main

    To interact with COM classes like NetFwMgr, the approach depends on your marshaling settings:

    Built-in COM Interop (Not AOT-compatible)

    Use standard C# casting (e.g., (INetFwMgr)new NetFwMgr()) to perform the equivalent of a native QueryInterface.

    COM Wrappers (AOT-compatible)

    Use ClassName.CreateInstance<IInterface>(). Note that in this mode, properties are replaced by get_ methods (e.g., get_LocalPolicy()), and some types return ComVariant instead of managed objects. Use ComVariantMarshaller.ConvertToManaged(variant) to convert these to managed interfaces.

    // Built-in COM Interop (Not AOT-compatible)
    var fwMgr = (INetFwMgr)new NetFwMgr();
    var authorizedApplications = fwMgr.LocalPolicy.CurrentProfile.AuthorizedApplications;
    var aaObjects = new object[authorizedApplications.Count];
    var applicationsEnum = (IEnumVARIANT)authorizedApplications._NewEnum;
    applicationsEnum.Next((uint)authorizedApplications.Count, aaObjects, out uint fetched);
    foreach (var aaObject in aaObjects)
    {
        var app = (INetFwAuthorizedApplication)aaObject;
        Console.WriteLine("---");
        Console.WriteLine($"Name: {app.Name.ToString()}");
        Console.WriteLine($"Enabled: {(bool)app.Enabled}");
        Console.WriteLine($"Remote Addresses: {app.RemoteAddresses.ToString()}");
        Console.WriteLine($"Scope: {app.Scope}");
        Console.WriteLine($"Process Image Filename: {app.ProcessImageFileName.ToString()}");
        Console.WriteLine($"IP Version: {app.IpVersion}");
    }
    
    // COM Wrappers (AOT-compatible)
    var fwMgr = NetFwMgr.CreateInstance<INetFwMgr>();
    var authorizedApplications = fwMgr.get_LocalPolicy().get_CurrentProfile().get_AuthorizedApplications();
    var aaObjects = new ComVariant[authorizedApplications.get_Count()];
    var applicationsEnum = (IEnumVARIANT)authorizedApplications.get__NewEnum();
    applicationsEnum.Next((uint)authorizedApplications.get_Count(), aaObjects, out uint fetched);
    foreach (var aaObject in aaObjects)
    {
        var app = (INetFwAuthorizedApplication)ComVariantMarshaller.ConvertToManaged(aaObject)!;
    
        Console.WriteLine("---");
        Console.WriteLine($"Name: {app.get_Name().ToString()}");
        Console.WriteLine($"Enabled: {(bool)app.get_Enabled()}");
        Console.WriteLine($"Remote Addresses: {app.get_RemoteAddresses().ToString()}");
        Console.WriteLine($"Scope: {app.get_Scope()}");
        Console.WriteLine($"Process Image Filename: {app.get_ProcessImageFileName().ToString()}");
        Console.WriteLine($"IP Version: {app.get_IpVersion()}");
    
        aaObject.Dispose();
    }
  9. Prerequisites for using CsWin32

    main

    To use the CsWin32 source generator, ensure your environment meets the following requirements:

    • SDK/IDE: .NET 8 SDK or Visual Studio 2022 Update 14 (16.14).
    • WPF Projects: Note that WPF projects have additional specific requirements.
    • C# Language Version: While the generator produces code compatible with .NET Framework, .NET Standard 2.0, and .NET, you must explicitly set your C# language version to at least C# 9 or higher in your project file. Using the latest available C# version is recommended for the best results.
    <LangVersion>9</LangVersion>
  10. Target multiple specific architectures in Visual Studio

    main

    You can support multiple architectures by producing one assembly (.dll) for each target architecture. In Visual Studio, use the Configuration Manager to set up multi-targeting:

    1. Open the Configuration Manager (Build menu -> Configuration Manager).
    2. Under Active solution platform, if your desired architecture (e.g., x64) is not listed, click <New...>.
    3. Select the platform you want to add (e.g., x64).
    4. Uncheck "Create new project platforms" if you only want to target this specific project and not the entire solution.
    5. Click OK.
    6. In the project grid below, ensure the project's own Platform dropdown matches the CPU architecture you just added.
    7. Repeat this process for every architecture you wish to support.

    Once configured, you can use the Active solution platform switcher in the Standard toolbar to toggle between architectures. CsWin32 will generate the appropriate arch-specific APIs for each non-AnyCPU platform.

  11. Package 3rd party metadata for NuGet distribution

    main

    To distribute 3rd party metadata so that consumers can easily generate interop APIs and receive the necessary native binaries, package your metadata into a NuGet package with the following structure:

    • buildTransitive\YourPackageId.props: Contains the MSBuild items to hook the metadata into CsWin32.
    • yournativelib.winmd: The metadata file located in the buildTransitive folder.
    • runtimes\{rid}\yournativelib.dll: The native library files for supported architectures (e.g., win-x86, win-x64, win-arm64).

    Your .props file should use $(MSBuildThisFileDirectory) to ensure paths to the .winmd file are relative to the package content.

    <Project>
      <ItemGroup>
        <ProjectionMetadataWinmd Include="$(MSBuildThisFileDirectory)yournativelib.winmd" />
        <AppLocalAllowedLibraries Include="yournativelib.dll" />
      </ItemGroup>
    </Project>