Skip to content

Commit e60664d

Browse files
Update Embind+GC integration, vol. 2. (#15874)
An updated version of #15327 (that was reverted at #15460). Finalization will only affect smart ptrs now, as discussed there. I also added some notes on this to documentation.
1 parent babf381 commit e60664d

File tree

5 files changed

+90
-26
lines changed

5 files changed

+90
-26
lines changed

site/source/docs/porting/connecting_cpp_and_javascript/embind.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ to enable the closure compiler.
202202
Memory management
203203
=================
204204

205-
JavaScript, specifically ECMA-262 Edition 5.1, does not support `finalizers`_
206-
or weak references with callbacks. Therefore there is no way for Emscripten
207-
to automatically call the destructors on C++ objects.
205+
JavaScript only gained support for `finalizers`_ in ECMAScript 2021, or ECMA-262
206+
Edition 12. The new API is called `FinalizationRegistry`_ and it still does not
207+
offer any guarantees that the provided finalization callback will be called.
208+
Embind uses this for cleanup if available, but only for smart pointers,
209+
and only as a last resort.
208210

209-
.. warning:: JavaScript code must explicitly delete any C++ object handles
210-
it has received, or the Emscripten heap will grow indefinitely.
211+
.. warning:: It is strongly recommended that JavaScript code explicitly deletes
212+
any C++ object handles it has received.
211213

212214
The :js:func:`delete()` JavaScript method is provided to manually signal that
213215
a C++ object is no longer needed and can be deleted:
@@ -1022,6 +1024,7 @@ real-world applications has proved to be more than acceptable.
10221024
.. _Connecting C++ and JavaScript on the Web with Embind: http://chadaustin.me/2014/09/connecting-c-and-javascript-on-the-web-with-embind/
10231025
.. _Boost.Python: http://www.boost.org/doc/libs/1_56_0/libs/python/doc/
10241026
.. _finalizers: http://en.wikipedia.org/wiki/Finalizer
1027+
.. _FinalizationRegistry: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
10251028
.. _Reference Counting: https://en.wikipedia.org/wiki/Reference_counting
10261029
.. _Boost.Python-like raw pointer policies: https://wiki.python.org/moin/boost.python/CallPolicy
10271030
.. _Backbone.js: http://backbonejs.org/#Model-extend

src/embind/embind.js

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/*global typeDependencies, flushPendingDeletes, getTypeName, getBasestPointer, throwBindingError, UnboundTypeError, _embind_repr, registeredInstances, registeredTypes, getShiftFromSize*/
1818
/*global ensureOverloadTable, embind__requireFunction, awaitingDependencies, makeLegalFunctionName, embind_charCodes:true, registerType, createNamedFunction, RegisteredPointer, throwInternalError*/
1919
/*global simpleReadValueFromPointer, floatReadValueFromPointer, integerReadValueFromPointer, enumReadValueFromPointer, replacePublicSymbol, craftInvokerFunction, tupleRegistrations*/
20-
/*global finalizationGroup, attachFinalizer, detachFinalizer, releaseClassHandle, runDestructor*/
20+
/*global finalizationRegistry, attachFinalizer, detachFinalizer, releaseClassHandle, runDestructor*/
2121
/*global ClassHandle, makeClassHandle, structRegistrations, whenDependentTypesAreResolved, BindingError, deletionQueue, delayFunction:true, upcastPointer*/
2222
/*global exposePublicSymbol, heap32VectorToArray, new_, RegisteredPointer_getPointee, RegisteredPointer_destructor, RegisteredPointer_deleteObject, char_0, char_9*/
2323
/*global getInheritedInstanceCount, getLiveInheritedInstances, setDelayFunction, InternalError, runDestructors*/
@@ -1734,39 +1734,53 @@ var LibraryEmbind = {
17341734
}
17351735
},
17361736

1737-
$finalizationGroup: false,
1737+
$finalizationRegistry: false,
17381738

1739-
$detachFinalizer_deps: ['$finalizationGroup'],
1739+
$detachFinalizer_deps: ['$finalizationRegistry'],
17401740
$detachFinalizer: function(handle) {},
17411741

1742-
$attachFinalizer__deps: ['$finalizationGroup', '$detachFinalizer',
1743-
'$releaseClassHandle'],
1742+
$attachFinalizer__deps: ['$finalizationRegistry', '$detachFinalizer',
1743+
'$releaseClassHandle', '$RegisteredPointer_fromWireType'],
17441744
$attachFinalizer: function(handle) {
1745-
if ('undefined' === typeof FinalizationGroup) {
1745+
if ('undefined' === typeof FinalizationRegistry) {
17461746
attachFinalizer = (handle) => handle;
17471747
return handle;
17481748
}
1749-
// If the running environment has a FinalizationGroup (see
1749+
// If the running environment has a FinalizationRegistry (see
17501750
// https://github.com/tc39/proposal-weakrefs), then attach finalizers
1751-
// for class handles. We check for the presence of FinalizationGroup
1751+
// for class handles. We check for the presence of FinalizationRegistry
17521752
// at run-time, not build-time.
1753-
finalizationGroup = new FinalizationGroup(function (iter) {
1754-
for (var result = iter.next(); !result.done; result = iter.next()) {
1755-
var $$ = result.value;
1756-
if (!$$.ptr) {
1757-
console.warn('object already deleted: ' + $$.ptr);
1758-
} else {
1759-
releaseClassHandle($$);
1760-
}
1761-
}
1753+
finalizationRegistry = new FinalizationRegistry((info) => {
1754+
#if ASSERTIONS
1755+
console.warn(info.leakWarning.stack.replace(/^Error: /, ''));
1756+
#endif
1757+
releaseClassHandle(info.$$);
17621758
});
17631759
attachFinalizer = (handle) => {
1764-
finalizationGroup.register(handle, handle.$$, handle.$$);
1760+
var $$ = handle.$$;
1761+
var hasSmartPtr = !!$$.smartPtr;
1762+
if (hasSmartPtr) {
1763+
// We should not call the destructor on raw pointers in case other code expects the pointee to live
1764+
var info = { $$: $$ };
1765+
#if ASSERTIONS
1766+
// Create a warning as an Error instance in advance so that we can store
1767+
// the current stacktrace and point to it when / if a leak is detected.
1768+
// This is more useful than the empty stacktrace of `FinalizationRegistry`
1769+
// callback.
1770+
var cls = $$.ptrType.registeredClass;
1771+
info.leakWarning = new Error("Embind found a leaked C++ instance " + cls.name + " <0x" + $$.ptr.toString(16) + ">.\n" +
1772+
"We'll free it automatically in this case, but this functionality is not reliable across various environments.\n" +
1773+
"Make sure to invoke .delete() manually once you're done with the instance instead.\n" +
1774+
"Originally allocated"); // `.stack` will add "at ..." after this sentence
1775+
if ('captureStackTrace' in Error) {
1776+
Error.captureStackTrace(info.leakWarning, RegisteredPointer_fromWireType);
1777+
}
1778+
#endif
1779+
finalizationRegistry.register(handle, info, handle);
1780+
}
17651781
return handle;
17661782
};
1767-
detachFinalizer = (handle) => {
1768-
finalizationGroup.unregister(handle.$$);
1769-
};
1783+
detachFinalizer = (handle) => finalizationRegistry.unregister(handle);
17701784
return attachFinalizer(handle);
17711785
},
17721786

tests/embind/test_finalization.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#include <emscripten/bind.h>
2+
#include <iostream>
3+
#include <memory>
4+
5+
class Foo {
6+
std::string mName;
7+
8+
public:
9+
Foo(std::string name) : mName(name) {}
10+
~Foo() { std::cout << mName << " destructed" << std::endl; }
11+
};
12+
13+
std::shared_ptr<Foo> foo() {
14+
return std::make_shared<Foo>("Constructed from C++");
15+
}
16+
17+
Foo* pFoo() { return new Foo("Foo*"); }
18+
19+
using namespace emscripten;
20+
21+
EMSCRIPTEN_BINDINGS(Marci) {
22+
class_<Foo>("Foo").smart_ptr_constructor<std::shared_ptr<Foo>>(
23+
"Foo", &std::make_shared<Foo, std::string>);
24+
25+
function("foo", foo);
26+
function("pFoo", pFoo, allow_raw_pointers());
27+
}

tests/embind/test_finalization.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Module.onRuntimeInitialized = () => {
2+
const foo1 = new Module.Foo("Constructed from JS");
3+
const foo2 = Module.foo();
4+
const foo3 = Module.pFoo();
5+
}
6+
7+
setTimeout(gc, 100);

tests/test_other.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2427,6 +2427,19 @@ def test_embind(self):
24272427
output = self.run_js('a.out.js')
24282428
self.assertNotContained('FAIL', output)
24292429

2430+
def test_embind_finalization(self):
2431+
self.run_process(
2432+
[EMXX,
2433+
test_file('embind/test_finalization.cpp'),
2434+
'--post-js', test_file('embind/test_finalization.js'),
2435+
'--bind']
2436+
)
2437+
self.node_args += ['--expose-gc']
2438+
output = self.run_js('a.out.js', engine=config.NODE_JS)
2439+
self.assertContained('Constructed from C++ destructed', output)
2440+
self.assertContained('Constructed from JS destructed', output)
2441+
self.assertNotContained('Foo* destructed', output)
2442+
24302443
def test_emconfig(self):
24312444
output = self.run_process([emconfig, 'LLVM_ROOT'], stdout=PIPE).stdout.strip()
24322445
self.assertEqual(output, config.LLVM_ROOT)

0 commit comments

Comments
 (0)