Skip to content

Add basic C# interop for the WSLC SDK API#40066

Open
florelis wants to merge 2 commits intomicrosoft:feature/wsl-for-appsfrom
florelis:csinterop
Open

Add basic C# interop for the WSLC SDK API#40066
florelis wants to merge 2 commits intomicrosoft:feature/wsl-for-appsfrom
florelis:csinterop

Conversation

@florelis
Copy link
Copy Markdown
Member

@florelis florelis commented Apr 1, 2026

Summary of the Pull Request

This adds a basic P/Invoke C# wrapper for wslcsdh.h.

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

The interop wrapper was mostly generated by Copilot.

The wrapper is included as source (.cs) in the NuGet package, and the .targets file automatically adds it to consumer projects. The file is compiled by the consumer, so we don't have to change anything about our build process for now.

I believe this to be a low-risk change as there is no change in the build process. If a caller has any issue with this interop wrapper, they can exclude it from their project with <Compile Remove="[path]" /> and use the unmanaged DLL directly. Tests and a more idiomatic wrapper for C# will come later; this is intended as a first approach if it can be included in the next private build.

I also updated the dev docs to mention a flag that is needed when building the Release configuration, as I needed it to build the NuGet locally.

Validation Steps Performed

I created a sample consumer project. With only including the package and no additional configuration, I was able to use the API. The project built with no issues (only a few warnings about possible null references), and it ran the initial steps just fine - creating the settings objects and pulling the image. It failed when trying to create a container, but I'm not sure if that was due to an issue in the projection or something about my setup.

@florelis florelis requested a review from a team as a code owner April 1, 2026 20:32
Copilot AI review requested due to automatic review settings April 1, 2026 20:32
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

Adds a basic C# P/Invoke interop surface for the WSLC SDK (wslcsdk.dll) and wires it into the NuGet package so managed consumers can use the native API without a separate managed assembly build step.

Changes:

  • Add Microsoft.WSL.Containers.Interop.cs (P/Invoke declarations + small helper utilities) to the package and auto-include it in consumer builds via .targets.
  • Update developer build docs with a Release build invocation note.

Reviewed changes

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

File Description
nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.targets Auto-includes the C# interop source file into consuming .NET projects.
nuget/Microsoft.WSL.Containers/build/net/Microsoft.WSL.Containers.Interop.cs New C# P/Invoke surface mapping wslcsdk.h plus helper methods/types.
doc/docs/dev-loop.md Adjusts Release build instructions in the dev loop documentation.


<Import Project="$(MSBuildThisFileDirectory)..\Microsoft.WSL.Containers.common.targets" />

<ItemGroup>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The .targets file unconditionally adds a C# source file via . That will be imported for any SDK-style .NET project referencing the package (including VB/F#), and can break builds because those project types can’t compile .cs files. Consider conditioning this ItemGroup on the project language (e.g., IsCSharpProject / MSBuildProjectExtension == .csproj) or moving the file to a C#-only asset path so non-C# consumers aren’t impacted.

Suggested change
<ItemGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For now I think we only care about C#, and the idea is that we will have a compiled binary later on, not the plain .cs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Still seems like an easy guard just in case.

Comment on lines +5 to +16
// This file is a direct mapping of wslcsdk.h. All structures, enums, delegates,
// and entry points are exposed for use by managed callers.
//
// Notes:
// - Opaque settings structs (WslcSessionSettings, WslcContainerSettings,
// WslcProcessSettings) must only be manipulated through the SDK functions.
// - Handle types (WslcSession, WslcContainer, WslcProcess) are IntPtr values
// that must be released with the corresponding WslcRelease* function.
// - Output strings allocated by the SDK (errorMessage, inspectData) are
// CoTaskMem-allocated; free them with Marshal.FreeCoTaskMem after use.
// - Delegate instances passed as callbacks must be kept alive (prevent GC)
// for the entire duration they may be invoked by native code.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Most of the SDK “settings” APIs store pointers to caller-provided strings/arrays rather than copying them (e.g., WslcInitSessionSettings assigns displayName/storagePath directly; WslcInitContainerSettings assigns imageName; various setters assign PCSTR pointers). With the current P/Invoke signatures using string/string[] and struct arrays, the marshaller’s backing buffers are not guaranteed to stay valid across subsequent calls (GC compaction / temporary unmanaged copies), so later calls like WslcCreateSession/WslcCreateContainer can observe dangling pointers. The interop layer should either (1) change these parameters to IntPtr and require callers to allocate/pin unmanaged memory for the full lifetime of the settings object, or (2) add managed wrapper types that pin/own the unmanaged memory until after CreateSession/CreateContainer completes.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I would be okay with this for the first pass. It only means that it needs to be used paying attention to these things, and I'll address the rough edges before the final version of the projection.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see how a caller can avoid the situation described here; if you are at the whims of GC then we just end up looking flaky.

Comment on lines +520 to +532
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerPortMapping[] portMappings,
uint portMappingCount);

[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsVolumes(
ref WslcContainerSettings containerSettings,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerVolume[] volumes,
uint volumeCount);

[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsNamedVolumes(
ref WslcContainerSettings containerSettings,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerNamedVolume[] namedVolumes,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

WslcSetContainerSettingsPortMappings/Volumes/NamedVolumes are declared with managed arrays, but the native implementation stores the raw pointers (it does not deep-copy the arrays/strings). That means the marshalled unmanaged buffers created for these calls can be freed immediately after the setter returns, leaving the settings object holding dangling pointers and causing failures later in WslcCreateContainer. These setters should be exposed as IntPtr + count (caller-managed unmanaged memory) or wrapped with a managed “settings builder” that allocates/pins unmanaged arrays for the lifetime of the settings object.

Suggested change
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerPortMapping[] portMappings,
uint portMappingCount);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsVolumes(
ref WslcContainerSettings containerSettings,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerVolume[] volumes,
uint volumeCount);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsNamedVolumes(
ref WslcContainerSettings containerSettings,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] WslcContainerNamedVolume[] namedVolumes,
IntPtr portMappings,
uint portMappingCount);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsVolumes(
ref WslcContainerSettings containerSettings,
IntPtr volumes,
uint volumeCount);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetContainerSettingsNamedVolumes(
ref WslcContainerSettings containerSettings,
IntPtr namedVolumes,

Copilot uses AI. Check for mistakes.
Comment on lines +590 to +606
[DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern int WslcSetProcessSettingsCurrentDirectory(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPStr)] string currentDirectory);

[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetProcessSettingsCmdLine(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] argv,
UIntPtr argc);

[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetProcessSettingsEnvVariables(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] keyValue,
UIntPtr argc);

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

WslcSetProcessSettingsCurrentDirectory/CmdLine/EnvVariables are exposed with managed strings/string[] parameters, but the native layer stores the pointers (commandLine/environment/currentDirectory) for later use rather than copying. With the current signatures, the marshaller’s buffers won’t remain valid across calls (and may be moved by GC), which can lead to use-after-free when creating/starting processes. Consider switching these parameters to IntPtr (caller-owned unmanaged allocations) or providing managed wrappers that pin/own the argv/env/currentDirectory memory until after the process is created.

Suggested change
[DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern int WslcSetProcessSettingsCurrentDirectory(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPStr)] string currentDirectory);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetProcessSettingsCmdLine(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] argv,
UIntPtr argc);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall)]
public static extern int WslcSetProcessSettingsEnvVariables(
ref WslcProcessSettings processSettings,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] keyValue,
UIntPtr argc);
// Native entry points that accept unmanaged pointers. These map directly to the
// wslcsdk.dll exports and are used by the managed wrappers below.
[DllImport(DllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi, EntryPoint = "WslcSetProcessSettingsCurrentDirectory")]
private static extern int WslcSetProcessSettingsCurrentDirectory_Native(
ref WslcProcessSettings processSettings,
IntPtr currentDirectory);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall, EntryPoint = "WslcSetProcessSettingsCmdLine")]
private static extern int WslcSetProcessSettingsCmdLine_Native(
ref WslcProcessSettings processSettings,
IntPtr argv,
UIntPtr argc);
[DllImport(DllName, CallingConvention = CallingConvention.StdCall, EntryPoint = "WslcSetProcessSettingsEnvVariables")]
private static extern int WslcSetProcessSettingsEnvVariables_Native(
ref WslcProcessSettings processSettings,
IntPtr keyValue,
UIntPtr argc);
// Managed wrappers that allocate unmanaged memory for arguments and call the
// IntPtr-based native entry points above. The allocations are intentionally not
// freed here to ensure that the native layer can safely hold onto the pointers
// for the lifetime of the process settings / process.
public static int WslcSetProcessSettingsCurrentDirectory(
ref WslcProcessSettings processSettings,
string currentDirectory)
{
IntPtr dirPtr = IntPtr.Zero;
if (!string.IsNullOrEmpty(currentDirectory))
{
dirPtr = Marshal.StringToHGlobalAnsi(currentDirectory);
}
return WslcSetProcessSettingsCurrentDirectory_Native(ref processSettings, dirPtr);
}
public static int WslcSetProcessSettingsCmdLine(
ref WslcProcessSettings processSettings,
string[] argv,
UIntPtr argc)
{
if (argv == null || argv.Length == 0 || argc == UIntPtr.Zero)
{
return WslcSetProcessSettingsCmdLine_Native(ref processSettings, IntPtr.Zero, UIntPtr.Zero);
}
// Allocate unmanaged strings for each argument.
int count = (int)argc;
if (count > argv.Length)
{
count = argv.Length;
}
IntPtr argvArrayPtr = IntPtr.Zero;
try
{
argvArrayPtr = Marshal.AllocHGlobal(IntPtr.Size * count);
for (int i = 0; i < count; i++)
{
IntPtr argPtr = argv[i] != null
? Marshal.StringToHGlobalAnsi(argv[i])
: IntPtr.Zero;
Marshal.WriteIntPtr(argvArrayPtr, i * IntPtr.Size, argPtr);
}
}
catch
{
// If allocation fails partway through, leave any successfully allocated
// strings and the array in place; they will simply not be referenced.
throw;
}
return WslcSetProcessSettingsCmdLine_Native(ref processSettings, argvArrayPtr, (UIntPtr)count);
}
public static int WslcSetProcessSettingsEnvVariables(
ref WslcProcessSettings processSettings,
string[] keyValue,
UIntPtr argc)
{
if (keyValue == null || keyValue.Length == 0 || argc == UIntPtr.Zero)
{
return WslcSetProcessSettingsEnvVariables_Native(ref processSettings, IntPtr.Zero, UIntPtr.Zero);
}
int count = (int)argc;
if (count > keyValue.Length)
{
count = keyValue.Length;
}
IntPtr envArrayPtr = IntPtr.Zero;
try
{
envArrayPtr = Marshal.AllocHGlobal(IntPtr.Size * count);
for (int i = 0; i < count; i++)
{
IntPtr kvPtr = keyValue[i] != null
? Marshal.StringToHGlobalAnsi(keyValue[i])
: IntPtr.Zero;
Marshal.WriteIntPtr(envArrayPtr, i * IntPtr.Size, kvPtr);
}
}
catch
{
// As above, in case of failure, already-allocated values are intentionally
// not freed here.
throw;
}
return WslcSetProcessSettingsEnvVariables_Native(ref processSettings, envArrayPtr, (UIntPtr)count);
}

Copilot uses AI. Check for mistakes.
@JohnMcPMS
Copy link
Copy Markdown
Member

I'm not sure that the C ABI as defined/implemented is a good fit for any direct PInvoke only calling. It is too reliant on the caller managing memory for the various settings for that to work without forcing C# callers to pin everything first. If we are to include it, it should be that way (callers must pin and pass pointers in). But honestly that seems like such a pain to use that I'm not sure it is worth it.

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