Skip to content

Commit 1f9e81a

Browse files
fix: __del__ not freeing up native resources (_ExtismFunctionMetadata, CompiledPlugin)
1 parent a1ac65b commit 1f9e81a

2 files changed

Lines changed: 51 additions & 5 deletions

File tree

extism/extism.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,10 +270,10 @@ def __init__(self, f: Function):
270270
_lib.extism_function_set_namespace(self.pointer, f.namespace.encode())
271271

272272
def __del__(self):
273-
if not hasattr(self, "pointer"):
273+
if not hasattr(self, "pointer") or self.pointer is None:
274274
return
275-
if self.pointer is not None:
276-
_lib.extism_function_free(self.pointer)
275+
_lib.extism_function_free(self.pointer)
276+
self.pointer = None
277277

278278

279279
def _map_arg(arg_name, xs) -> Tuple[ValType, Callable[[Any, Any], Any]]:
@@ -507,7 +507,7 @@ def __init__(
507507
raise Error(msg.decode())
508508

509509
def __del__(self):
510-
if not hasattr(self, "pointer"):
510+
if not hasattr(self, "pointer") or self.pointer == -1:
511511
return
512512
_lib.extism_compiled_plugin_free(self.pointer)
513513
self.pointer = -1

tests/test_extism.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from datetime import datetime, timedelta
1010
from os.path import join, dirname
1111
from threading import Thread
12-
from unittest.mock import patch
1312

1413
import extism
14+
from extism.extism import CompiledPlugin, _ExtismFunctionMetadata, TypeInferredFunction
1515

1616

1717
# A pickle-able object.
@@ -69,6 +69,52 @@ def test_plugin_del_frees_native_resources(self):
6969
self.assertEqual(plugin.plugin, -1,
7070
"Expected plugin.plugin to be -1 after __del__, indicating extism_plugin_free was called")
7171

72+
def test_compiled_plugin_del_frees_native_resources(self):
73+
"""Test that CompiledPlugin.__del__ properly frees native resources.
74+
75+
Unlike Plugin, CompiledPlugin has no context manager so __del__ is only
76+
called once by garbage collection. This also tests that __del__ can be
77+
safely called multiple times without causing double-free errors.
78+
"""
79+
compiled = CompiledPlugin(self._manifest(), functions=[])
80+
# Verify pointer exists before deletion
81+
self.assertTrue(hasattr(compiled, 'pointer'))
82+
self.assertNotEqual(compiled.pointer, -1)
83+
84+
# Create a plugin from compiled to ensure it works
85+
plugin = extism.Plugin(compiled)
86+
j = json.loads(plugin.call("count_vowels", "test"))
87+
self.assertEqual(j["count"], 1)
88+
89+
# Clean up plugin first
90+
plugin.__del__()
91+
self.assertEqual(plugin.plugin, -1)
92+
93+
# Now clean up compiled plugin
94+
compiled.__del__()
95+
96+
# Verify compiled plugin was freed
97+
self.assertEqual(compiled.pointer, -1,
98+
"Expected compiled.pointer to be -1 after __del__, indicating extism_compiled_plugin_free was called")
99+
100+
def test_extism_function_metadata_del_frees_native_resources(self):
101+
"""Test that _ExtismFunctionMetadata.__del__ properly frees native resources."""
102+
def test_host_fn(inp: str) -> str:
103+
return inp
104+
105+
func = TypeInferredFunction(None, "test_func", test_host_fn, [])
106+
metadata = _ExtismFunctionMetadata(func)
107+
108+
# Verify pointer exists before deletion
109+
self.assertTrue(hasattr(metadata, 'pointer'))
110+
self.assertIsNotNone(metadata.pointer)
111+
112+
metadata.__del__()
113+
114+
# Verify function was freed (pointer set to None)
115+
self.assertIsNone(metadata.pointer,
116+
"Expected metadata.pointer to be None after __del__, indicating extism_function_free was called")
117+
72118
def test_errors_on_bad_manifest(self):
73119
self.assertRaises(
74120
extism.Error, lambda: extism.Plugin({"invalid_manifest": True})

0 commit comments

Comments
 (0)