Skip to content

Conversation

@sigmaSd
Copy link
Contributor

@sigmaSd sigmaSd commented Oct 25, 2025

built on top of #87

This commit resolves memory management issues with Python callbacks,
including segfaults, premature callback destruction, and resource leaks.

Callbacks had multiple lifetime management issues:
1. Callbacks were freed prematurely when JS PyObject wrappers were GC'd,
   even though Python still held references (causing segfaults)
2. The same Python object could have multiple PyObject wrappers, each
   registering with refregistry and causing duplicate Py_DecRef calls
3. Resource leaks occurred when callbacks were never properly cleaned up
4. Auto-created callbacks (from JS functions) had no cleanup mechanism

Implement PyCapsule-based callback cleanup:

1. **PyCapsule destructor**: When creating a callback, we:
   - Create a PyCapsule containing the callback's handle
   - Use the capsule as the m_self parameter of PyCFunction_NewEx
   - Register a destructor that Python calls when freeing the PyCFunction
   - In the destructor, remove the callback from callbackRegistry and destroy it

2. **Separate lifetime tracking**: Callbacks use callbackRegistry instead of
   refregistry. They are NOT registered with refregistry because:
   - The initial reference from PyCFunction_NewEx is sufficient
   - Cleanup happens via the capsule destructor, not FinalizationRegistry
   - This prevents artificial refcount inflation

3. **Handle-based deduplication**: Track registered handles in a Set (not
   PyObject instances) to prevent duplicate registration when multiple
   PyObject wrappers exist for the same Python object

4. **Auto-created callback cleanup**: callbackCleanupRegistry handles
   callbacks created from raw JS functions that are never passed to Python

- src/python.ts: Implement PyCapsule-based callback cleanup
- src/symbols.ts: Add PyCapsule_New and PyCapsule_GetPointer symbols
- test/test_with_gc.ts: Add proper cleanup to tests

All existing tests pass with resource sanitization on
…tration

Callbacks are now only added to callbackRegistry when .owned is called
(i.e., when actually passed to Python), not during creation. This allows
callbackCleanupRegistry to properly clean up callbacks that are created
but never passed to Python.

Changes:
- Store Callback reference in PyObject.#callback during creation
- Register with callbackRegistry in .owned getter when callback is passed to Python
- Add test case for auto-created callbacks that are never passed to Python
@sigmaSd
Copy link
Contributor Author

sigmaSd commented Oct 25, 2025

the relevant commit is 7b4d6f0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant