From e596e3899da0cfb590febd5d0cd0cbf3093cad64 Mon Sep 17 00:00:00 2001 From: Ehsan Date: Mon, 22 Dec 2025 10:09:09 +0100 Subject: [PATCH 1/5] [embind] Add iterable protocol support for bound classes Add `class_::iterable()` to implement `Symbol.iterator`. Use this in `register_vector` so bound vectors have better ergonomics, such as working with `for...of` and `Array.from()`. This is tangentially related to emscripten-core/emscripten#11070. --- src/lib/libembind.js | 46 +++++++++++++++++++++++++++ src/lib/libembind_gen.js | 19 +++++++++-- system/include/emscripten/bind.h | 23 +++++++++++++- test/embind/embind.test.js | 28 ++++++++++++++++ test/embind/embind_test.cpp | 22 +++++++++++++ test/other/embind_tsgen.cpp | 16 ++++++++++ test/other/embind_tsgen.d.ts | 10 +++++- test/other/embind_tsgen_ignore_1.d.ts | 10 +++++- test/other/embind_tsgen_ignore_2.d.ts | 10 +++++- test/other/embind_tsgen_ignore_3.d.ts | 10 +++++- test/other/embind_tsgen_module.d.ts | 10 +++++- 11 files changed, 196 insertions(+), 8 deletions(-) diff --git a/src/lib/libembind.js b/src/lib/libembind.js index 7ed186cacd587..bd44fa12a1e44 100644 --- a/src/lib/libembind.js +++ b/src/lib/libembind.js @@ -421,6 +421,39 @@ var LibraryEmbind = { return this.fromWireType({{{ makeGetValue('pointer', '0', '*') }}}); }, + $installIndexedIterator: (proto, sizeName, getName) => { + if (typeof Symbol === 'undefined' || !Symbol.iterator) { + return; + } + + const makeIterator = (size, getValue) => { + const useBigInt = typeof size === 'bigint'; + const one = useBigInt ? 1n : 1; + let index = useBigInt ? 0n : 0; + return { + next() { + if (index >= size) { + return { done: true }; + } + const current = index; + index += one; + const value = getValue(current); + return { value, done: false }; + }, + [Symbol.iterator]() { + return this; + }, + }; + }; + + if (!proto[Symbol.iterator]) { + proto[Symbol.iterator] = function() { + const size = this[sizeName](); + return makeIterator(size, (i) => this[getName](i)); + }; + } + }, + _embind_register_std_string__deps: [ '$AsciiToString', '$registerType', '$readPointer', '$throwBindingError', @@ -1723,6 +1756,19 @@ var LibraryEmbind = { ); }, + _embind_register_iterable__deps: [ + '$whenDependentTypesAreResolved', '$installIndexedIterator', '$AsciiToString', + ], + _embind_register_iterable: (rawClassType, rawElementType, sizeName, getName) => { + sizeName = AsciiToString(sizeName); + getName = AsciiToString(getName); + whenDependentTypesAreResolved([], [rawClassType, rawElementType], (types) => { + const classType = types[0]; + installIndexedIterator(classType.registeredClass.instancePrototype, sizeName, getName); + return []; + }); + }, + _embind_register_class_constructor__deps: [ '$heap32VectorToArray', '$embind__requireFunction', '$whenDependentTypesAreResolved', diff --git a/src/lib/libembind_gen.js b/src/lib/libembind_gen.js index 4d797c34f8e74..f900b383d099f 100644 --- a/src/lib/libembind_gen.js +++ b/src/lib/libembind_gen.js @@ -177,6 +177,7 @@ var LibraryEmbind = { this.constructors = []; this.base = base; this.properties = []; + this.iterableElementType = null; this.destructorType = 'none'; if (base) { this.destructorType = 'stack'; @@ -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 = []; @@ -652,6 +658,15 @@ var LibraryEmbind = { ); }, + _embind_register_iterable__deps: ['$whenDependentTypesAreResolved'], + _embind_register_iterable: (rawClassType, rawElementType, sizeName, getName) => { + 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, diff --git a/system/include/emscripten/bind.h b/system/include/emscripten/bind.h index ad5d836a886fb..a313c481f8a33 100644 --- a/system/include/emscripten/bind.h +++ b/system/include/emscripten/bind.h @@ -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* sizeName, + const char* getName); + EM_VAL _embind_create_inheriting_constructor( const char* constructorName, TYPEID wrapperType, @@ -1587,6 +1593,19 @@ class class_ { return *this; } + template + EMSCRIPTEN_ALWAYS_INLINE const class_& iterable( + const char* sizeName, + const char* getName) const { + using namespace internal; + _embind_register_iterable( + TypeID::get(), + TypeID::get(), + sizeName, + getName); + return *this; + } + template< typename FieldType, typename... Policies, @@ -1847,6 +1866,8 @@ template> class_> register_vector(const char* name) { typedef std::vector VecType; register_optional(); + using VectorElementType = + typename internal::RawPointerTransformer::value>::type; return class_(name) .template constructor<>() @@ -1855,7 +1876,7 @@ class_> register_vector(const char* name) { .function("size", internal::VectorAccess::size, allow_raw_pointers()) .function("get", internal::VectorAccess::get, allow_raw_pointers()) .function("set", internal::VectorAccess::set, allow_raw_pointers()) - ; + .template iterable("size", "get"); } //////////////////////////////////////////////////////////////////////////////// diff --git a/test/embind/embind.test.js b/test/embind/embind.test.js index 67ca8966a9587..a26b1d02ab1a0 100644 --- a/test/embind/embind.test.js +++ b/test/embind/embind.test.js @@ -1141,6 +1141,34 @@ module({ small.delete(); vec.delete(); }); + + test("std::vector is iterable", function() { + if (typeof Symbol === "undefined" || !Symbol.iterator) { + return; + } + 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() { + if (typeof Symbol === "undefined" || !Symbol.iterator) { + return; + } + 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(); + }); }); BaseFixture.extend("map", function() { diff --git a/test/embind/embind_test.cpp b/test/embind/embind_test.cpp index 7352850270d25..f47802ff7bb2c 100644 --- a/test/embind/embind_test.cpp +++ b/test/embind/embind_test.cpp @@ -1304,6 +1304,22 @@ std::vector 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 values_; +}; + void test_string_with_vec(const std::string& p1, std::vector& v1) { // THIS DOES NOT WORK -- need to get as val and then call vecFromJSArray printf("%s\n", p1.c_str()); @@ -1908,6 +1924,12 @@ EMSCRIPTEN_BINDINGS(tests) { register_vector>("IntegerVectorVector"); register_vector("SmallClassPointerVector"); + class_("CustomIterable") + .constructor<>() + .function("count", &CustomIterable::count) + .function("at", &CustomIterable::at) + .iterable("count", "at"); + class_("DummyForPointer"); function("mallinfo", &emval_test_mallinfo); diff --git a/test/other/embind_tsgen.cpp b/test/other/embind_tsgen.cpp index be2faa8545231..15dab4842bb9c 100644 --- a/test/other/embind_tsgen.cpp +++ b/test/other/embind_tsgen.cpp @@ -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 class_unique_ptr_returning_fn() { @@ -234,6 +244,12 @@ EMSCRIPTEN_BINDINGS(Test) { register_vector("IntVec"); + class_("IterableClass") + .constructor<>() + .function("count", &IterableClass::count) + .function("at", &IterableClass::at) + .iterable("count", "at"); + register_map("MapIntInt"); class_("Foo").function("process", &Foo::process); diff --git a/test/other/embind_tsgen.d.ts b/test/other/embind_tsgen.d.ts index ba39197ac2959..5f0a63bddcb02 100644 --- a/test/other/embind_tsgen.d.ts +++ b/test/other/embind_tsgen.d.ts @@ -47,7 +47,7 @@ export type EmptyEnum = never/* Empty Enumerator */; export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ]; -export interface IntVec extends ClassHandle { +export interface IntVec extends ClassHandle, Iterable { push_back(_0: number): void; resize(_0: number, _1: number): void; size(): number; @@ -55,6 +55,11 @@ export interface IntVec extends ClassHandle { set(_0: number, _1: number): boolean; } +export interface IterableClass extends ClassHandle, Iterable { + count(): number; + at(_0: number): number; +} + export interface MapIntInt extends ClassHandle { keys(): IntVec; get(_0: number): number | undefined; @@ -131,6 +136,9 @@ interface EmbindModule { IntVec: { new(): IntVec; }; + IterableClass: { + new(): IterableClass; + }; MapIntInt: { new(): MapIntInt; }; diff --git a/test/other/embind_tsgen_ignore_1.d.ts b/test/other/embind_tsgen_ignore_1.d.ts index 94f7b132197cd..fb959de83e2ad 100644 --- a/test/other/embind_tsgen_ignore_1.d.ts +++ b/test/other/embind_tsgen_ignore_1.d.ts @@ -58,7 +58,7 @@ export type EmptyEnum = never/* Empty Enumerator */; export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ]; -export interface IntVec extends ClassHandle { +export interface IntVec extends ClassHandle, Iterable { push_back(_0: number): void; resize(_0: number, _1: number): void; size(): number; @@ -66,6 +66,11 @@ export interface IntVec extends ClassHandle { set(_0: number, _1: number): boolean; } +export interface IterableClass extends ClassHandle, Iterable { + count(): number; + at(_0: number): number; +} + export interface MapIntInt extends ClassHandle { keys(): IntVec; get(_0: number): number | undefined; @@ -142,6 +147,9 @@ interface EmbindModule { IntVec: { new(): IntVec; }; + IterableClass: { + new(): IterableClass; + }; MapIntInt: { new(): MapIntInt; }; diff --git a/test/other/embind_tsgen_ignore_2.d.ts b/test/other/embind_tsgen_ignore_2.d.ts index b622ebb645c48..7502f0c7289bd 100644 --- a/test/other/embind_tsgen_ignore_2.d.ts +++ b/test/other/embind_tsgen_ignore_2.d.ts @@ -46,7 +46,7 @@ export type EmptyEnum = never/* Empty Enumerator */; export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ]; -export interface IntVec extends ClassHandle { +export interface IntVec extends ClassHandle, Iterable { push_back(_0: number): void; resize(_0: number, _1: number): void; size(): number; @@ -54,6 +54,11 @@ export interface IntVec extends ClassHandle { set(_0: number, _1: number): boolean; } +export interface IterableClass extends ClassHandle, Iterable { + count(): number; + at(_0: number): number; +} + export interface MapIntInt extends ClassHandle { keys(): IntVec; get(_0: number): number | undefined; @@ -130,6 +135,9 @@ interface EmbindModule { IntVec: { new(): IntVec; }; + IterableClass: { + new(): IterableClass; + }; MapIntInt: { new(): MapIntInt; }; diff --git a/test/other/embind_tsgen_ignore_3.d.ts b/test/other/embind_tsgen_ignore_3.d.ts index ba39197ac2959..5f0a63bddcb02 100644 --- a/test/other/embind_tsgen_ignore_3.d.ts +++ b/test/other/embind_tsgen_ignore_3.d.ts @@ -47,7 +47,7 @@ export type EmptyEnum = never/* Empty Enumerator */; export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ]; -export interface IntVec extends ClassHandle { +export interface IntVec extends ClassHandle, Iterable { push_back(_0: number): void; resize(_0: number, _1: number): void; size(): number; @@ -55,6 +55,11 @@ export interface IntVec extends ClassHandle { set(_0: number, _1: number): boolean; } +export interface IterableClass extends ClassHandle, Iterable { + count(): number; + at(_0: number): number; +} + export interface MapIntInt extends ClassHandle { keys(): IntVec; get(_0: number): number | undefined; @@ -131,6 +136,9 @@ interface EmbindModule { IntVec: { new(): IntVec; }; + IterableClass: { + new(): IterableClass; + }; MapIntInt: { new(): MapIntInt; }; diff --git a/test/other/embind_tsgen_module.d.ts b/test/other/embind_tsgen_module.d.ts index ea6b7353a68a9..f503e6cce19e0 100644 --- a/test/other/embind_tsgen_module.d.ts +++ b/test/other/embind_tsgen_module.d.ts @@ -47,7 +47,7 @@ export type EmptyEnum = never/* Empty Enumerator */; export type ValArrIx = [ FirstEnum, FirstEnum, FirstEnum, FirstEnum ]; -export interface IntVec extends ClassHandle { +export interface IntVec extends ClassHandle, Iterable { push_back(_0: number): void; resize(_0: number, _1: number): void; size(): number; @@ -55,6 +55,11 @@ export interface IntVec extends ClassHandle { set(_0: number, _1: number): boolean; } +export interface IterableClass extends ClassHandle, Iterable { + count(): number; + at(_0: number): number; +} + export interface MapIntInt extends ClassHandle { keys(): IntVec; get(_0: number): number | undefined; @@ -131,6 +136,9 @@ interface EmbindModule { IntVec: { new(): IntVec; }; + IterableClass: { + new(): IterableClass; + }; MapIntInt: { new(): MapIntInt; }; From e67658d7fe7c1e9f6e16eeed04d402f341ca8de0 Mon Sep 17 00:00:00 2001 From: Ehsan Date: Mon, 22 Dec 2025 11:47:27 +0100 Subject: [PATCH 2/5] Add missing signature --- src/lib/libsigs.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index ba71e1e5b30d7..83a71a9056a9e 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -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', From 340ad723ed9e6badeee818c485514d60de80622d Mon Sep 17 00:00:00 2001 From: Ehsan Date: Tue, 23 Dec 2025 11:16:45 +0100 Subject: [PATCH 3/5] Rename parameters for clarity --- src/lib/libembind.js | 14 +++++++------- src/lib/libembind_gen.js | 2 +- system/include/emscripten/bind.h | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/lib/libembind.js b/src/lib/libembind.js index bd44fa12a1e44..b6e50daa60561 100644 --- a/src/lib/libembind.js +++ b/src/lib/libembind.js @@ -421,7 +421,7 @@ var LibraryEmbind = { return this.fromWireType({{{ makeGetValue('pointer', '0', '*') }}}); }, - $installIndexedIterator: (proto, sizeName, getName) => { + $installIndexedIterator: (proto, sizeMethodName, getMethodName) => { if (typeof Symbol === 'undefined' || !Symbol.iterator) { return; } @@ -448,8 +448,8 @@ var LibraryEmbind = { if (!proto[Symbol.iterator]) { proto[Symbol.iterator] = function() { - const size = this[sizeName](); - return makeIterator(size, (i) => this[getName](i)); + const size = this[sizeMethodName](); + return makeIterator(size, (i) => this[getMethodName](i)); }; } }, @@ -1759,12 +1759,12 @@ var LibraryEmbind = { _embind_register_iterable__deps: [ '$whenDependentTypesAreResolved', '$installIndexedIterator', '$AsciiToString', ], - _embind_register_iterable: (rawClassType, rawElementType, sizeName, getName) => { - sizeName = AsciiToString(sizeName); - getName = AsciiToString(getName); + _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, sizeName, getName); + installIndexedIterator(classType.registeredClass.instancePrototype, sizeMethodName, getMethodName); return []; }); }, diff --git a/src/lib/libembind_gen.js b/src/lib/libembind_gen.js index f900b383d099f..bdc75e4c8a7e8 100644 --- a/src/lib/libembind_gen.js +++ b/src/lib/libembind_gen.js @@ -659,7 +659,7 @@ var LibraryEmbind = { }, _embind_register_iterable__deps: ['$whenDependentTypesAreResolved'], - _embind_register_iterable: (rawClassType, rawElementType, sizeName, getName) => { + _embind_register_iterable: (rawClassType, rawElementType, sizeMethodName, getMethodName) => { whenDependentTypesAreResolved([], [rawClassType, rawElementType], (types) => { const classType = types[0]; const elementType = types[1]; diff --git a/system/include/emscripten/bind.h b/system/include/emscripten/bind.h index a313c481f8a33..06e4a863e92d6 100644 --- a/system/include/emscripten/bind.h +++ b/system/include/emscripten/bind.h @@ -220,8 +220,8 @@ void _embind_register_class_class_property( void _embind_register_iterable( TYPEID classType, TYPEID elementType, - const char* sizeName, - const char* getName); + const char* sizeMethodName, + const char* getMethodName); EM_VAL _embind_create_inheriting_constructor( const char* constructorName, @@ -1595,14 +1595,14 @@ class class_ { template EMSCRIPTEN_ALWAYS_INLINE const class_& iterable( - const char* sizeName, - const char* getName) const { + const char* sizeMethodName, + const char* getMethodName) const { using namespace internal; _embind_register_iterable( TypeID::get(), TypeID::get(), - sizeName, - getName); + sizeMethodName, + getMethodName); return *this; } From 3db19f49017b654e588eb8c83d17d450d6b00ef6 Mon Sep 17 00:00:00 2001 From: Ehsan Date: Tue, 23 Dec 2025 11:17:01 +0100 Subject: [PATCH 4/5] Add some documentation --- site/source/docs/api_reference/bind.h.rst | 32 +++++++++++++++++++ .../connecting_cpp_and_javascript/embind.rst | 9 ++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/site/source/docs/api_reference/bind.h.rst b/site/source/docs/api_reference/bind.h.rst index 1d5f6401a51f2..8f0ec53472ba8 100644 --- a/site/source/docs/api_reference/bind.h.rst +++ b/site/source/docs/api_reference/bind.h.rst @@ -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 + 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") + .function("size", &MyContainer::size) + .function("get", &MyContainer::get) + .iterable("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 diff --git a/site/source/docs/porting/connecting_cpp_and_javascript/embind.rst b/site/source/docs/porting/connecting_cpp_and_javascript/embind.rst index b697a996bda38..3af11ed452a04 100644 --- a/site/source/docs/porting/connecting_cpp_and_javascript/embind.rst +++ b/site/source/docs/porting/connecting_cpp_and_javascript/embind.rst @@ -1237,9 +1237,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 From 281d6b934ba15ad94bcb62e8d2a7a9f26e803212 Mon Sep 17 00:00:00 2001 From: Ehsan Date: Tue, 30 Dec 2025 22:05:07 +0100 Subject: [PATCH 5/5] Remove feature checks for Symbol.iterator --- src/lib/libembind.js | 4 ---- test/embind/embind.test.js | 6 ------ 2 files changed, 10 deletions(-) diff --git a/src/lib/libembind.js b/src/lib/libembind.js index b6e50daa60561..36ae7ec0ff5ee 100644 --- a/src/lib/libembind.js +++ b/src/lib/libembind.js @@ -422,10 +422,6 @@ var LibraryEmbind = { }, $installIndexedIterator: (proto, sizeMethodName, getMethodName) => { - if (typeof Symbol === 'undefined' || !Symbol.iterator) { - return; - } - const makeIterator = (size, getValue) => { const useBigInt = typeof size === 'bigint'; const one = useBigInt ? 1n : 1; diff --git a/test/embind/embind.test.js b/test/embind/embind.test.js index a26b1d02ab1a0..c6621b8d28aee 100644 --- a/test/embind/embind.test.js +++ b/test/embind/embind.test.js @@ -1143,9 +1143,6 @@ module({ }); test("std::vector is iterable", function() { - if (typeof Symbol === "undefined" || !Symbol.iterator) { - return; - } var vec = cm.emval_test_return_vector(); var values = []; for (var value of vec) { @@ -1157,9 +1154,6 @@ module({ }); test("custom class is iterable", function() { - if (typeof Symbol === "undefined" || !Symbol.iterator) { - return; - } var iterable = new cm.CustomIterable(); var values = []; for (var value of iterable) {