feat: add PipeWire loader and raw registration
- commit
- f2fb7ae63cc008917656e6f5f097ee59445b9155
- author
- brice · · 2026-04-09 05:46 UTC
- parent
- a0e3d34
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +156 -14
M .mockery.yml +1 -0
| @@ -13,3 +13,4 @@ packages: | |||
| 13 | 13 | include-auto-generated: true | |
| 14 | 14 | interfaces: | |
| 15 | 15 | InitAPI: | |
| 16 | + | LoopAPI: | |
| @@ -13,3 +13,4 @@ packages: | |||||
| 13 | include-auto-generated: true | 13 | 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 | |||
| 76 | 76 | 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 | 88 | 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 | |
| 83 | 93 | }{ | |
| 84 | - | Group: group, | |
| 85 | - | Symbols: groupSymbols, | |
| 86 | - | LibraryMap: libMap, | |
| 94 | + | Group: group, | |
| 95 | + | Symbols: groupSymbols, | |
| 96 | + | LibraryMap: libMap, | |
| 97 | + | NeedsUnsafe: needsUnsafe, | |
| 87 | 98 | } | |
| 88 | 99 | ||
| 89 | 100 | var buf bytes.Buffer | |
| @@ -107,6 +118,7 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) { | |||
| 107 | 118 | } | |
| 108 | 119 | ||
| 109 | 120 | methods := make([]Method, 0, len(groupSymbols)) | |
| 121 | + | needsUnsafe := false | |
| 110 | 122 | for _, sym := range groupSymbols { | |
| 111 | 123 | params, results := parseSignature(sym.Signature) | |
| 112 | 124 | methods = append(methods, Method{ | |
| @@ -116,14 +128,20 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) { | |||
| 116 | 128 | Results: results, | |
| 117 | 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 | 137 | data := struct { | |
| 122 | - | Group model.Group | |
| 123 | - | Methods []Method | |
| 138 | + | Group model.Group | |
| 139 | + | Methods []Method | |
| 140 | + | NeedsUnsafe bool | |
| 124 | 141 | }{ | |
| 125 | - | Group: group, | |
| 126 | - | Methods: methods, | |
| 142 | + | Group: group, | |
| 143 | + | Methods: methods, | |
| 144 | + | NeedsUnsafe: needsUnsafe, | |
| 127 | 145 | } | |
| 128 | 146 | ||
| 129 | 147 | var buf bytes.Buffer | |
| @@ -223,6 +241,9 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT. | |||
| 223 | 241 | package capi | |
| 224 | 242 | ||
| 225 | 243 | import "github.com/ebitengine/purego" | |
| 244 | + | {{if .NeedsUnsafe}} | |
| 245 | + | import "unsafe" | |
| 246 | + | {{end}} | |
| 226 | 247 | {{range .Symbols}} | |
| 227 | 248 | // {{.Name}}Func is the function type for {{.Name}}. | |
| 228 | 249 | type {{.Name}}Func {{.Signature}} | |
| @@ -244,7 +265,9 @@ func register{{.Group.Name | title}}(handle uintptr) { | |||
| 244 | 265 | const portTemplate = `// Code generated by pipewiregen. DO NOT EDIT. | |
| 245 | 266 | ||
| 246 | 267 | package out | |
| 247 | - | ||
| 268 | + | {{if .NeedsUnsafe}} | |
| 269 | + | import "unsafe" | |
| 270 | + | {{end}} | |
| 248 | 271 | // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations. | |
| 249 | 272 | type {{.Group.Interface}} interface { | |
| 250 | 273 | {{- range .Methods}} | |
| @@ -76,14 +76,25 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib | |||||
| 76 | libMap[lib.Name] = lib.SOName | 76 | 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.Group | 89 | + | Group model.Group |
| 81 | - | Symbols []model.Symbol | 90 | + | Symbols []model.Symbol |
| 82 | - | LibraryMap map[string]string | 91 | + | 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.Buffer | 100 | 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.Group | 138 | + | Group model.Group |
| 123 | - | Methods []Method | 139 | + | 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.Buffer | 147 | var buf bytes.Buffer | ||
| @@ -223,6 +241,9 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT. | |||||
| 223 | package capi | 241 | 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 out | 267 | 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 @@ | |||
| 1 | 1 | { | |
| 2 | 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 | 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 | 10 | "symbols": [ | |
| 9 | 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 | } | |
| @@ -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 | + | } | |||