detached f2fb7ae

feat: add PipeWire loader and raw registration

commit
f2fb7ae63cc008917656e6f5f097ee59445b9155
author
brice · · 2026-04-09 05:46 UTC
parent
a0e3d34
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
9 files · +156 -14
M .mockery.yml +1 -0
@@ -13,3 +13,4 @@ packages:
1313 include-auto-generated: true
1414 interfaces:
1515 InitAPI:
16+ LoopAPI:
@@ -13,3 +13,4 @@ packages:
13 include-auto-generated: true13 include-auto-generated: true
14 interfaces:14 interfaces:
15 InitAPI:15 InitAPI:
16+ LoopAPI:
M cmd/pipewiregen/internal/emitter/emitter.go +34 -11
@@ -76,14 +76,25 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
7676 libMap[lib.Name] = lib.SOName
7777 }
7878
79+ // Check if any symbol needs unsafe import
80+ needsUnsafe := false
81+ for _, sym := range groupSymbols {
82+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
83+ needsUnsafe = true
84+ break
85+ }
86+ }
87+
7988 data := struct {
80- Group model.Group
81- Symbols []model.Symbol
82- LibraryMap map[string]string
89+ Group model.Group
90+ Symbols []model.Symbol
91+ LibraryMap map[string]string
92+ NeedsUnsafe bool
8393 }{
84- Group: group,
85- Symbols: groupSymbols,
86- LibraryMap: libMap,
94+ Group: group,
95+ Symbols: groupSymbols,
96+ LibraryMap: libMap,
97+ NeedsUnsafe: needsUnsafe,
8798 }
8899
89100 var buf bytes.Buffer
@@ -107,6 +118,7 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
107118 }
108119
109120 methods := make([]Method, 0, len(groupSymbols))
121+ needsUnsafe := false
110122 for _, sym := range groupSymbols {
111123 params, results := parseSignature(sym.Signature)
112124 methods = append(methods, Method{
@@ -116,14 +128,20 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
116128 Results: results,
117129 CName: sym.Name,
118130 })
131+ // Check if signature uses unsafe.Pointer
132+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
133+ needsUnsafe = true
134+ }
119135 }
120136
121137 data := struct {
122- Group model.Group
123- Methods []Method
138+ Group model.Group
139+ Methods []Method
140+ NeedsUnsafe bool
124141 }{
125- Group: group,
126- Methods: methods,
142+ Group: group,
143+ Methods: methods,
144+ NeedsUnsafe: needsUnsafe,
127145 }
128146
129147 var buf bytes.Buffer
@@ -223,6 +241,9 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
223241 package capi
224242
225243 import "github.com/ebitengine/purego"
244+{{if .NeedsUnsafe}}
245+import "unsafe"
246+{{end}}
226247 {{range .Symbols}}
227248 // {{.Name}}Func is the function type for {{.Name}}.
228249 type {{.Name}}Func {{.Signature}}
@@ -244,7 +265,9 @@ func register{{.Group.Name | title}}(handle uintptr) {
244265 const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
245266
246267 package out
247-
268+{{if .NeedsUnsafe}}
269+import "unsafe"
270+{{end}}
248271 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
249272 type {{.Group.Interface}} interface {
250273 {{- range .Methods}}
@@ -76,14 +76,25 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
76 libMap[lib.Name] = lib.SOName76 libMap[lib.Name] = lib.SOName
77 }77 }
78 78
79+ // Check if any symbol needs unsafe import
80+ needsUnsafe := false
81+ for _, sym := range groupSymbols {
82+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
83+ needsUnsafe = true
84+ break
85+ }
86+ }
87+
79 data := struct {88 data := struct {
80- Group model.Group89+ Group model.Group
81- Symbols []model.Symbol90+ Symbols []model.Symbol
82- LibraryMap map[string]string91+ LibraryMap map[string]string
92+ NeedsUnsafe bool
83 }{93 }{
84- Group: group,94+ Group: group,
85- Symbols: groupSymbols,95+ Symbols: groupSymbols,
86- LibraryMap: libMap,96+ LibraryMap: libMap,
97+ NeedsUnsafe: needsUnsafe,
87 }98 }
88 99
89 var buf bytes.Buffer100 var buf bytes.Buffer
@@ -107,6 +118,7 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
107 }118 }
108 119
109 methods := make([]Method, 0, len(groupSymbols))120 methods := make([]Method, 0, len(groupSymbols))
121+ needsUnsafe := false
110 for _, sym := range groupSymbols {122 for _, sym := range groupSymbols {
111 params, results := parseSignature(sym.Signature)123 params, results := parseSignature(sym.Signature)
112 methods = append(methods, Method{124 methods = append(methods, Method{
@@ -116,14 +128,20 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
116 Results: results,128 Results: results,
117 CName: sym.Name,129 CName: sym.Name,
118 })130 })
131+ // Check if signature uses unsafe.Pointer
132+ if strings.Contains(sym.Signature, "unsafe.Pointer") {
133+ needsUnsafe = true
134+ }
119 }135 }
120 136
121 data := struct {137 data := struct {
122- Group model.Group138+ Group model.Group
123- Methods []Method139+ Methods []Method
140+ NeedsUnsafe bool
124 }{141 }{
125- Group: group,142+ Group: group,
126- Methods: methods,143+ Methods: methods,
144+ NeedsUnsafe: needsUnsafe,
127 }145 }
128 146
129 var buf bytes.Buffer147 var buf bytes.Buffer
@@ -223,6 +241,9 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
223 package capi241 package capi
224 242
225 import "github.com/ebitengine/purego"243 import "github.com/ebitengine/purego"
244+{{if .NeedsUnsafe}}
245+import "unsafe"
246+{{end}}
226 {{range .Symbols}}247 {{range .Symbols}}
227 // {{.Name}}Func is the function type for {{.Name}}.248 // {{.Name}}Func is the function type for {{.Name}}.
228 type {{.Name}}Func {{.Signature}}249 type {{.Name}}Func {{.Signature}}
@@ -244,7 +265,9 @@ func register{{.Group.Name | title}}(handle uintptr) {
244 const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT.265 const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
245 266
246 package out267 package out
247-268+{{if .NeedsUnsafe}}
269+import "unsafe"
270+{{end}}
248 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.271 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
249 type {{.Group.Interface}} interface {272 type {{.Group.Interface}} interface {
250 {{- range .Methods}}273 {{- range .Methods}}
M gen/pipewire.json +8 -3
@@ -1,12 +1,17 @@
11 {
22 "libraries": [
3- {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
3+ {"name": "pipewire", "soname": "libpipewire-0.3.so.0"},
4+ {"name": "spa", "soname": "libspa-0.2.so.0"}
45 ],
56 "groups": [
6- {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}
7+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]}
79 ],
810 "symbols": [
911 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}
12+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"},
13+ {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
14+ {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
15+ {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"}
1116 ]
1217 }
@@ -1,12 +1,17 @@
1 {1 {
2 "libraries": [2 "libraries": [
3- {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3+ {"name": "pipewire", "soname": "libpipewire-0.3.so.0"},
4+ {"name": "spa", "soname": "libspa-0.2.so.0"}
4 ],5 ],
5 "groups": [6 "groups": [
6- {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}7+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]},
8+ {"name": "loop", "interface": "LoopAPI", "package": "out", "symbols": ["pw_main_loop_new", "pw_main_loop_destroy", "pw_main_loop_run"]}
7 ],9 ],
8 "symbols": [10 "symbols": [
9 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},11 {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}12+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"},
13+ {"name": "pw_main_loop_new", "library": "pipewire", "group": "loop", "signature": "func(props unsafe.Pointer) unsafe.Pointer"},
14+ {"name": "pw_main_loop_destroy", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer)"},
15+ {"name": "pw_main_loop_run", "library": "pipewire", "group": "loop", "signature": "func(loop unsafe.Pointer) int32"}
11 ]16 ]
12 }17 }
M internal/capi/init_gen.go +1 -0 generated file: collapsed
A internal/capi/loop_gen.go +38 -0 generated file: collapsed
A internal/capi/register.go +11 -0
@@ -0,0 +1,11 @@
1+package capi
2+
3+import "github.com/bnema/purego-pipewire/internal/loader"
4+
5+func Register() error {
6+ h, err := loader.Open()
7+ if err != nil {
8+ return err
9+ }
10+ return registerAll(h)
11+}
@@ -0,0 +1,11 @@
1+package capi
2+
3+import "github.com/bnema/purego-pipewire/internal/loader"
4+
5+func Register() error {
6+ h, err := loader.Open()
7+ if err != nil {
8+ return err
9+ }
10+ return registerAll(h)
11+}
A internal/capi/register_all.go +9 -0
@@ -0,0 +1,9 @@
1+package capi
2+
3+import "github.com/bnema/purego-pipewire/internal/loader"
4+
5+func registerAll(h loader.Handles) error {
6+ registerInit(h.PipeWire)
7+ registerLoop(h.PipeWire)
8+ return nil
9+}
@@ -0,0 +1,9 @@
1+package capi
2+
3+import "github.com/bnema/purego-pipewire/internal/loader"
4+
5+func registerAll(h loader.Handles) error {
6+ registerInit(h.PipeWire)
7+ registerLoop(h.PipeWire)
8+ return nil
9+}
A internal/loader/loader.go +45 -0
@@ -0,0 +1,45 @@
1+package loader
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+
8+ "github.com/ebitengine/purego"
9+)
10+
11+type Handles struct {
12+ PipeWire uintptr
13+ SPA uintptr
14+}
15+
16+func Open() (Handles, error) {
17+ pw, err := openOne(resolveLib("PIPEWIRE_LIB_PATH", "libpipewire-0.3.so.0"))
18+ if err != nil {
19+ return Handles{}, err
20+ }
21+ spa, err := openOne(resolveLib("SPA_LIB_PATH", "libspa-0.2.so.0"))
22+ if err != nil {
23+ return Handles{}, err
24+ }
25+ return Handles{PipeWire: pw, SPA: spa}, nil
26+}
27+
28+func openOne(path string) (uintptr, error) {
29+ h, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL)
30+ if err != nil {
31+ return 0, fmt.Errorf("dlopen %s: %w", path, err)
32+ }
33+ return h, nil
34+}
35+
36+func resolveLib(envName, fallback string) string {
37+ if v := os.Getenv(envName); v != "" {
38+ return v
39+ }
40+ return fallback
41+}
42+
43+func libFileName(base, soversion string) string {
44+ return filepath.Base(base) + ".so." + soversion
45+}
@@ -0,0 +1,45 @@
1+package loader
2+
3+import (
4+ "fmt"
5+ "os"
6+ "path/filepath"
7+
8+ "github.com/ebitengine/purego"
9+)
10+
11+type Handles struct {
12+ PipeWire uintptr
13+ SPA uintptr
14+}
15+
16+func Open() (Handles, error) {
17+ pw, err := openOne(resolveLib("PIPEWIRE_LIB_PATH", "libpipewire-0.3.so.0"))
18+ if err != nil {
19+ return Handles{}, err
20+ }
21+ spa, err := openOne(resolveLib("SPA_LIB_PATH", "libspa-0.2.so.0"))
22+ if err != nil {
23+ return Handles{}, err
24+ }
25+ return Handles{PipeWire: pw, SPA: spa}, nil
26+}
27+
28+func openOne(path string) (uintptr, error) {
29+ h, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_GLOBAL)
30+ if err != nil {
31+ return 0, fmt.Errorf("dlopen %s: %w", path, err)
32+ }
33+ return h, nil
34+}
35+
36+func resolveLib(envName, fallback string) string {
37+ if v := os.Getenv(envName); v != "" {
38+ return v
39+ }
40+ return fallback
41+}
42+
43+func libFileName(base, soversion string) string {
44+ return filepath.Base(base) + ".so." + soversion
45+}
A internal/loader/loader_test.go +9 -0
@@ -0,0 +1,9 @@
1+package loader
2+
3+import "testing"
4+
5+func TestLibFileNameLinuxDefault(t *testing.T) {
6+ if got, want := libFileName("libpipewire-0.3", "0"), "libpipewire-0.3.so.0"; got != want {
7+ t.Fatalf("libFileName=%q want %q", got, want)
8+ }
9+}
@@ -0,0 +1,9 @@
1+package loader
2+
3+import "testing"
4+
5+func TestLibFileNameLinuxDefault(t *testing.T) {
6+ if got, want := libFileName("libpipewire-0.3", "0"), "libpipewire-0.3.so.0"; got != want {
7+ t.Fatalf("libFileName=%q want %q", got, want)
8+ }
9+}

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