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
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ See docs/process.md for more on how version tagging works.
the per-call garbage on such calls. The registration ABI gained size and
triviality parameters, so object files built against an older `bind.h` need
to be rebuilt. (#27610)
- Embind `value_object` and `value_array` element writes are now generated
per value type when `DYNAMIC_EXECUTION` is enabled, giving each element's
wasm setter its own call site. On V8 this keeps non-integer number fields
from being boxed on the way in, which was measured as 16 bytes of garbage
per such field in modules with many value types. (#27751)
- `emscripten_clear_timeout` now releases the runtime keepalive held by the
pending timeout, and both `emscripten_clear_timeout` and
`emscripten_clear_immediate` are no-ops for ids that already fired. (#27720)
Expand Down
147 changes: 106 additions & 41 deletions src/lib/libembind.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ var LibraryEmbind = {
// https://www.w3.org/TR/wasm-js-api-1/#towebassemblyvalue
return value;
},
#if !ASSERTIONS
// With no checks above, toWireType is the identity: the VM's own
// JS-to-Wasm coercion does the work. Value types use this to pass
// element values straight to their wasm setters (see makeValueWriter).
identityToWireType: true,
#endif
readValueFromPointer: integerReadValueFromPointer(name, size, minRange !== 0),
destructorFunction: null, // This type does not need a destructor
});
Expand Down Expand Up @@ -411,6 +417,10 @@ var LibraryEmbind = {
// https://www.w3.org/TR/wasm-js-api-1/#towebassemblyvalue
return value;
},
#if !ASSERTIONS
// See _embind_register_integer.
identityToWireType: true,
#endif
readValueFromPointer: floatReadValueFromPointer(name, size),
destructorFunction: null, // This type does not need a destructor
});
Expand Down Expand Up @@ -958,9 +968,81 @@ var LibraryEmbind = {
});
},

// Builds the element write step of a value_array or value_object: a
// function that passes each element of `o` to the wasm setter for its
// slot. `keys` are the indices or field names to read from `o`.
//
// Element types that register no destructor skip the per-write
// destructors array. Stack-allocating types still get one: a null
// destructors argument means an invoker-managed stack frame, which a
// nested write cannot assume.
//
// Number element values must reach the wasm setter without crossing a JS
// call V8 cannot inline: every non-Smi double passing through such a call
// is boxed (16 bytes of garbage per element), and V8 only inlines a
// JS-to-wasm call whose site has a single known target. A per-element
// closure built from one literal serves every element of every value type
// in the module, so its setter call site turns megamorphic once a few
// value types are registered. With dynamic execution the writer is
// generated per value type with one call per element, each site
// monomorphic, and number types with an identity conversion (see
// _embind_register_integer) are passed straight through.
$makeValueWriter__deps: ['$runDestructors'],
$makeValueWriter: (setters, setterContexts, setterArgumentTypes, keys) => {
#if DYNAMIC_EXECUTION
var params = ['runDestructors'];
var args = [runDestructors];
var body = '';
for (var i = 0; i < setters.length; ++i) {
var type = setterArgumentTypes[i];
params.push(`s${i}`, `c${i}`);
args.push(setters[i], setterContexts[i]);
var value = `o[${JSON.stringify(keys[i])}]`;
var needsDestructors = type.destructorFunction !== null || type.argStackAlloc;
if (!type.identityToWireType) {
// Bound and passed in rather than named in the generated source, so
// closure compiler property renaming stays out of the picture.
params.push(`toWire${i}`);
args.push(type.toWireType.bind(type));
if (needsDestructors) {
body += `var d${i} = [];\n`;
value = `toWire${i}(d${i}, ${value})`;
} else {
value = `toWire${i}(null, ${value})`;
}
}
body += `s${i}(c${i}, ptr, ${value});\n`;
if (needsDestructors) {
body += `runDestructors(d${i});\n`;
}
}
return new Function(...params, `return (ptr, o) => {\n${body}};`)(...args);
#else
var writers = setters.map((setter, i) => {
var context = setterContexts[i];
var type = setterArgumentTypes[i];
var key = keys[i];
if (type.destructorFunction === null && !type.argStackAlloc) {
return (ptr, o) => setter(context, ptr, type.toWireType(null, o[key]));
}
return (ptr, o) => {
var destructors = [];
setter(context, ptr, type.toWireType(destructors, o[key]));
runDestructors(destructors);
};
});
return (ptr, o) => {
for (var i = 0; i < writers.length; ++i) {
writers[i](ptr, o);
}
};
#endif
},

_embind_finalize_value_array__deps: [
'$tupleRegistrations', '$runDestructors',
'$readPointer', '$whenDependentTypesAreResolved', '$stackAlloc', '$zeroMemory'],
'$tupleRegistrations',
'$readPointer', '$whenDependentTypesAreResolved', '$stackAlloc', '$zeroMemory',
'$makeValueWriter'],
_embind_finalize_value_array: (rawTupleType) => {
var reg = tupleRegistrations[rawTupleType];
delete tupleRegistrations[rawTupleType];
Expand All @@ -979,24 +1061,13 @@ var LibraryEmbind = {
const getterReturnType = elementTypes[i];
const getter = elt.getter;
const getterContext = elt.getterContext;
const setterArgumentType = elementTypes[i + elementsLength];
const setter = elt.setter;
const setterContext = elt.setterContext;
elt.read = (ptr) => getterReturnType.fromWireType(getter(getterContext, ptr));
if (setterArgumentType.destructorFunction === null && !setterArgumentType.argStackAlloc) {
// The element type never registers a destructor, so skip the
// per-write destructors array. (Stack-allocating types still need
// the array here: a null destructors argument means an
// invoker-managed stack frame, which a nested write cannot assume.)
elt.write = (ptr, o) => setter(setterContext, ptr, setterArgumentType.toWireType(null, o));
} else {
elt.write = (ptr, o) => {
var destructors = [];
setter(setterContext, ptr, setterArgumentType.toWireType(destructors, o));
runDestructors(destructors);
};
}
}
var writeElements = makeValueWriter(
elements.map((elt) => elt.setter),
elements.map((elt) => elt.setterContext),
elementTypes.slice(elementsLength),
elements.map((elt, i) => i));

return [{
name: reg.name,
Expand Down Expand Up @@ -1029,9 +1100,7 @@ var LibraryEmbind = {
destructors.push(rawDestructor, ptr);
}
}
for (var i = 0; i < elementsLength; ++i) {
elements[i].write(ptr, o[i]);
}
writeElements(ptr, o);
return ptr;
},
readValueFromPointer: readPointer,
Expand Down Expand Up @@ -1091,8 +1160,9 @@ var LibraryEmbind = {
},

_embind_finalize_value_object__deps: [
'$structRegistrations', '$runDestructors',
'$readPointer', '$whenDependentTypesAreResolved', '$stackAlloc', '$zeroMemory'],
'$structRegistrations',
'$readPointer', '$whenDependentTypesAreResolved', '$stackAlloc', '$zeroMemory',
'$makeValueWriter'],
_embind_finalize_value_object: (structType) => {
var reg = structRegistrations[structType];
delete structRegistrations[structType];
Expand All @@ -1110,26 +1180,23 @@ var LibraryEmbind = {
const getterReturnType = fieldTypes[i];
const getter = field.getter;
const getterContext = field.getterContext;
const setterArgumentType = fieldTypes[i + fieldRecords.length];
const setter = field.setter;
const setterContext = field.setterContext;
var write;
if (setterArgumentType.destructorFunction === null && !setterArgumentType.argStackAlloc) {
// See the matching element-write logic in _embind_finalize_value_array.
write = (ptr, o) => setter(setterContext, ptr, setterArgumentType.toWireType(null, o));
} else {
write = (ptr, o) => {
var destructors = [];
setter(setterContext, ptr, setterArgumentType.toWireType(destructors, o));
runDestructors(destructors);
};
}
fields[field.fieldName] = {
read: (ptr) => getterReturnType.fromWireType(getter(getterContext, ptr)),
write,
setter: field.setter,
setterContext: field.setterContext,
setterArgumentType: fieldTypes[i + fieldRecords.length],
optional: getterReturnType.optional,
};
}
// Write in the enumeration order of `fields`, one write per surviving
// name (a field registered twice keeps its last registration), the same
// order and set the reads use.
var fieldNames = Object.keys(fields);
var writeFields = makeValueWriter(
fieldNames.map((name) => fields[name].setter),
fieldNames.map((name) => fields[name].setterContext),
fieldNames.map((name) => fields[name].setterArgumentType),
fieldNames);

return [{
name: reg.name,
Expand Down Expand Up @@ -1163,9 +1230,7 @@ var LibraryEmbind = {
destructors.push(rawDestructor, ptr);
}
}
for (fieldName in fields) {
fields[fieldName].write(ptr, o[fieldName]);
}
writeFields(ptr, o);
return ptr;
},
readValueFromPointer: readPointer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.html": 548,
"a.html.gz": 371,
"a.js": 7487,
"a.js.gz": 3437,
"a.js": 7497,
"a.js.gz": 3441,
"a.wasm": 7201,
"a.wasm.gz": 3303,
"total": 15236,
"total_gz": 7111
"total": 15246,
"total_gz": 7115
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.html": 548,
"a.html.gz": 371,
"a.js": 5349,
"a.js.gz": 2515,
"a.js": 5359,
"a.js.gz": 2520,
"a.wasm": 5817,
"a.wasm.gz": 2736,
"total": 11714,
"total_gz": 5622
"total": 11724,
"total_gz": 5627
}
79 changes: 79 additions & 0 deletions test/embind/test_embind_value_object_field_order.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2026 The Emscripten Authors. All rights reserved.
// Emscripten is available under two separate licenses, the MIT license and the
// University of Illinois/NCSA Open Source License. Both these licenses can be
// found in the LICENSE file.

// A value_object writes its fields in the enumeration order of its field
// table, integer-like names first, and a name registered twice keeps only
// its last registration. Both must hold on the generated and the closure
// write paths, and match the read order.

#include <assert.h>
#include <stdio.h>

#include <emscripten/bind.h>
#include <emscripten/emscripten.h>

struct Rec {
int b;
int two;
int a;
int first;
int last;
};

static int lastSetterCalls = 0;
static int firstSetterCalls = 0;

int getA(const Rec& r) { return r.a; }
void setA(Rec& r, int v) { r.a = v; }
int getB(const Rec& r) { return r.b; }
void setB(Rec& r, int v) { r.b = v; }
int getTwo(const Rec& r) { return r.two; }
void setTwo(Rec& r, int v) { r.two = v; }
int getFirst(const Rec& r) { return r.first; }
void setFirst(Rec& r, int v) { ++firstSetterCalls; r.first = v; }
int getLast(const Rec& r) { return r.last; }
void setLast(Rec& r, int v) { ++lastSetterCalls; r.last = v; }

int sum(Rec r) {
return r.b + r.two + r.a + r.first + r.last;
}

int firstCalls() { return firstSetterCalls; }
int lastCalls() { return lastSetterCalls; }

EMSCRIPTEN_BINDINGS(field_order) {
emscripten::value_object<Rec>("Rec")
.field("b", &getB, &setB)
.field("2", &getTwo, &setTwo)
.field("a", &getA, &setA)
// "x" registered twice: only the second registration must write.
.field("x", &getFirst, &setFirst)
.field("x", &getLast, &setLast);

emscripten::function("sum", &sum);
emscripten::function("firstCalls", &firstCalls);
emscripten::function("lastCalls", &lastCalls);
}

int main() {
EM_ASM({
var order = [];
var o = {};
var define = (name, value) => Object.defineProperty(o, name, {
get: () => { order.push(name); return value; }, enumerable: true
});
define('b', 1);
define('2', 20);
define('a', 300);
define('x', 4000);
if (Module['sum'](o) !== 4321) throw 'sum';
// Integer-like names enumerate first, then insertion order.
if (order.join() !== '2,b,a,x') throw 'write order: ' + order.join();
if (Module['firstCalls']() !== 0) throw 'first registration of x wrote';
if (Module['lastCalls']() !== 1) throw 'last registration of x did not write once';
});
printf("done\n");
return 0;
}
9 changes: 9 additions & 0 deletions test/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -3652,6 +3652,15 @@ def test_embind_trivial_value_stack(self, *extra_args):
self.require_wasm64()
self.do_runf('embind/test_embind_trivial_value_stack.cpp', 'done\n', cflags=['-lembind', *extra_args])

@parameterized({
'': (),
'no_dynamic': ('-sDYNAMIC_EXECUTION=0',),
})
def test_embind_value_object_field_order(self, *extra_args):
# Field writes follow the field table's enumeration order and a name
# registered twice keeps its last registration, on both write paths.
self.do_runf('embind/test_embind_value_object_field_order.cpp', 'done\n', cflags=['-lembind', *extra_args])

@requires_node_25
def test_embind_resource_management(self):
self.node_args.append('--js-explicit-resource-management')
Expand Down
Loading