Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ See docs/process.md for more on how version tagging works.

6.0.3 (in development)
----------------------
- Fixed `UTF8ToString` with `GROWABLE_ARRAYBUFFERS` set. String decoding now
copies the data when the heap buffer is resizable, just like it does in
the shared memory case. (#27242)
- Added support for compiling FMA intrinsics. All 32 FMA intrinsics are
supported, with 256-bit variants emulated via two 128-bit operations. Pass
``-msimd128 -mfma`` to enable. With ``-mrelaxed-simd -mfma``, Wasm relaxed
Expand Down
48 changes: 31 additions & 17 deletions src/parseTools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,27 +1081,41 @@ function runtimeKeepalivePop() {
return 'runtimeKeepalivePop();';
}

// Some web functions like TextDecoder.decode() may not work with a view of a
// SharedArrayBuffer, see https://github.com/whatwg/encoding/issues/172
// To avoid that, this function allows obtaining an unshared copy of an
// ArrayBuffer.
// Some web functions like TextDecoder.decode() do not work with a view of a
// SharedArrayBuffer (see https://github.com/whatwg/encoding/issues/172) or of
// a resizable ArrayBuffer (see
// https://github.com/emscripten-core/emscripten/issues/27241).
// To avoid that, this function allows obtaining a copy in those cases.
function getUnsharedTextDecoderView(heap, start, end) {
const shared = `${heap}.slice(${start}, ${end})`;
const unshared = `${heap}.subarray(${start}, ${end})`;
const copy = `${heap}.slice(${start}, ${end})`;
const view = `${heap}.subarray(${start}, ${end})`;

// No need to worry about this in non-shared memory builds
if (!SHARED_MEMORY) return unshared;
// No need to worry about this in builds where the buffer can be neither
// shared nor resizable.
if (!SHARED_MEMORY && !(ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS)) return view;

// If asked to get an unshared view to what we know will be a shared view, or
// if in -Oz, then unconditionally do a .slice() for smallest code size.
// If in -Oz, then unconditionally do a .slice() for smallest code size.
// This is guaranteed to work but could be slower since it performs a copy.
if (SHRINK_LEVEL == 2 || heap.startsWith('HEAP')) return shared;

// Otherwise, generate a runtime type check: must do a .slice() if looking at
// a SAB, or can use .subarray() otherwise. Note: We compare with
// `ArrayBuffer` here to avoid referencing `SharedArrayBuffer` which could be
// undefined.
return `${heap}.buffer instanceof ArrayBuffer ? ${unshared} : ${shared}`;
if (SHRINK_LEVEL == 2) return copy;

if (SHARED_MEMORY) {
// If asked to get an unshared view to what we know will be a shared view,
// then unconditionally do a .slice().
if (heap.startsWith('HEAP')) return copy;

// Otherwise, generate a runtime type check: must do a .slice() if looking
// at a SAB, or can use .subarray() otherwise. Note: We compare with
// `ArrayBuffer` here to avoid referencing `SharedArrayBuffer` which could
// be undefined.
return `${heap}.buffer instanceof ArrayBuffer ? ${view} : ${copy}`;
}

// With GROWABLE_ARRAYBUFFERS == 2 the heap is always resizable; with
// GROWABLE_ARRAYBUFFERS == 1 resizability is feature-detected at runtime,
// and non-heap views passed to UTF8ArrayToString may not be resizable at
// all, so generate a runtime check in those cases.
if (GROWABLE_ARRAYBUFFERS == 2 && heap.startsWith('HEAP')) return copy;
return `${heap}.buffer.resizable ? ${copy} : ${view}`;
}

function getEntryFunction() {
Expand Down
34 changes: 34 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -15537,6 +15537,40 @@ def test_TextDecoder_invalid(self):
expected = '#error "TEXTDECODER must be either 1 or 2"'
self.assert_fail([EMCC, test_file('hello_world.c'), '-sTEXTDECODER=3'], expected)

# Verify that strings can be decoded when the heap is backed by a resizable
# ArrayBuffer (ALLOW_MEMORY_GROWTH + GROWABLE_ARRAYBUFFERS).
# TextDecoder.decode() rejects views of resizable ArrayBuffers, so we must
# pass it a copy of the data instead. Node's TextDecoder happens to accept
# such views, so emulate the browser behaviour in a --pre-js.
# See https://github.com/emscripten-core/emscripten/issues/27241
@requires_node_26
@parameterized({
'': ([],),
'textdecoder_2': (['-sTEXTDECODER=2'],),
})
def test_TextDecoder_resizable_buffer(self, args):
create_file('pre.js', '''
var realDecode = TextDecoder.prototype.decode;
TextDecoder.prototype.decode = function(data) {
if (data && data.buffer && (data.buffer.resizable || data.buffer.growable)) {
throw new TypeError('The provided ArrayBuffer value must not be resizable');
}
return realDecode.call(this, data);
};
''')
create_file('main.c', r'''
#include <emscripten/console.h>

int main() {
// Long enough (> 16 bytes) that UTF8ToString on the string takes the
// TextDecoder path.
emscripten_out("the quick brown fox jumps over the lazy dog");
return 0;
}
''')
self.do_runf('main.c', 'the quick brown fox jumps over the lazy dog',
cflags=['--pre-js=pre.js', '-sALLOW_MEMORY_GROWTH', '-sGROWABLE_ARRAYBUFFERS=1'] + args)

def test_reallocarray(self):
self.do_other_test('test_reallocarray.c')

Expand Down