detached fcdec78

feat: generate PipeWire adapter and composite glue

commit
fcdec78c92ee8ad7aef44fa6dfaf2f2e64cc42d1
author
brice · · 2026-04-10 12:38 UTC
parent
28a2e52
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +274 -34
M cmd/pipewiregen/internal/emitter/emitter.go +192 -30
@@ -14,8 +14,10 @@ import (
1414 )
1515
1616 var (
17- capiTmpl *template.Template
18- portTmpl *template.Template
17+ capiTmpl *template.Template
18+ portTmpl *template.Template
19+ adapterTmpl *template.Template
20+ compositeTmpl *template.Template
1921 )
2022
2123 func init() {
@@ -26,6 +28,14 @@ func init() {
2628 portTmpl = template.Must(template.New("port").Funcs(template.FuncMap{
2729 "title": titleCase,
2830 }).Parse(portTemplate))
31+
32+ adapterTmpl = template.Must(template.New("adapter").Funcs(template.FuncMap{
33+ "title": titleCase,
34+ }).Parse(adapterTemplate))
35+
36+ compositeTmpl = template.Must(template.New("composite").Funcs(template.FuncMap{
37+ "title": titleCase,
38+ }).Parse(compositeTemplate))
2939 }
3040
3141 // titleCase converts a string to title case (first letter uppercase, rest lowercase).
@@ -62,6 +72,21 @@ func Emit(m *model.Model, root string) (map[string][]byte, error) {
6272 }
6373 out[portPath] = portContent
6474 }
75+
76+ // Generate adapters file
77+ adapterContent, err := renderAdapters(m)
78+ if err != nil {
79+ return nil, fmt.Errorf("render adapters: %w", err)
80+ }
81+ out[filepath.Join("internal", "capi", "adapters_gen.go")] = adapterContent
82+
83+ // Generate composite port file
84+ compositeContent, err := renderComposite(m)
85+ if err != nil {
86+ return nil, fmt.Errorf("render composite: %w", err)
87+ }
88+ out[filepath.Join("internal", "ports", "out", "capi_gen.go")] = compositeContent
89+
6590 return out, writeAll(root, out)
6691 }
6792
@@ -85,37 +110,43 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
85110 }
86111 }
87112
88- // Filter callbacks and event structs for stream_playback group
113+ // Filter callbacks and event structs for this group
89114 var groupCallbacks []model.Callback
90115 var groupEventStructs []model.EventStruct
91- if group.Name == "stream_playback" {
92- groupCallbacks = callbacks
93- groupEventStructs = eventStructs
94- // Check if callbacks need unsafe
95- for _, cb := range callbacks {
96- if strings.Contains(cb.Signature, "unsafe.Pointer") {
97- needsUnsafe = true
98- break
99- }
116+ for _, cb := range callbacks {
117+ if cb.Group == group.Name {
118+ groupCallbacks = append(groupCallbacks, cb)
119+ }
120+ }
121+ for _, es := range eventStructs {
122+ if es.Group == group.Name {
123+ groupEventStructs = append(groupEventStructs, es)
124+ }
125+ }
126+ // Check if callbacks need unsafe
127+ for _, cb := range groupCallbacks {
128+ if strings.Contains(cb.Signature, "unsafe.Pointer") {
129+ needsUnsafe = true
130+ break
100131 }
101132 }
102133
103134 data := struct {
104- Group model.Group
105- Symbols []model.Symbol
106- LibraryMap map[string]string
107- NeedsUnsafe bool
108- Callbacks []model.Callback
109- EventStructs []model.EventStruct
110- IsStreamPlayback bool
135+ Group model.Group
136+ Symbols []model.Symbol
137+ LibraryMap map[string]string
138+ NeedsUnsafe bool
139+ Callbacks []model.Callback
140+ EventStructs []model.EventStruct
141+ HasCallbacks bool
111142 }{
112- Group: group,
113- Symbols: groupSymbols,
114- LibraryMap: libMap,
115- NeedsUnsafe: needsUnsafe,
116- Callbacks: groupCallbacks,
117- EventStructs: groupEventStructs,
118- IsStreamPlayback: group.Name == "stream_playback",
143+ Group: group,
144+ Symbols: groupSymbols,
145+ LibraryMap: libMap,
146+ NeedsUnsafe: needsUnsafe,
147+ Callbacks: groupCallbacks,
148+ EventStructs: groupEventStructs,
149+ HasCallbacks: len(groupCallbacks) > 0,
119150 }
120151
121152 var buf bytes.Buffer
@@ -155,10 +186,12 @@ func renderPort(group model.Group, symbols []model.Symbol, eventStructs []model.
155186 }
156187 }
157188
158- // Filter event structs for stream_playback group
189+ // Filter event structs for this group
159190 var groupEventStructs []model.EventStruct
160- if group.Name == "stream_playback" {
161- groupEventStructs = eventStructs
191+ for _, es := range eventStructs {
192+ if es.Group == group.Name {
193+ groupEventStructs = append(groupEventStructs, es)
194+ }
162195 }
163196
164197 data := struct {
@@ -250,6 +283,100 @@ func toGoName(cName string) string {
250283 return strings.Join(parts, "")
251284 }
252285
286+// toCamelCase converts a snake_case name like stream_playback to StreamPlayback.
287+func toCamelCase(s string) string {
288+ parts := strings.Split(s, "_")
289+ for i, p := range parts {
290+ parts[i] = titleCase(p)
291+ }
292+ return strings.Join(parts, "")
293+}
294+
295+// renderAdapters generates one thin adapter struct per group with forwarding methods.
296+func renderAdapters(m *model.Model) ([]byte, error) {
297+ type AdapterMethod struct {
298+ CName string
299+ GoName string
300+ Params string
301+ Results string
302+ }
303+
304+ type AdapterGroup struct {
305+ AdapterName string
306+ Methods []AdapterMethod
307+ }
308+
309+ var groups []AdapterGroup
310+ needsUnsafe := false
311+ for _, group := range m.Groups {
312+ groupSyms := filterSymbolsByGroup(m.Symbols, group.Name)
313+ methods := make([]AdapterMethod, 0, len(groupSyms))
314+
315+ for _, sym := range groupSyms {
316+ params, results := parseSignature(sym.Signature)
317+ methods = append(methods, AdapterMethod{
318+ CName: sym.Name,
319+ GoName: toGoName(sym.Name),
320+ Params: params,
321+ Results: results,
322+ })
323+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
324+ needsUnsafe = true
325+ }
326+ }
327+
328+ groups = append(groups, AdapterGroup{
329+ AdapterName: toCamelCase(group.Name) + "CAPIAdapter",
330+ Methods: methods,
331+ })
332+ }
333+
334+ data := struct {
335+ Groups []AdapterGroup
336+ NeedsUnsafe bool
337+ }{
338+ Groups: groups,
339+ NeedsUnsafe: needsUnsafe,
340+ }
341+
342+ var buf bytes.Buffer
343+ if err := adapterTmpl.Execute(&buf, data); err != nil {
344+ return nil, fmt.Errorf("execute adapter template: %w", err)
345+ }
346+ return buf.Bytes(), nil
347+}
348+
349+// renderComposite generates the composite CAPI interface that embeds all group interfaces.
350+func renderComposite(m *model.Model) ([]byte, error) {
351+ // Determine if any group uses unsafe.Pointer
352+ needsUnsafe := false
353+ for _, group := range m.Groups {
354+ for _, sym := range filterSymbolsByGroup(m.Symbols, group.Name) {
355+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
356+ needsUnsafe = true
357+ break
358+ }
359+ }
360+ if needsUnsafe {
361+ break
362+ }
363+ }
364+
365+ data := struct {
366+ Groups []model.Group
367+ NeedsUnsafe bool
368+ }{
369+ Groups: m.Groups,
370+ NeedsUnsafe: needsUnsafe,
371+ }
372+
373+ var buf bytes.Buffer
374+ if err := compositeTmpl.Execute(&buf, data); err != nil {
375+ return nil, fmt.Errorf("execute composite template: %w", err)
376+ }
377+ return buf.Bytes(), nil
378+}
379+
253380 // writeAll writes all generated files to disk.
254381 func writeAll(root string, files map[string][]byte) error {
255382 for path, content := range files {
@@ -277,7 +404,7 @@ import "unsafe"
277404 // {{.Name}}Func is the function type for {{.Name}}.
278405 type {{.Name}}Func {{.Signature}}
279406 {{end}}
280-{{if .IsStreamPlayback}}
407+{{if .HasCallbacks}}
281408 {{range .Callbacks}}
282409 // {{.Name}} represents the callback struct for {{.Name}}.
283410 type {{.Name}} {{.Signature}}
@@ -310,3 +437,38 @@ type {{.Group.Interface}} interface {
310437 {{- end}}
311438 }
312439 `
440+
441+const adapterTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
442+
443+package capi
444+{{if .NeedsUnsafe}}
445+import "unsafe"
446+{{end}}
447+{{range $group := .Groups}}
448+// {{$group.AdapterName}} forwards calls to the generated CAPI variables.
449+type {{$group.AdapterName}} struct{}
450+{{range $group.Methods}}
451+func (a *{{$group.AdapterName}}) {{.GoName}}{{.Params}}{{if .Results}} {{.Results}}{{end}} {
452+{{- if .Results}}
453+ return {{.CName}}{{.Params}}
454+{{- else}}
455+ {{.CName}}{{.Params}}
456+{{- end}}
457+}
458+{{end}}
459+{{end}}
460+`
461+
462+const compositeTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
463+
464+package out
465+{{if .NeedsUnsafe}}
466+import "unsafe"
467+{{end}}
468+// CAPI is the composite interface that embeds all generated group interfaces.
469+type CAPI interface {
470+{{- range .Groups}}
471+ {{.Interface}}
472+{{- end}}
473+}
474+`
@@ -14,8 +14,10 @@ import (
14 )14 )
15 15
16 var (16 var (
17- capiTmpl *template.Template17+ capiTmpl *template.Template
18- portTmpl *template.Template18+ portTmpl *template.Template
19+ adapterTmpl *template.Template
20+ compositeTmpl *template.Template
19 )21 )
20 22
21 func init() {23 func init() {
@@ -26,6 +28,14 @@ func init() {
26 portTmpl = template.Must(template.New("port").Funcs(template.FuncMap{28 portTmpl = template.Must(template.New("port").Funcs(template.FuncMap{
27 "title": titleCase,29 "title": titleCase,
28 }).Parse(portTemplate))30 }).Parse(portTemplate))
31+
32+ adapterTmpl = template.Must(template.New("adapter").Funcs(template.FuncMap{
33+ "title": titleCase,
34+ }).Parse(adapterTemplate))
35+
36+ compositeTmpl = template.Must(template.New("composite").Funcs(template.FuncMap{
37+ "title": titleCase,
38+ }).Parse(compositeTemplate))
29 }39 }
30 40
31 // titleCase converts a string to title case (first letter uppercase, rest lowercase).41 // titleCase converts a string to title case (first letter uppercase, rest lowercase).
@@ -62,6 +72,21 @@ func Emit(m *model.Model, root string) (map[string][]byte, error) {
62 }72 }
63 out[portPath] = portContent73 out[portPath] = portContent
64 }74 }
75+
76+ // Generate adapters file
77+ adapterContent, err := renderAdapters(m)
78+ if err != nil {
79+ return nil, fmt.Errorf("render adapters: %w", err)
80+ }
81+ out[filepath.Join("internal", "capi", "adapters_gen.go")] = adapterContent
82+
83+ // Generate composite port file
84+ compositeContent, err := renderComposite(m)
85+ if err != nil {
86+ return nil, fmt.Errorf("render composite: %w", err)
87+ }
88+ out[filepath.Join("internal", "ports", "out", "capi_gen.go")] = compositeContent
89+
65 return out, writeAll(root, out)90 return out, writeAll(root, out)
66 }91 }
67 92
@@ -85,37 +110,43 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
85 }110 }
86 }111 }
87 112
88- // Filter callbacks and event structs for stream_playback group113+ // Filter callbacks and event structs for this group
89 var groupCallbacks []model.Callback114 var groupCallbacks []model.Callback
90 var groupEventStructs []model.EventStruct115 var groupEventStructs []model.EventStruct
91- if group.Name == "stream_playback" {116+ for _, cb := range callbacks {
92- groupCallbacks = callbacks117+ if cb.Group == group.Name {
93- groupEventStructs = eventStructs118+ groupCallbacks = append(groupCallbacks, cb)
94- // Check if callbacks need unsafe119+ }
95- for _, cb := range callbacks {120+ }
96- if strings.Contains(cb.Signature, "unsafe.Pointer") {121+ for _, es := range eventStructs {
97- needsUnsafe = true122+ if es.Group == group.Name {
98- break123+ groupEventStructs = append(groupEventStructs, es)
99- }124+ }
125+ }
126+ // Check if callbacks need unsafe
127+ for _, cb := range groupCallbacks {
128+ if strings.Contains(cb.Signature, "unsafe.Pointer") {
129+ needsUnsafe = true
130+ break
100 }131 }
101 }132 }
102 133
103 data := struct {134 data := struct {
104- Group model.Group135+ Group model.Group
105- Symbols []model.Symbol136+ Symbols []model.Symbol
106- LibraryMap map[string]string137+ LibraryMap map[string]string
107- NeedsUnsafe bool138+ NeedsUnsafe bool
108- Callbacks []model.Callback139+ Callbacks []model.Callback
109- EventStructs []model.EventStruct140+ EventStructs []model.EventStruct
110- IsStreamPlayback bool141+ HasCallbacks bool
111 }{142 }{
112- Group: group,143+ Group: group,
113- Symbols: groupSymbols,144+ Symbols: groupSymbols,
114- LibraryMap: libMap,145+ LibraryMap: libMap,
115- NeedsUnsafe: needsUnsafe,146+ NeedsUnsafe: needsUnsafe,
116- Callbacks: groupCallbacks,147+ Callbacks: groupCallbacks,
117- EventStructs: groupEventStructs,148+ EventStructs: groupEventStructs,
118- IsStreamPlayback: group.Name == "stream_playback",149+ HasCallbacks: len(groupCallbacks) > 0,
119 }150 }
120 151
121 var buf bytes.Buffer152 var buf bytes.Buffer
@@ -155,10 +186,12 @@ func renderPort(group model.Group, symbols []model.Symbol, eventStructs []model.
155 }186 }
156 }187 }
157 188
158- // Filter event structs for stream_playback group189+ // Filter event structs for this group
159 var groupEventStructs []model.EventStruct190 var groupEventStructs []model.EventStruct
160- if group.Name == "stream_playback" {191+ for _, es := range eventStructs {
161- groupEventStructs = eventStructs192+ if es.Group == group.Name {
193+ groupEventStructs = append(groupEventStructs, es)
194+ }
162 }195 }
163 196
164 data := struct {197 data := struct {
@@ -250,6 +283,100 @@ func toGoName(cName string) string {
250 return strings.Join(parts, "")283 return strings.Join(parts, "")
251 }284 }
252 285
286+// toCamelCase converts a snake_case name like stream_playback to StreamPlayback.
287+func toCamelCase(s string) string {
288+ parts := strings.Split(s, "_")
289+ for i, p := range parts {
290+ parts[i] = titleCase(p)
291+ }
292+ return strings.Join(parts, "")
293+}
294+
295+// renderAdapters generates one thin adapter struct per group with forwarding methods.
296+func renderAdapters(m *model.Model) ([]byte, error) {
297+ type AdapterMethod struct {
298+ CName string
299+ GoName string
300+ Params string
301+ Results string
302+ }
303+
304+ type AdapterGroup struct {
305+ AdapterName string
306+ Methods []AdapterMethod
307+ }
308+
309+ var groups []AdapterGroup
310+ needsUnsafe := false
311+ for _, group := range m.Groups {
312+ groupSyms := filterSymbolsByGroup(m.Symbols, group.Name)
313+ methods := make([]AdapterMethod, 0, len(groupSyms))
314+
315+ for _, sym := range groupSyms {
316+ params, results := parseSignature(sym.Signature)
317+ methods = append(methods, AdapterMethod{
318+ CName: sym.Name,
319+ GoName: toGoName(sym.Name),
320+ Params: params,
321+ Results: results,
322+ })
323+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
324+ needsUnsafe = true
325+ }
326+ }
327+
328+ groups = append(groups, AdapterGroup{
329+ AdapterName: toCamelCase(group.Name) + "CAPIAdapter",
330+ Methods: methods,
331+ })
332+ }
333+
334+ data := struct {
335+ Groups []AdapterGroup
336+ NeedsUnsafe bool
337+ }{
338+ Groups: groups,
339+ NeedsUnsafe: needsUnsafe,
340+ }
341+
342+ var buf bytes.Buffer
343+ if err := adapterTmpl.Execute(&buf, data); err != nil {
344+ return nil, fmt.Errorf("execute adapter template: %w", err)
345+ }
346+ return buf.Bytes(), nil
347+}
348+
349+// renderComposite generates the composite CAPI interface that embeds all group interfaces.
350+func renderComposite(m *model.Model) ([]byte, error) {
351+ // Determine if any group uses unsafe.Pointer
352+ needsUnsafe := false
353+ for _, group := range m.Groups {
354+ for _, sym := range filterSymbolsByGroup(m.Symbols, group.Name) {
355+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
356+ needsUnsafe = true
357+ break
358+ }
359+ }
360+ if needsUnsafe {
361+ break
362+ }
363+ }
364+
365+ data := struct {
366+ Groups []model.Group
367+ NeedsUnsafe bool
368+ }{
369+ Groups: m.Groups,
370+ NeedsUnsafe: needsUnsafe,
371+ }
372+
373+ var buf bytes.Buffer
374+ if err := compositeTmpl.Execute(&buf, data); err != nil {
375+ return nil, fmt.Errorf("execute composite template: %w", err)
376+ }
377+ return buf.Bytes(), nil
378+}
379+
253 // writeAll writes all generated files to disk.380 // writeAll writes all generated files to disk.
254 func writeAll(root string, files map[string][]byte) error {381 func writeAll(root string, files map[string][]byte) error {
255 for path, content := range files {382 for path, content := range files {
@@ -277,7 +404,7 @@ import "unsafe"
277 // {{.Name}}Func is the function type for {{.Name}}.404 // {{.Name}}Func is the function type for {{.Name}}.
278 type {{.Name}}Func {{.Signature}}405 type {{.Name}}Func {{.Signature}}
279 {{end}}406 {{end}}
280-{{if .IsStreamPlayback}}407+{{if .HasCallbacks}}
281 {{range .Callbacks}}408 {{range .Callbacks}}
282 // {{.Name}} represents the callback struct for {{.Name}}.409 // {{.Name}} represents the callback struct for {{.Name}}.
283 type {{.Name}} {{.Signature}}410 type {{.Name}} {{.Signature}}
@@ -310,3 +437,38 @@ type {{.Group.Interface}} interface {
310 {{- end}}437 {{- end}}
311 }438 }
312 `439 `
440+
441+const adapterTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
442+
443+package capi
444+{{if .NeedsUnsafe}}
445+import "unsafe"
446+{{end}}
447+{{range $group := .Groups}}
448+// {{$group.AdapterName}} forwards calls to the generated CAPI variables.
449+type {{$group.AdapterName}} struct{}
450+{{range $group.Methods}}
451+func (a *{{$group.AdapterName}}) {{.GoName}}{{.Params}}{{if .Results}} {{.Results}}{{end}} {
452+{{- if .Results}}
453+ return {{.CName}}{{.Params}}
454+{{- else}}
455+ {{.CName}}{{.Params}}
456+{{- end}}
457+}
458+{{end}}
459+{{end}}
460+`
461+
462+const compositeTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
463+
464+package out
465+{{if .NeedsUnsafe}}
466+import "unsafe"
467+{{end}}
468+// CAPI is the composite interface that embeds all generated group interfaces.
469+type CAPI interface {
470+{{- range .Groups}}
471+ {{.Interface}}
472+{{- end}}
473+}
474+`
M cmd/pipewiregen/internal/emitter/emitter_test.go +39 -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)}"},
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"},
5959 },
6060 EventStructs: []model.EventStruct{
61- {Name: "pw_stream_events", Callbacks: []string{"process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"}},
61+ {Name: "pw_stream_events", Callbacks: []string{"process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"}, Group: "stream_playback"},
6262 },
6363 }
6464 }
@@ -101,6 +101,43 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
101101 }
102102 }
103103
104+func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) {
105+ paths, err := Emit(testPlayerModel(), t.TempDir())
106+ if err != nil {
107+ t.Fatalf("Emit returned error: %v", err)
108+ }
109+
110+ // Verify adapters file exists
111+ adapterContent, ok := paths["internal/capi/adapters_gen.go"]
112+ if !ok {
113+ t.Fatal("missing adapters_gen.go output")
114+ }
115+
116+ // Verify adapter contains streamPlaybackCAPIAdapter with PWStreamConnect method
117+ adapterStr := string(adapterContent)
118+ if !strings.Contains(adapterStr, "StreamPlaybackCAPIAdapter") {
119+ t.Fatal("adapters output missing StreamPlaybackCAPIAdapter struct")
120+ }
121+ if !strings.Contains(adapterStr, "PWStreamConnect") {
122+ t.Fatal("adapters output missing PWStreamConnect method")
123+ }
124+
125+ // Verify composite port file exists
126+ compositeContent, ok := paths["internal/ports/out/capi_gen.go"]
127+ if !ok {
128+ t.Fatal("missing capi_gen.go composite port output")
129+ }
130+
131+ // Verify composite interface embeds StreamPlaybackAPI
132+ compositeStr := string(compositeContent)
133+ if !strings.Contains(compositeStr, "CAPI") {
134+ t.Fatal("composite output missing CAPI type")
135+ }
136+ if !strings.Contains(compositeStr, "StreamPlaybackAPI") {
137+ t.Fatal("composite output missing StreamPlaybackAPI embed")
138+ }
139+}
140+
104141 // testCleanupModel returns a model with cleanup symbols that should be generated.
105142 func testCleanupModel() *model.Model {
106143 return &model.Model{
@@ -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)}"},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"},
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"}},61+ {Name: "pw_stream_events", Callbacks: []string{"process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"}, Group: "stream_playback"},
62 },62 },
63 }63 }
64 }64 }
@@ -101,6 +101,43 @@ func TestEmitWritesPlayerStreamFiles(t *testing.T) {
101 }101 }
102 }102 }
103 103
104+func TestEmitWritesAdaptersAndCompositeFiles(t *testing.T) {
105+ paths, err := Emit(testPlayerModel(), t.TempDir())
106+ if err != nil {
107+ t.Fatalf("Emit returned error: %v", err)
108+ }
109+
110+ // Verify adapters file exists
111+ adapterContent, ok := paths["internal/capi/adapters_gen.go"]
112+ if !ok {
113+ t.Fatal("missing adapters_gen.go output")
114+ }
115+
116+ // Verify adapter contains streamPlaybackCAPIAdapter with PWStreamConnect method
117+ adapterStr := string(adapterContent)
118+ if !strings.Contains(adapterStr, "StreamPlaybackCAPIAdapter") {
119+ t.Fatal("adapters output missing StreamPlaybackCAPIAdapter struct")
120+ }
121+ if !strings.Contains(adapterStr, "PWStreamConnect") {
122+ t.Fatal("adapters output missing PWStreamConnect method")
123+ }
124+
125+ // Verify composite port file exists
126+ compositeContent, ok := paths["internal/ports/out/capi_gen.go"]
127+ if !ok {
128+ t.Fatal("missing capi_gen.go composite port output")
129+ }
130+
131+ // Verify composite interface embeds StreamPlaybackAPI
132+ compositeStr := string(compositeContent)
133+ if !strings.Contains(compositeStr, "CAPI") {
134+ t.Fatal("composite output missing CAPI type")
135+ }
136+ if !strings.Contains(compositeStr, "StreamPlaybackAPI") {
137+ t.Fatal("composite output missing StreamPlaybackAPI embed")
138+ }
139+}
140+
104 // testCleanupModel returns a model with cleanup symbols that should be generated.141 // testCleanupModel returns a model with cleanup symbols that should be generated.
105 func testCleanupModel() *model.Model {142 func testCleanupModel() *model.Model {
106 return &model.Model{143 return &model.Model{
M cmd/pipewiregen/internal/model/model.go +2 -0
@@ -36,10 +36,12 @@ type Symbol struct {
3636 type Callback struct {
3737 Name string `json:"name"`
3838 Signature string `json:"signature"`
39+ Group string `json:"group"`
3940 }
4041
4142 // EventStruct represents an event struct with its associated callbacks.
4243 type EventStruct struct {
4344 Name string `json:"name"`
4445 Callbacks []string `json:"callbacks"`
46+ Group string `json:"group"`
4547 }
@@ -36,10 +36,12 @@ type Symbol struct {
36 type Callback struct {36 type Callback struct {
37 Name string `json:"name"`37 Name string `json:"name"`
38 Signature string `json:"signature"`38 Signature string `json:"signature"`
39+ Group string `json:"group"`
39 }40 }
40 41
41 // EventStruct represents an event struct with its associated callbacks.42 // EventStruct represents an event struct with its associated callbacks.
42 type EventStruct struct {43 type EventStruct struct {
43 Name string `json:"name"`44 Name string `json:"name"`
44 Callbacks []string `json:"callbacks"`45 Callbacks []string `json:"callbacks"`
46+ Group string `json:"group"`
45 }47 }
M cmd/pipewiregen/internal/parser/parser.go +12 -0
@@ -79,6 +79,12 @@ func validate(m *model.Model) error {
7979 if cb.Signature == "" {
8080 return fmt.Errorf("callback %q has empty signature", cb.Name)
8181 }
82+ if cb.Group == "" {
83+ return fmt.Errorf("callback %q has empty group", cb.Name)
84+ }
85+ if !groupNames[cb.Group] {
86+ return fmt.Errorf("callback %q references unknown group %q", cb.Name, cb.Group)
87+ }
8288 }
8389
8490 // Validate event structs
@@ -86,6 +92,12 @@ func validate(m *model.Model) error {
8692 if es.Name == "" {
8793 return fmt.Errorf("event struct at index %d has empty name", i)
8894 }
95+ if es.Group == "" {
96+ return fmt.Errorf("event struct %q has empty group", es.Name)
97+ }
98+ if !groupNames[es.Group] {
99+ return fmt.Errorf("event struct %q references unknown group %q", es.Name, es.Group)
100+ }
89101 }
90102
91103 return nil
@@ -79,6 +79,12 @@ func validate(m *model.Model) error {
79 if cb.Signature == "" {79 if cb.Signature == "" {
80 return fmt.Errorf("callback %q has empty signature", cb.Name)80 return fmt.Errorf("callback %q has empty signature", cb.Name)
81 }81 }
82+ if cb.Group == "" {
83+ return fmt.Errorf("callback %q has empty group", cb.Name)
84+ }
85+ if !groupNames[cb.Group] {
86+ return fmt.Errorf("callback %q references unknown group %q", cb.Name, cb.Group)
87+ }
82 }88 }
83 89
84 // Validate event structs90 // Validate event structs
@@ -86,6 +92,12 @@ func validate(m *model.Model) error {
86 if es.Name == "" {92 if es.Name == "" {
87 return fmt.Errorf("event struct at index %d has empty name", i)93 return fmt.Errorf("event struct at index %d has empty name", i)
88 }94 }
95+ if es.Group == "" {
96+ return fmt.Errorf("event struct %q has empty group", es.Name)
97+ }
98+ if !groupNames[es.Group] {
99+ return fmt.Errorf("event struct %q references unknown group %q", es.Name, es.Group)
100+ }
89 }101 }
90 102
91 return nil103 return nil
M cmd/pipewiregen/internal/parser/parser_test.go +13 -0
@@ -151,3 +151,16 @@ func TestLoadEmptyEventStructNameReturnsError(t *testing.T) {
151151 t.Fatalf("error message should mention event struct empty name, got: %v", err)
152152 }
153153 }
154+
155+func TestLoadCallbackWithUnknownGroupReturnsError(t *testing.T) {
156+ _, err := Load("testdata/callback_unknown_group.json")
157+ if err == nil {
158+ t.Fatal("expected error for callback with unknown group, got nil")
159+ }
160+ if !strings.Contains(err.Error(), "validation failed") {
161+ t.Fatalf("error message should contain 'validation failed', got: %v", err)
162+ }
163+ if !strings.Contains(err.Error(), "unknown group") {
164+ t.Fatalf("error message should contain 'unknown group', got: %v", err)
165+ }
166+}
@@ -151,3 +151,16 @@ func TestLoadEmptyEventStructNameReturnsError(t *testing.T) {
151 t.Fatalf("error message should mention event struct empty name, got: %v", err)151 t.Fatalf("error message should mention event struct empty name, got: %v", err)
152 }152 }
153 }153 }
154+
155+func TestLoadCallbackWithUnknownGroupReturnsError(t *testing.T) {
156+ _, err := Load("testdata/callback_unknown_group.json")
157+ if err == nil {
158+ t.Fatal("expected error for callback with unknown group, got nil")
159+ }
160+ if !strings.Contains(err.Error(), "validation failed") {
161+ t.Fatalf("error message should contain 'validation failed', got: %v", err)
162+ }
163+ if !strings.Contains(err.Error(), "unknown group") {
164+ t.Fatalf("error message should contain 'unknown group', got: %v", err)
165+ }
166+}
A cmd/pipewiregen/internal/parser/testdata/callback_unknown_group.json +14 -0
@@ -0,0 +1,14 @@
1+{
2+ "libraries": [
3+ {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4+ ],
5+ "groups": [
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
7+ ],
8+ "symbols": [
9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"}
10+ ],
11+ "callbacks": [
12+ {"name": "pw_stream_events", "signature": "struct{}", "group": "nonexistent"}
13+ ]
14+}⏎̸
@@ -0,0 +1,14 @@
1+{
2+ "libraries": [
3+ {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4+ ],
5+ "groups": [
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
7+ ],
8+ "symbols": [
9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"}
10+ ],
11+ "callbacks": [
12+ {"name": "pw_stream_events", "signature": "struct{}", "group": "nonexistent"}
13+ ]
14+}⏎̸
M gen/pipewire.json +2 -2
@@ -25,9 +25,9 @@
2525 {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
2626 ],
2727 "callbacks": [
28- {"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)}"}
28+ {"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"}
2929 ],
3030 "event_structs": [
31- {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"]}
31+ {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"], "group": "stream_playback"}
3232 ]
3333 }
@@ -25,9 +25,9 @@
25 {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}25 {"name": "pw_stream_destroy", "library": "pipewire", "group": "stream_playback", "signature": "func(stream unsafe.Pointer)"}
26 ],26 ],
27 "callbacks": [27 "callbacks": [
28- {"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)}"}28+ {"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 ],29 ],
30 "event_structs": [30 "event_structs": [
31- {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"]}31+ {"name": "pw_stream_events", "callbacks": ["process", "param_changed", "add_buffer", "remove_buffer", "flush", "drained", "io_changed"], "group": "stream_playback"}
32 ]32 ]
33 }33 }

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