Skip to content

Add scaffolding for C# projection of the SDK#40212

Open
florelis wants to merge 12 commits intomicrosoft:feature/wsl-for-appsfrom
florelis:csProjection
Open

Add scaffolding for C# projection of the SDK#40212
florelis wants to merge 12 commits intomicrosoft:feature/wsl-for-appsfrom
florelis:csProjection

Conversation

@florelis
Copy link
Copy Markdown
Member

@florelis florelis commented Apr 16, 2026

Summary of the Pull Request

This adds the scaffolding for building a C# projection of the WSLC SDK. The projection is created by first making a C++/WinRT wrapper over the base API, and then using CsWinRT to project that to C#. This PR includes only the changes to the build process, and a projection of a few types as a proof of concept.

PR Checklist

  • Closes: Link to issue #xxx
  • Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • Tests: Added/updated if needed and all pass
  • Localization: All end user facing strings can be localized
  • Dev docs: Added/updated if needed
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

This PR builds on top of #40182, so I'll mark it as a draft until that one is merged.

The implementation is made with C++/WinRT because C++ is better suited than C# for the kind of calls we need to make to use the SDK (like getting function outputs by passing in pointers), and WinRT allows projecting to C# without us having to directly deal with PInvoke interop.

The C++/WinRT is built on top of the SDK. This unfortunately means that a consumer will need to have 3 DLLs: the SDK, the C++/WinRT layer, and the C# projection. As an improvement, we could build the C++/WinRT layer on top of the service API to avoid going through the SDK.

At the C# layer no code is needed, as everything is generated by CsWinRT. If the projection proves to be unsuitable (for example, if we don't like the types it uses), we can mark the CsWinRT projection as private (CsWinRTIncludesPrivate) and create a thin C# layer on top of it that won't have to deal with interop at all.

Validation Steps Performed

Manually tested building a local NuGet package and using it in a project like this:

Project .csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
	  <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <TargetFramework>net10.0-windows10.0.22000.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.WSL.Containers" Version="2.7.0-build688" />
  </ItemGroup>
</Project>

Program.cs:

using Microsoft.WSL.Containers;
var ss = new SessionSettings("session", @"C:\storage\path");
ss.MemoryMb = 1024;
ss.CpuCount = 5;
ss.FeatureFlags = SessionFeatureFlags.EnableGpu;
ss.VhdRequirements = new VhdRequirements("vhd", 1024, VhdType.Dynamic);
var s = Session.Create(ss);

Copilot AI review requested due to automatic review settings April 16, 2026 22:56

This comment was marked as outdated.

Copilot AI review requested due to automatic review settings April 17, 2026 23:09

This comment was marked as resolved.

Copy link
Copy Markdown
Member

@JohnMcPMS JohnMcPMS left a comment

Choose a reason for hiding this comment

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

If we are going with C++/WinRT, I would prefer we do the winget model and compile it into the WSLC SDK dll. That would mean one less binary is required and remove the potential for consumers to mix and match. What that looks like in cmake 🤷

Comment thread src/windows/WslcSDK/winrt/SessionSettings.cpp
@florelis
Copy link
Copy Markdown
Member Author

If we are going with C++/WinRT, I would prefer we do the winget model and compile it into the WSLC SDK dll. That would mean one less binary is required and remove the potential for consumers to mix and match. What that looks like in cmake 🤷

Sure. I just didn't want to take the time to understand if there would be any conflict from exposing both from the same DLL or how to set it up. If we do go with this implementation, I can add it later, as we probably care more about covering the API surface than the DLL detail.

Copilot AI review requested due to automatic review settings April 21, 2026 00:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 7 comments.

Comment on lines +17 to +22
constexpr uint32_t s_DefaultCPUCount = 2;
constexpr uint32_t s_DefaultMemoryMB = 2000;
// Maximum value per use with HVSOCKET_CONNECT_TIMEOUT_MAX
constexpr ULONG s_DefaultBootTimeout = 300000;
// Default to 1 GB
constexpr UINT64 s_DefaultStorageSize = 1000 * 1000 * 1000;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Defaults.h uses Windows typedefs (ULONG, UINT64) but doesn’t include a header that defines them, making it sensitive to include order (and harder to reuse from non-Windows-header contexts). Consider switching these constants to fixed-width types (uint32_t/uint64_t) or adding the minimal required include so the header is self-contained.

Suggested change
constexpr uint32_t s_DefaultCPUCount = 2;
constexpr uint32_t s_DefaultMemoryMB = 2000;
// Maximum value per use with HVSOCKET_CONNECT_TIMEOUT_MAX
constexpr ULONG s_DefaultBootTimeout = 300000;
// Default to 1 GB
constexpr UINT64 s_DefaultStorageSize = 1000 * 1000 * 1000;
#include <cstdint>
constexpr uint32_t s_DefaultCPUCount = 2;
constexpr uint32_t s_DefaultMemoryMB = 2000;
// Maximum value per use with HVSOCKET_CONNECT_TIMEOUT_MAX
constexpr uint32_t s_DefaultBootTimeout = 300000;
// Default to 1 GB
constexpr uint64_t s_DefaultStorageSize = 1000 * 1000 * 1000;

Copilot uses AI. Check for mistakes.
Comment thread cmake/FindCSharp.cmake
Comment thread src/windows/WslcSDK/csharp/CMakeLists.txt

void implementation::SessionSettings::CpuCount(uint32_t value)
{
m_cpuCount = value;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

In the WinRT wrapper setters, setting CpuCount to 0 will reset the underlying SDK value to the default (see WslcSetSessionSettingsCpuCount), but the wrapper caches and returns 0 via CpuCount() afterward. This makes the projected object observably inconsistent. Consider updating m_cpuCount to s_DefaultCPUCount when the caller passes 0 (or otherwise syncing the cached value to what the SDK actually stores).

Suggested change
m_cpuCount = value;
m_cpuCount = (value == 0) ? s_DefaultCPUCount : value;

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +57
m_memoryMb = value;
winrt::check_hresult(WslcSetSessionSettingsMemory(&m_sessionSettings, m_memoryMb));
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Similarly, setting MemoryMb to 0 resets the underlying SDK value to the default (WslcSetSessionSettingsMemory treats 0 as 'use default'), but the wrapper caches 0 and will return 0 from MemoryMb(). Update the cached value to s_DefaultMemoryMB when the input is 0 (or otherwise reflect the SDK-stored value).

Suggested change
m_memoryMb = value;
winrt::check_hresult(WslcSetSessionSettingsMemory(&m_sessionSettings, m_memoryMb));
winrt::check_hresult(WslcSetSessionSettingsMemory(&m_sessionSettings, value));
m_memoryMb = (value == 0) ? s_DefaultMemoryMB : value;

Copilot uses AI. Check for mistakes.

void implementation::SessionSettings::TimeoutMS(uint32_t value)
{
m_timeoutMS = value;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Setting TimeoutMS to 0 resets the underlying SDK value to the default (WslcSetSessionSettingsTimeout treats 0 as 'use default'), but the wrapper caches 0 and returns 0 from TimeoutMS(). Consider updating m_timeoutMS to s_DefaultBootTimeout when the caller passes 0 (or otherwise syncing the cached value with the SDK).

Suggested change
m_timeoutMS = value;
m_timeoutMS = (value == 0) ? s_DefaultBootTimeout : value;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 21, 2026 19:06
@florelis florelis marked this pull request as ready for review April 21, 2026 19:06
@florelis florelis requested a review from a team as a code owner April 21, 2026 19:06
@florelis
Copy link
Copy Markdown
Member Author

Note: I want to keep this PR scoped to the concept of using C++/WinRT to create the C# projection and the changes needed to the build process. For comments about the details of the C++/WinRT implementation (e.g., accepting 0 to reset to the default), I'll make note of them but I'll make the changes in a future PR focused on the implementation and including more of the API surface.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 27 changed files in this pull request and generated 3 comments.

Comment on lines +20 to +32
auto _hr = (hr); \
if (FAILED(_hr)) \
{ \
auto _msg = (msg).get(); \
if (!_msg) \
{ \
throw winrt::hresult_error(_hr); \
} \
else \
{ \
throw winrt::hresult_error(_hr, winrt::to_hstring(_msg)); \
} \
} \
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The custom THROW_MSG_IF_FAILED macro uses a raw if (FAILED(hr)) check and throws directly. This diverges from the repo’s standard HRESULT handling (WIL THROW_* / RETURN_* helpers) and makes error handling inconsistent with the rest of the SDK code. Please replace this macro with the established WIL error helpers (while still incorporating the optional COM error message) or otherwise route failures through the same centralized helpers used elsewhere in the repo.

Suggested change
auto _hr = (hr); \
if (FAILED(_hr)) \
{ \
auto _msg = (msg).get(); \
if (!_msg) \
{ \
throw winrt::hresult_error(_hr); \
} \
else \
{ \
throw winrt::hresult_error(_hr, winrt::to_hstring(_msg)); \
} \
} \
const auto _hr = (hr); \
auto _msg = (msg).get(); \
if (_msg) \
{ \
THROW_HR_IF_MSG(_hr, FAILED(_hr), "%ls", _msg); \
} \
else \
{ \
THROW_IF_FAILED(_hr); \
} \

Copilot uses AI. Check for mistakes.
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />
<ItemGroup Condition="'$(_wslcPlatform)' != ''">
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

When RuntimeIdentifier is not set, this targets file only adds the native DLLs to ReferenceCopyLocalPaths, but it does not add a reference to the managed projection assembly (the package places it under runtimes/win-*/lib/<tfm>/wslcsdkcs.dll). In that configuration NuGet restore typically won’t select RID-specific runtimes/*/lib assets, so consumers may fail to compile because Microsoft.WSL.Containers isn’t referenced at all. Consider adding a non-RID lib/<tfm>/ (or ref/<tfm>/) managed assembly for compile-time, or extend the fallback branch to add an explicit <Reference>/<Compile> reference to the managed DLL based on PlatformTarget.

Suggested change
<ItemGroup Condition="'$(_wslcPlatform)' != ''">
<ItemGroup Condition="'$(_wslcPlatform)' != ''">
<Reference Include="wslcsdkcs">
<HintPath>$(MSBuildThisFileDirectory)..\..\runtimes\win-$(_wslcPlatform)\lib\$(TargetFramework)\wslcsdkcs.dll</HintPath>
<Private>true</Private>
</Reference>

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +27
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\${CMAKE_BUILD_TYPE}\wslcsdk.lib" target="runtimes\win-x64"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\${CMAKE_BUILD_TYPE}\wslcsdk.dll" target="runtimes\win-x64\native"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\${CMAKE_BUILD_TYPE}\wslcsdkwinrt.dll" target="runtimes\win-x64\native\Microsoft.WSL.Containers.dll"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\x64\${CMAKE_BUILD_TYPE}\wslcsdkcs.dll" target="runtimes\win-x64\lib\${NUGET_TARGET_FRAMEWORK}"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\${CMAKE_BUILD_TYPE}\wslcsdk.lib" target="runtimes\win-arm64"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\${CMAKE_BUILD_TYPE}\wslcsdk.dll" target="runtimes\win-arm64\native"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\${CMAKE_BUILD_TYPE}\wslcsdkwinrt.dll" target="runtimes\win-arm64\native\Microsoft.WSL.Containers.dll"/>
<file src="${CMAKE_SOURCE_DIR_NATIVE}\bin\arm64\${CMAKE_BUILD_TYPE}\wslcsdkcs.dll" target="runtimes\win-arm64\lib\${NUGET_TARGET_FRAMEWORK}"/>
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The package only ships the managed projection (wslcsdkcs.dll) under runtimes/win-*/lib/${NUGET_TARGET_FRAMEWORK} and does not provide a lib/${NUGET_TARGET_FRAMEWORK} (or ref/${NUGET_TARGET_FRAMEWORK}) assembly. Without a RuntimeIdentifier, NuGet restore typically won’t pick RID-specific managed assets, so consumers may be unable to compile against Microsoft.WSL.Containers even if they set PlatformTarget. Consider also placing the managed assembly (or a reference assembly) under lib/ or ref/ for the TFM, and keep the RID-specific variant only for runtime-specific implementation if needed.

Copilot uses AI. Check for mistakes.
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.

3 participants