detached 768bff5

feat: generate first raw PipeWire bindings

commit
768bff5fb26c49e4f2046ba7a053bc1071112054
author
brice · · 2026-04-09 05:23 UTC
parent
9f72b60
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +292 -7
M cmd/pipewiregen/internal/emitter/emitter.go +197 -0
@@ -1,2 +1,199 @@
11 // Package emitter renders generated Go source files from the binding model.
22 package emitter
3+
4+import (
5+ "bytes"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "text/template"
11+
12+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
13+)
14+
15+// Emit generates Go source files from the model and writes them to the root directory.
16+// It returns a map of generated file paths to their content.
17+func Emit(m *model.Model, root string) (map[string][]byte, error) {
18+ out := make(map[string][]byte)
19+ for _, group := range m.Groups {
20+ capiPath := filepath.Join("internal", "capi", group.Name+"_gen.go")
21+ portPath := filepath.Join("internal", "ports", "out", group.Name+"_gen.go")
22+
23+ capiContent, err := renderCAPI(group, m.Symbols, m.Libraries)
24+ if err != nil {
25+ return nil, fmt.Errorf("render capi for group %q: %w", group.Name, err)
26+ }
27+ out[capiPath] = capiContent
28+
29+ portContent, err := renderPort(group, m.Symbols)
30+ if err != nil {
31+ return nil, fmt.Errorf("render port for group %q: %w", group.Name, err)
32+ }
33+ out[portPath] = portContent
34+ }
35+ return out, writeAll(root, out)
36+}
37+
38+// renderCAPI generates the raw C API function types and registration helpers.
39+func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Library) ([]byte, error) {
40+ // Find symbols belonging to this group
41+ groupSymbols := filterSymbolsByGroup(symbols, group.Name)
42+
43+ // Build library map for lookup
44+ libMap := make(map[string]string)
45+ for _, lib := range libraries {
46+ libMap[lib.Name] = lib.SOName
47+ }
48+
49+ data := struct {
50+ Group model.Group
51+ Symbols []model.Symbol
52+ LibraryMap map[string]string
53+ }{
54+ Group: group,
55+ Symbols: groupSymbols,
56+ LibraryMap: libMap,
57+ }
58+
59+ tmpl, err := template.New("capi").Funcs(template.FuncMap{
60+ "title": strings.Title,
61+ }).Parse(capiTemplate)
62+ if err != nil {
63+ return nil, err
64+ }
65+
66+ var buf bytes.Buffer
67+ if err := tmpl.Execute(&buf, data); err != nil {
68+ return nil, err
69+ }
70+ return buf.Bytes(), nil
71+}
72+
73+// renderPort generates the outbound interface definitions.
74+func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
75+ groupSymbols := filterSymbolsByGroup(symbols, group.Name)
76+
77+ // Convert symbols to methods with proper Go names
78+ type Method struct {
79+ Name string
80+ GoName string
81+ Signature string
82+ CName string
83+ }
84+
85+ methods := make([]Method, 0, len(groupSymbols))
86+ for _, sym := range groupSymbols {
87+ methods = append(methods, Method{
88+ Name: sym.Name,
89+ GoName: toGoName(sym.Name),
90+ Signature: sym.Signature,
91+ CName: sym.Name,
92+ })
93+ }
94+
95+ data := struct {
96+ Group model.Group
97+ Methods []Method
98+ }{
99+ Group: group,
100+ Methods: methods,
101+ }
102+
103+ tmpl, err := template.New("port").Funcs(template.FuncMap{
104+ "title": strings.Title,
105+ }).Parse(portTemplate)
106+ if err != nil {
107+ return nil, err
108+ }
109+
110+ var buf bytes.Buffer
111+ if err := tmpl.Execute(&buf, data); err != nil {
112+ return nil, err
113+ }
114+ return buf.Bytes(), nil
115+}
116+
117+// filterSymbolsByGroup returns symbols that belong to the given group.
118+func filterSymbolsByGroup(symbols []model.Symbol, groupName string) []model.Symbol {
119+ var result []model.Symbol
120+ for _, sym := range symbols {
121+ if sym.Group == groupName {
122+ result = append(result, sym)
123+ }
124+ }
125+ return result
126+}
127+
128+// toGoName converts a C function name like pw_init to a Go name like PWInit.
129+func toGoName(cName string) string {
130+ // Handle pw_ prefix specially
131+ if strings.HasPrefix(cName, "pw_") {
132+ name := cName[3:]
133+ parts := strings.Split(name, "_")
134+ for i, p := range parts {
135+ parts[i] = strings.Title(p)
136+ }
137+ return "PW" + strings.Join(parts, "")
138+ }
139+ // Generic conversion
140+ parts := strings.Split(cName, "_")
141+ for i, p := range parts {
142+ parts[i] = strings.Title(p)
143+ }
144+ return strings.Join(parts, "")
145+}
146+
147+// writeAll writes all generated files to disk.
148+func writeAll(root string, files map[string][]byte) error {
149+ for path, content := range files {
150+ fullPath := filepath.Join(root, path)
151+ dir := filepath.Dir(fullPath)
152+ if err := os.MkdirAll(dir, 0755); err != nil {
153+ return fmt.Errorf("mkdir %s: %w", dir, err)
154+ }
155+ if err := os.WriteFile(fullPath, content, 0644); err != nil {
156+ return fmt.Errorf("write %s: %w", fullPath, err)
157+ }
158+ }
159+ return nil
160+}
161+
162+const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
163+
164+package capi
165+
166+import "github.com/ebitengine/purego"
167+
168+{{range .Symbols}}
169+// {{.Name}}Func is the function type for {{.Name}}.
170+type {{.Name}}Func {{.Signature}}
171+{{end}}
172+
173+var (
174+{{range .Symbols}}
175+ {{.Name}} {{.Name}}Func
176+{{end}}
177+)
178+
179+func register{{.Group.Name | title}}(handle uintptr) error {
180+{{range .Symbols}}
181+ if err := purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}"); err != nil {
182+ return err
183+ }
184+{{end}}
185+ return nil
186+}
187+`
188+
189+const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
190+
191+package out
192+
193+// {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
194+type {{.Group.Interface}} interface {
195+{{range .Methods}}
196+ {{.GoName}} {{.Signature}}
197+{{end}}
198+}
199+`
@@ -1,2 +1,199 @@
1 // Package emitter renders generated Go source files from the binding model.1 // Package emitter renders generated Go source files from the binding model.
2 package emitter2 package emitter
3+
4+import (
5+ "bytes"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "text/template"
11+
12+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
13+)
14+
15+// Emit generates Go source files from the model and writes them to the root directory.
16+// It returns a map of generated file paths to their content.
17+func Emit(m *model.Model, root string) (map[string][]byte, error) {
18+ out := make(map[string][]byte)
19+ for _, group := range m.Groups {
20+ capiPath := filepath.Join("internal", "capi", group.Name+"_gen.go")
21+ portPath := filepath.Join("internal", "ports", "out", group.Name+"_gen.go")
22+
23+ capiContent, err := renderCAPI(group, m.Symbols, m.Libraries)
24+ if err != nil {
25+ return nil, fmt.Errorf("render capi for group %q: %w", group.Name, err)
26+ }
27+ out[capiPath] = capiContent
28+
29+ portContent, err := renderPort(group, m.Symbols)
30+ if err != nil {
31+ return nil, fmt.Errorf("render port for group %q: %w", group.Name, err)
32+ }
33+ out[portPath] = portContent
34+ }
35+ return out, writeAll(root, out)
36+}
37+
38+// renderCAPI generates the raw C API function types and registration helpers.
39+func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Library) ([]byte, error) {
40+ // Find symbols belonging to this group
41+ groupSymbols := filterSymbolsByGroup(symbols, group.Name)
42+
43+ // Build library map for lookup
44+ libMap := make(map[string]string)
45+ for _, lib := range libraries {
46+ libMap[lib.Name] = lib.SOName
47+ }
48+
49+ data := struct {
50+ Group model.Group
51+ Symbols []model.Symbol
52+ LibraryMap map[string]string
53+ }{
54+ Group: group,
55+ Symbols: groupSymbols,
56+ LibraryMap: libMap,
57+ }
58+
59+ tmpl, err := template.New("capi").Funcs(template.FuncMap{
60+ "title": strings.Title,
61+ }).Parse(capiTemplate)
62+ if err != nil {
63+ return nil, err
64+ }
65+
66+ var buf bytes.Buffer
67+ if err := tmpl.Execute(&buf, data); err != nil {
68+ return nil, err
69+ }
70+ return buf.Bytes(), nil
71+}
72+
73+// renderPort generates the outbound interface definitions.
74+func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
75+ groupSymbols := filterSymbolsByGroup(symbols, group.Name)
76+
77+ // Convert symbols to methods with proper Go names
78+ type Method struct {
79+ Name string
80+ GoName string
81+ Signature string
82+ CName string
83+ }
84+
85+ methods := make([]Method, 0, len(groupSymbols))
86+ for _, sym := range groupSymbols {
87+ methods = append(methods, Method{
88+ Name: sym.Name,
89+ GoName: toGoName(sym.Name),
90+ Signature: sym.Signature,
91+ CName: sym.Name,
92+ })
93+ }
94+
95+ data := struct {
96+ Group model.Group
97+ Methods []Method
98+ }{
99+ Group: group,
100+ Methods: methods,
101+ }
102+
103+ tmpl, err := template.New("port").Funcs(template.FuncMap{
104+ "title": strings.Title,
105+ }).Parse(portTemplate)
106+ if err != nil {
107+ return nil, err
108+ }
109+
110+ var buf bytes.Buffer
111+ if err := tmpl.Execute(&buf, data); err != nil {
112+ return nil, err
113+ }
114+ return buf.Bytes(), nil
115+}
116+
117+// filterSymbolsByGroup returns symbols that belong to the given group.
118+func filterSymbolsByGroup(symbols []model.Symbol, groupName string) []model.Symbol {
119+ var result []model.Symbol
120+ for _, sym := range symbols {
121+ if sym.Group == groupName {
122+ result = append(result, sym)
123+ }
124+ }
125+ return result
126+}
127+
128+// toGoName converts a C function name like pw_init to a Go name like PWInit.
129+func toGoName(cName string) string {
130+ // Handle pw_ prefix specially
131+ if strings.HasPrefix(cName, "pw_") {
132+ name := cName[3:]
133+ parts := strings.Split(name, "_")
134+ for i, p := range parts {
135+ parts[i] = strings.Title(p)
136+ }
137+ return "PW" + strings.Join(parts, "")
138+ }
139+ // Generic conversion
140+ parts := strings.Split(cName, "_")
141+ for i, p := range parts {
142+ parts[i] = strings.Title(p)
143+ }
144+ return strings.Join(parts, "")
145+}
146+
147+// writeAll writes all generated files to disk.
148+func writeAll(root string, files map[string][]byte) error {
149+ for path, content := range files {
150+ fullPath := filepath.Join(root, path)
151+ dir := filepath.Dir(fullPath)
152+ if err := os.MkdirAll(dir, 0755); err != nil {
153+ return fmt.Errorf("mkdir %s: %w", dir, err)
154+ }
155+ if err := os.WriteFile(fullPath, content, 0644); err != nil {
156+ return fmt.Errorf("write %s: %w", fullPath, err)
157+ }
158+ }
159+ return nil
160+}
161+
162+const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
163+
164+package capi
165+
166+import "github.com/ebitengine/purego"
167+
168+{{range .Symbols}}
169+// {{.Name}}Func is the function type for {{.Name}}.
170+type {{.Name}}Func {{.Signature}}
171+{{end}}
172+
173+var (
174+{{range .Symbols}}
175+ {{.Name}} {{.Name}}Func
176+{{end}}
177+)
178+
179+func register{{.Group.Name | title}}(handle uintptr) error {
180+{{range .Symbols}}
181+ if err := purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}"); err != nil {
182+ return err
183+ }
184+{{end}}
185+ return nil
186+}
187+`
188+
189+const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
190+
191+package out
192+
193+// {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
194+type {{.Group.Interface}} interface {
195+{{range .Methods}}
196+ {{.GoName}} {{.Signature}}
197+{{end}}
198+}
199+`
M cmd/pipewiregen/internal/emitter/emitter_test.go +34 -0
@@ -1,1 +1,35 @@
11 package emitter
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
7+)
8+
9+func testModel() *model.Model {
10+ return &model.Model{
11+ Libraries: []model.Library{
12+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
13+ },
14+ Groups: []model.Group{
15+ {Name: "init", Interface: "InitAPI", Package: "out", Symbols: []string{"pw_init", "pw_deinit"}},
16+ },
17+ Symbols: []model.Symbol{
18+ {Name: "pw_init", Library: "pipewire", Group: "init", Signature: "func(argc *int32, argv ***byte)"},
19+ {Name: "pw_deinit", Library: "pipewire", Group: "init", Signature: "func()"},
20+ },
21+ }
22+}
23+
24+func TestEmitWritesCAPIAndPortFiles(t *testing.T) {
25+ paths, err := Emit(testModel(), t.TempDir())
26+ if err != nil {
27+ t.Fatalf("Emit returned error: %v", err)
28+ }
29+ if _, ok := paths["internal/capi/init_gen.go"]; !ok {
30+ t.Fatalf("missing capi output")
31+ }
32+ if _, ok := paths["internal/ports/out/init_gen.go"]; !ok {
33+ t.Fatalf("missing port output")
34+ }
35+}
@@ -1,1 +1,35 @@
1 package emitter1 package emitter
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
7+)
8+
9+func testModel() *model.Model {
10+ return &model.Model{
11+ Libraries: []model.Library{
12+ {Name: "pipewire", SOName: "libpipewire-0.3.so.0"},
13+ },
14+ Groups: []model.Group{
15+ {Name: "init", Interface: "InitAPI", Package: "out", Symbols: []string{"pw_init", "pw_deinit"}},
16+ },
17+ Symbols: []model.Symbol{
18+ {Name: "pw_init", Library: "pipewire", Group: "init", Signature: "func(argc *int32, argv ***byte)"},
19+ {Name: "pw_deinit", Library: "pipewire", Group: "init", Signature: "func()"},
20+ },
21+ }
22+}
23+
24+func TestEmitWritesCAPIAndPortFiles(t *testing.T) {
25+ paths, err := Emit(testModel(), t.TempDir())
26+ if err != nil {
27+ t.Fatalf("Emit returned error: %v", err)
28+ }
29+ if _, ok := paths["internal/capi/init_gen.go"]; !ok {
30+ t.Fatalf("missing capi output")
31+ }
32+ if _, ok := paths["internal/ports/out/init_gen.go"]; !ok {
33+ t.Fatalf("missing port output")
34+ }
35+}
M cmd/pipewiregen/internal/model/model.go +6 -2
@@ -15,8 +15,10 @@ type Library struct {
1515
1616 // Group represents a logical grouping of symbols.
1717 type Group struct {
18- Name string `json:"name"`
19- Symbols []string `json:"symbols"`
18+ Name string `json:"name"`
19+ Symbols []string `json:"symbols"`
20+ Interface string `json:"interface"`
21+ Package string `json:"package"`
2022 }
2123
2224 // Symbol represents a single C function to bind.
@@ -24,4 +26,6 @@ type Symbol struct {
2426 Name string `json:"name"`
2527 Library string `json:"library"`
2628 Signature string `json:"signature"`
29+ Optional bool `json:"optional,omitempty"`
30+ Group string `json:"group"`
2731 }
@@ -15,8 +15,10 @@ type Library struct {
15 15
16 // Group represents a logical grouping of symbols.16 // Group represents a logical grouping of symbols.
17 type Group struct {17 type Group struct {
18- Name string `json:"name"`18+ Name string `json:"name"`
19- Symbols []string `json:"symbols"`19+ Symbols []string `json:"symbols"`
20+ Interface string `json:"interface"`
21+ Package string `json:"package"`
20 }22 }
21 23
22 // Symbol represents a single C function to bind.24 // Symbol represents a single C function to bind.
@@ -24,4 +26,6 @@ type Symbol struct {
24 Name string `json:"name"`26 Name string `json:"name"`
25 Library string `json:"library"`27 Library string `json:"library"`
26 Signature string `json:"signature"`28 Signature string `json:"signature"`
29+ Optional bool `json:"optional,omitempty"`
30+ Group string `json:"group"`
27 }31 }
M cmd/pipewiregen/main.go +6 -2
@@ -5,6 +5,7 @@ import (
55 "os"
66 "path/filepath"
77
8+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/emitter"
89 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
910 )
1011
@@ -14,10 +15,13 @@ func main() {
1415 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
1516 os.Exit(1)
1617 }
17- _, err = parser.Load(filepath.Join(root, "gen", "pipewire.json"))
18+ pipewireModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
1819 if err != nil {
1920 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
2021 os.Exit(1)
2122 }
22- // Emit step will be added in Task 2.
23+ if _, err := emitter.Emit(pipewireModel, root); err != nil {
24+ fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
25+ os.Exit(1)
26+ }
2327 }
@@ -5,6 +5,7 @@ import (
5 "os"5 "os"
6 "path/filepath"6 "path/filepath"
7 7
8+ "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/emitter"
8 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"9 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/parser"
9 )10 )
10 11
@@ -14,10 +15,13 @@ func main() {
14 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)15 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
15 os.Exit(1)16 os.Exit(1)
16 }17 }
17- _, err = parser.Load(filepath.Join(root, "gen", "pipewire.json"))18+ pipewireModel, err := parser.Load(filepath.Join(root, "gen", "pipewire.json"))
18 if err != nil {19 if err != nil {
19 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)20 fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
20 os.Exit(1)21 os.Exit(1)
21 }22 }
22- // Emit step will be added in Task 2.23+ if _, err := emitter.Emit(pipewireModel, root); err != nil {
24+ fmt.Fprintf(os.Stderr, "pipewiregen: %v\n", err)
25+ os.Exit(1)
26+ }
23 }27 }
M gen/pipewire.json +3 -3
@@ -3,10 +3,10 @@
33 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
44 ],
55 "groups": [
6- {"name": "init", "symbols": ["pw_init", "pw_deinit"]}
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}
77 ],
88 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "signature": "func()"}
9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}
1111 ]
1212 }
@@ -3,10 +3,10 @@
3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4 ],4 ],
5 "groups": [5 "groups": [
6- {"name": "init", "symbols": ["pw_init", "pw_deinit"]}6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}
7 ],7 ],
8 "symbols": [8 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": "func(argc *int32, argv ***byte)"},9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "signature": "func()"}10+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}
11 ]11 ]
12 }12 }
A internal/capi/.gitkeep +0 -0

no textual change.

A internal/capi/init_gen.go +34 -0 generated file: collapsed
A internal/ports/out/.gitkeep +0 -0

no textual change.

A internal/ports/out/init_gen.go +12 -0 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