Skip to content

Commit fef769c

Browse files
fix: Plugin.__del__ should also free compiled_plugin when owned
When Plugin creates its own CompiledPlugin internally, it should also free it in __del__. Added _owns_compiled_plugin flag to track ownership and avoid freeing externally-passed CompiledPlugin instances. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1f9e81a commit fef769c

3 files changed

Lines changed: 21 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ poetry-installer-error-*.log
44
docs/_build
55
.DS_Store
66
dist/
7+
.idea/

extism/extism.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,9 @@ def __init__(
551551
config: Optional[Any] = None,
552552
functions: Optional[List[Function]] = HOST_FN_REGISTRY,
553553
):
554-
if not isinstance(plugin, CompiledPlugin):
554+
# Track if we created the CompiledPlugin (so we know to free it)
555+
self._owns_compiled_plugin = not isinstance(plugin, CompiledPlugin)
556+
if self._owns_compiled_plugin:
555557
plugin = CompiledPlugin(plugin, wasi, functions)
556558

557559
self.compiled_plugin = plugin
@@ -629,6 +631,10 @@ def __del__(self):
629631
return
630632
_lib.extism_plugin_free(self.plugin)
631633
self.plugin = -1
634+
# Free the compiled plugin if we created it
635+
if getattr(self, "_owns_compiled_plugin", False) and self.compiled_plugin is not None:
636+
self.compiled_plugin.__del__()
637+
self.compiled_plugin = None
632638

633639
def __enter__(self):
634640
return self

tests/test_extism.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,15 @@ def test_plugin_del_frees_native_resources(self):
6464
with extism.Plugin(self._manifest(), functions=[]) as plugin:
6565
j = json.loads(plugin.call("count_vowels", "test"))
6666
self.assertEqual(j["count"], 1)
67+
# Plugin should own the compiled plugin it created
68+
self.assertTrue(plugin._owns_compiled_plugin)
6769

6870
# Verify plugin was freed after exiting context
6971
self.assertEqual(plugin.plugin, -1,
7072
"Expected plugin.plugin to be -1 after __del__, indicating extism_plugin_free was called")
73+
# Verify compiled plugin was also freed (since Plugin owned it)
74+
self.assertIsNone(plugin.compiled_plugin,
75+
"Expected compiled_plugin to be None after __del__, indicating it was also freed")
7176

7277
def test_compiled_plugin_del_frees_native_resources(self):
7378
"""Test that CompiledPlugin.__del__ properly frees native resources.
@@ -86,11 +91,18 @@ def test_compiled_plugin_del_frees_native_resources(self):
8691
j = json.loads(plugin.call("count_vowels", "test"))
8792
self.assertEqual(j["count"], 1)
8893

94+
# Plugin should NOT own the compiled plugin (it was passed in)
95+
self.assertFalse(plugin._owns_compiled_plugin)
96+
8997
# Clean up plugin first
9098
plugin.__del__()
9199
self.assertEqual(plugin.plugin, -1)
92100

93-
# Now clean up compiled plugin
101+
# Compiled plugin should NOT have been freed by Plugin.__del__
102+
self.assertNotEqual(compiled.pointer, -1,
103+
"Expected compiled.pointer to NOT be -1 since Plugin didn't own it")
104+
105+
# Now clean up compiled plugin manually
94106
compiled.__del__()
95107

96108
# Verify compiled plugin was freed

0 commit comments

Comments
 (0)