WSL 容器 API 的 C 投影:Microsoft.WSL.Containers 下 CsWinRT 类型映射完全指南
本文是 WSL 容器 API C# 参考系列中的类型映射专篇,聚焦 Microsoft.WSL.Containers 命名空间下 WinRT 类型 → C# 投影类型 的完整对应关系。它适用于所有通过 C# 调用 WSL 容器 SDK(Session → Container → Process 三层模型)进行 Windows 桌面/服务端应用开发的读者;读完本文,你将能准确读懂 C# API 参考 中每个方法的签名,正确选用集合类型、可空值类型与异步进度类型,避免在事件回调、异步等待与属性赋值时出现类型不匹配或投影语义误解。
背景:C# 投影从何而来
WSL 容器 SDK 的公共 C# 表面并非手写的一套独立 API,而是对 WinRT 层实现的镜像投影。仓库中定义 WinRT 投影的权威来源是 wslcsdk.idl,其命名空间即为 Microsoft.WSL.Containers;而 Projection.cs 与 WinRTActivation.cs 构成 C# 侧的激活与投影包装。正如 Overview 所述:“The public C# surface mirrors the WinRT surface implemented by the winrt_*.h / winrt_*.cpp wrappers.”
因此,理解 C# API 的正确姿势是:先看 IDL 中该 WinRT 类型的声明,再套用本文的映射表翻译成 C# 类型。CsWinRT(C#/WinRT 投影生成器)负责在编译期把 IDL 中的 WinRT 类型自动投影为对应的 .NET 类型,这正是本文映射表存在的底层机制。
核心映射表(完整清单)
以下 14 项映射是 C# 投影中最常遇到的对应关系,也是本文的骨架:
| WinRT 类型 | C# 投影类型 |
|---|---|
hstring |
string |
Windows.Foundation.Uri |
System.Uri |
Windows.Foundation.TimeSpan |
System.TimeSpan |
IReference<uint32_t> |
uint? |
IReference<TimeSpan> |
TimeSpan? |
IReference<ContainerNetworkingMode> |
ContainerNetworkingMode? |
IVector<T> |
IList<T> |
IVectorView<T> |
IReadOnlyList<T> |
IMap<string, string> |
IDictionary<string, string> |
Windows.Foundation.DateTime |
DateTimeOffset |
com_array<uint8_t> 事件负载 |
byte[] |
Windows.Networking.HostName |
Windows.Networking.HostName |
IAsyncActionWithProgress<T> |
可等待的 WinRT 异步操作(awaitable WinRT async operation) |
下面逐类展开,说明每一行的投影语义,并给出在 wslcsdk.idl 中的真实使用位置。
字符串与基础类型:hstring → string
WinRT 的 hstring(不可变字符串)在 C# 中投影为 string,这是最基础也最常用的映射。在 IDL 中几乎每个 runtimeclass 的属性都用到它,例如:
SessionSettings.Name、SessionSettings.StoragePath(idl 第 54-55 行);ProcessSettings.WorkingDirectory、ContainerSettings.ImageName、ContainerSettings.Name、HostName、DomainName;ImageInfo.Name、PullImageOptions.Uri、PushImageOptions.Image、TagImageOptions三个属性。
在 C# 侧这些字段全部以 string 出现,可直接与 Console.WriteLine、Path API、字符串插值等互操作,无需任何转换。
可空值类型:IReference<T> → T?
WinRT 没有“可空值类型”语法,可空性通过 Windows.Foundation.IReference<T> 表达;CsWinRT 将 IReference<T> 投影为 C# 的可空值类型 T?。这通常用于可选配置项——未赋值即为 null,赋值后为对应值。映射表里出现的三种 IReference 用法都可以在 IDL 中找到:
| IDL 声明(wslcsdk.idl) | C# 投影 |
|---|---|
Windows.Foundation.IReference<UInt32> CpuCount;(SessionSettings,第 57 行) |
uint? |
Windows.Foundation.IReference<UInt32> MemorySizeInMB;(SessionSettings,第 58 行) |
uint? |
Windows.Foundation.IReference<Windows.Foundation.TimeSpan> Timeout;(SessionSettings,第 59 行) |
TimeSpan? |
Windows.Foundation.IReference<ContainerNetworkingMode> NetworkingMode;(ContainerSettings,第 164 行) |
ContainerNetworkingMode? |
典型用法是 Session 与 ContainerSettings 的初始化:
var sessionSettings = new SessionSettings("MyApp", @"C:\WslcData")
{
CpuCount = 4, // uint?
MemorySizeInMB = 4096, // uint?
Timeout = TimeSpan.FromMinutes(5) // TimeSpan?
};
var containerSettings = new ContainerSettings("alpine:latest")
{
Name = "hello-container",
NetworkingMode = ContainerNetworkingMode.Bridged // ContainerNetworkingMode?
};
枚举可空类型(如 ContainerNetworkingMode?)在业务上很有价值:null 表示“使用默认网络模式”,显式赋值则覆盖默认行为。从 idl 第 99-103 行 可以看到 ContainerNetworkingMode 枚举实际只有 None = 0 与 Bridged = 1 两个成员,C# 中按 ContainerNetworkingMode? 使用即可。
集合类型:IVector / IVectorView / IMap
WinRT 集合接口在 C# 中投影为对应的 .NET 集合接口,三者语义差异在于可变性:
| WinRT 类型 | C# 投影 | 语义 |
|---|---|---|
IVector<T> |
IList<T> |
可读可写、支持按索引访问 |
IVectorView<T> |
IReadOnlyList<T> |
只读快照 |
IMap<string, string> |
IDictionary<string, string> |
键值对字典 |
IVector → IList(可变集合)
IDL 中 ContainerSettings 与 ProcessSettings 大量使用 IVector:
IVector<ContainerPortMapping> PortMappings;(第 170 行)IVector<ContainerVolume> Volumes;(第 171 行)IVector<ContainerNamedVolume> NamedVolumes;(第 172 行)IVector<String> CommandLine;(ProcessSettings,第 219 行)
C# 侧使用 IList<T> 初始化:
var settings = new ProcessSettings
{
CommandLine = new[] { "/bin/echo", "Hello from WSL Container!" },
EnvironmentVariables = new Dictionary<string, string> { { "MY_VAR", "value" } }
};
注意 CommandLine 被投影为 IList<string>,因此数组字面量 new[] {...} 可以直接赋值(数组实现了 IList<T>)。
IVectorView → IReadOnlyList(只读快照)
IDL 中返回查询结果的 API 均使用 IVectorView<T>,表示调用方拿到的是只读快照:
IVectorView<ImageInfo> GetImages();(Session,第 92 行)IVectorView<Component> GetMissingComponents();(WslcService,第 283 行)
C# 中对应 IReadOnlyList<T>,典型消费方式:
var missing = WslcService.GetMissingComponents(); // IReadOnlyList<Component>
if (missing.Count > 0) { /* 提示运行 wsl --install */ }
foreach (var image in session.GetImages()) // IReadOnlyList<ImageInfo>
{
Console.WriteLine(image.Name);
}
IMap → IDictionary
ProcessSettings.EnvironmentVariables 在 IDL 中声明为 IMap<String, String>(第 220 行),C# 投影为 IDictionary<string, string>,可像普通字典一样赋值、遍历与读写:
settings.EnvironmentVariables = new Dictionary<string, string>
{
{ "TERM", "xterm-256color" },
{ "LANG", "C.UTF-8" }
};
时间与日期:TimeSpan / DateTime
| WinRT 类型 | C# 投影 | 说明 |
|---|---|---|
Windows.Foundation.TimeSpan |
System.TimeSpan |
时长 |
Windows.Foundation.DateTime |
DateTimeOffset |
时间点(时刻) |
TimeSpan → System.TimeSpan
IDL 中 TimeSpan 出现于:
SessionSettings.Timeout(IReference<TimeSpan>,第 59 行);Container.Stop(Signal signal, Windows.Foundation.TimeSpan timeout)(第 188 行)。
C# 中直接用 TimeSpan 表达时长,例如 end-to-end-example 中的停止容器:
container.Stop(Signal.SIGTERM, TimeSpan.FromSeconds(10));
以及等待 init 进程退出的 30 秒超时:
var completed = await Task.WhenAny(exited.Task, Task.Delay(TimeSpan.FromSeconds(30)));
DateTime → DateTimeOffset
Windows.Foundation.DateTime 投影为 DateTimeOffset(而非 DateTime),因为 WinRT 的 DateTime 自带 UTC 语义与 100ns 精度。IDL 中用于:
ProcessCrashInformation.Timestamp(Windows.Foundation.DateTime,第 32 行);ImageInfo.CreatedTimestamp(第 364 行)。
C# 中可作为 DateTimeOffset 直接格式化输出,例如崩溃信息处理:
session.ProcessCrashed += info =>
Console.WriteLine($"[{info.Timestamp:O}] {info.ProcessName} ({info.Pid}) crashed");
字节数组事件负载:com_array<uint8_t> → byte[]
ProcessOutputHandler 委托在 IDL 中声明为 delegate void ProcessOutputHandler(UInt8[] data);(第 232 行)。IDL 里的 UInt8[] 属于 ABI 层的 com_array<uint8_t>(COM 风格数组),CsWinRT 将其投影为 C# 的 byte[]。这直接决定了事件回调的签名:
public delegate void ProcessOutputHandler(byte[] data);
事件负载是原始字节,而非字符串,因此输出编码需要调用方自行处理(通常是 UTF-8)。delegates-and-events 文档给出了标准写法:
using System.Text;
container.InitProcess.OutputReceived += data => Console.Write(Encoding.UTF8.GetString(data));
container.InitProcess.ErrorReceived += data => Console.Error.Write(Encoding.UTF8.GetString(data));
这也是 known-gaps 中所说的“原始原生句柄被包装,不直接暴露”的一个体现:C API 的 WslcGetProcessIOHandle 在 C# 中不直接出现,取而代之的是 OutputReceived/ErrorReceived 事件与 WinRT 流。
Uri 与 HostName:投影为同名/框架类型
| WinRT 类型 | C# 投影 | IDL 位置 |
|---|---|---|
Windows.Foundation.Uri |
System.Uri |
Session.Authenticate(Windows.Foundation.Uri serverAddress, ...),第 90 行 |
Windows.Networking.HostName |
Windows.Networking.HostName(保持同名) |
ContainerPortMapping.WindowsAddress,第 119 行 |
Windows.Foundation.Uri 投影为框架类型 System.Uri,因此 Authenticate 的调用非常自然:
string token = session.Authenticate(
new Uri("https://registry.example.com"),
"user1",
"password");
而 Windows.Networking.HostName 在 WinRT 与 .NET 中本就同名同命名空间,投影后类型不变,属于“透传”映射:
var mapping = new ContainerPortMapping(8080, 80, PortProtocol.TCP)
{
WindowsAddress = new Windows.Networking.HostName("127.0.0.1")
};
异步操作:IAsyncActionWithProgress<T> 与进度回调
IDL 中所有耗时操作都以 Windows.Foundation.IAsyncActionWithProgress<T> 返回,例如:
IAsyncActionWithProgress<ImageProgress> PullImageAsync(PullImageOptions options);(第 75 行)IAsyncActionWithProgress<ImageProgress> PushImageAsync(...)(第 81 行)IAsyncActionWithProgress<ImageProgress> ImportImageAsync(...)/LoadImageAsync(...)(第 78、80 行)IAsyncActionWithProgress<InstallProgress> InstallWithDependenciesAsync(InstallOptions options);(第 286 行)
该类型投影为可等待的 WinRT 异步操作:既可以直接 await,又暴露 Progress 事件用于接收进度。Session 文档中的标准用法:
var pullOp = session.PullImageAsync(new PullImageOptions("docker.io/library/alpine:latest"));
pullOp.Progress = (op, progress) =>
Console.WriteLine($"Pull: {progress.Status} {progress.CurrentBytes}/{progress.TotalBytes}");
await pullOp;
ImageProgress 由 IDL 第 326-332 行定义:Id、Status(ImageProgressStatus 枚举,取值覆盖 Pulling/Waiting/Downloading/Verifying/Extracting/Complete)、CurrentBytes 与 TotalBytes。Progress 事件在 await 之前订阅即可捕获整个下载过程,await 之后进度已经结束。
需要特别指出:C# 投影将同步方法(PullImage/PushImage/ImportImage/LoadImage/InstallWithDependencies)与异步方法(*Async)同时保留(参见 wslcsdk.idl 第 74-82 行),UI 线程场景应优先使用 *Async 版本,避免阻塞。
事件与委托:WinRT 委托在 C# 中的落地
类型映射不仅作用于属性和方法参数,也作用于事件。IDL 中声明的四个委托(第 24、35、232、233 行)投影为普通 C# 委托,事件则按标准 C# 事件消费:
public delegate void SessionTerminationHandler(SessionTerminationReason reason);
public delegate void ProcessCrashHandler(ProcessCrashInformation information);
public delegate void ProcessOutputHandler(byte[] data);
public delegate void ProcessExitHandler(int exitCode);
典型订阅示例(来自 delegates-and-events):
session.Terminated += reason => Console.WriteLine($"Session ended: {reason}");
session.ProcessCrashed += info => Console.WriteLine($"Process crashed: {info.ProcessName} ({info.Pid})");
container.InitProcess.OutputReceived += data => Console.Write(Encoding.UTF8.GetString(data));
container.InitProcess.ErrorReceived += data => Console.Error.Write(Encoding.UTF8.GetString(data));
container.InitProcess.Exited += code => Console.WriteLine($"Init exited: {code}");
注意 ProcessExitHandler 的负载是 int(对应 IDL 的 Int32),而 ProcessOutputHandler 的负载是 byte[](对应 com_array<uint8_t>),两者不要混淆。
投影边界:不在 C# 中出现的类型
映射表之外,理解哪些 C API 能力没有被投影同样重要,这能避免写出无法编译或语义错误的代码。Known Gaps 完整列出了四类差异:
| C API 功能 | C# 状态 |
|---|---|
WslcImportSessionImage / WslcLoadSessionImage 接收原始 HANDLE + 字节数的重载 |
不投影;C# 只暴露基于文件路径的 ImportImage/ImportImageAsync/LoadImage/LoadImageAsync |
原始原生句柄(WslcGetSessionTerminationEvent、WslcGetProcessExitEvent、WslcGetProcessIOHandle) |
包装而非暴露;改用 C# 事件与 WinRT 流 |
WslcProcessCallbacks 注册面 |
包装为事件;使用 OutputReceived、ErrorReceived、Exited |
WslcContainerStartFlags |
不直接暴露;Container.Start() 在 init 进程采用 ProcessOutputMode.Event 或 ProcessOutputMode.Stream 时自动设置 ATTACH 标志 |
例如“事件负载为 byte[]”的映射(第 5 节)正是“句柄被包装”这一缺口的 C# 侧表象。
实战要点小结
- 可空配置项:
CpuCount、MemorySizeInMB、Timeout、NetworkingMode在 C# 中都是可空类型,未赋值为null,判断“是否显式设置”直接用hasValue语义,不要与 0 混淆。 - 只读快照:
GetImages()与GetMissingComponents()返回IReadOnlyList<T>,适合遍历与 LINQ 查询,但不适合原地修改。 - 字节 vs 字符串:所有进程输出事件负载均为
byte[],务必按已知编码(通常 UTF-8)解码。 - 异步进度:在
await之前订阅Progress才能观察到完整进度序列。 - 类型翻译法:遇到任何 C# 签名不确定时,回查 wslcsdk.idl 中对应成员的 WinRT 声明,再按本文映射表翻译即可。
将以上映射规则套入 End-to-End Example 的完整生命周期示例(检查前置组件 → 打印版本 → 创建会话 → 拉取镜像 → 配置 init 进程 → 创建并启动容器 → 等待退出 → 清理),即可写出类型正确、进度可控、事件完整的 C# WSL 容器应用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00