Skip to content
Open
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
6 changes: 6 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ See docs/process.md for more on how version tagging works.

6.0.3 (in development)
----------------------
- The `GROWABLE_ARRAYBUFFERS` setting default was reverted back to 0 (it was
changed to 1 in 6.0.2) because `TextDecoder.decode()` rejects views of
resizable ArrayBuffers in browsers, breaking `UTF8ToString` in programs
built with `ALLOW_MEMORY_GROWTH`. String decoding now copies the data when
the heap buffer is resizable, so the setting can still be enabled
explicitly. (#27241)
- 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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 35 additions & 17 deletions src/parseTools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,27 +1078,45 @@ 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;
// The heap can be backed by a resizable ArrayBuffer in non-shared memory
// builds with memory growth enabled.
const maybeResizable = !SHARED_MEMORY && ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS;

Comment on lines +1090 to 1093

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// The heap can be backed by a resizable ArrayBuffer in non-shared memory
// builds with memory growth enabled.
const maybeResizable = !SHARED_MEMORY && ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS;

// 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.
// This is guaranteed to work but could be slower since it performs a copy.
if (SHRINK_LEVEL == 2 || heap.startsWith('HEAP')) return shared;
// No need to worry about this in builds where the buffer can be neither
// shared nor resizable.
if (!SHARED_MEMORY && !maybeResizable) return view;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably neater/simpler to just inline this.

Suggested change
if (!SHARED_MEMORY && !maybeResizable) return view;
if (!SHARED_MEMORY && !(ALLOW_MEMORY_GROWTH && GROWABLE_ARRAYBUFFERS)) return view;


// 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 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) 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
4 changes: 2 additions & 2 deletions src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2276,13 +2276,13 @@ var JS_BASE64_API = false;
// Enable support for growable views of Wasm memory. This is a recent Web
// platform feature that can make growing the Wasm memory more efficient,
// especially in multi-threaded builds.
// The default setting of 1 will auto-detect the presence of this API and use
// Setting this to 1 will auto-detect the presence of this API and use
// it when available.
// Setting this to 2 will unconditionally require it. This is the only way
// to completely remove the overhead of growable memory + pthreads.
// This settings does nothing unless ALLOW_MEMORY_GROWTH is set.
// [link]
var GROWABLE_ARRAYBUFFERS = 1;
var GROWABLE_ARRAYBUFFERS = 0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets make this a separate PR


// If the emscripten-generated program is hosted on separate origin then
// starting new pthread worker can violate CSP rules. Enabling
Expand Down
34 changes: 34 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -15492,6 +15492,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.h>

int main() {
// Long enough (> 16 bytes) that UTF8ToString on the script string
// takes the TextDecoder path.
emscripten_run_script("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
Loading