资讯动态

Silk.NET「Better Strings」提案解析:面向原生 API 的 Char8/16/32 与 String8/16/32 字符串类型体系设计

发布时间:2026/10/13 8:41:47 来源:尧图企业网站定制
图形学3D渲染游戏开发【免费下载链接】Silk.NETThe high-speed OpenGL, OpenCL, OpenAL, OpenXR, GLFW, SDL, Vulkan, Assimp, WebGPU, and DirectX bindings library your mother warned you about.项目地址https://gitcode.com/gh_mirrors/si/Silk.NET点击查看免费下载本文解析 Silk.NET 历史上的一份重要提案——(Rejected) Proposal - Better Strings提案原文%20Proposal%20-%20Better%20Strings.md)。该提案直面 C# 互操作中原生字符串表示混乱的痛点设计了一套以Char8/Char16/Char32与String8/String16/String32为核心的编码感知字符串类型体系。阅读本文你将理解为什么char*在技术上是错误的原生字符串表示、这套 API 如何兼顾技术正确性与易用性以及它最终被拒绝后 Silk.NET 实际采用了怎样的字符串互操作方案。一、提案背景为什么char*是技术上不正确的提案开篇即点明核心矛盾目前 Silk.NET乃至整个 C# 生态中原生字符串的表示方式是一团糟an utter mess。当时 Silk.NET 在生成的所有绑定中统一使用char*来表示 C/C 侧返回或接收的字符串例如 OpenGL 的glGetString、GLFW 的glfwSetWindowTitle等函数的签名。许多用户指出这种表示技术上并不正确原因在于char*无法表达编码语义。C 语言中的char是一个字节而 C# 中的char是一个 UTF-16 代码单元16 位。同一个指针底层可能是 UTF-8 编码的字节序列如 OpenGL、GLFW 绝大多数 API、UTF-16 编码如 Windows 原生 API甚至可能是 32 位码点序列如某些 Unicode API。用单一类型指代所有情况等于把编码判断责任完全甩给调用方。编组marshalling语义含糊。char*到底是指向单个字符还是指向以空字符结尾的字符串是传入缓冲还是返回静态字符串这些信息都无法从类型本身读出。易用性差。调用方拿到char*后必须自行完成按哪种编码、读取到什么长度的解码逻辑才能转换回 C# 的string反之亦然。该提案的目标就是建立一套更丰富、定义更明确的原生字符串 API同时兼顾技术正确性technical correctness、编组易用性ease of marshalling与使用便利性ease of use三个维度。提案基于 Silk.NET1.0.2编写计划引入 2.0.0。二、核心设计Char8 / Char16 / Char32 三层字符类型提案的出发点是把字符按编码宽度显式区分设计了三层值类型public struct Char8 { private byte _value; // 8-bit character public Char8(byte v) _value v; public static explicit operator Char8(char c); public static implicit operator Char8(byte c); // and vice versa public char ToChar(); public byte ToByte(); public uint ToUInt32(); } public struct Char16 { private char _value; // 16-bit character public Char16(char v) _value v; // insert appropriate operators and methods here as seen in Char8 } public struct Char32 { private uint _value; // 32-bit character public Char32(char hi, char lo) _value unchecked((uint) char.ConvertToUtf32(hi, lo)); // insert appropriate operators and methods here as seen in Char8 }三者的设计要点Char8 以byte承载一个 8 位字符对应 UTF-8 的单一字节单元。byte → Char8是隐式转换无损、必然成功而char → Char8是显式转换因为 UTF-16 的char无法无损塞进一个字节转换可能丢信息故必须显式。Char16 以char承载一个 16 位字符即 UTF-16 代码单元与 C# 原生char完全同宽转换天然顺滑。Char32 以uint承载一个 32 位码点其构造函数直接复用char.ConvertToUtf32(hi, lo)——这正是 .NET 中把一对 UTF-16 代理项surrogate pair转换为 Unicode 码点的标准 API说明 Char32 面向的是完整的 Unicode 标量值而非代理项本身。每个类型都提供ToChar()/ToByte()/ToUInt32()等转换方法供需要脱离类型系统直接操作原始数值的场景使用。提案还明确要求所有类型都能与既有 C#/.NET 类型隐式互转使用户不必处理太多细节。三、字符串层ref struct 形态的 String8 / String16 / String32在字符类型之上提案定义了三个ref struct字符串类型它们内部以SpanCharXX承载数据public ref struct String8 { private SpanChar8 _value; public String8(SpanChar8 v) _value v; public static implicit operator String8(SpanChar8 v); public static implicit operator String8(string x); // not recommended for use, as well have to assume the encoding the user wants is UTF8. public static implicit operator String8(Char8* v); // treat the Char8* as null-terminated public static implicit operator String8(byte* v); // cast to Char8*, pass to the above. public static explicit operator String8(char* v); // cast to byte*, pass to the above. for maintaining back-compat with Silk.NET 1.0. // and vice versa public override string ToString(); public ref readonly Char8 GetPinnableReference(); } public ref struct String16 { private SpanChar16 _value; public String16(SpanChar16 v) _value v; // insert appropriate methods and operators here as seen in String8 public override string ToString(); public ref readonly Char16 GetPinnableReference(); } public ref struct String32 { private SpanChar32 _value; public String16(SpanChar32 v) _value v; // 注原文此处为笔误应为 String32 构造函数 // insert appropriate methods and operators here as seen in String8 public override string ToString(); public ref readonly Char32 GetPinnableReference(); }这套 API 设计中有几个值得深挖的点选择ref struct而非普通 struct/class。ref struct只能存在于栈上不能装箱、不能进入堆分配的对象图。这与SpanT的约束一致——因为String8内部就持有一个SpanChar8。好处是零堆分配、可与Span/指针互操作代价是它无法像string一样被自由地存储到类字段、闭包或异步方法中。这是提案为原生互操作场景做的明确取舍。GetPinnableReference()是打通 fixed 的关键。实现GetPinnableReference()后String8就能直接出现在 C# 的fixed语句中例如fixed (Char8* p str8)从而以零拷贝方式把栈上的字符串视图直接交给原生函数。ref readonly返回值意味着该引用只读配合Span的切片语义天然表达借用视图、不拥有内存的意图。仓库现有代码中GetPinnableReference已在多个互操作类型上使用参见 ComPtr1.cs、GlobalMemory.cs 以及 BuildTools 的 SpanOverloader.cs可见这是 Silk.NET 生态认可的模式。指针 → String 的隐式转换假定空字符结尾。Char8*/byte*到String8的隐式转换按 null-terminated以\0结尾处理这正对应绝大多数 C 原生 API 的字符串契约如 GLFW、OpenGL。string → String8被显式标注为不推荐使用。因为从 C#stringUTF-16转换到String8UTF-8必须假定用户想要的编码是 UTF-8——这是一个有损假设提案特意在代码注释中提醒调用者。这也体现了该设计对编码语义必须显式这一原则的坚持。char*到String8使用显式转换其注释点明目的是维持与 Silk.NET 1.0 的向后兼容——因为 1.0 时代绑定签名就是char*显式转换让老代码迁移时有明确的转型痕迹。四、Binder 输出示例glGetString 的理想形态提案进一步给出了如果这套 API 落地绑定生成器binder会输出什么的示例delegate cdecl*GLEnum, Char8* _glGetString; public String8 GetString(GLEnum name) (String8) _glGetString(name); // No need for a string overload, as you can implicitly cast String8 to string. // Users migrating to 2.0 will simply have to cast the String8 to string (better than them having to marshal it!)解读这个示例底层委托_glGetString返回Char8*技术上正确的原生字符串指针而公开的GetString方法返回String8——一个编码感知、带边界语义的视图类型由于String8定义了到string的隐式转换无需再为每个函数生成一个string重载调用方写string s gl.GetString(...)即可相比让用户在拿到byte*/char*后自行Marshal.PtrToStringUTF8迁移成本被压缩为一次隐式转换这正是提案强调的比他们自己编组要强得多。对照仓库当前实现见下文五可以清楚看到提案设想中的GetString与现存的GetString/GetStringS双方法方案之间的差异。五、GLFW 变更示例SetWindowTitle 的理想形态提案对 GLFW 层给出了对应的示例delegate cdecl*WindowHandle*, Char8*, void _glfwSetWindowTitle; public void SetWindowTitle(WindowHandle* h, String8 t);这里String8作为入参出现GLFW 的glfwSetWindowTitle接收 UTF-8 编码、空字符结尾的字符串用String8表达这一契约后调用方可以传入string走隐式转换、SpanChar8、Char8*或byte*类型系统会自动适配编码路径底层委托签名Char8*让 P/Invoke 层看到的仍然是指向 UTF-8 字节的指针符合原生 ABI。You kinda get the gist from here...——提案作者以此表示该模式可推广到 GLFW、OpenGL、OpenAL、SDL 等所有带字符串参数的绑定函数上形成统一的字符串互操作约定。六、仓库现状对照提案为何被拒绝实际采用了什么该提案的状态清单明确显示Proposed 已勾选但 Discussed with API Review Board (ARB)、Approved、Implemented 三项均未完成最终以 Rejected被拒绝收场。从当前仓库的生成代码可以确认这套 CharXX/StringXX API 从未落地Silk.NET 实际走的是另一条路线1. GLFW 侧string[UnmanagedType(UnmanagedType.LPUTF8Str)]特性。Glfw.cs 中SetWindowTitle的实际签名为public unsafe partial void SetWindowTitle(WindowHandle* window, [UnmanagedType(UnmanagedType.LPUTF8Str)] string title);CreateWindow的title参数同样使用[UnmanagedType(UnmanagedType.LPUTF8Str)]见 Glfw.cs。这是利用 .NET 运行时自带的LPUTF8Str编组行为C# 侧直接写stringP/Invoke 层自动按 UTF-8 编码传入原生函数。上游窗口封装 GlfwWindow.cs 中Title属性的 setter 也直接转发到_glfw.SetWindowTitle。2. OpenGL 侧byte*原始指针 string便捷重载并存。GL.gen.cs 中glGetString生成了两套 API[NativeApi(EntryPoint glGetString, Convention CallingConvention.Winapi)] public unsafe partial byte* GetString([Flow(Silk.NET.Core.Native.FlowDirection.In)] GLEnum name); [return: UnmanagedType(Silk.NET.Core.Native.UnmanagedType.LPUTF8Str)] [NativeApi(EntryPoint glGetString, Convention CallingConvention.Winapi)] public partial string GetStringS([Flow(Silk.NET.Core.Native.FlowDirection.In)] GLEnum name);GetString返回裸byte*保留完整控制力配合StringName枚举使用枚举定义见 StringName.gen.cs包含Vendor、Renderer、Version、Extensions、ShadingLanguageVersion五个取值GetStringSS 后缀返回string由[return: UnmanagedType(...LPUTF8Str)]特性驱动运行时自动解码。3. 结论与启示。从源码结构可以推断Silk.NET 最终选择stringLPUTF8Str特性 原始byte*/char*重载的组合而非提案中自定义CharXX/StringXX类型体系。这一取舍的合理之处在于LPUTF8Str特性让运行时接管了最常见的 UTF-8 编组避免了为每个字符串 API 引入新类型而byte*重载保留了高级用户需要的零开销路径。代价是提案所批评的编码语义含糊在 API 表层仍然存在byte*不带编码信息这正是该提案试图解决的问题。另外值得注意的是仓库中 CodeWorthKeepingAround.txt 明确记录了已开发但从未使用、未来可能有用的代码片段其中包括针对char16、wchar等 C 类型宽度的处理逻辑——这类存档性质的文件也印证了提案类思路在演进过程中被尝试过、最终未进入主线。七、设计评估这套方案的价值与局限作为一份被拒绝但仍值得研读的提案其设计思想可以从两个角度评估价值与亮点编码显式化Char8/Char16/Char32让字符宽度进入类型系统从根源上避免char*时代一个类型指代所有编码的歧义边界语义显式化指针到字符串的隐式转换约定 null-terminated 语义让互操作契约可读、可文档化零拷贝视图ref structSpanGetPinnableReference()的组合支持fixed固定与指针直传不牺牲原生性能迁移友好与既有char*、string的大量隐式/显式转换把 1.0 到 2.0 的迁移成本降到最低。局限与风险ref struct的传播限制String8无法存入类字段、无法被async/闭包捕获对习惯string便利性的用户是明显心智负担也会限制其在高层 API如窗口系统中的普及string → String8的编码假设隐式转换必须假定 UTF-8而String8类型本身并未携带是否真的 UTF-8的运行时校验错误的输入可能产生静默的错误编码生态成本为所有绑定函数引入新类型会改变整个 API 表面breaking change而当时 .NET 运行时已提供LPUTF8Str/LPWStr等编组特性边际收益未必高于成本。八、结语(Rejected) Proposal - Better Strings是一份问题真实、方案完整、最终未采纳的典型设计档案。它记录了 Silk.NET 对原生字符串如何优雅互操作这一经典难题的深入思考Char8/16/32与String8/16/32的类型分层、ref structSpanGetPinnableReference的零拷贝设计、以及围绕隐式转换的迁移策略即便在今天仍然是设计互操作型字符串 API 时可借鉴的范本。而仓库现状LPUTF8Str特性 byte*/string双重载则展示了 Silk.NET 最终选择的务实路线。读者如需进一步研究可对照阅读 提案原文%20Proposal%20-%20Better%20Strings.md)、GLFW 绑定实现 与 OpenGL 生成代码观察理想设计与实际落地之间的权衡全貌。赞分享图形学3D渲染游戏开发【免费下载链接】Silk.NETThe high-speed OpenGL, OpenCL, OpenAL, OpenXR, GLFW, SDL, Vulkan, Assimp, WebGPU, and DirectX bindings library your mother warned you about.项目地址https://gitcode.com/gh_mirrors/si/Silk.NET点击查看免费下载相关推荐3个真实故事告诉你为什么Boss-Key老板键正在重新定义Windows隐私保护3个真实故事告诉你为什么Boss Key老板键正在重新定义Windows隐私保护 下午3点27分程序员小李的屏幕右下角突然弹出了老板的微信消息现在有空吗桌面应用pix2tex模型对比ViT-B/32与ViT-L/16性能差异pix2tex模型对比ViT B/32与ViT L/16性能差异 引言LaTeX公式识别的性能瓶颈 在学术研究、技术文档撰写和教育领域将图像格式的数学公式人工智能计算机视觉OCRNLP预训练上一篇EAappEmulater1个工具绕过EA Desktop启动战地全系列EA游戏党必藏下一篇LibreHardwareMonitor免费实时硬件监控创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑