Skip to content

Commit f02c685

Browse files
committed
python: fix various cases of callbacks not being freed
1 parent e922d5e commit f02c685

7 files changed

Lines changed: 277 additions & 229 deletions

File tree

bindings/python/example_rawio.py

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,79 +8,91 @@
88
"""
99
import pylibremidi as lm
1010

11-
# --- MIDI 1 Raw I/O ---
12-
print("=== MIDI 1 Raw I/O ===")
1311

14-
# The library will give us a callback to call when bytes arrive
15-
stored_cb = None
12+
def midi1_example():
13+
print("=== MIDI 1 Raw I/O ===")
1614

17-
# Input: receive and print parsed MIDI messages
18-
in_config = lm.InputConfiguration()
19-
in_config.on_message = lambda msg: print(f" Received: {msg}")
20-
in_config.direct = True # Call Python callbacks directly (no poll needed)
15+
# The library will give us a callback to call when bytes arrive
16+
stored_cb = [None]
2117

22-
rawio_in = lm.RawioInputConfiguration()
23-
rawio_in.set_receive_callback = lambda cb: globals().update(stored_cb=cb)
24-
rawio_in.stop_receive = lambda: globals().update(stored_cb=None)
18+
# Input: receive and print parsed MIDI messages
19+
in_config = lm.InputConfiguration()
20+
in_config.on_message = lambda msg: print(f" Received: {msg}")
21+
in_config.direct = True # Call Python callbacks directly (no poll needed)
2522

26-
midi_in = lm.MidiIn(in_config, rawio_in)
27-
midi_in.open_virtual_port("rawio_in")
23+
rawio_in = lm.RawioInputConfiguration()
24+
rawio_in.set_receive_callback = lambda cb: stored_cb.__setitem__(0, cb)
25+
rawio_in.stop_receive = lambda: stored_cb.__setitem__(0, None)
2826

29-
# Output: loopback written bytes into the input
30-
out_config = lm.OutputConfiguration()
27+
midi_in = lm.MidiIn(in_config, rawio_in)
28+
midi_in.open_virtual_port("rawio_in")
3129

32-
def loopback_write(data):
33-
if stored_cb:
34-
stored_cb(data, 0)
35-
return lm.Error()
30+
# Output: loopback written bytes into the input
31+
out_config = lm.OutputConfiguration()
3632

37-
rawio_out = lm.RawioOutputConfiguration()
38-
rawio_out.write_bytes = loopback_write
33+
def loopback_write(data):
34+
if stored_cb[0]:
35+
stored_cb[0](data, 0)
36+
return lm.Error()
3937

40-
midi_out = lm.MidiOut(out_config, rawio_out)
41-
midi_out.open_virtual_port("rawio_out")
38+
rawio_out = lm.RawioOutputConfiguration()
39+
rawio_out.write_bytes = loopback_write
4240

43-
# Send some MIDI messages - they loop back through the rawio transport
44-
print("Sending Note On C4...")
45-
midi_out.send_message(0x90, 60, 100)
41+
midi_out = lm.MidiOut(out_config, rawio_out)
42+
midi_out.open_virtual_port("rawio_out")
4643

47-
print("Sending Note Off C4...")
48-
midi_out.send_message(0x80, 60, 0)
44+
# Send some MIDI messages - they loop back through the rawio transport
45+
print("Sending Note On C4...")
46+
midi_out.send_message(0x90, 60, 100)
4947

50-
print("Sending CC Volume=80...")
51-
midi_out.send_message(0xB0, 7, 80)
48+
print("Sending Note Off C4...")
49+
midi_out.send_message(0x80, 60, 0)
5250

53-
# --- MIDI 2 Raw I/O (UMP) ---
54-
print("\n=== MIDI 2 Raw I/O (UMP) ===")
51+
print("Sending CC Volume=80...")
52+
midi_out.send_message(0xB0, 7, 80)
5553

56-
stored_ump_cb = None
54+
# close_port breaks the reference cycle: midi_out -> write -> stored_cb -> C++ callback -> midi_in
55+
midi_in.close_port()
56+
midi_out.close_port()
5757

58-
ump_in_config = lm.UmpInputConfiguration()
59-
ump_in_config.on_message = lambda msg: print(f" Received UMP: {msg}")
60-
ump_in_config.direct = True
6158

62-
rawio_ump_in = lm.RawioUmpInputConfiguration()
63-
rawio_ump_in.set_receive_callback = lambda cb: globals().update(stored_ump_cb=cb)
64-
rawio_ump_in.stop_receive = lambda: globals().update(stored_ump_cb=None)
59+
def midi2_example():
60+
print("\n=== MIDI 2 Raw I/O (UMP) ===")
6561

66-
ump_midi_in = lm.MidiIn(ump_in_config, rawio_ump_in)
67-
ump_midi_in.open_virtual_port("rawio_ump_in")
62+
stored_cb = [None]
6863

69-
ump_out_config = lm.OutputConfiguration()
64+
ump_in_config = lm.UmpInputConfiguration()
65+
ump_in_config.on_message = lambda msg: print(f" Received UMP: {msg}")
66+
ump_in_config.direct = True
7067

71-
def loopback_write_ump(data):
72-
if stored_ump_cb:
73-
stored_ump_cb(data, 0)
74-
return lm.Error()
68+
rawio_ump_in = lm.RawioUmpInputConfiguration()
69+
rawio_ump_in.set_receive_callback = lambda cb: stored_cb.__setitem__(0, cb)
70+
rawio_ump_in.stop_receive = lambda: stored_cb.__setitem__(0, None)
7571

76-
rawio_ump_out = lm.RawioUmpOutputConfiguration()
77-
rawio_ump_out.write_ump = loopback_write_ump
72+
midi_in = lm.MidiIn(ump_in_config, rawio_ump_in)
73+
midi_in.open_virtual_port("rawio_ump_in")
7874

79-
ump_midi_out = lm.MidiOut(ump_out_config, rawio_ump_out)
80-
ump_midi_out.open_virtual_port("rawio_ump_out")
75+
ump_out_config = lm.OutputConfiguration()
8176

82-
# Send a MIDI 2.0 Note On UMP (group 0, channel 0, note 60)
83-
print("Sending UMP Note On...")
84-
ump_midi_out.send_ump(0x4090003C, 0xC0000000)
77+
def loopback_write_ump(data):
78+
if stored_cb[0]:
79+
stored_cb[0](data, 0)
80+
return lm.Error()
8581

82+
rawio_ump_out = lm.RawioUmpOutputConfiguration()
83+
rawio_ump_out.write_ump = loopback_write_ump
84+
85+
midi_out = lm.MidiOut(ump_out_config, rawio_ump_out)
86+
midi_out.open_virtual_port("rawio_ump_out")
87+
88+
# Send a MIDI 2.0 Note On UMP (group 0, channel 0, note 60)
89+
print("Sending UMP Note On...")
90+
midi_out.send_ump(0x4090003C, 0xC0000000)
91+
92+
midi_in.close_port()
93+
midi_out.close_port()
94+
95+
96+
midi1_example()
97+
midi2_example()
8698
print("\nDone.")

bindings/python/pylibremidi.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ struct observer_poll_wrapper {
6868

6969
explicit observer_poll_wrapper(observer_configuration conf, libremidi::observer_api_configuration api_conf) : conf{conf}, impl{process(std::move(conf)), std::move(api_conf)} {}
7070

71+
~observer_poll_wrapper() {
72+
conf = {};
73+
}
74+
7175
observer_configuration process(observer_configuration &&obs) {
7276
if (obs.on_error)
7377
obs.on_error = [this](std::string_view errorText, const source_location &) { queue.enqueue(poll_queue::error_message{std::string{errorText}}); };
@@ -128,6 +132,12 @@ struct midi_in_poll_wrapper {
128132
explicit midi_in_poll_wrapper(ump_input_configuration_wrapper conf) noexcept : python_ump_callbacks{conf}, impl{this->process(std::move(conf))} {}
129133
explicit midi_in_poll_wrapper(ump_input_configuration_wrapper conf, input_api_configuration api_conf) : python_ump_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}
130134

135+
~midi_in_poll_wrapper() {
136+
impl.close_port();
137+
python_midi1_callbacks = {};
138+
python_ump_callbacks = {};
139+
}
140+
131141
input_configuration process(input_configuration_wrapper obs) {
132142
python_midi1_callbacks = obs;
133143

@@ -326,6 +336,11 @@ struct midi_out_poll_wrapper {
326336
explicit midi_out_poll_wrapper(const output_configuration_wrapper &conf) noexcept : python_midi1_callbacks{conf}, impl{this->process(std::move(conf))} {}
327337
explicit midi_out_poll_wrapper(output_configuration_wrapper conf, output_api_configuration api_conf) : python_midi1_callbacks{conf}, impl{this->process(std::move(conf)), std::move(api_conf)} {}
328338

339+
~midi_out_poll_wrapper() {
340+
impl.close_port();
341+
python_midi1_callbacks = {};
342+
}
343+
329344
output_configuration process(output_configuration_wrapper obs) {
330345
python_midi1_callbacks = obs;
331346

include/libremidi/backends/rawio/midi_in.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ class midi_in final
5151
{
5252
if (configuration.stop_receive)
5353
configuration.stop_receive();
54+
// Clear all callbacks to break reference cycles (prevents leaks in binding layers)
55+
configuration.set_receive_callback = nullptr;
56+
configuration.stop_receive = nullptr;
5457
return stdx::error{};
5558
}
5659

include/libremidi/backends/rawio/midi_in_ump.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class midi_in final
5050
{
5151
if (configuration.stop_receive)
5252
configuration.stop_receive();
53+
configuration.set_receive_callback = nullptr;
54+
configuration.stop_receive = nullptr;
5355
return stdx::error{};
5456
}
5557

include/libremidi/backends/rawio/midi_out.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ class midi_out final
2929

3030
stdx::error open_virtual_port(std::string_view) override { return stdx::error{}; }
3131

32-
stdx::error close_port() override { return stdx::error{}; }
32+
stdx::error close_port() override
33+
{
34+
configuration.write_bytes = nullptr;
35+
return stdx::error{};
36+
}
3337

3438
stdx::error set_port_name(std::string_view) override { return stdx::error{}; }
3539

include/libremidi/backends/rawio/midi_out_ump.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ class midi_out final
2929

3030
stdx::error open_virtual_port(std::string_view) override { return stdx::error{}; }
3131

32-
stdx::error close_port() override { return stdx::error{}; }
32+
stdx::error close_port() override
33+
{
34+
configuration.write_ump = nullptr;
35+
return stdx::error{};
36+
}
3337

3438
stdx::error set_port_name(std::string_view) override { return stdx::error{}; }
3539

0 commit comments

Comments
 (0)