detached ce2ad83

fix: align generated PipeWire stream events ABI

commit
ce2ad83ced4d24ba8a0a2e897cb9740c463ac76a
author
brice · · 2026-04-10 15:22 UTC
parent
33fbbb4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
5555 {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"},
5656 },
5757 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"},
5959 },
6060 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"},
6262 },
6363 }
6464 }
@@ -101,6 +101,58 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
101101 }
102102 }
103103
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+
104156 func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) {
105157 paths, err := Emit(testPlayerModel(), t.TempDir())
106158 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 @@
2626 {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
2727 ],
2828 "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"}
3030 ],
3131 "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"}
3333 ]
3434 }
@@ -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 }
M internal/capi/stream_playback_gen.go +1 -1 generated file: collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close