fix: align generated PipeWire stream events ABI
- commit
- ce2ad83ced4d24ba8a0a2e897cb9740c463ac76a
- author
- brice · · 2026-04-10 15:22 UTC
- parent
- 33fbbb4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +57 -5
M cmd/pipewiregen/internal/emitter/emitter_test.go +54 -2
| @@ -55,10 +55,10 @@ func testPlayerModel() *model.Model { | |||
| 55 | 55 | {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}, | |
| 56 | 56 | }, | |
| 57 | 57 | Callbacks: []model.Callback{ | |
| 58 | - | {Name: "pw_stream_events", Signature: "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}", Group: "stream_playback"}, | |
| 58 | + | {Name: "pw_stream_events", Signature: "struct{version uint32; destroy *func(stream unsafe.Pointer); state_changed *func(stream unsafe.Pointer, old uint32, state uint32, error *byte); control_info *func(stream unsafe.Pointer, id uint32, control unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); process *func(stream unsafe.Pointer); drained *func(stream unsafe.Pointer); command *func(stream unsafe.Pointer, cmd unsafe.Pointer); trigger_done *func(stream unsafe.Pointer)}", Group: "stream_playback"}, | |
| 59 | 59 | }, | |
| 60 | 60 | EventStructs: []model.EventStruct{ | |
| 61 | - | {Name: "pw_stream_events", Callbacks: []string{"process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"}, Group: "stream_playback"}, | |
| 61 | + | {Name: "pw_stream_events", Callbacks: []string{"destroy", "state_changed", "control_info", "io_changed", "param_changed", "add_buffer", "remove_buffer", "process", "drained", "command", "trigger_done"}, Group: "stream_playback"}, | |
| 62 | 62 | }, | |
| 63 | 63 | } | |
| 64 | 64 | } | |
| @@ -101,6 +101,58 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) { | |||
| 101 | 101 | } | |
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | + | // TestPWStreamEventsABILayout verifies the generated pw_stream_events struct | |
| 105 | + | // has fields in the exact order that matches the PipeWire C ABI. | |
| 106 | + | // This catches layout drift that would silently corrupt callback dispatch. | |
| 107 | + | func TestPWStreamEventsABILayout(t *testing.T) { | |
| 108 | + | paths, err := Emit(testPlayerModel(), t.TempDir()) | |
| 109 | + | if err != nil { | |
| 110 | + | t.Fatalf("Emit returned error: %v", err) | |
| 111 | + | } | |
| 112 | + | capiContent := string(paths["internal/capi/stream_playback_gen.go"]) | |
| 113 | + | ||
| 114 | + | // The correct C ABI field order from <pipewire/stream.h> | |
| 115 | + | expectedFields := []string{ | |
| 116 | + | "version uint32", | |
| 117 | + | "destroy *func(", | |
| 118 | + | "state_changed *func(", | |
| 119 | + | "control_info *func(", | |
| 120 | + | "io_changed *func(", | |
| 121 | + | "param_changed *func(", | |
| 122 | + | "add_buffer *func(", | |
| 123 | + | "remove_buffer *func(", | |
| 124 | + | "process *func(", | |
| 125 | + | "drained *func(", | |
| 126 | + | "command *func(", | |
| 127 | + | "trigger_done *func(", | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | // Verify all expected fields are present | |
| 131 | + | for _, field := range expectedFields { | |
| 132 | + | if !strings.Contains(capiContent, field) { | |
| 133 | + | t.Errorf("pw_stream_events missing expected field %q", field) | |
| 134 | + | } | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | // Verify the spurious "flush" field is NOT present | |
| 138 | + | if strings.Contains(capiContent, "flush *func(") { | |
| 139 | + | t.Error("pw_stream_events contains spurious 'flush' field — this does not exist in the PipeWire C ABI") | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | // Verify field order: each field must appear after the previous one | |
| 143 | + | lastIdx := 0 | |
| 144 | + | for _, field := range expectedFields { | |
| 145 | + | idx := strings.Index(capiContent, field) | |
| 146 | + | if idx < 0 { | |
| 147 | + | continue // already reported above | |
| 148 | + | } | |
| 149 | + | if idx < lastIdx { | |
| 150 | + | t.Errorf("pw_stream_events field %q appears at wrong position (before previous field)", field) | |
| 151 | + | } | |
| 152 | + | lastIdx = idx | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 104 | 156 | func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) { | |
| 105 | 157 | paths, err := Emit(testPlayerModel(), t.TempDir()) | |
| 106 | 158 | if err != nil { | |
| @@ -55,10 +55,10 @@ func testPlayerModel() *model.Model { | |||||
| 55 | {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}, | 55 | {Name: "pw_stream_add_listener", Library: "pipewire", Group: "stream_playback", Signature: "func(stream unsafe.Pointer, listener unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) int32"}, | ||
| 56 | }, | 56 | }, | ||
| 57 | Callbacks: []model.Callback{ | 57 | Callbacks: []model.Callback{ | ||
| 58 | - | {Name: "pw_stream_events", Signature: "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}", Group: "stream_playback"}, | 58 | + | {Name: "pw_stream_events", Signature: "struct{version uint32; destroy *func(stream unsafe.Pointer); state_changed *func(stream unsafe.Pointer, old uint32, state uint32, error *byte); control_info *func(stream unsafe.Pointer, id uint32, control unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); process *func(stream unsafe.Pointer); drained *func(stream unsafe.Pointer); command *func(stream unsafe.Pointer, cmd unsafe.Pointer); trigger_done *func(stream unsafe.Pointer)}", Group: "stream_playback"}, |
| 59 | }, | 59 | }, | ||
| 60 | EventStructs: []model.EventStruct{ | 60 | EventStructs: []model.EventStruct{ | ||
| 61 | - | {Name: "pw_stream_events", Callbacks: []string{"process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"}, Group: "stream_playback"}, | 61 | + | {Name: "pw_stream_events", Callbacks: []string{"destroy", "state_changed", "control_info", "io_changed", "param_changed", "add_buffer", "remove_buffer", "process", "drained", "command", "trigger_done"}, Group: "stream_playback"}, |
| 62 | }, | 62 | }, | ||
| 63 | } | 63 | } | ||
| 64 | } | 64 | } | ||
| @@ -101,6 +101,58 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) { | |||||
| 101 | } | 101 | } | ||
| 102 | } | 102 | } | ||
| 103 | 103 | ||||
| 104 | + | // TestPWStreamEventsABILayout verifies the generated pw_stream_events struct | |||
| 105 | + | // has fields in the exact order that matches the PipeWire C ABI. | |||
| 106 | + | // This catches layout drift that would silently corrupt callback dispatch. | |||
| 107 | + | func TestPWStreamEventsABILayout(t *testing.T) { | |||
| 108 | + | paths, err := Emit(testPlayerModel(), t.TempDir()) | |||
| 109 | + | if err != nil { | |||
| 110 | + | t.Fatalf("Emit returned error: %v", err) | |||
| 111 | + | } | |||
| 112 | + | capiContent := string(paths["internal/capi/stream_playback_gen.go"]) | |||
| 113 | + | ||||
| 114 | + | // The correct C ABI field order from <pipewire/stream.h> | |||
| 115 | + | expectedFields := []string{ | |||
| 116 | + | "version uint32", | |||
| 117 | + | "destroy *func(", | |||
| 118 | + | "state_changed *func(", | |||
| 119 | + | "control_info *func(", | |||
| 120 | + | "io_changed *func(", | |||
| 121 | + | "param_changed *func(", | |||
| 122 | + | "add_buffer *func(", | |||
| 123 | + | "remove_buffer *func(", | |||
| 124 | + | "process *func(", | |||
| 125 | + | "drained *func(", | |||
| 126 | + | "command *func(", | |||
| 127 | + | "trigger_done *func(", | |||
| 128 | + | } | |||
| 129 | + | ||||
| 130 | + | // Verify all expected fields are present | |||
| 131 | + | for _, field := range expectedFields { | |||
| 132 | + | if !strings.Contains(capiContent, field) { | |||
| 133 | + | t.Errorf("pw_stream_events missing expected field %q", field) | |||
| 134 | + | } | |||
| 135 | + | } | |||
| 136 | + | ||||
| 137 | + | // Verify the spurious "flush" field is NOT present | |||
| 138 | + | if strings.Contains(capiContent, "flush *func(") { | |||
| 139 | + | t.Error("pw_stream_events contains spurious 'flush' field — this does not exist in the PipeWire C ABI") | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | // Verify field order: each field must appear after the previous one | |||
| 143 | + | lastIdx := 0 | |||
| 144 | + | for _, field := range expectedFields { | |||
| 145 | + | idx := strings.Index(capiContent, field) | |||
| 146 | + | if idx < 0 { | |||
| 147 | + | continue // already reported above | |||
| 148 | + | } | |||
| 149 | + | if idx < lastIdx { | |||
| 150 | + | t.Errorf("pw_stream_events field %q appears at wrong position (before previous field)", field) | |||
| 151 | + | } | |||
| 152 | + | lastIdx = idx | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | ||||
| 104 | func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) { | 156 | func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) { | ||
| 105 | paths, err := Emit(testPlayerModel(), t.TempDir()) | 157 | paths, err := Emit(testPlayerModel(), t.TempDir()) | ||
| 106 | if err != nil { | 158 | if err != nil { | ||
M gen/pipewire.json +2 -2
| @@ -26,9 +26,9 @@ | |||
| 26 | 26 | {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"} | |
| 27 | 27 | ], | |
| 28 | 28 | "callbacks": [ | |
| 29 | - | {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}", "group": "stream_playback"} | |
| 29 | + | {"name": "pw_stream_events", "signature": "struct{version uint32; destroy *func(stream unsafe.Pointer); state_changed *func(stream unsafe.Pointer, old uint32, state uint32, error *byte); control_info *func(stream unsafe.Pointer, id uint32, control unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); process *func(stream unsafe.Pointer); drained *func(stream unsafe.Pointer); command *func(stream unsafe.Pointer, cmd unsafe.Pointer); trigger_done *func(stream unsafe.Pointer)}", "group": "stream_playback"} | |
| 30 | 30 | ], | |
| 31 | 31 | "event_structs": [ | |
| 32 | - | {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"], "group": "stream_playback"} | |
| 32 | + | {"name": "pw_stream_events", "callbacks": ["destroy", "state_changed", "control_info", "io_changed", "param_changed", "add_buffer", "remove_buffer", "process", "drained", "command", "trigger_done"], "group": "stream_playback"} | |
| 33 | 33 | ] | |
| 34 | 34 | } | |
| @@ -26,9 +26,9 @@ | |||||
| 26 | {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"} | 26 | {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"} | ||
| 27 | ], | 27 | ], | ||
| 28 | "callbacks": [ | 28 | "callbacks": [ | ||
| 29 | - | {"name": "pw_stream_events", "signature": "struct{version uint32; process *func(stream unsafe.Pointer); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); flush *func(stream unsafe.Pointer, drain bool); drained *func(stream unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32)}", "group": "stream_playback"} | 29 | + | {"name": "pw_stream_events", "signature": "struct{version uint32; destroy *func(stream unsafe.Pointer); state_changed *func(stream unsafe.Pointer, old uint32, state uint32, error *byte); control_info *func(stream unsafe.Pointer, id uint32, control unsafe.Pointer); io_changed *func(stream unsafe.Pointer, id uint32, area unsafe.Pointer, size uint32); param_changed *func(stream unsafe.Pointer, id uint32, param unsafe.Pointer); add_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); remove_buffer *func(stream unsafe.Pointer, buffer unsafe.Pointer); process *func(stream unsafe.Pointer); drained *func(stream unsafe.Pointer); command *func(stream unsafe.Pointer, cmd unsafe.Pointer); trigger_done *func(stream unsafe.Pointer)}", "group": "stream_playback"} |
| 30 | ], | 30 | ], | ||
| 31 | "event_structs": [ | 31 | "event_structs": [ | ||
| 32 | - | {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"], "group": "stream_playback"} | 32 | + | {"name": "pw_stream_events", "callbacks": ["destroy", "state_changed", "control_info", "io_changed", "param_changed", "add_buffer", "remove_buffer", "process", "drained", "command", "trigger_done"], "group": "stream_playback"} |
| 33 | ] | 33 | ] | ||
| 34 | } | 34 | } | ||