Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ See docs/process.md for more on how version tagging works.
- A new `-sEXECUTABLE` setting was added which adds a #! line to the resulting
JavaScript and makes it executable. This setting defaults to true when the
output filename has no extension, or ends in `.out` (e.g. `a.out`) (#26085)
JavaScript and makes it executable. (#26085)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks like a bad merge? Delete this line maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, my bad.

- Embind now supports the JS iterable protocol on bound classes via
`class_<T>::iterable()`. `register_vector` uses this so bound `std::vector`
works with `for...of`/`Array.from()`/spread.

4.0.23 - 01/10/26
-----------------
Expand Down
32 changes: 32 additions & 0 deletions site/source/docs/api_reference/bind.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,38 @@ Classes
:param typename... Policies: |policies-argument|
:returns: |class_-function-returns|

.. cpp:function:: const class_& iterable() const

.. code-block:: cpp

// prototype
template<typename ElementType>
EMSCRIPTEN_ALWAYS_INLINE const class_& iterable(const char* sizeMethodName, const char* getMethodName) const

Makes a bound class iterable in JavaScript by installing ``Symbol.iterator``.
This enables use with ``for...of`` loops, ``Array.from()``, and spread syntax.

:tparam ElementType: The type of elements yielded by the iterator.

:param sizeMethodName: Name of the bound method that returns the number of elements.

:param getMethodName: Name of the bound method that retrieves an element by index.

:returns: |class_-function-returns|

.. code-block:: cpp

class_<MyContainer>("MyContainer")
.function("size", &MyContainer::size)
.function("get", &MyContainer::get)
.iterable<int>("size", "get");

.. code-block:: javascript

const container = new Module.MyContainer();
for (const item of container) { /* ... */ }
const arr = Array.from(container);


.. cpp:function:: const class_& property() const

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1239,9 +1239,12 @@ The following JavaScript can be used to interact with the above C++.
// push value into vector
retVector.push_back(12);

// retrieve value from the vector
for (var i = 0; i < retVector.size(); i++) {
console.log("Vector Value: ", retVector.get(i));
// retrieve a value from the vector
console.log("Vector Value at index 0: ", retVector.get(0));

// iterate over vector
for (var value of retVector) {
console.log("Vector Value: ", value);
}

// expand vector size
Expand Down
51 changes: 51 additions & 0 deletions src/lib/libembind.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,44 @@ var LibraryEmbind = {
return this.fromWireType({{{ makeGetValue('pointer', '0', '*') }}});
},

$installIndexedIterator: (proto, sizeMethodName, getMethodName) => {
const makeIterator = (size, getValue) => {
#if MEMORY64
// size can be either a number or a bigint on wasm64
const useBigInt = typeof size === 'bigint';
const one = useBigInt ? 1n : 1;
let index = useBigInt ? 0n : 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we simply dictate that interables using size_t i.e. bigint on wasm64 and number on wasm32?

I don't image we will ever see 64-bit iterators on wasm32, right?

Copy link
Contributor Author

@aestuans aestuans Dec 23, 2025

Choose a reason for hiding this comment

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

I tried doing this, but register_vector seems to use unsigned int on purpose, so I presume forcing Bigint on wasm64 is undesirable?

Copy link
Collaborator

Choose a reason for hiding this comment

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

In that case wouldn't we only need to support number here? i.e. if register_vector is using unsigned int couldn't we mandate that all iterators do the same?

Copy link
Collaborator

Choose a reason for hiding this comment

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

i.e. could we just assert typeof size === 'number' here, or are there cases (in the test code) where this is not true?

Copy link
Contributor Author

@aestuans aestuans Jan 13, 2026

Choose a reason for hiding this comment

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

I thought it would be better to leave it to the users whether they want size_t/bigint in their container classes or not, since it's fairly trivial to support both for the iterable.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair enough, but if we want explicitly support this then maybe we should add test for it?

Also, should these lines be behind #if MEMORY64, of do you think somebody might using int64 as an interator an wasm32 ? Seems unlikely. Purring them behind #if MEMORY64 would also help signal the intent here I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Done.

#else
let index = 0;
#endif
return {
next() {
if (index >= size) {
return { done: true };
}
const current = index;
#if MEMORY64
index += one;
#else
index++;
#endif
const value = getValue(current);
return { value, done: false };
},
[Symbol.iterator]() {
return this;
},
};
};

if (!proto[Symbol.iterator]) {
proto[Symbol.iterator] = function() {
const size = this[sizeMethodName]();
return makeIterator(size, (i) => this[getMethodName](i));
};
}
},

_embind_register_std_string__deps: [
'$AsciiToString', '$registerType',
'$readPointer', '$throwBindingError',
Expand Down Expand Up @@ -1723,6 +1761,19 @@ var LibraryEmbind = {
);
},

_embind_register_iterable__deps: [
'$whenDependentTypesAreResolved', '$installIndexedIterator', '$AsciiToString',
],
_embind_register_iterable: (rawClassType, rawElementType, sizeMethodName, getMethodName) => {
sizeMethodName = AsciiToString(sizeMethodName);
getMethodName = AsciiToString(getMethodName);
whenDependentTypesAreResolved([], [rawClassType, rawElementType], (types) => {
const classType = types[0];
installIndexedIterator(classType.registeredClass.instancePrototype, sizeMethodName, getMethodName);
return [];
});
},

_embind_register_class_constructor__deps: [
'$heap32VectorToArray', '$embind__requireFunction',
'$whenDependentTypesAreResolved',
Expand Down
19 changes: 17 additions & 2 deletions src/lib/libembind_gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ var LibraryEmbind = {
this.constructors = [];
this.base = base;
this.properties = [];
this.iterableElementType = null;
this.destructorType = 'none';
if (base) {
this.destructorType = 'stack';
Expand All @@ -185,11 +186,16 @@ var LibraryEmbind = {

print(nameMap, out) {
out.push(`export interface ${this.name}`);
const extendsParts = [];
if (this.base) {
out.push(` extends ${this.base.name}`);
extendsParts.push(this.base.name);
} else {
out.push(' extends ClassHandle');
extendsParts.push('ClassHandle');
}
if (this.iterableElementType) {
extendsParts.push(`Iterable<${nameMap(this.iterableElementType, true)}>`);
}
out.push(` extends ${extendsParts.join(', ')}`);
out.push(' {\n');
for (const property of this.properties) {
const props = [];
Expand Down Expand Up @@ -652,6 +658,15 @@ var LibraryEmbind = {
);

},
_embind_register_iterable__deps: ['$whenDependentTypesAreResolved'],
_embind_register_iterable: (rawClassType, rawElementType, sizeMethodName, getMethodName) => {
whenDependentTypesAreResolved([], [rawClassType, rawElementType], (types) => {
const classType = types[0];
const elementType = types[1];
classType.iterableElementType = elementType;
return [];
});
},
_embind_register_class_constructor__deps: ['$whenDependentTypesAreResolved', '$createFunctionDefinition'],
_embind_register_class_constructor: function(
rawClassType,
Expand Down
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ sigs = {
_embind_register_float__sig: 'vppp',
_embind_register_function__sig: 'vpippppii',
_embind_register_integer__sig: 'vpppii',
_embind_register_iterable__sig: 'vpppp',
_embind_register_memory_view__sig: 'vpip',
_embind_register_optional__sig: 'vpp',
_embind_register_smart_ptr__sig: 'vpppipppppppp',
Expand Down
23 changes: 22 additions & 1 deletion system/include/emscripten/bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ void _embind_register_class_class_property(
const char* setterSignature,
GenericFunction setter);

void _embind_register_iterable(
TYPEID classType,
TYPEID elementType,
const char* sizeMethodName,
const char* getMethodName);

EM_VAL _embind_create_inheriting_constructor(
const char* constructorName,
TYPEID wrapperType,
Expand Down Expand Up @@ -1587,6 +1593,19 @@ class class_ {
return *this;
}

template<typename ElementType>
EMSCRIPTEN_ALWAYS_INLINE const class_& iterable(
const char* sizeMethodName,
const char* getMethodName) const {
using namespace internal;
_embind_register_iterable(
TypeID<ClassType>::get(),
TypeID<ElementType>::get(),
sizeMethodName,
getMethodName);
return *this;
}

template<
typename FieldType,
typename... Policies,
Expand Down Expand Up @@ -1847,6 +1866,8 @@ template<typename T, class Allocator=std::allocator<T>>
class_<std::vector<T, Allocator>> register_vector(const char* name) {
typedef std::vector<T, Allocator> VecType;
register_optional<T>();
using VectorElementType =
typename internal::RawPointerTransformer<T, std::is_pointer<T>::value>::type;

return class_<VecType>(name)
.template constructor<>()
Expand All @@ -1855,7 +1876,7 @@ class_<std::vector<T, Allocator>> register_vector(const char* name) {
.function("size", internal::VectorAccess<VecType>::size, allow_raw_pointers())
.function("get", internal::VectorAccess<VecType>::get, allow_raw_pointers())
.function("set", internal::VectorAccess<VecType>::set, allow_raw_pointers())
;
.template iterable<VectorElementType>("size", "get");
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
38 changes: 38 additions & 0 deletions test/embind/embind.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,44 @@ module({
small.delete();
vec.delete();
});

test("std::vector is iterable", function() {
var vec = cm.emval_test_return_vector();
var values = [];
for (var value of vec) {
values.push(value);
}
assert.deepEqual([10, 20, 30], values);
assert.deepEqual([10, 20, 30], Array.from(vec));
vec.delete();
});

test("custom class is iterable", function() {
var iterable = new cm.CustomIterable();
var values = [];
for (var value of iterable) {
values.push(value);
}
assert.deepEqual([1, 2, 3], values);
assert.deepEqual([1, 2, 3], Array.from(iterable));
iterable.delete();
});

test("custom class with size_t is iterable", function() {
var iterable = new cm.CustomIterableSizeT();
if (cm.getCompilerSetting('MEMORY64')) {
assert.equal(typeof iterable.count(), 'bigint');
} else {
assert.equal(typeof iterable.count(), 'number');
}
var values = [];
for (var value of iterable) {
values.push(value);
}
assert.deepEqual([10, 20, 30], values);
assert.deepEqual([10, 20, 30], Array.from(iterable));
iterable.delete();
});
});

BaseFixture.extend("map", function() {
Expand Down
44 changes: 44 additions & 0 deletions test/embind/embind_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,38 @@ std::vector<SmallClass*> emval_test_return_vector_pointers() {
return vec;
}

class CustomIterable {
public:
CustomIterable() : values_({1, 2, 3}) {}

unsigned int count() const {
return values_.size();
}

int at(unsigned int index) const {
return values_[index];
}

private:
std::vector<int> values_;
};

class CustomIterableSizeT {
public:
CustomIterableSizeT() : values_({10, 20, 30}) {}

size_t count() const {
return values_.size();
}

int at(size_t index) const {
return values_[index];
}

private:
std::vector<int> values_;
};

void test_string_with_vec(const std::string& p1, std::vector<std::string>& v1) {
// THIS DOES NOT WORK -- need to get as val and then call vecFromJSArray
printf("%s\n", p1.c_str());
Expand Down Expand Up @@ -1908,6 +1940,18 @@ EMSCRIPTEN_BINDINGS(tests) {
register_vector<std::vector<int>>("IntegerVectorVector");
register_vector<SmallClass*>("SmallClassPointerVector");

class_<CustomIterable>("CustomIterable")
.constructor<>()
.function("count", &CustomIterable::count)
.function("at", &CustomIterable::at)
.iterable<int>("count", "at");

class_<CustomIterableSizeT>("CustomIterableSizeT")
.constructor<>()
.function("count", &CustomIterableSizeT::count)
.function("at", &CustomIterableSizeT::at)
.iterable<int>("count", "at");

class_<DummyForPointer>("DummyForPointer");

function("mallinfo", &emval_test_mallinfo);
Expand Down
16 changes: 16 additions & 0 deletions test/other/embind_tsgen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ class Foo {
void process(const Test& input) {}
};

class IterableClass {
public:
IterableClass() : data{1, 2, 3} {}
unsigned int count() const { return 3; }
int at(unsigned int index) const { return data[index]; }

private:
int data[3];
};

Test class_returning_fn() { return Test(); }

std::unique_ptr<Test> class_unique_ptr_returning_fn() {
Expand Down Expand Up @@ -234,6 +244,12 @@ EMSCRIPTEN_BINDINGS(Test) {

register_vector<int>("IntVec");

class_<IterableClass>("IterableClass")
.constructor<>()
.function("count", &IterableClass::count)
.function("at", &IterableClass::at)
.iterable<int>("count", "at");

register_map<int, int>("MapIntInt");

class_<Foo>("Foo").function("process", &Foo::process);
Expand Down
10 changes: 9 additions & 1 deletion test/other/embind_tsgen.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,19 @@ export type EmptyEnum = never/* Empty Enumerator */;

export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ];

export interface IntVec extends ClassHandle {
export interface IntVec extends ClassHandle, Iterable<number> {
push_back(_0: number): void;
resize(_0: number, _1: number): void;
size(): number;
get(_0: number): number | undefined;
set(_0: number, _1: number): boolean;
}

export interface IterableClass extends ClassHandle, Iterable<number> {
count(): number;
at(_0: number): number;
}

export interface MapIntInt extends ClassHandle {
keys(): IntVec;
get(_0: number): number | undefined;
Expand Down Expand Up @@ -131,6 +136,9 @@ interface EmbindModule {
IntVec: {
new(): IntVec;
};
IterableClass: {
new(): IterableClass;
};
MapIntInt: {
new(): MapIntInt;
};
Expand Down
Loading