7 ms·
Requiem for a stringref
- mike_hock 3y agoRequiem for hacks upon hacks all the way down the stack to make historic brain farts work. A sensible way forward would be to deprecate APIs for direct UTF-16 code unit access, but implement them for backward compatibility on top of an internal UTF-8 representation. On both sides. We've lived with JS bloat for two decades, you think a few string copies/conversions are gonna kill us? Any non-toy uses of WebAssembly are gonna be new developments. Old shit that nobody is gonna run on WebAssembly other than to go "yeah, huh, it runs on WebAssembly" and then never use it again, doesn't need to run great.
- alexvitkov 3y agoA few copies aren't gonna kill us, but this may very well might, if charAt() is now O(n): for (int i = 0; i < str.length(); i++) doSomethingWith(str.charAt(i)); UTF-8 just a text encoding. If you're making something new, yeah, it's the obvious choice, but it's not better enough to justify breaking all sorts of shit just to switch over.
- davexunit 3y agoA copy for every string passed to the DOM API, to name just one thing, will be a significant limiting factor.
- alexvitkov 3y agoThere are some Rust front-end web frameworks that presumably manipulate the DOM, and in C++/Rust to pass a string to JS you need to run a TextDecoder over your WASM memory, so it's probably not a deal breaker. But like... if you're writing a website, just use JavaScript.
- davexunit 3y agoC++/Rust use WASM linear memory, but this article is about reference types via WASM GC. UTF-8 data in an (array i8) or UTF-16 data in an (array i16) are opaque to the host.
- alexvitkov 3y agoYeah, and you still have to marshal strings at the JS/WASM boundry, same as if you used (array i8/16) over JS strings in Java. In the case of non-managed strings, this overhead hasn't been big enough to stop people from writing fairly fast (by Web standards) frontend frameworks in Rust.
- Ygg2 3y agoThat code makes zero sense in Unicode. First question how are representing your umlauts, followed by Zero Join Width Characters. You never work on characters, you work on grapheme clusters or whatnot but never characters.
- alexvitkov 3y agoI'm not advocating you write this, I'm saying people have written it, probably hundreds of thousands of times, and if charAt() becomes O(n) instead of O(1), this code suddenly hangs your CPU for 10 seconds on a long string, thus you can't really swap out UTF-16 for UTF-8 transparently.
- Ygg2 3y agoYour point doesn't stand for UTF-16 either. It's not a fixed length encoding either. It's broken in UTF-16 as well. It was always O(n). Of course assuming you aren't using UTF-32, which has its own set of problems (BE or LE), and sees little usage outside of China.
- taway1237 3y ago...it's not O(n). Many languages, JS, Java and C# included, have O(1) access to a character at a given position. You correctly note that it won't work well with international strings, but GP is right that A LOT of code like this was written by western ASCII-brained developers.
- alexvitkov 3y agoHaven't used Java in a while but I believe charAt() returns a UTF-16 codepoint and is constant time access. So something like the above works not only for ASCII, as well as for the majority of Western languages and special characters you may encounter on a day to day basis.
- Ygg2 3y agoIt's constant time iff you ignore surrogate pairs and Unicode. By that logic UTF8 is constant time if you ignore anything not ASCII because most text is in English. Saying it works fine if you ignore errors and avoid edge cases is just a clever rephrashing of it worked on my machine. Plus Emojis are Unicode U+1F600 and above, so even in Western language you are bound to find such "exceptions" .
- kevingadd 3y agoThe amount of memory and CPU overhead involved in sending strings across the wasm/JS boundary to do something like put text in a textarea is a lot bigger than you might think. It's really severe.
- azakai 3y ago> Any non-toy uses of WebAssembly are gonna be new developments. Major uses of WebAssembly include things like Photoshop, Unity, and many other large existing codebases.
- kibwen 3y agoJust because you can't get rid of UTF-16 doesn't mean you can't let people opt-in to UTF-8 string semantics. Just like there's a `use strict` pragma, there could be a `use utf-8` pragma.
- cyco130 3y agoI think it's very different: "use strict" is strictly local (function or script scoped), "use utf-8" would require the entire JavaScript context to cooperate. It means you can't safely use a library that expects UTF-16 in an app with "use utf-8". E.g. you can't include Google Analytics in your "use utf-8" web page.
- ykonstant 3y ago> E.g. you can't include Google Analytics in your "use utf-8" web page. Well, what are we waiting for, then.
- Someone 3y agoJava and JavaScript being high-level languages, it’s easy to switch the internal representation of strings. In fact, the JVM already has moved to a mix of ISO-8859-1/Latin-1 and UTF-16 (https://openjdk.org/jeps/254 https://openjdk.org/jeps/254), and I expect many performant JavaScript implementations also do something in that direction.
- nvm0n2 3y agoAccording to the article they don't, actually? Apparently they're thinking about it but aren't sure if it's worth it. For Java it was largely because it reduced time spent in GC (less memory usage = less frequent need to collect).
- aardvark179 3y agoThat was the enhancement proposal targeting Java 9 which came out about 6 years ago. For Java it’s a good saving because it reduces the overall heap size, and if JS has a similar distribution of objects then it should work well there as well. It may already do so, the internal storage format of things like strings and arrays is deliberately opaque.
- paroneayea 3y agoStringref is an extremely thoughtful proposal for strings in WebAssembly. It’s surprising, in a way, how thoughtful one need be about strings. Here is an aside, I promise it’ll be relevant. I once visited Gerry Sussman in his office, he was very busy preparing for a class and I was surprised to see that he was preparing his slides on oldschool overhead projector transparencies. “It’s because I hate computers” he said, and complained about how he could design a computer from top to bottom and all its operating system components but found any program that wasn’t emacs or a terminal frustrating and difficult and unintuitive to use (picking up and dropping his mouse to dramatic effect). And he said another thing, with a sigh, which has stuck with me: “Strings aren’t strings anymore.” If you lived through the Python 2 to Python 3 transition, and especially if you lived through the world of using Python 2 where most of the applications you worked with were (with an anglophone-centric bias) probably just using ascii to suddenly having unicode errors all the time as you built internationally-viable applications, you’ll also recognize the motivation to redesign strings as a very thoughtful and separate thing from “bytestrings”, as Python 3 did. Python 2 to Python 3 may have been a painful transition, but dealing with text in Python 3 is mountains better than beforehand. The WebAssembly world has not, as a whole, learned this lesson yet. This will probably start to change soon as more and more higher level languages start to enter the world thanks to WASM GC landing, but for right now the thinking about strings for most of the world is very C-brained, very Python 2. Stringref recognizes that if WASM is going to be the universal VM it hopes to be, strings are one of the things that need to be designed very thoughtfully, both for the future we want and for the present we have to live in (ugh, all that UTF-16 surrogate pair pain!). Perhaps it is too early or too beautiful for this world. I hope it gets a good chance.
- tomcam 3y agoI think the WebAssembly people have been judicious about features. Watching it evolve has made me feel that they truly respect how important it is to keep things well thought out and as efficient as possible. I feel like it’s in very good hands.
- paroneayea 3y agoI agree with this assertion. WebAssembly, on its whole, is extremely good. The string stuff is, IMO, something the group has not come to realize the "right direction" on, but so much has been done right! Hopefully strings can get there too. :)
- tomcam 3y agoIn other news, WebAssembly now has garbage collection. From the article: “The GC support gives you the ability to define a number of different kinds of aggregate data types: structs (records), arrays, and functions-as-values.” Happy to hear this.
- ReleaseCandidat 3y agoWell, "now" and "has" ... There is a GC proposal: https://webassembly.github.io/gc/core/ https://webassembly.github.io/gc/core/ V8 implemented it some months ago (ChromeNode and I guess Deno, with a experimental switch to enable support). WasmEdge isn't there yet: https://github.com/WasmEdge/WasmEdge/issues/1122#issuecomment-1636690425 https://github.com/WasmEdge/WasmEdge/issues/1122#issuecommen... WasmTime finished the RFC for the implementation details in June: https://github.com/bytecodealliance/wasmtime/issues/5032 https://github.com/bytecodealliance/wasmtime/issues/5032
- davexunit 3y agoYeah the non-web runtimes are playing catch-up with GC, but the upcoming Firefox 120 and Chrome 119 releases will have GC enabled by default. Not sure what state Safari is in but presumably not far behind. WASM GC should be usable in all major browsers by the end of the year.
- layer8 3y ago> Java (and JavaScript) is outdated: if you were designing them today, their strings would not be UTF-16. Except that UTF-16 makes a lot of sense on Windows, which won’t change anytime soon. Since you always have to deal with noncharacters, initial vs. non-initial BOMs, isolated combining characters, etc., and you have to validate your inputs anyway (meaning you almost always need a failure path for unvalidated strings anyway), I’m not sure if (unpaired) surrogates constitute that much more of a complication.
- chubot 3y agoIt probably won't change soon, but Microsoft (or at least some teams in it) have acknowledged the mistake of UTF-8, AND they have taken some steps toward UTF-8: http://www.oilshell.org/blog/2023/06/surrogate-pair.html#future-windows-and-python-are-moving-toward-utf-8 http://www.oilshell.org/blog/2023/06/surrogate-pair.html#fut...
- chubot 3y agoedit: s/mistake of UTF-8/mistake of UTF-16/
- haberman 3y agoI really have trouble understanding the push to provide higher-level features as intrinsic types and instructions. AIUI, the main alternative to stringref (linked in the article: https://github.com/WebAssembly/js-string-builtins/blob/main/proposals/js-string-builtins/Overview.md https://github.com/WebAssembly/js-string-builtins/blob/main/...) is to provide string types via WebAssembly's normal "import" mechanism. This alternative makes more sense to me. When you write extension modules for languages like Python, Ruby, PHP, or even JavaScript (via V8 or JavaScriptCore), you are always importing APIs like strings (eg. #include <ruby.h>). It seems natural to me that WebAssembly would be the same way. Now ideally a WebAssembly module wouldn't be specific to one particular embedding environment. You would want to standardize this string API, so that a single API can be used efficiently with multiple host languages. So you'd want something more like #include <wasm/wtf16string.h>. But if it's an import, it also leaves open the possibility that such an API can evolve over time, or that a competing API could supplant it as the landscape of language engines changes. By putting it directly into the instruction set, you'd be baking in assumptions about how JS engines in 2023 function, even though those assumptions can (and hopefully will) change over time. It's true that wasm/wtf16string.h probably won't be available in environments that don't have JS-style strings. But that seems for the best. I'd rather have a leaner WebAssembly than make every WebAssembly engine provide JS-style strings. In the case that the host environment doesn't already have a JS-style string that you are trying to interoperate with, why not just have Java implement its string type directly in WebAssembly? Or once shared libraries are available, wasm/wtf16string.h could be provided via some third-party library, shared across multiple modules, leaving open the possibility of passing strings around between modules. I guess, to the article's point, why shouldn't memcpy() be an import also? What is gained by making it part of the instruction set? I assume that a WebAssembly implementation could recognize special imports, and optimize them the same as if it were a built-in instruction.
- csjh 3y agoI think string builtins seem to be the direction the wasm committee is moving towards anyways? Unless I'm mistaken
- 3y ago
- LoganDark 3y agoFor anyone else who's also struggling to find that footnote, search for "when I mention UTF-16". It's a fake. I do wish they had used some symbol that can actually be pasted into the "Find" dialog, rather than being normalized into the number 1, which matches at least 75 (!) times.
- charcircuit 3y agoGrapheme clusters were mentioned. Grapheme clusters should not be handled at the string layer. It's not possible to know what makes a grapheme cluster without knowing the font that is rendering it. >= can be 2 grapheme clusters is some fonts and 1 grapheme cluster in other fonts which show it as ≥
- amluto 3y agoNo, you’re thinking of glyphs, not grapheme clusters.
- charcircuit 3y agoFrom the Unicode standard Display of Grapheme Clusters. Grapheme clusters are not the same as ligatures. For example, the grapheme cluster “ch” in Slovak is not normally a ligature and, conversely, the ligature “fi” is not a grapheme cluster. Default grapheme clusters do not necessarily reflect text display. For example, the sequence <f, i> may be displayed as a single glyph on the screen, but would still be two grapheme clusters.
- Jasper_ 3y agoRight, so "f" and "i" are two separate grapheme clusters (and always are), but might map to one rendered glyph under certain fonts that combine them into a ligature. Grapheme clusters have a specific definition that doesn't depend on the font in use. That definition is updated over time as the Unicode standard is updated, which means it can vary over time, but it does not change based on the font.
- 10000truths 3y agoIt pains me to see people inventing opcodes for operations with unbounded execution times. WebAssembly is a sandboxed runtime first and foremost, and part of a sandbox's security is the ability to limit resource usage. I don't want untrusted user code to DoS my WASM engine, and such opcodes are the perfect vector for this kind of attack. Lua made this mistake with "string.find" [1], and I wish the WASM committee would not repeat it with GC and stringref. [1] http://lua-users.org/lists/lua-l/2011-02/msg01595.html http://lua-users.org/lists/lua-l/2011-02/msg01595.html
- slaymaker1907 3y agoI don't think this is really a problem that the WASM standard needs to be too concerned about. Execution engines themselves should provide ways to preempt execution. I'm more concerned with these proposals that they are introducing a lot of complexity to implementing WASM.
- 10000truths 3y ago> Execution engines themselves should provide ways to preempt execution. Yes, and that preemption occurs at the opcode level. Which is completely defeated if your string matching opcode can hang for an inordinate amount of time because the user supplies a very large string and/or crafts string inputs that force O(n^2) or worse behaviour.
- davexunit 3y agoI implemented a WASM interpreter recently. Didn't know WASM when I started. GC wasn't particularly difficult. Type canonicalization was the trickiest bit. Did a small subset of stringref, too. If there's anything I'm intimidated by with WASM implementation it's all the SIMD instructions.
- kevingadd 3y agoThere are a few use cases here and I personally think only one of them is really important in the sense that it would provide a lot of value for many people. What JS in general needs is a 'StringView' type, which is most useful for asm.js/wasm scenarios, but is useful for other scenarios too. You could define it for UTF16 only, or define it to work with both UTF8 and UTF16. Why? Well, a very common use case for asm.js/wasm scenarios is that you have a big wasm application that needs to chat with things outside of the wasm sandbox. The vast majority of APIs available to a wasm application are browser APIs, which communicate using objects (we have a solution for those, externref), strings, doubles (native wasm support), i32s (semi-native support - v8 only supports 31-bit integers, but it's usually good enough) and booleans (just use i31s). Right now if you want to send a string from wasm to a browser API, you need the help of some JS glue to construct a brand new JS string on demand from bytes living in the wasm heap. This glue is Not Fast, and it becomes increasingly Not Fast the larger your strings are. If you have a great many strings - for example, text that you want to cram into textareas or spans inside of a table in the DOM - you will waste a lot of time doing this. And then if you want to transfer strings back into wasm, you have to do the reverse, painstakingly copying the string back into the wasm heap one character at a time. If you're lucky, the browser has native APIs that accelerate this process by decoding/encoding UTF16, but those APIs have limitations - for example, TextDecoder does not support SharedArrayBuffer, so enabling multithreading in your wasm application will instantly make it slower to send strings across the js/wasm boundary. Cool. The widely used wasm platform I work on (.NET / Blazor) does a lot of sending strings across the js/wasm boundary, so we've had to jump through some hoops to optimize this as much as possible. For example, we maintain an 'interned string' table on both sides of the boundary so that commonly reused strings like method names or enum value names don't have to get encoded/decoded each time they cross. This significantly increases memory usage and means we have to jump through hoops to avoid using too much memory, but the advantage in terms of performance is measurable, upwards of 10% per boundary crossing even for small strings. A typical application has lots of grids, tables and lists filled with string data, so it spends a sizable amount of time doing all this string encoding/decoding. Incidentally this interning table is harder to implement efficiently because nobody is willing to expose a getHashCode equivalent for JS types or even specifically for JS strings, but I can understand that decision at least... If you had a StringView type, you could basically go "here's an ArrayView, I promise it contains WTF16 (or UTF16) data, and I promise I won't mutate it. You can make a defensive copy if you want. Please turn it into something string-like so I can use it with DOM properties like textContent or APIs like WebSocket, thanks." JS runtimes already have support under the hood for diverse string storage formats, whether it's ropes, latin1 buffers, or utf16 buffers, so this would just be a new one of those. Maybe we'll get one before 2030. Not holding out hope though, this was a known need at the very beginning of the WebAssembly spec process and I doubt anyone has forgotten about it, it just doesn't seem to be considered that important compared to features like "having multiple distinct address spaces for some reason" and "64-bit address spaces that probably won't be available in web browsers for 20 years because Chrome on Android is still 32-bit"
- a1o 3y agoI am a bit in need for help here. Why does string matter to webassembly? Shouldn't be possible to implement any string type on top of it? Is it really required for it to have some special handling for strings? I am used to C and C++ so I guess I never thought of wasm as a runtime like C#+.NET so any help is appreciated.
- Skinney 3y agoIt is possible to implement your own strings by only using WASM primitives. There are two, as I see it, major reasons for including specialized support for it anyway: 1. One of the biggest WASM runtimes are browsers, and so it's likely that one would want to exchange strings between WASM and JS/DOM _efficiently_ 2. The article states that most modern languages, if compiled to WASM, would likely need a lot of functionality in order to deal with unicode correctly (was how I interpreted the reference to libICU), which again would greatly increase the size of WASM blobs for those languages. Having stuff like that builtin would mitigate that.