Add scaffolding for C# projection of the SDK#40212
Add scaffolding for C# projection of the SDK#40212florelis wants to merge 16 commits intomicrosoft:feature/wsl-for-appsfrom
Conversation
JohnMcPMS
left a comment
There was a problem hiding this comment.
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. |
| 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; |
There was a problem hiding this comment.
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.
| 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; |
|
|
||
| void implementation::SessionSettings::CpuCount(uint32_t value) | ||
| { | ||
| m_cpuCount = value; |
There was a problem hiding this comment.
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).
| m_cpuCount = value; | |
| m_cpuCount = (value == 0) ? s_DefaultCPUCount : value; |
| m_memoryMb = value; | ||
| winrt::check_hresult(WslcSetSessionSettingsMemory(&m_sessionSettings, m_memoryMb)); |
There was a problem hiding this comment.
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).
| 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; |
|
|
||
| void implementation::SessionSettings::TimeoutMS(uint32_t value) | ||
| { | ||
| m_timeoutMS = value; |
There was a problem hiding this comment.
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).
| m_timeoutMS = value; | |
| m_timeoutMS = (value == 0) ? s_DefaultBootTimeout : value; |
|
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. |
| 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)); \ | ||
| } \ | ||
| } \ |
There was a problem hiding this comment.
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.
| 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); \ | |
| } \ |
| </PropertyGroup> | ||
|
|
||
| <Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" /> | ||
| <ItemGroup Condition="'$(_wslcPlatform)' != ''"> |
There was a problem hiding this comment.
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.
| <ItemGroup Condition="'$(_wslcPlatform)' != ''"> | |
| <ItemGroup Condition="'$(_wslcPlatform)' != ''"> | |
| <Reference Include="wslcsdkcs"> | |
| <HintPath>$(MSBuildThisFileDirectory)..\..\runtimes\win-$(_wslcPlatform)\lib\$(TargetFramework)\wslcsdkcs.dll</HintPath> | |
| <Private>true</Private> | |
| </Reference> |
| <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}"/> |
There was a problem hiding this comment.
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.
|
|
||
| cmake_host_system_information( | ||
| RESULT WINDOWS_SDK_DIR | ||
| QUERY WINDOWS_REGISTRY "HKLM/SOFTWARE/Microsoft/Windows Kits/Installed Roots" |
There was a problem hiding this comment.
Does this mean that developers will have to install additional devkits for this to build ?
There was a problem hiding this comment.
This is just querying where the Windows SDK is installed to, which I assume is already a requirement to build.
| target_link_libraries(wslcsdkwinrt ${COMMON_LINK_LIBRARIES} wslcsdk) | ||
|
|
||
| set_target_properties( | ||
| wslcsdkwinrt |
There was a problem hiding this comment.
Would it be possible to merge this with the existing dll ? My understanding is that they're both native libraries
There was a problem hiding this comment.
Yes, it is (winget does it). That is my goal as well.
There was a problem hiding this comment.
Updated to merge in the existing DLL.
WinRT assembly lookup rules require that the assembly name matches the namespace for the types, so I had to rename wslcsdk.dll to Microsoft.WSL.Containers.dll (for the NuGet package only).
There was a problem hiding this comment.
WinRT projection search conventions use the namespaces to look for DLLs to load after the attempt to RoActivate.... You can use a fusion manifest to allow the OS APIs to properly link your type names with your implementation DLL, regardless of the file name.
There was a problem hiding this comment.
An example manifest that includes both the COM (CoCreateInstance by CLSID GUID) and WinRT (RoActivateInstance by type name string) registrations:
https://github.com/microsoft/winget-cli/blob/master/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest
With this, the various projections will succeed in their RoActivateInstance attempt and not use the fallback "call LoadLibrary on namespace.dll". Only the WinRT-style named type registrations are needed for this.
If you include such a manifest in the C# projection that you build in this PR, the DLL can remain the existing name.
|
|
||
| --*/ | ||
|
|
||
| namespace Microsoft.WSL.Containers |
There was a problem hiding this comment.
What kind of ABI does this actually generate in the dll? Will this generate a C ABI and C++ headers that consume it, or an actual C++ ABI ?
There was a problem hiding this comment.
It creates a COM ABI for lack of a better term. You export the well-known COM+WinRT object factory C functions and callers use COM (typically through types generated by language projections, but we could theoretically ship the C/C++ COM header). CsWinRT generates the .NET wrapper that creates those objects and exposes them, largely adapting the WinRT types to .NET interfaces were appropriate.
| <ItemGroup> | ||
| <ReferenceCopyLocalPaths Include="$(MSBuildThisFileDirectory)..\..\runtimes\win-$(WslcPlatform)\native\wslcsdk.dll" /> | ||
| </ItemGroup> |
There was a problem hiding this comment.
This file copies runtimes\win-$(WslcPlatform)\native\Microsoft.WSL.Containers.dll, but the package currently places the managed projection under lib\<TFM>\wslcsdkcs.dll and the native runtime under runtimes\win-<arch>\native\wslcsdk.dll. As-is, the copy step points at a non-existent file and also no longer copies wslcsdk.dll for native (VC++) consumers, which will lead to runtime load failures. Update ReferenceCopyLocalPaths to copy the actual native DLL (and only copy managed assets if they’re actually shipped under runtimes).
| <license type="expression">MIT</license> | ||
| <readme>docs\README.MD</readme> | ||
| <dependencies> | ||
| <group targetFramework="${NUGET_TARGET_FRAMEWORK}" /> |
There was a problem hiding this comment.
The package now ships a managed projection (wslcsdkcs.dll) that depends on the CsWinRT/WinRT.Runtime assemblies (e.g., WinRTActivation.cs uses the WinRT namespace). The nuspec defines an empty dependency group, so consumers won’t automatically restore the required runtime package(s), leading to missing-assembly failures at runtime. Add an explicit NuGet dependency on the appropriate runtime package (e.g., Microsoft.Windows.CsWinRT or WinRT.Runtime), or alternatively include the required runtime DLLs in this package.
| <group targetFramework="${NUGET_TARGET_FRAMEWORK}" /> | |
| <group targetFramework="${NUGET_TARGET_FRAMEWORK}"> | |
| <dependency id="WinRT.Runtime" version="[2.2.0,3.0.0)" /> | |
| </group> |
| WindowsCreateString(typeName, (uint)typeName.Length, out var hstring); | ||
| try | ||
| { | ||
| if (s_getDllFactory(hstring, out var factory) < 0) | ||
| { | ||
| return IntPtr.Zero; | ||
| } | ||
|
|
||
| if (iid == IID_IActivationFactory) | ||
| { | ||
| return factory; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| return Marshal.QueryInterface(factory, ref iid, out var queried) >= 0 ? queried : IntPtr.Zero; | ||
| } | ||
| finally | ||
| { | ||
| Marshal.Release(factory); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| WindowsDeleteString(hstring); | ||
| } |
There was a problem hiding this comment.
WindowsCreateString/WindowsDeleteString return HRESULTs but the code ignores them. If WindowsCreateString fails, hstring may be invalid and the subsequent calls into s_getDllFactory / WindowsDeleteString can misbehave. Capture and validate the return value from these P/Invokes (and ensure WindowsDeleteString is only called when creation succeeded).
| #define THROW_MSG_IF_FAILED(hr, msg) \ | ||
| do \ | ||
| { \ | ||
| 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)); \ | ||
| } \ | ||
| } \ | ||
| } while (0) | ||
|
|
There was a problem hiding this comment.
The repo generally uses WIL HRESULT helpers/macros (e.g., THROW_IF_FAILED_MSG) rather than hand-rolled if (FAILED(hr)) checks. This custom THROW_MSG_IF_FAILED macro duplicates that behavior, adds a new local convention, and bypasses the standard WIL patterns used elsewhere. Consider replacing it with existing WIL helpers (or a small inline helper function) so error handling stays consistent and easier to audit.
| #define THROW_MSG_IF_FAILED(hr, msg) \ | |
| do \ | |
| { \ | |
| 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)); \ | |
| } \ | |
| } \ | |
| } while (0) | |
| template <typename TMessageProvider> | |
| inline void ThrowMsgIfFailed(HRESULT hr, TMessageProvider&& messageProvider) | |
| { | |
| if (SUCCEEDED(hr)) | |
| { | |
| return; | |
| } | |
| auto message = messageProvider(); | |
| auto rawMessage = message.get(); | |
| if (!rawMessage) | |
| { | |
| throw winrt::hresult_error(hr); | |
| } | |
| throw winrt::hresult_error(hr, winrt::to_hstring(rawMessage)); | |
| } | |
| #define THROW_MSG_IF_FAILED(hr, msg) ThrowMsgIfFailed((hr), [&]() { return (msg); }) |
| <PropertyGroup> | ||
| <WslcPlatform Condition="'$(WslcPlatform)' == ''">$(Platform)</WslcPlatform> | ||
| <_wslcIsInvalidPlatform Condition="'$(WslcPlatform)' != 'x64' and '$(WslcPlatform)' != 'arm64'">true</_wslcIsInvalidPlatform> | ||
| </PropertyGroup> |
There was a problem hiding this comment.
WslcPlatform defaults to $(Platform), but the validation compares case-sensitively against x64/arm64. In VS native builds $(Platform) is commonly ARM64, which will trip _wslcIsInvalidPlatform and fail the build even though the package supports ARM64. Consider normalizing $(WslcPlatform) to lower-case (or accepting both ARM64/arm64).
| <AdditionalDependencies> | ||
| wslcsdk.lib; | ||
| %(AdditionalDependencies) | ||
| </AdditionalDependencies> |
There was a problem hiding this comment.
The package (per Microsoft.WSL.Containers.nuspec.in) ships wslcsdk.lib under runtimes\win-<arch>\, but this targets file adds Microsoft.WSL.Containers.lib to AdditionalDependencies. As written, native consumers will fail to link because that import library isn’t present. Please align this to the actual shipped import library name (or update the nuspec to ship the renamed .lib).
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
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:Program.cs: