这个问题是伴随着“WebAssembly、Unicode 和 Web 平台”的讨论。 演示是预先录制的,这是我们决定在https://github.com/WebAssembly/meetings/pull/775 中尝试的内容,讨论时间安排在6 月 22 日的 CG 视频会议上。
请注意,我提到了一些我希望 CG 成员广为人知的概念,但我还是决定将它们包括在内,以使不熟悉该主题的人也能接受演示。 欢迎反馈!
相关问题:
也许到目前为止我从离线反馈中收集了一些潜在的解决方案,以供考虑:
在接口类型中,定义:
string := list char
string16 := list u16
定义在链接期间应用的强制转换,有以下情况:
| 来自 | 至 | 期待
|------------|------------|------------
| string
| string16
| 从 UTF-8 重新编码为 UTF-16
| string16
| string
| 从 WTF-16 重新编码为 UTF-8(替换选项)
强制确保string16
模块在 WASI 主机上运行,分别确保string
和string16
模块可以相互连接,即使两者都是string
和string16
模块或主机调用相同的string
或string16
导出,否则会产生歧义。
这在 Web 嵌入中也引入了歧义,因为将list u16
传递给 JS 可能会变成Uint16Array
或DOMString
。 从Uint16Array
到DOMString
的 JS 范围强制转换似乎是不可取的,但是可以通过显式使用别名string16
(具有自己的二进制 ID, string16 :> list u16
来暗示 JS 类型list u16
。 因此别名。 在这种情况下, string16
将成为DOMString
而list u16
将成为Uint16Array
。
我并不是特别喜欢string16
这个名字,可以使用任何其他名称,或者任何不需要名称/id 来解决歧义的替代名称。
这里不需要类似于list.is_canon
的优化,因为可以使用list.count
。 此外,通过在list.*_canon
适配器指令中为将来的立即数保留空间,可以保持通向 UTF-any 和潜在的 Latin1 优化的大门,如下所示。
在接口类型中,定义:
list.lift_canon $unit [...]
list.is_canon $unit [...]
list.lower_canon $unit [...]
where the $unit immediate is
0: 8-bit (UTF-8, ASCII-compatible)
1: 16-bit (UTF-16)
2: 32-bit (UTF-32)
3: 8-bit (Latin1, narrow UTF-16)
在需要格式良好的地方可以考虑这种潜在的解决方案。 它将避免双重重新编码开销和对代码大小的间接影响,但不会解决代理问题。 请注意, $unit
1-3 可能会在 MVP 之后添加作为进一步优化,或者我们可能会立即开始使用其中的一些。
在接口类型中,定义:
list.lift_canon $unit [...]
list.is_canon $unit [...]
list.lower_canon $unit [...]
where the $unit immediate is
0: 8-bit (WTF-8, ASCII-compatible)
1: 16-bit (WTF-16)
2: 32-bit (Code points except surrogate pairs)
3: 8-bit (Latin1, narrow WTF-16)
这种潜在的解决方案还需要将char
从 Unicode 标量值重新定义char
列表不包含代理对(但允许隔离代理),这可能在提升时强制执行。 同样,MVP 中具体的$unit
是有争议的。
这个本身不会引入损失,所以其他一切确实变成了 MVP 后的优化。
在接口类型中,定义:
passthrough
选项以获取“Unicode 代码点列表”。 这是一项功能性附加功能,可实现无损直通。通过这样做,我们实现了:
IIUC,根本问题是 IT 希望字符串是 unicode 代码点序列,但某些语言认为字符串是 i8 或 i16 值的序列,这些值可能与格式良好的 unicode 字符串相对应,也可能不对应。 一种简单的解决方案是让接受或产生无效 unicode 字符串的语言/API 使用(list u8)
或(list u16)
(可能使用一些不错的别名,如byte_string
来传达意图)而不是IT string
类型,IIRC 是(list char)
的别名。 有没有在任何地方讨论过这样做的权衡?
我认为这个问题有点微妙,因为 IT 想要将char
定义为“Unicode 标量值”,它们在代理代码点所在的位置有一个洞,因此不能代表孤立的代理. 另一方面,WTF 是没有此限制的“Unicode 代码点”,但序列被限制为不包含代理代码点对(这些将被替换为补充代码点 > U+FFFF,而孤立代理是可以的)。 这是你的意思吗?
除此之外,我认为从const char*
的类似 C 的字节字符串还没有讨论过。 不过,我可能已经错过了。
一种简单的解决方案是让接受或产生无效 unicode 字符串的语言/API 使用
(list u8)
或(list u16)
(可能使用一些不错的别名,如byte_string
来传达意图)而不是ITstring
类型,IIRC 是(list char)
的别名。
这也是目前我的首选解决方案 - wtf16string
类型将是(list u16)
的别名,就像string
当前被定义为(list char)
的别名一样(list u16)
的函数的结果将显示为 JS 列表(数字),而返回wtf16string
的函数的结果
向规范的 ABI 草案添加一个额外的wtf16string
别名似乎相对不干扰。
另一方面,WTF 是没有此限制的“Unicode 代码点”,但序列被限制为不包含代理代码点对(这些将被替换为补充代码点 > U+FFFF,而孤立代理是可以的)。
啊,这是否意味着 WTF-8 与普通的(list u16)
因为它有这个添加限制? 我没有意识到这种细微差别。 我的直觉是,同时拥有一个表示格式良好的 unicode 标量值序列的string
类型和一个几乎是(list u16)
的wtf16string
类型是多余的有额外的限制。 对于不强制执行 unicode 格式良好的系统,为不受限制的(list u16)
使用别名是否足够好? WTF-8 规范中的这个注释表明它会。
啊,这是否意味着 WTF-8 与普通(列表 u16)不同,因为它有这个添加限制?
它指出“就像人为地将 UTF-8 限制为 Unicode 文本以匹配 UTF-16 一样,WTF-8 也被人为地限制为排除代理代码点对以匹配可能格式错误的 UTF-16。” iiuc,它处理这些类似于 UTF-8 处理超长或截断字节序列的方式。 WTF-8 可以代表任何(list u16)
,但并非每个(list u8)
都是有效的 WTF-8。
对于不强制执行 unicode 格式良好的系统,对不受限制的(列表 u16)使用别名是否足够好?
WTF-16 将 1:1 映射到随机u16
值,它仅取决于这些值的解释方式,所以是的, (list u16)
会起作用。
IIUC,WTF-8 与任意的list u8
并不完全相同。 例如,它禁止“代理对字节序列”(参见此处)。
但是,WTF-16 _is_ 与list u16
。 他们共享一个命名主题有点奇怪。
编辑:应该刷新:)
我发布了第一个问题/答案,仅关注interface-types/#135 中的代理问题。 我认为这是高阶位,如果我们能就这一点达成一致,那么后续关于支持一种或多种编码格式的讨论会更简单。
谢谢你,卢克。
如果您愿意支持上述“单独的 WTF-16”(强制对于启用访问 WASI API 和在没有胶水代码的情况下与 JavaScript 进行交互至关重要),我会对建议的char
感到满意值范围。 然后,WTF-16 语言将具有他们需要集成的逃生舱口,以及使用相同语言 JavaScript 编写的模块以及通过替换为 UTF-* 语言的方式获得的逃生舱口。 顺便说一句,我也对 WASI 感觉好多了,因为字符串编码不匹配带来的主要痛点将通过强制转换得到解决。
拥有一个单独的string16
类型,就像你建议的代理一样,仍然会存在interface-types/#135 中概述的代理的所有问题,所以我认为拥有两种字符串类型与. 一个(特别是如果它们是隐式相互转换的;那么它们就不是有意义的分离类型)。 拥有两种字符串类型也会给每个界面设计者和消费者带来心理负担(“为什么有两种类型?有什么区别?我什么时候应该使用一种或另一种?”),从而使事情变得更糟。 最后,添加对 WTF-16 的支持通常会违背也在interface-types/#135 中提到的未来标准演进指南 Web/IETF。 因此,我认为我们不应该考虑添加包含代理的类型,除非我们有实际的具体证据表明没有它接口类型是不可行的。
对于 Web 专有用例,我认为在 JS 或 Web API 中解决问题是有意义的。 例如,很容易想象用于“绑定”wasm 导入和导出的 JS API。 这已经是其他新兴 JS API 中采用的一种方法,例如堆栈切换,而且我一直在想,我们要去的地方是否是能够处理 Web 的通用“绑定导入”/“绑定导出”JS API - Promises、JS 字符串和类型化数组视图的特定情况。
拥有一个单独的 string16 类型,就像你建议的代理一样,仍然会遇到 interface-types/135 中概述的代理的所有问题
从技术上讲是正确的,但也忽略了字符串至少总是在使用相同语言、任何兼容语言和 JavaScript 的单独编译的模块之间工作,即使没有预先知道一个模块与哪种模块接口。 这通常是我认为的大多数情况。 因此对我来说似乎是一个合理的妥协,还因为它允许将所需的char
值范围专用于格式良好的 (USV) 字符串。
拥有两种字符串类型也会给每个界面设计者和消费者带来心理负担,从而使事情变得更糟(“为什么有两种类型?有什么区别?我什么时候应该使用一种或另一种?”)
偶尔破损的替代方案对我来说似乎更糟,所以如果这就是它所需要的,我认为大多数人都会接受它。 也许第二个字符串类型( domstring
?)的好名字足以缓解这个小问题。
最后,添加对 WTF-16 的支持通常会违背 Web/IETF 的未来标准演进指南
不幸的是,在没有针对受影响语言的逃生舱口的情况下,任何人对所谓趋势的推理有多合理对我来说都无关紧要,因为只要 IT MVP 会为某人在某处破坏某些东西,并且基本上是无用的对于我正在研究的类似 JavaScript 的语言,我只能反对它。
因此,我试图找到一个每个人都能接受的合理解决方案或妥协,如果我们能合作,我会很高兴。
我不明白你所说的如何解决interface-types/#135 中提出的问题,或者提供了反证,即如果不包含新的domstring
类型,IT通常是不可行的。 现有的 JS API 已经提供了一个通用的逃生舱,用于在边界处进行任意值转换,所以我不知道在这个早期的时间点如何需要第二个逃生舱。 我认为我们只需要更多基于经验的证据来抵消我们得到的强有力的指导,以反对进一步传播包含代理的字符串。
(FWIW,如果我们可以就没有代理达成一致,我认为讨论支持U TF-16 作为string
的规范 ABI 中的附加编码是有意义的。但这是一个完全独立的主题几个选项,所以我不想将它与需要首先理解的抽象字符串语义混淆。)
我很欣赏你的第二段,因为它已经解决了一些非常烦人的问题。 我同意支持 UTF-16 是单独有用的,我很高兴将它添加到解释器/MVP 中。 算我一个!
然而,我很难理解你在第一段中的论点。 也许你不相信我,这里是 Linus Torvalds 解释了一个非常重要的规则,我认为它超出了 Linux 内核:不要破坏用户空间。 这是他在同一个演讲中,坚持程序员的智慧如果这是人们依赖的错误,那就不是错误,它是一个功能,只是继续:
当整个系统中最核心的库可以破坏东西时,只要事情“改进”并且他们“修复”ABI,这真的很可悲。
不必担心代理确实是一种功能,因为用户可以在这里或那里做一个粗心的substring(0, 1)
并用它调用导入的函数,或者可以split("")
,传递和再次join()
,或者创建一个StringBuilder
作为一个模块,它不会偶尔产生双重替换字符,就好像它是魔术一样。 我的意思是,一堆非常流行的语言选择不强制执行格式良好是有原因的,当 Wasm 想要很好地支持这些语言及其用户时,Wasm 变得越模块化,边界就越多,判断一个函数在哪个模块中会变得更难,问题也会变得越明显。
我真的不知道我还需要多少证据来证明以一种忽视当前现实的方式设计某些东西是一个坏主意。 事实上,这似乎仅适用于接口类型,而我们将所有其他提案保持在非常高的标准。 虽然我不是这方面的专家,但我认为是 Unicode 标准本身通过坚持使用 USV 在 UCS-2 语言的需求方面犯了同样的错误,导致大约十年的类似绝望讨论(可以推荐整个线程,但特别是在它沉默之前的最后一条评论),在 2014 年最终描述了普遍应用的实用解决方案,即 WTF-8 编码。
请注意,在比特流中遇到字符编码错误时发出替换字符是一种众所周知的危险静默数据损坏形式,需要完整性的系统禁止这样做。
相关地,如果 codePointAt 在点击一个单独的代理时会抛出一个异常,你很可能最终会遇到一个破坏整个应用程序的错误,因为有人不小心将表情符号字符放在了数据库字符串中的错误位置
不幸的是,ecmascript 很难确保您不会在其中某处生成带有未配对代理代码点的字符串,这就像从字符串中取出前 157 个 .length 单位并附加“...”来缩写它一样简单。 如果在实践中确实发生了这种情况,那将是一个奇怪的事故,因为非 BMP 字符很少见。 我们应该非常不愿意引入危害,希望改善我们的 Unicode 卫生。
JS、Java 和 C# 拥有它们所做的字符串的原因是,当 Unicode 意识到 2 个字节是不够的,因此 UCS-2 不可行时,已经编写了一堆代码,所以这些语言根本就没有没有选择。 同样,对于暴露给用户空间的 Linux 系统调用。 相比之下,目前不存在使用 IT 中定义的 API 的代码,因此我们没有相同的向后兼容性要求。 出于多种原因,wasm 和接口类型有意不寻求完美模拟现有的单一语言或系统调用 ABI。 它可能是一个有效的目标,但与组件模型相比,这将是一个单独的项目/标准/层。 这就是分层和范围界定的好处:我们不需要一件事就能实现所有可能的目标。
我想再次强调,当然在组件API的语义。 至于已经定义的 Web API:
因此,我仍然认为我们没有任何证据表明如果不推进这些 WTF-16 字符串语义,IT 将不可行,我认为这是 MVP 的适当问题。
我不同意的几点:
我不明白你说的如何解决 interface-types/#135 中提出的问题
现在这是一个单独的问题,在我之前的帖子中,我正在谈论我认为是解决损失的合理妥协。 特别是,我会同意您在单独问题中的推理,但前提是有可用的无损回退。 在我看来,这不是非此即彼。 如果不是,我仍然认为 WTF-8/16 是更具包容性、更少限制的选择,因此更可取,因为 Wasm 的高级目标之一是与 Web 平台无缝集成,分别保持向后- Web 的兼容特性,这也适用于接口类型。
现有的 JS API 已经提供了一个通用的逃生舱,用于在边界处进行任意值转换,所以我不知道在这个早期的时间点如何需要第二个逃生舱。
总是有使用自定义 JS API 绑定的逃生舱口
遗憾的是,这在我们的情况下还不够,我们目前有如下胶水代码:
const STRING_SMALLSIZE = 192; // break-even point in V8
const STRING_CHUNKSIZE = 1024; // mitigate stack overflow
const utf16 = new TextDecoder("utf-16le", { fatal: true }); // != wtf16
/** Gets a string from memory. */
function getStringImpl(buffer, ptr) {
let len = new Uint32Array(buffer)[ptr + SIZE_OFFSET >>> 2] >>> 1;
const wtf16 = new Uint16Array(buffer, ptr, len);
if (len <= STRING_SMALLSIZE) return String.fromCharCode(...wtf16);
try {
return utf16.decode(wtf16);
} catch {
let str = "", off = 0;
while (len - off > STRING_CHUNKSIZE) {
str += String.fromCharCode(...wtf16.subarray(off, off += STRING_CHUNKSIZE));
}
return str + String.fromCharCode(...wtf16.subarray(off));
}
}
首先,由于我们非常关心 Chrome 和 Node.js,我们发现 V8 的TextDecoder
for UTF-16LE 比其他引擎慢得多(SM 的真的很快),所以String.fromCharCode
是实际上在 V8 中更快达到一定的收支平衡点。 所以我们决定暂时围绕它进行优化。 接下来,WTF-16 不存在TextDecoder
(这很烦人),所以我们首先尝试解码格式良好的 UTF-16,如果失败,我们让它抛出并回退到分块慢得多的String.fromCharCode
。 分块是必要的,因为不能简单地将String.fromCharCode
应用于长字符串,因为这可能会溢出堆栈。
另一方面,例如 Rust 就不需要这个,这也是我认为 IT 现在不像它应该的那样中立的原因之一。 总的来说,我认为 IT string
的重点确实是能够很好地与 JS 交互,这仍然是我们主要的互操作目标。
现在不存在使用 IT 中定义的 API 的代码,因此我们没有相同的向后兼容性要求
前半部分在技术上是正确的,因为 IT 尚不存在,但 IIUC 我们的要求确实包括改进现有用例,例如考虑上面笨拙的粘合代码块。 理想情况下,尽可能多的语言,因此后 MVP 确实变成了“只是一种优化”,正如您在演示文稿中所说的那样。 相反,现在 IT 基本上从已经可以使用 UTF-8 编码器/解码器的语言的优化开始,我认为这不是中立的。
wasm 和接口类型故意不寻求完美地模拟现有的单一语言或系统调用 ABI
我读这篇文章就好像我持有这种观点,但我完全不是。 我愿意在这里为您提供怀疑的好处,但我想补充一点,在我看来,IT 目前受到不必要的限制,因此只能很好地服务于一组非常特定的语言。 相反,WTF-8/16 是更具包容性的编码,我希望它成为逻辑默认值,也因为它往返于 JS 字符串。 我们在这里不同意,但只是在没有适当的逃生舱口的情况下。 如果存在可行的无损替代方案,那么没有人会被破坏或处于不必要的不利地位,我会接受您对默认字符串类型的推理。
我们有很多理由相信通过代理是没有必要的(而且通常没有意义)
我们在这里不同意。 特别是,我认为我的陈述和评论提出了合理的怀疑,即在某些情况下,即使很少见,也可能非常有意义(比如需要诚信的地方),并且我认为“我们应该非常不愿意介绍希望改善我们的 Unicode 卫生的危害。” 也就是说,如果可以的话,我相信我们应该以一种保证在以下重要情况下也能工作的方式来设计规范的 ABI:Java/C#/AS<->JS、Java/C#/AS<-> Java/C#/AS。 其他路径上的替换可能是不可避免的,但至少语言和用户可以选择,在极少数情况下,默认值还没有被破坏。
我仍然不认为我们有任何证据表明不推进这些 WTF-16 字符串语义 IT 将不可行
在存在合理怀疑和缺乏探索我认为合理妥协的意愿的情况下,我希望现在举证责任在你身上。 同样,我愿意将默认字符串留给您和一个结构良好的未来,但不会以不考虑可能罕见但仍然存在的危险为代价。 许多流行的语言可能会受到这种影响,一旦他们意识到这一点,将来可能会变得非常难以证明其合理性。
我同意 JS 粘合代码并不理想,但我认为正确的解决方法是在 JS API 或 JS 中,而不是通过将 wtf-16-string 的概念添加到整个未来的组件生态系统中。 除此之外,我没有看到尚未回复的新信息。 似乎我们在目标/范围问题上主要存在分歧。
我希望TextDecoder
异常在 JS 中更难修复,因为它显然已经决定这超出了范围。 JS 可以做到这一点,因为 JS 中的TextDecoder
不是在两次函数调用之间调用的东西,而是主要用于通过网络或存储检索数据。
然而,更有趣的异常是,UTF-16LE 甚至没有TextEncoder
,所以必须这样做:
/** Allocates a new string in the module's memory and returns its pointer. */
function __newString(str) {
if (str == null) return 0;
const length = str.length;
const ptr = __new(length << 1, STRING_ID);
const U16 = new Uint16Array(memory.buffer);
for (var i = 0, p = ptr >>> 1; i < length; ++i) U16[p + i] = str.charCodeAt(i);
return ptr;
}
正如您所看到的,这是 Java、C#、AS 等的一个主要痛点,当list u16
被传递时,这两个仍然是必要的。 在这个问题的上下文中,它并不是 JS API 独有的,因为相同语言的两个模块之间的双重重新编码 + 损失并没有太大不同:(
除了TextEncoder
/ TextDecoder
,还有很多关于如何解决 Web 上的用例的选项。 另一个是扩展new WebAssembly.Function()
(已经在某些浏览器中实现)通过向构造函数添加额外的可选参数来执行字符串转换。 这种方法还将使功能也可用于 wasm 的非组件使用(并且可能更快),从而强调 JS API 将是解决此用例的正确位置。
仅供参考:将https://github.com/WebAssembly/interface-types/issues/135#issuecomment -863493832 中出现的“Integrated W/UTF-any”选项添加到上述建议列表中:)
最有用的评论
这也是目前我的首选解决方案 -
wtf16string
类型将是(list u16)
的别名,就像string
当前被定义为(list char)
的别名一样(list u16)
的函数的结果将显示为 JS 列表(数字),而返回wtf16string
的函数的结果向规范的 ABI 草案添加一个额外的
wtf16string
别名似乎相对不干扰。