1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

core/wayland: thread-safety meta fixes + cleanups + hypr workaround

- fork go-wayland/client and modify to make it thread-safe internally
- use sync.Map and atomic values in many places to cut down on mutex
  boilerplate
- do not create extworkspace client unless explicitly requested
This commit is contained in:
bbedward
2025-11-15 14:41:00 -05:00
parent 20f7d60147
commit 91891a14ed
54 changed files with 8610 additions and 698 deletions

View File

@@ -0,0 +1,3 @@
// Keep this sorted
rajveermalviya

View File

@@ -0,0 +1,24 @@
Copyright 2021 go-wayland authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,25 @@
# Wayland implementation in Go
[![Go Reference](https://pkg.go.dev/badge/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland.svg)](https://pkg.go.dev/github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland)
This module contains pure Go implementation of the Wayland protocol.
Currently only wayland-client functionality is supported.
Go code is generated from protocol XML files using
[`go-wayland-scanner`](cmd/go-wayland-scanner/scanner.go).
To load cursor, minimal port of `wayland-cursor` & `xcursor` in pure Go
is located at [`wayland/cursor`](wayland/cursor) & [`wayland/cursor/xcursor`](wayland/cursor/xcursor)
respectively.
To demonstrate the functionality of this module
[`examples/imageviewer`](examples/imageviewer) contains a simple image
viewer. It demos displaying a top-level window, resizing of window,
cursor themes, pointer and keyboard. Because it's in pure Go, it can be
compiled without CGO. You can try it using the following commands:
```sh
CGO_ENABLED=0 go install github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/examples/imageviewer@latest
imageviewer file.jpg
```

4
core/pkg/go-wayland/generate Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
cd ./wayland
go generate -x ./...

9
core/pkg/go-wayland/generatep Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Runs go generate for each directory, but in parallel. Any arguments are appended to the
# go generate command.
# Usage: $ ./generatep [go generate arguments]
# Print all generate commands: $ ./generatep -x
cd ./wayland
find . -type f -name '*.go' -exec dirname {} \; | sort -u | parallel -j 0 go generate $1 {}/.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
package client
type Dispatcher interface {
Dispatch(opcode uint32, fd int, data []byte)
}
type Proxy interface {
Context() *Context
SetContext(ctx *Context)
ID() uint32
SetID(id uint32)
}
type BaseProxy struct {
ctx *Context
id uint32
}
func (p *BaseProxy) ID() uint32 {
return p.id
}
func (p *BaseProxy) SetID(id uint32) {
p.id = id
}
func (p *BaseProxy) Context() *Context {
return p.ctx
}
func (p *BaseProxy) SetContext(ctx *Context) {
p.ctx = ctx
}

View File

@@ -0,0 +1,110 @@
package client
import (
"errors"
"fmt"
"net"
"os"
"sync"
)
type Context struct {
conn *net.UnixConn
objects sync.Map // map[uint32]Proxy - thread-safe concurrent map
currentID uint32
idMu sync.Mutex // protects currentID increment
}
func (ctx *Context) Register(p Proxy) {
ctx.idMu.Lock()
ctx.currentID++
id := ctx.currentID
ctx.idMu.Unlock()
p.SetID(id)
p.SetContext(ctx)
ctx.objects.Store(id, p)
}
func (ctx *Context) Unregister(p Proxy) {
ctx.objects.Delete(p.ID())
}
func (ctx *Context) GetProxy(id uint32) Proxy {
if val, ok := ctx.objects.Load(id); ok {
return val.(Proxy)
}
return nil
}
func (ctx *Context) Close() error {
return ctx.conn.Close()
}
// Dispatch reads and processes incoming messages and calls [client.Dispatcher.Dispatch] on the
// respective wayland protocol.
// Dispatch must be called on the same goroutine as other interactions with the Context.
// If a multi goroutine approach is desired, use [Context.GetDispatch] instead.
// Dispatch blocks if there are no incoming messages.
// A Dispatch loop is usually used to handle incoming messages.
func (ctx *Context) Dispatch() error {
return ctx.GetDispatch()()
}
var ErrDispatchSenderNotFound = errors.New("dispatch: unable to find sender")
var ErrDispatchSenderUnsupported = errors.New("dispatch: sender does not implement Dispatch method")
var ErrDispatchUnableToReadMsg = errors.New("dispatch: unable to read msg")
// GetDispatch reads incoming messages and returns the dispatch function which calls
// [client.Dispatcher.Dispatch] on the respective wayland protocol.
// This function is now thread-safe and can be called from multiple goroutines.
// GetDispatch blocks if there are no incoming messages.
func (ctx *Context) GetDispatch() func() error {
senderID, opcode, fd, data, err := ctx.ReadMsg() // Blocks if there are no incoming messages
if err != nil {
return func() error {
return fmt.Errorf("%w: %w", ErrDispatchUnableToReadMsg, err)
}
}
return func() error {
val, ok := ctx.objects.Load(senderID)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderNotFound, senderID)
}
sender, ok := val.(Dispatcher)
if !ok {
return fmt.Errorf("%w (senderID=%d)", ErrDispatchSenderUnsupported, senderID)
}
sender.Dispatch(opcode, fd, data)
return nil
}
}
func Connect(addr string) (*Display, error) {
if addr == "" {
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
if runtimeDir == "" {
return nil, errors.New("env XDG_RUNTIME_DIR not set")
}
if addr == "" {
addr = os.Getenv("WAYLAND_DISPLAY")
}
if addr == "" {
addr = "wayland-0"
}
addr = runtimeDir + "/" + addr
}
ctx := &Context{}
conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: addr, Net: "unix"})
if err != nil {
return nil, err
}
ctx.conn = conn
return NewDisplay(ctx), nil
}

View File

@@ -0,0 +1,111 @@
package client_test
import (
"errors"
"fmt"
"log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
// Shows a dispatch loop that will block the goroutine.
// This approach has no risk of data races but the loop blocks the goroutine when no messages are
// received. This can be a valid approach if there are no more changes that need to be made after
// setting up and starting the loop.
// For a multi goroutine approach, use [client.Context.GetDispatch].
func ExampleContext_Dispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
for {
err := display.Context().Dispatch()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
// Shows how the dispatch loop can be done in another goroutine.
// This prevents the goroutine from being blocked and allows making changes to wayland objects while
// the dispatch loop is blocking another goroutine.
func ExampleContext_GetDispatch() {
display, err := client.Connect("")
if err != nil {
log.Fatalf("Error connecting to Wayland server: %v", err)
}
registry, err := display.GetRegistry()
if err != nil {
log.Fatalf("Error getting Wayland registry: %v", err)
}
var seat *client.Seat
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
switch e.Interface {
case client.SeatInterfaceName:
seat = client.NewSeat(display.Context())
err := registry.Bind(e.Name, e.Interface, e.Version, seat)
if err != nil {
log.Fatalf("unable to bind %s interface: %v", client.SeatInterfaceName, err)
}
}
})
display.Roundtrip()
display.Roundtrip()
dispatchQueue := make(chan func() error)
go func() {
for {
dispatchQueue <- display.Context().GetDispatch()
}
}()
keyboard, err := seat.GetKeyboard()
if err != nil {
log.Printf("Error getting keyboard: %v", err)
}
log.Printf("Got keyboard: %v\n", keyboard)
err = errors.Join(keyboard.Release(), seat.Release(), display.Context().Close())
if err != nil {
fmt.Printf("Error cleaning up: %v\n", err)
}
for {
select {
// Add other cases here to do other things
case dispatchFunc := <-dispatchQueue:
err := dispatchFunc()
if err != nil {
log.Printf("Dispatch error: %v\n", err)
}
}
}
}

View File

@@ -0,0 +1,37 @@
package client
import (
"fmt"
"log"
)
// Roundtrip blocks until all pending request are processed by the server.
// It is the implementation of [wl_display_roundtrip].
//
// [wl_display_roundtrip]: https://wayland.freedesktop.org/docs/html/apb.html#Client-classwl__display_1ab60f38c2f80980ac84f347e932793390
func (i *Display) Roundtrip() error {
callback, err := i.Sync()
if err != nil {
return fmt.Errorf("unable to get sync callback: %w", err)
}
defer func() {
if err2 := callback.Destroy(); err2 != nil {
log.Printf("unable to destroy callback: %v\n", err2)
}
}()
done := false
callback.SetDoneHandler(func(_ CallbackDoneEvent) {
done = true
})
// Wait for callback to return
for !done {
err := i.Context().GetDispatch()()
if err != nil {
return fmt.Errorf("roundtrip: failed to dispatch: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,6 @@
// Package client is Go port of wayland-client library
// for writing pure Go GUI software for wayland supported
// platforms.
package client
//go:generate go run github.com/yaslama/go-wayland/cmd/go-wayland-scanner -pkg client -prefix wl -o client.go -i https://gitlab.freedesktop.org/wayland/wayland/-/raw/1.23.0/protocol/wayland.xml?ref_type=tags

View File

@@ -0,0 +1,120 @@
package client
import (
"bytes"
"fmt"
"unsafe"
"golang.org/x/sys/unix"
_ "unsafe"
)
var oobSpace = unix.CmsgSpace(4)
func (ctx *Context) ReadMsg() (senderID uint32, opcode uint32, fd int, msg []byte, err error) {
fd = -1
oob := make([]byte, oobSpace)
header := make([]byte, 8)
n, oobn, _, _, err := ctx.conn.ReadMsgUnix(header, oob)
if err != nil {
return senderID, opcode, fd, msg, err
}
if n != 8 {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for header (n=%d)", n)
}
if oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "header")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
senderID = Uint32(header[:4])
opcodeAndSize := Uint32(header[4:8])
opcode = opcodeAndSize & 0xffff
size := opcodeAndSize >> 16
msgSize := int(size) - 8
if msgSize == 0 {
return senderID, opcode, fd, nil, nil
}
msg = make([]byte, msgSize)
if fd == -1 {
// if something was read before, then zero it out
if oobn > 0 {
oob = make([]byte, oobSpace)
}
n, oobn, _, _, err = ctx.conn.ReadMsgUnix(msg, oob)
} else {
n, err = ctx.conn.Read(msg)
}
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if n != msgSize {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: incorrect number of bytes read for msg (n=%d, msgSize=%d)", n, msgSize)
}
if fd == -1 && oobn > 0 {
fds, err := getFdsFromOob(oob, oobn, "msg")
if err != nil {
return senderID, opcode, fd, msg, fmt.Errorf("ctx.ReadMsg: %w", err)
}
if len(fds) > 0 {
fd = fds[0]
}
}
return senderID, opcode, fd, msg, nil
}
func getFdsFromOob(oob []byte, oobn int, source string) ([]int, error) {
if oobn > len(oob) {
return nil, fmt.Errorf("getFdsFromOob: incorrect number of bytes read from %s for oob (oobn=%d)", source, oobn)
}
scms, err := unix.ParseSocketControlMessage(oob)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse control message from %s: %w", source, err)
}
var fdsRet []int
for _, scm := range scms {
fds, err := unix.ParseUnixRights(&scm)
if err != nil {
return nil, fmt.Errorf("getFdsFromOob: unable to parse unix rights from %s: %w", source, err)
}
fdsRet = append(fdsRet, fds...)
}
return fdsRet, nil
}
func Uint32(src []byte) uint32 {
_ = src[3]
return *(*uint32)(unsafe.Pointer(&src[0]))
}
func String(src []byte) string {
idx := bytes.IndexByte(src, 0)
src = src[:idx:idx]
return *(*string)(unsafe.Pointer(&src))
}
func Fixed(src []byte) float64 {
_ = src[3]
fx := *(*int32)(unsafe.Pointer(&src[0]))
return fixedToFloat64(fx)
}

View File

@@ -0,0 +1,44 @@
package client
import (
"fmt"
"unsafe"
)
func (ctx *Context) WriteMsg(b []byte, oob []byte) error {
n, oobn, err := ctx.conn.WriteMsgUnix(b, oob, nil)
if err != nil {
return err
}
if n != len(b) || oobn != len(oob) {
return fmt.Errorf("ctx.WriteMsg: incorrect number of bytes written (n=%d oobn=%d)", n, oobn)
}
return nil
}
func PutUint32(dst []byte, v uint32) {
_ = dst[3]
*(*uint32)(unsafe.Pointer(&dst[0])) = v
}
func PutFixed(dst []byte, f float64) {
fx := fixedFromfloat64(f)
_ = dst[3]
*(*int32)(unsafe.Pointer(&dst[0])) = fx
}
// PutString places a string in Wayland's wire format on the destination buffer.
// It first places the length of the string (plus one for the null terminator) and then the string
// followed by a null byte.
// The length of dst must be equal to, or greater than, len(v) + 5.
func PutString(dst []byte, v string) {
PutUint32(dst[:4], uint32(len(v)+1))
copy(dst[4:], v)
dst[4+len(v)] = '\x00' // To cause panic if dst is not large enough
}
func PutArray(dst []byte, a []byte) {
PutUint32(dst[:4], uint32(len(a)))
copy(dst[4:], a)
}

View File

@@ -0,0 +1,24 @@
package client
import "math"
// From wayland/wayland-util.h
func fixedToFloat64(f int32) float64 {
u_i := (1023+44)<<52 + (1 << 51) + int64(f)
u_d := math.Float64frombits(uint64(u_i))
return u_d - (3 << 43)
}
func fixedFromfloat64(d float64) int32 {
u_d := d + (3 << (51 - 8))
u_i := int64(math.Float64bits(u_d))
return int32(u_i)
}
func PaddedLen(l int) int {
if (l & 0x3) != 0 {
return l + (4 - (l & 0x3))
}
return l
}