本文基于 Sang.ImprovWifi 的实践分享我如何把 Improv 协议层抽离成可复用内核再通过 Windows/Linux/AOT 三种宿主实现完成跨平台落地。1. 引言前面我已经写过几篇关于 Improv Wi-Fi 的文章主要是介绍其如何使用。这里我想从设计的角度聊聊我在实现 Sang.ImprovWifi 时的一些思路和教训尤其是如何把协议层做薄、宿主层做稳从而实现真正可维护的跨平台代码。这篇文章就来聊聊这个设计思路的细节在开发跨平台的服务时经常遇到一个看似普通、实则很痛的问题同样的业务逻辑一换平台就要重写一遍 BLE 代码。这件事最直接的后果不是开发慢而是质量不一致。Windows 版稳定Linux 版可能刚起步Linux 版修了一个 bugWindows 版又漏同步。久而久之项目维护会越来越吃力。在设计 Sang.ImprovWifi[1]时我的核心尝试就是把这件事反过来协议层只做协议宿主层只做平台业务层只做业务听起来像一句“架构口号”但实践下来它确实能显著降低复杂度。2. 为什么要先做协议层Improv 本身是开放协议核心其实不复杂设备暴露固定 Service 和 Characteristic客户端写入 RPC Command如 Identify、Wi-Fi 配置设备更新状态并返回 RPC Result URL真正复杂的是“状态一致性”和“事件语义”比如授权前后状态怎么切校验失败时返回哪个错误码何时触发ProvisioningRequestedRpcResult的 payload 如何编码这些都属于协议语义天然应该独立于平台。所以我在项目里先做了ImprovWifi.Protocol把这些东西收敛成可复用模型而不是散落在每个平台实现里。Protocol3. 三层结构项目结构大致如下ImprovWifi.Protocol协议常量ImprovPacketCodecImprovService状态机事件模型与通用传输抽象平台传输层ImprovWifi.Transport.WindowsImprovWifi.Transport.LinuxImprovWifi.Transport.Linux.AotDemo 层ImprovWifi.Demo.WindowsImprovWifi.Demo.LinuxImprovWifi.Demo.Linux.Aot这个分层的核心好处是把“变化频率不同”的代码分离。协议变化频率低但正确性要求高平台实现变化频率高且受系统能力影响大Demo 变化更快主要用于验证场景把它们揉在一起维护成本会指数上升。4. 事件驱动模型我在ImprovTransportServer里提供了一组业务友好的事件ProvisioningRequestedIdentifyRequestedCurrentStateChangedErrorStateChangedRpcResultChanged这组事件的价值在于平台差异被屏蔽业务代码只关注输入和输出上层可以非常自然地接入真实联网逻辑例如你可以在ProvisioningRequested里调用系统网络栈成功后调CompleteProvisioning(url)失败就调FailProvisioning(error)。这个编程模型在 Windows 和 Linux 上是一致的。5. Linux AOT 版的意义给受限设备一条工程化路径ImprovWifi.Transport.Linux.Aot不是“多写一个版本”而是为了真实设备场景。很多 ARM 小板上AOT native 是更现实的组合启动路径更可控依赖更可控跨架构分发更可控在这个版本里我做的不是“把 C# 全部改成 Rust”而是只把最贴近 BlueZ 的部分放进 Rust再通过 C ABI 暴露能力。这样我们保留了 .NET 协议层和上层业务的一致性。5.1 为什么选择 Rust早期ImprovWifi.Transport.Linux依赖的是Linux.Bluetooth这条路径在 AOT 下问题比较明显核心不是业务逻辑而是运行时兼容和底层行为稳定性。后面我又尝试替换到Tmds.DBus路线这一步确实解决了一部分问题但在我这块测试板上AOT 仍然不是“稳定可交付”的状态尤其是在重复启动和异常恢复这类真实场景里。这里做了一个对比测试使用了“假 AOT”方式也就是单文件发布 裁剪不是严格意义上的 Native AOT这样在板子上测试发现业务流程本身是没问题的。也就是说问题并不在 Improv 协议逻辑而是在 AOT Linux BLE 宿主这层的可控性。到这里方向就很清晰了协议层保留在 .NETLinux BLE 宿主下沉到更可控的 native 层用稳定 C ABI 连接两侧于是我最终选择了 Rust 自研这条路要把 AOT 场景做稳必须把最底层那部分拿回可控范围。5.2 Rust 项目和 C ABI 设计这条路也并不是一帆风顺中间折腾了不少经历了打包 NuGet 里 native 文件路径不对回调递归调用导致死循环CPU 飙升、URL 重复打印首次启动正常二次启动就报错最后定位到 BLE 资源释放不彻底这里不再展开实现细节。具体的 Rust 项目可以在 GitHub 项目的native/improvwifi_transport_linux_aot_native目录下看到。下面这段代码主要用于说明 C ABI 接口边界核心设计思路是Rust 负责实现 BlueZ 广播、GATT、D-Bus 对象导出通过 C ABI 导出一组句柄式 API 给 .NET 调用.NET 通过回调表接收 native 事件再映射回ImprovTransportServer的语义事件在 native 侧我导出了一组句柄式 API在 .NET 包装层中通过回调表接收 native 事件using System.Runtime.InteropServices; namespaceImprovWifi.Transport.Linux.Aot.Native; internalstaticpartialclassLinuxAotNativeMethods { privateconststring LibraryName improvwifi_transport_linux; [StructLayout(LayoutKind.Sequential)] internalstruct CallbackTable { public IntPtr OnIdentifyRequested; public IntPtr OnProvisioningRequested; public IntPtr OnStateChanged; public IntPtr OnErrorChanged; public IntPtr OnRpcResultReady; } [LibraryImport(LibraryName, EntryPoint improv_server_create)] internalstaticpartial IntPtr Create(); [LibraryImport(LibraryName, EntryPoint improv_server_destroy)] internalstaticpartialvoidDestroy(IntPtr handle); [LibraryImport(LibraryName, EntryPoint improv_server_start)] internalstaticpartialintStart(IntPtr handle); [LibraryImport(LibraryName, EntryPoint improv_server_stop)] internalstaticpartialintStop(IntPtr handle); [LibraryImport(LibraryName, EntryPoint improv_server_set_callbacks)] internalstaticpartialintSetCallbacks(IntPtr handle, in CallbackTable callbacks, IntPtr userData); [LibraryImport(LibraryName, EntryPoint improv_server_set_authorized)] internalstaticpartialintSetAuthorized(IntPtr handle, [MarshalAs(UnmanagedType.I1)] bool authorized); [LibraryImport(LibraryName, EntryPoint improv_server_set_config)] internalstaticpartialintSetConfig( IntPtr handle, byte[] adapterNameUtf8, nuint adapterNameLength, byte[] deviceNameUtf8, nuint deviceNameLength, [MarshalAs(UnmanagedType.I1)] bool discoverable, [MarshalAs(UnmanagedType.I1)] bool includeTxPower); [LibraryImport(LibraryName, EntryPoint improv_server_complete_provisioning)] internalstaticpartialintCompleteProvisioning(IntPtr handle, byte[] urlUtf8, nuint urlLength); [LibraryImport(LibraryName, EntryPoint improv_server_fail_provisioning)] internalstaticpartialintFailProvisioning(IntPtr handle, byte errorCode); }这个模式的优势是边界非常明确native 只关心“事件发生了”协议层只关心“状态应该怎么变”双方都不需要知道对方内部实现6. 结语跨平台开发最难的从来不是“支持几个平台”而是“在平台差异存在的前提下仍然保持语义一致和迭代效率”。通过开发Sang.ImprovWifi这套实践给我的最大反馈是当你把协议层做薄、宿主层做稳很多复杂问题会自然变简单。除了让拆分和分层更易于理解与维护外最大的好处是在排查问题时可以快速判断问题属于哪一层。是协议层的逻辑问题还是宿主层的实现问题当你在 AOT 版本上遇到问题时就知道先从 native 层的日志和状态入手而不是怀疑协议层的实现。References[1]Sang.ImprovWifi:https://github.com/sangyuxiaowu/Sang.ImprovWifi?wt.mc_idDT-MVP-5005195