feat(server): prove libwayland-server bindings without cgo
Bind non-varargs libwayland-server entry points through purego, build wl_interface tables in mmap memory, and route requests through one shared dispatcher. Feasibility tests cover wayland-info, request/event roundtrip, and resource cleanup on disconnect.
- commit
- 54d04f6111e760cd53c6cd28a9e8e8273f8c6ada
- author
- brice · · 2026-09-23 09:52 UTC
- parent
- none (root commit)
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
14 files · +775 -0
A .github/workflows/ci.yml +17 -0
| @@ -0,0 +1,17 @@ | |||
| 1 | + | name: ci | |
| 2 | + | ||
| 3 | + | on: | |
| 4 | + | push: | |
| 5 | + | pull_request: | |
| 6 | + | ||
| 7 | + | jobs: | |
| 8 | + | test: | |
| 9 | + | runs-on: ubuntu-latest | |
| 10 | + | steps: | |
| 11 | + | - uses: actions/checkout@v4 | |
| 12 | + | - uses: actions/setup-go@v5 | |
| 13 | + | with: | |
| 14 | + | go-version: '1.27' | |
| 15 | + | - run: sudo apt-get install -y libwayland-server0 libwayland-client0 wayland-utils | |
| 16 | + | - run: make check | |
| 17 | + | - run: make race | |
| @@ -0,0 +1,17 @@ | |||||
| 1 | + | name: ci | |||
| 2 | + | ||||
| 3 | + | on: | |||
| 4 | + | push: | |||
| 5 | + | pull_request: | |||
| 6 | + | ||||
| 7 | + | jobs: | |||
| 8 | + | test: | |||
| 9 | + | runs-on: ubuntu-latest | |||
| 10 | + | steps: | |||
| 11 | + | - uses: actions/checkout@v4 | |||
| 12 | + | - uses: actions/setup-go@v5 | |||
| 13 | + | with: | |||
| 14 | + | go-version: '1.27' | |||
| 15 | + | - run: sudo apt-get install -y libwayland-server0 libwayland-client0 wayland-utils | |||
| 16 | + | - run: make check | |||
| 17 | + | - run: make race | |||
A .gitignore +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | *.test | |
| 2 | + | *.out | |
| 3 | + | coverage.txt | |
| 4 | + | .DS_Store | |
| 5 | + | .worktrees/ | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | *.test | |||
| 2 | + | *.out | |||
| 3 | + | coverage.txt | |||
| 4 | + | .DS_Store | |||
| 5 | + | .worktrees/ | |||
A AGENTS.md +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | # Repository rules | |
| 2 | + | ||
| 3 | + | - Go 1.27 only. | |
| 4 | + | - Library code must build with `CGO_ENABLED=0`. | |
| 5 | + | - Use only non-varargs libwayland entry points (no `va_list`). | |
| 6 | + | - Make all libwayland calls on one OS-locked goroutine. | |
| 7 | + | - C-visible memory must never be Go-GC-managed. | |
| 8 | + | - Use signed conventional commits: `type(scope): description`. | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | # Repository rules | |||
| 2 | + | ||||
| 3 | + | - Go 1.27 only. | |||
| 4 | + | - Library code must build with `CGO_ENABLED=0`. | |||
| 5 | + | - Use only non-varargs libwayland entry points (no `va_list`). | |||
| 6 | + | - Make all libwayland calls on one OS-locked goroutine. | |||
| 7 | + | - C-visible memory must never be Go-GC-managed. | |||
| 8 | + | - Use signed conventional commits: `type(scope): description`. | |||
A LICENSE +21 -0
| @@ -0,0 +1,21 @@ | |||
| 1 | + | MIT License | |
| 2 | + | ||
| 3 | + | Copyright (c) 2026 bnema | |
| 4 | + | ||
| 5 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | + | of this software and associated documentation files (the "Software"), to deal | |
| 7 | + | in the Software without restriction, including without limitation the rights | |
| 8 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | + | copies of the Software, and to permit persons to whom the Software is | |
| 10 | + | furnished to do so, subject to the following conditions: | |
| 11 | + | ||
| 12 | + | The above copyright notice and this permission notice shall be included in all | |
| 13 | + | copies or substantial portions of the Software. | |
| 14 | + | ||
| 15 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | + | SOFTWARE. | |
| @@ -0,0 +1,21 @@ | |||||
| 1 | + | MIT License | |||
| 2 | + | ||||
| 3 | + | Copyright (c) 2026 bnema | |||
| 4 | + | ||||
| 5 | + | Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| 6 | + | of this software and associated documentation files (the "Software"), to deal | |||
| 7 | + | in the Software without restriction, including without limitation the rights | |||
| 8 | + | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| 9 | + | copies of the Software, and to permit persons to whom the Software is | |||
| 10 | + | furnished to do so, subject to the following conditions: | |||
| 11 | + | ||||
| 12 | + | The above copyright notice and this permission notice shall be included in all | |||
| 13 | + | copies or substantial portions of the Software. | |||
| 14 | + | ||||
| 15 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| 16 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| 17 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| 18 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| 19 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| 20 | + | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| 21 | + | SOFTWARE. | |||
A Makefile +13 -0
| @@ -0,0 +1,13 @@ | |||
| 1 | + | .PHONY: test vet race check | |
| 2 | + | ||
| 3 | + | test: | |
| 4 | + | CGO_ENABLED=0 go test ./... | |
| 5 | + | ||
| 6 | + | vet: | |
| 7 | + | CGO_ENABLED=0 go vet ./... | |
| 8 | + | ||
| 9 | + | # The race detector needs cgo only for the test binary; library code stays cgo-free. | |
| 10 | + | race: | |
| 11 | + | CGO_ENABLED=1 go test -race ./... | |
| 12 | + | ||
| 13 | + | check: vet test | |
| @@ -0,0 +1,13 @@ | |||||
| 1 | + | .PHONY: test vet race check | |||
| 2 | + | ||||
| 3 | + | test: | |||
| 4 | + | CGO_ENABLED=0 go test ./... | |||
| 5 | + | ||||
| 6 | + | vet: | |||
| 7 | + | CGO_ENABLED=0 go vet ./... | |||
| 8 | + | ||||
| 9 | + | # The race detector needs cgo only for the test binary; library code stays cgo-free. | |||
| 10 | + | race: | |||
| 11 | + | CGO_ENABLED=1 go test -race ./... | |||
| 12 | + | ||||
| 13 | + | check: vet test | |||
A README.md +21 -0
| @@ -0,0 +1,21 @@ | |||
| 1 | + | # purego-libwayland | |
| 2 | + | ||
| 3 | + | Go bindings for libwayland without cgo, loaded at runtime through github.com/bnema/purego. | |
| 4 | + | ||
| 5 | + | ## Status | |
| 6 | + | ||
| 7 | + | Early v0.x. Server side first; client side planned. | |
| 8 | + | ||
| 9 | + | ## Requirements | |
| 10 | + | ||
| 11 | + | Linux, Go 1.27, and `libwayland-server.so.0` at runtime. | |
| 12 | + | ||
| 13 | + | ## Build | |
| 14 | + | ||
| 15 | + | ```sh | |
| 16 | + | CGO_ENABLED=0 go build ./... | |
| 17 | + | ``` | |
| 18 | + | ||
| 19 | + | ## License | |
| 20 | + | ||
| 21 | + | MIT. | |
| @@ -0,0 +1,21 @@ | |||||
| 1 | + | # purego-libwayland | |||
| 2 | + | ||||
| 3 | + | Go bindings for libwayland without cgo, loaded at runtime through github.com/bnema/purego. | |||
| 4 | + | ||||
| 5 | + | ## Status | |||
| 6 | + | ||||
| 7 | + | Early v0.x. Server side first; client side planned. | |||
| 8 | + | ||||
| 9 | + | ## Requirements | |||
| 10 | + | ||||
| 11 | + | Linux, Go 1.27, and `libwayland-server.so.0` at runtime. | |||
| 12 | + | ||||
| 13 | + | ## Build | |||
| 14 | + | ||||
| 15 | + | ```sh | |||
| 16 | + | CGO_ENABLED=0 go build ./... | |||
| 17 | + | ``` | |||
| 18 | + | ||||
| 19 | + | ## License | |||
| 20 | + | ||||
| 21 | + | MIT. | |||
A go.mod +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | module github.com/bnema/purego-libwayland | |
| 2 | + | ||
| 3 | + | go 1.27 | |
| 4 | + | ||
| 5 | + | require ( | |
| 6 | + | github.com/bnema/purego v0.11.0-bnema.4 | |
| 7 | + | github.com/bnema/wlturbo v0.2.0 | |
| 8 | + | golang.org/x/sys v0.48.0 | |
| 9 | + | ) | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | module github.com/bnema/purego-libwayland | |||
| 2 | + | ||||
| 3 | + | go 1.27 | |||
| 4 | + | ||||
| 5 | + | require ( | |||
| 6 | + | github.com/bnema/purego v0.11.0-bnema.4 | |||
| 7 | + | github.com/bnema/wlturbo v0.2.0 | |||
| 8 | + | golang.org/x/sys v0.48.0 | |||
| 9 | + | ) | |||
A go.sum +6 -0 generated file: collapsed
A internal/proto/core.go +55 -0
| @@ -0,0 +1,55 @@ | |||
| 1 | + | // Package proto holds hand-written core interfaces for the feasibility | |
| 2 | + | // prototype. The generator replaces this package. | |
| 3 | + | package proto | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "sync" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | var ( | |
| 12 | + | Compositor = &server.Interface{Name: "wl_compositor", Version: 1} | |
| 13 | + | Surface = &server.Interface{Name: "wl_surface", Version: 1} | |
| 14 | + | Region = &server.Interface{Name: "wl_region", Version: 1} | |
| 15 | + | Callback = &server.Interface{Name: "wl_callback", Version: 1} | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // Opcodes used by the prototype. | |
| 19 | + | const ( | |
| 20 | + | CompositorCreateSurface = 0 | |
| 21 | + | CompositorCreateRegion = 1 | |
| 22 | + | SurfaceDestroy = 0 | |
| 23 | + | SurfaceFrame = 3 | |
| 24 | + | SurfaceCommit = 6 | |
| 25 | + | RegionDestroy = 0 | |
| 26 | + | CallbackDone = 0 | |
| 27 | + | ) | |
| 28 | + | ||
| 29 | + | var initOnce = sync.OnceValue(initInterfaces) | |
| 30 | + | ||
| 31 | + | // Init builds the C tables once per process. | |
| 32 | + | func Init() error { return initOnce() } | |
| 33 | + | ||
| 34 | + | func initInterfaces() error { | |
| 35 | + | Compositor.Requests = []server.Message{ | |
| 36 | + | {Name: "create_surface", Signature: "n", Types: []*server.Interface{Surface}}, | |
| 37 | + | {Name: "create_region", Signature: "n", Types: []*server.Interface{Region}}, | |
| 38 | + | } | |
| 39 | + | Surface.Requests = []server.Message{ | |
| 40 | + | {Name: "destroy", Signature: ""}, | |
| 41 | + | {Name: "attach", Signature: "?oii", Types: []*server.Interface{nil, nil, nil}}, | |
| 42 | + | {Name: "damage", Signature: "iiii"}, | |
| 43 | + | {Name: "frame", Signature: "n", Types: []*server.Interface{Callback}}, | |
| 44 | + | {Name: "set_opaque_region", Signature: "?o", Types: []*server.Interface{Region}}, | |
| 45 | + | {Name: "set_input_region", Signature: "?o", Types: []*server.Interface{Region}}, | |
| 46 | + | {Name: "commit", Signature: ""}, | |
| 47 | + | } | |
| 48 | + | Region.Requests = []server.Message{ | |
| 49 | + | {Name: "destroy", Signature: ""}, | |
| 50 | + | {Name: "add", Signature: "iiii"}, | |
| 51 | + | {Name: "subtract", Signature: "iiii"}, | |
| 52 | + | } | |
| 53 | + | Callback.Events = []server.Message{{Name: "done", Signature: "u"}} | |
| 54 | + | return server.NewInterfaces(Compositor, Surface, Region, Callback) | |
| 55 | + | } | |
| @@ -0,0 +1,55 @@ | |||||
| 1 | + | // Package proto holds hand-written core interfaces for the feasibility | |||
| 2 | + | // prototype. The generator replaces this package. | |||
| 3 | + | package proto | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "sync" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/purego-libwayland/server" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | var ( | |||
| 12 | + | Compositor = &server.Interface{Name: "wl_compositor", Version: 1} | |||
| 13 | + | Surface = &server.Interface{Name: "wl_surface", Version: 1} | |||
| 14 | + | Region = &server.Interface{Name: "wl_region", Version: 1} | |||
| 15 | + | Callback = &server.Interface{Name: "wl_callback", Version: 1} | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // Opcodes used by the prototype. | |||
| 19 | + | const ( | |||
| 20 | + | CompositorCreateSurface = 0 | |||
| 21 | + | CompositorCreateRegion = 1 | |||
| 22 | + | SurfaceDestroy = 0 | |||
| 23 | + | SurfaceFrame = 3 | |||
| 24 | + | SurfaceCommit = 6 | |||
| 25 | + | RegionDestroy = 0 | |||
| 26 | + | CallbackDone = 0 | |||
| 27 | + | ) | |||
| 28 | + | ||||
| 29 | + | var initOnce = sync.OnceValue(initInterfaces) | |||
| 30 | + | ||||
| 31 | + | // Init builds the C tables once per process. | |||
| 32 | + | func Init() error { return initOnce() } | |||
| 33 | + | ||||
| 34 | + | func initInterfaces() error { | |||
| 35 | + | Compositor.Requests = []server.Message{ | |||
| 36 | + | {Name: "create_surface", Signature: "n", Types: []*server.Interface{Surface}}, | |||
| 37 | + | {Name: "create_region", Signature: "n", Types: []*server.Interface{Region}}, | |||
| 38 | + | } | |||
| 39 | + | Surface.Requests = []server.Message{ | |||
| 40 | + | {Name: "destroy", Signature: ""}, | |||
| 41 | + | {Name: "attach", Signature: "?oii", Types: []*server.Interface{nil, nil, nil}}, | |||
| 42 | + | {Name: "damage", Signature: "iiii"}, | |||
| 43 | + | {Name: "frame", Signature: "n", Types: []*server.Interface{Callback}}, | |||
| 44 | + | {Name: "set_opaque_region", Signature: "?o", Types: []*server.Interface{Region}}, | |||
| 45 | + | {Name: "set_input_region", Signature: "?o", Types: []*server.Interface{Region}}, | |||
| 46 | + | {Name: "commit", Signature: ""}, | |||
| 47 | + | } | |||
| 48 | + | Region.Requests = []server.Message{ | |||
| 49 | + | {Name: "destroy", Signature: ""}, | |||
| 50 | + | {Name: "add", Signature: "iiii"}, | |||
| 51 | + | {Name: "subtract", Signature: "iiii"}, | |||
| 52 | + | } | |||
| 53 | + | Callback.Events = []server.Message{{Name: "done", Signature: "u"}} | |||
| 54 | + | return server.NewInterfaces(Compositor, Surface, Region, Callback) | |||
| 55 | + | } | |||
A internal/proto/feasibility_test.go +199 -0
| @@ -0,0 +1,199 @@ | |||
| 1 | + | package proto_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "os/exec" | |
| 6 | + | "path/filepath" | |
| 7 | + | "strings" | |
| 8 | + | "testing" | |
| 9 | + | "time" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/purego-libwayland/internal/proto" | |
| 12 | + | "github.com/bnema/purego-libwayland/server" | |
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | "golang.org/x/sys/unix" | |
| 15 | + | ) | |
| 16 | + | ||
| 17 | + | // startServer runs a display with a wl_compositor global on a private socket. | |
| 18 | + | // commits counts wl_surface.commit requests received by Go handlers. | |
| 19 | + | func startServer(t *testing.T) (socket string, d *server.Display, commits chan uint32) { | |
| 20 | + | t.Helper() | |
| 21 | + | if err := proto.Init(); err != nil { | |
| 22 | + | t.Fatal(err) | |
| 23 | + | } | |
| 24 | + | d, err := server.NewDisplay() | |
| 25 | + | if err != nil { | |
| 26 | + | t.Fatal(err) | |
| 27 | + | } | |
| 28 | + | socket = filepath.Join(t.TempDir(), "wayland-test") | |
| 29 | + | // libwayland takes ownership of the listening fd. | |
| 30 | + | fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 31 | + | if err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | if err := unix.Bind(fd, &unix.SockaddrUnix{Name: socket}); err != nil { | |
| 35 | + | t.Fatal(err) | |
| 36 | + | } | |
| 37 | + | if err := unix.Listen(fd, 128); err != nil { | |
| 38 | + | t.Fatal(err) | |
| 39 | + | } | |
| 40 | + | if err := d.AddSocketFD(fd); err != nil { | |
| 41 | + | t.Fatal(err) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | commits = make(chan uint32, 16) | |
| 45 | + | var serial uint32 | |
| 46 | + | surface := func(r *server.Resource, op uint32, args []server.Arg) { | |
| 47 | + | switch op { | |
| 48 | + | case proto.SurfaceDestroy: | |
| 49 | + | r.Destroy() | |
| 50 | + | case proto.SurfaceFrame: | |
| 51 | + | cb, err := r.Client().CreateResource(proto.Callback, 1, args[0].NewID(), nil) | |
| 52 | + | if err != nil { | |
| 53 | + | t.Error(err) | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | serial++ | |
| 57 | + | cb.PostEvent(proto.CallbackDone, server.Uint(serial)) | |
| 58 | + | cb.Destroy() | |
| 59 | + | case proto.SurfaceCommit: | |
| 60 | + | commits <- r.ID() | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | compositor := func(r *server.Resource, op uint32, args []server.Arg) { | |
| 64 | + | iface, h := proto.Surface, server.Handler(surface) | |
| 65 | + | if op == proto.CompositorCreateRegion { | |
| 66 | + | iface, h = proto.Region, func(r *server.Resource, op uint32, _ []server.Arg) { | |
| 67 | + | if op == proto.RegionDestroy { | |
| 68 | + | r.Destroy() | |
| 69 | + | } | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | if _, err := r.Client().CreateResource(iface, r.Version(), args[0].NewID(), h); err != nil { | |
| 73 | + | t.Error(err) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | err = d.CreateGlobal(proto.Compositor, 1, func(c server.Client, version, id uint32) { | |
| 77 | + | if _, err := c.CreateResource(proto.Compositor, int32(version), id, compositor); err != nil { | |
| 78 | + | t.Error(err) | |
| 79 | + | } | |
| 80 | + | }) | |
| 81 | + | if err != nil { | |
| 82 | + | t.Fatal(err) | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 86 | + | done := make(chan error, 1) | |
| 87 | + | go func() { done <- d.Run(ctx) }() | |
| 88 | + | t.Cleanup(func() { | |
| 89 | + | cancel() | |
| 90 | + | if err := <-done; err != nil { | |
| 91 | + | t.Error(err) | |
| 92 | + | } | |
| 93 | + | }) | |
| 94 | + | return socket, d, commits | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // Gates 1 and 2: the global exists and wayland-info lists it. | |
| 98 | + | func TestWaylandInfoListsCompositor(t *testing.T) { | |
| 99 | + | if _, err := exec.LookPath("wayland-info"); err != nil { | |
| 100 | + | t.Skip("wayland-info not installed") | |
| 101 | + | } | |
| 102 | + | socket, _, _ := startServer(t) | |
| 103 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 104 | + | defer cancel() | |
| 105 | + | cmd := exec.CommandContext(ctx, "wayland-info") | |
| 106 | + | cmd.Env = []string{"XDG_RUNTIME_DIR=" + filepath.Dir(socket), "WAYLAND_DISPLAY=" + filepath.Base(socket)} | |
| 107 | + | out, err := cmd.CombinedOutput() | |
| 108 | + | if err != nil { | |
| 109 | + | t.Fatalf("wayland-info: %v\n%s", err, out) | |
| 110 | + | } | |
| 111 | + | if !strings.Contains(string(out), "wl_compositor") { | |
| 112 | + | t.Fatalf("wl_compositor missing from wayland-info output:\n%s", out) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Gates 3 and 4: requests reach Go handlers, events reach the client, and a | |
| 117 | + | // disconnect frees every resource. Run repeatedly and under -race. | |
| 118 | + | func TestRequestEventRoundtripAndDisconnect(t *testing.T) { | |
| 119 | + | socket, d, commits := startServer(t) | |
| 120 | + | ||
| 121 | + | for round := range 20 { | |
| 122 | + | c, err := wlturbo.Connect(socket) | |
| 123 | + | if err != nil { | |
| 124 | + | t.Fatal(err) | |
| 125 | + | } | |
| 126 | + | if err := c.Roundtrip(); err != nil { | |
| 127 | + | t.Fatal(err) | |
| 128 | + | } | |
| 129 | + | g, ok := c.Registry().FindGlobal("wl_compositor") | |
| 130 | + | if !ok { | |
| 131 | + | t.Fatal("wl_compositor not advertised") | |
| 132 | + | } | |
| 133 | + | comp, err := c.Registry().BindID(g.Name, g.Interface, 1) | |
| 134 | + | if err != nil { | |
| 135 | + | t.Fatal(err) | |
| 136 | + | } | |
| 137 | + | surf := c.AllocateID() | |
| 138 | + | must(t, c.SendRequest(comp, proto.CompositorCreateSurface, surf)) | |
| 139 | + | cb := c.AllocateID() | |
| 140 | + | gotDone := make(chan uint32, 1) | |
| 141 | + | cbProxy := &doneProxy{done: gotDone} | |
| 142 | + | cbProxy.SetID(cb) | |
| 143 | + | c.Context().Register(cbProxy) | |
| 144 | + | must(t, c.SendRequest(surf, proto.SurfaceFrame, cb)) | |
| 145 | + | must(t, c.SendRequest(surf, proto.SurfaceCommit)) | |
| 146 | + | must(t, c.Roundtrip()) | |
| 147 | + | ||
| 148 | + | select { | |
| 149 | + | case id := <-commits: | |
| 150 | + | if id != surf { | |
| 151 | + | t.Fatalf("commit on %d, want %d", id, surf) | |
| 152 | + | } | |
| 153 | + | case <-time.After(2 * time.Second): | |
| 154 | + | t.Fatal("commit never reached the Go handler") | |
| 155 | + | } | |
| 156 | + | select { | |
| 157 | + | case serial := <-gotDone: | |
| 158 | + | if serial != uint32(round+1) { | |
| 159 | + | t.Fatalf("callback serial %d, want %d", serial, round+1) | |
| 160 | + | } | |
| 161 | + | default: | |
| 162 | + | t.Fatal("wl_callback.done never reached the client") | |
| 163 | + | } | |
| 164 | + | _ = c.Close() | |
| 165 | + | ||
| 166 | + | // libwayland frees the client's resources once it notices the hangup. | |
| 167 | + | deadline := time.Now().Add(2 * time.Second) | |
| 168 | + | for { | |
| 169 | + | var n int | |
| 170 | + | d.Do(func() { n = server.LiveResources() }) | |
| 171 | + | if n == 0 { | |
| 172 | + | break | |
| 173 | + | } | |
| 174 | + | if time.Now().After(deadline) { | |
| 175 | + | t.Fatalf("round %d: %d resources still alive after disconnect", round, n) | |
| 176 | + | } | |
| 177 | + | time.Sleep(5 * time.Millisecond) | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // doneProxy records wl_callback.done. | |
| 183 | + | type doneProxy struct { | |
| 184 | + | wlturbo.BaseProxy | |
| 185 | + | done chan uint32 | |
| 186 | + | } | |
| 187 | + | ||
| 188 | + | func (p *doneProxy) Dispatch(e *wlturbo.Event) { | |
| 189 | + | if e.Opcode == proto.CallbackDone { | |
| 190 | + | p.done <- e.Uint32() | |
| 191 | + | } | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | func must(t *testing.T, err error) { | |
| 195 | + | t.Helper() | |
| 196 | + | if err != nil { | |
| 197 | + | t.Fatal(err) | |
| 198 | + | } | |
| 199 | + | } | |
| @@ -0,0 +1,199 @@ | |||||
| 1 | + | package proto_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "os/exec" | |||
| 6 | + | "path/filepath" | |||
| 7 | + | "strings" | |||
| 8 | + | "testing" | |||
| 9 | + | "time" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/purego-libwayland/internal/proto" | |||
| 12 | + | "github.com/bnema/purego-libwayland/server" | |||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "golang.org/x/sys/unix" | |||
| 15 | + | ) | |||
| 16 | + | ||||
| 17 | + | // startServer runs a display with a wl_compositor global on a private socket. | |||
| 18 | + | // commits counts wl_surface.commit requests received by Go handlers. | |||
| 19 | + | func startServer(t *testing.T) (socket string, d *server.Display, commits chan uint32) { | |||
| 20 | + | t.Helper() | |||
| 21 | + | if err := proto.Init(); err != nil { | |||
| 22 | + | t.Fatal(err) | |||
| 23 | + | } | |||
| 24 | + | d, err := server.NewDisplay() | |||
| 25 | + | if err != nil { | |||
| 26 | + | t.Fatal(err) | |||
| 27 | + | } | |||
| 28 | + | socket = filepath.Join(t.TempDir(), "wayland-test") | |||
| 29 | + | // libwayland takes ownership of the listening fd. | |||
| 30 | + | fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 31 | + | if err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | if err := unix.Bind(fd, &unix.SockaddrUnix{Name: socket}); err != nil { | |||
| 35 | + | t.Fatal(err) | |||
| 36 | + | } | |||
| 37 | + | if err := unix.Listen(fd, 128); err != nil { | |||
| 38 | + | t.Fatal(err) | |||
| 39 | + | } | |||
| 40 | + | if err := d.AddSocketFD(fd); err != nil { | |||
| 41 | + | t.Fatal(err) | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | commits = make(chan uint32, 16) | |||
| 45 | + | var serial uint32 | |||
| 46 | + | surface := func(r *server.Resource, op uint32, args []server.Arg) { | |||
| 47 | + | switch op { | |||
| 48 | + | case proto.SurfaceDestroy: | |||
| 49 | + | r.Destroy() | |||
| 50 | + | case proto.SurfaceFrame: | |||
| 51 | + | cb, err := r.Client().CreateResource(proto.Callback, 1, args[0].NewID(), nil) | |||
| 52 | + | if err != nil { | |||
| 53 | + | t.Error(err) | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | serial++ | |||
| 57 | + | cb.PostEvent(proto.CallbackDone, server.Uint(serial)) | |||
| 58 | + | cb.Destroy() | |||
| 59 | + | case proto.SurfaceCommit: | |||
| 60 | + | commits <- r.ID() | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | compositor := func(r *server.Resource, op uint32, args []server.Arg) { | |||
| 64 | + | iface, h := proto.Surface, server.Handler(surface) | |||
| 65 | + | if op == proto.CompositorCreateRegion { | |||
| 66 | + | iface, h = proto.Region, func(r *server.Resource, op uint32, _ []server.Arg) { | |||
| 67 | + | if op == proto.RegionDestroy { | |||
| 68 | + | r.Destroy() | |||
| 69 | + | } | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | if _, err := r.Client().CreateResource(iface, r.Version(), args[0].NewID(), h); err != nil { | |||
| 73 | + | t.Error(err) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | err = d.CreateGlobal(proto.Compositor, 1, func(c server.Client, version, id uint32) { | |||
| 77 | + | if _, err := c.CreateResource(proto.Compositor, int32(version), id, compositor); err != nil { | |||
| 78 | + | t.Error(err) | |||
| 79 | + | } | |||
| 80 | + | }) | |||
| 81 | + | if err != nil { | |||
| 82 | + | t.Fatal(err) | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 86 | + | done := make(chan error, 1) | |||
| 87 | + | go func() { done <- d.Run(ctx) }() | |||
| 88 | + | t.Cleanup(func() { | |||
| 89 | + | cancel() | |||
| 90 | + | if err := <-done; err != nil { | |||
| 91 | + | t.Error(err) | |||
| 92 | + | } | |||
| 93 | + | }) | |||
| 94 | + | return socket, d, commits | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // Gates 1 and 2: the global exists and wayland-info lists it. | |||
| 98 | + | func TestWaylandInfoListsCompositor(t *testing.T) { | |||
| 99 | + | if _, err := exec.LookPath("wayland-info"); err != nil { | |||
| 100 | + | t.Skip("wayland-info not installed") | |||
| 101 | + | } | |||
| 102 | + | socket, _, _ := startServer(t) | |||
| 103 | + | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |||
| 104 | + | defer cancel() | |||
| 105 | + | cmd := exec.CommandContext(ctx, "wayland-info") | |||
| 106 | + | cmd.Env = []string{"XDG_RUNTIME_DIR=" + filepath.Dir(socket), "WAYLAND_DISPLAY=" + filepath.Base(socket)} | |||
| 107 | + | out, err := cmd.CombinedOutput() | |||
| 108 | + | if err != nil { | |||
| 109 | + | t.Fatalf("wayland-info: %v\n%s", err, out) | |||
| 110 | + | } | |||
| 111 | + | if !strings.Contains(string(out), "wl_compositor") { | |||
| 112 | + | t.Fatalf("wl_compositor missing from wayland-info output:\n%s", out) | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | ||||
| 116 | + | // Gates 3 and 4: requests reach Go handlers, events reach the client, and a | |||
| 117 | + | // disconnect frees every resource. Run repeatedly and under -race. | |||
| 118 | + | func TestRequestEventRoundtripAndDisconnect(t *testing.T) { | |||
| 119 | + | socket, d, commits := startServer(t) | |||
| 120 | + | ||||
| 121 | + | for round := range 20 { | |||
| 122 | + | c, err := wlturbo.Connect(socket) | |||
| 123 | + | if err != nil { | |||
| 124 | + | t.Fatal(err) | |||
| 125 | + | } | |||
| 126 | + | if err := c.Roundtrip(); err != nil { | |||
| 127 | + | t.Fatal(err) | |||
| 128 | + | } | |||
| 129 | + | g, ok := c.Registry().FindGlobal("wl_compositor") | |||
| 130 | + | if !ok { | |||
| 131 | + | t.Fatal("wl_compositor not advertised") | |||
| 132 | + | } | |||
| 133 | + | comp, err := c.Registry().BindID(g.Name, g.Interface, 1) | |||
| 134 | + | if err != nil { | |||
| 135 | + | t.Fatal(err) | |||
| 136 | + | } | |||
| 137 | + | surf := c.AllocateID() | |||
| 138 | + | must(t, c.SendRequest(comp, proto.CompositorCreateSurface, surf)) | |||
| 139 | + | cb := c.AllocateID() | |||
| 140 | + | gotDone := make(chan uint32, 1) | |||
| 141 | + | cbProxy := &doneProxy{done: gotDone} | |||
| 142 | + | cbProxy.SetID(cb) | |||
| 143 | + | c.Context().Register(cbProxy) | |||
| 144 | + | must(t, c.SendRequest(surf, proto.SurfaceFrame, cb)) | |||
| 145 | + | must(t, c.SendRequest(surf, proto.SurfaceCommit)) | |||
| 146 | + | must(t, c.Roundtrip()) | |||
| 147 | + | ||||
| 148 | + | select { | |||
| 149 | + | case id := <-commits: | |||
| 150 | + | if id != surf { | |||
| 151 | + | t.Fatalf("commit on %d, want %d", id, surf) | |||
| 152 | + | } | |||
| 153 | + | case <-time.After(2 * time.Second): | |||
| 154 | + | t.Fatal("commit never reached the Go handler") | |||
| 155 | + | } | |||
| 156 | + | select { | |||
| 157 | + | case serial := <-gotDone: | |||
| 158 | + | if serial != uint32(round+1) { | |||
| 159 | + | t.Fatalf("callback serial %d, want %d", serial, round+1) | |||
| 160 | + | } | |||
| 161 | + | default: | |||
| 162 | + | t.Fatal("wl_callback.done never reached the client") | |||
| 163 | + | } | |||
| 164 | + | _ = c.Close() | |||
| 165 | + | ||||
| 166 | + | // libwayland frees the client's resources once it notices the hangup. | |||
| 167 | + | deadline := time.Now().Add(2 * time.Second) | |||
| 168 | + | for { | |||
| 169 | + | var n int | |||
| 170 | + | d.Do(func() { n = server.LiveResources() }) | |||
| 171 | + | if n == 0 { | |||
| 172 | + | break | |||
| 173 | + | } | |||
| 174 | + | if time.Now().After(deadline) { | |||
| 175 | + | t.Fatalf("round %d: %d resources still alive after disconnect", round, n) | |||
| 176 | + | } | |||
| 177 | + | time.Sleep(5 * time.Millisecond) | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // doneProxy records wl_callback.done. | |||
| 183 | + | type doneProxy struct { | |||
| 184 | + | wlturbo.BaseProxy | |||
| 185 | + | done chan uint32 | |||
| 186 | + | } | |||
| 187 | + | ||||
| 188 | + | func (p *doneProxy) Dispatch(e *wlturbo.Event) { | |||
| 189 | + | if e.Opcode == proto.CallbackDone { | |||
| 190 | + | p.done <- e.Uint32() | |||
| 191 | + | } | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | func must(t *testing.T, err error) { | |||
| 195 | + | t.Helper() | |||
| 196 | + | if err != nil { | |||
| 197 | + | t.Fatal(err) | |||
| 198 | + | } | |||
| 199 | + | } | |||
A server/arena.go +46 -0
| @@ -0,0 +1,46 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "fmt" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "golang.org/x/sys/unix" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // arena is C-visible memory the Go GC never moves or frees. It holds | |
| 12 | + | // wl_interface and wl_message tables and their strings for the whole | |
| 13 | + | // process lifetime. | |
| 14 | + | type arena struct { | |
| 15 | + | mem []byte | |
| 16 | + | base uintptr | |
| 17 | + | off int | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func newArena(size int) (*arena, error) { | |
| 21 | + | mem, err := unix.Mmap(-1, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) | |
| 22 | + | if err != nil { | |
| 23 | + | return nil, fmt.Errorf("arena: mmap: %w", err) | |
| 24 | + | } | |
| 25 | + | return &arena{mem: mem, base: uintptr(unsafe.Pointer(&mem[0]))}, nil | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // alloc returns the offset and C address of n zeroed, 8-byte aligned bytes. | |
| 29 | + | func (a *arena) alloc(n int) (int, uintptr) { | |
| 30 | + | a.off = (a.off + 7) &^ 7 | |
| 31 | + | if a.off+n > len(a.mem) { | |
| 32 | + | panic("purego-libwayland: arena exhausted") | |
| 33 | + | } | |
| 34 | + | off := a.off | |
| 35 | + | a.off += n | |
| 36 | + | return off, a.base + uintptr(off) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | func (a *arena) cstring(s string) uintptr { | |
| 40 | + | off, addr := a.alloc(len(s) + 1) | |
| 41 | + | copy(a.mem[off:], s) | |
| 42 | + | return addr | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (a *arena) putPtr(off int, v uintptr) { binary.LittleEndian.PutUint64(a.mem[off:], uint64(v)) } | |
| 46 | + | func (a *arena) putI32(off int, v int32) { binary.LittleEndian.PutUint32(a.mem[off:], uint32(v)) } | |
| @@ -0,0 +1,46 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "fmt" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "golang.org/x/sys/unix" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // arena is C-visible memory the Go GC never moves or frees. It holds | |||
| 12 | + | // wl_interface and wl_message tables and their strings for the whole | |||
| 13 | + | // process lifetime. | |||
| 14 | + | type arena struct { | |||
| 15 | + | mem []byte | |||
| 16 | + | base uintptr | |||
| 17 | + | off int | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func newArena(size int) (*arena, error) { | |||
| 21 | + | mem, err := unix.Mmap(-1, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) | |||
| 22 | + | if err != nil { | |||
| 23 | + | return nil, fmt.Errorf("arena: mmap: %w", err) | |||
| 24 | + | } | |||
| 25 | + | return &arena{mem: mem, base: uintptr(unsafe.Pointer(&mem[0]))}, nil | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | // alloc returns the offset and C address of n zeroed, 8-byte aligned bytes. | |||
| 29 | + | func (a *arena) alloc(n int) (int, uintptr) { | |||
| 30 | + | a.off = (a.off + 7) &^ 7 | |||
| 31 | + | if a.off+n > len(a.mem) { | |||
| 32 | + | panic("purego-libwayland: arena exhausted") | |||
| 33 | + | } | |||
| 34 | + | off := a.off | |||
| 35 | + | a.off += n | |||
| 36 | + | return off, a.base + uintptr(off) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | func (a *arena) cstring(s string) uintptr { | |||
| 40 | + | off, addr := a.alloc(len(s) + 1) | |||
| 41 | + | copy(a.mem[off:], s) | |||
| 42 | + | return addr | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (a *arena) putPtr(off int, v uintptr) { binary.LittleEndian.PutUint64(a.mem[off:], uint64(v)) } | |||
| 46 | + | func (a *arena) putI32(off int, v int32) { binary.LittleEndian.PutUint32(a.mem[off:], uint32(v)) } | |||
A server/display.go +204 -0
| @@ -0,0 +1,204 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 6 | + | "fmt" | |
| 7 | + | "runtime" | |
| 8 | + | "unsafe" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // Arg is one raw union wl_argument value (int, uint, fixed, object pointer, | |
| 12 | + | // new_id, or fd). | |
| 13 | + | type Arg uint64 | |
| 14 | + | ||
| 15 | + | func Uint(v uint32) Arg { return Arg(v) } | |
| 16 | + | func Int(v int32) Arg { return Arg(uint32(v)) } | |
| 17 | + | func Object(r *Resource) Arg { return Arg(r.c) } | |
| 18 | + | func (a Arg) Uint() uint32 { return uint32(a) } | |
| 19 | + | func (a Arg) Int() int32 { return int32(uint32(a)) } | |
| 20 | + | func (a Arg) NewID() uint32 { return uint32(a) } | |
| 21 | + | func (a Arg) Resource() *Resource { return live.resources[uintptr(a)] } | |
| 22 | + | ||
| 23 | + | // Handler receives every request sent to a resource. | |
| 24 | + | type Handler func(r *Resource, opcode uint32, args []Arg) | |
| 25 | + | ||
| 26 | + | // BindFunc runs when a client binds a global. | |
| 27 | + | type BindFunc func(c Client, version, id uint32) | |
| 28 | + | ||
| 29 | + | // Client is a connected wl_client. | |
| 30 | + | type Client struct{ c uintptr } | |
| 31 | + | ||
| 32 | + | // Resource is a server-side wl_resource. | |
| 33 | + | type Resource struct { | |
| 34 | + | c uintptr | |
| 35 | + | iface *Interface | |
| 36 | + | handler Handler | |
| 37 | + | OnDestroy func() | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // live holds Go state reachable from C callbacks. It is only touched on the | |
| 41 | + | // display goroutine, so it needs no lock. | |
| 42 | + | var live = struct { | |
| 43 | + | resources map[uintptr]*Resource | |
| 44 | + | binds map[uintptr]BindFunc | |
| 45 | + | nextBind uintptr | |
| 46 | + | }{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}} | |
| 47 | + | ||
| 48 | + | // Display owns a wl_display. All methods except Do must run on the goroutine | |
| 49 | + | // that calls Run, or before Run starts. | |
| 50 | + | type Display struct { | |
| 51 | + | c uintptr | |
| 52 | + | loop uintptr | |
| 53 | + | calls chan func() | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func NewDisplay() (*Display, error) { | |
| 57 | + | if err := load(); err != nil { | |
| 58 | + | return nil, err | |
| 59 | + | } | |
| 60 | + | c := wlDisplayCreate() | |
| 61 | + | if c == 0 { | |
| 62 | + | return nil, errors.New("purego-libwayland: wl_display_create failed") | |
| 63 | + | } | |
| 64 | + | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // AddSocketFD serves clients on an already bound, listening unix socket. | |
| 68 | + | func (d *Display) AddSocketFD(fd int) error { | |
| 69 | + | if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 { | |
| 70 | + | return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd) | |
| 71 | + | } | |
| 72 | + | return nil | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error { | |
| 76 | + | live.nextBind++ | |
| 77 | + | token := live.nextBind | |
| 78 | + | live.binds[token] = bindFn | |
| 79 | + | if wlGlobalCreate(d.c, iface.c, version, token, cbBind) == 0 { | |
| 80 | + | delete(live.binds, token) | |
| 81 | + | return fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name) | |
| 82 | + | } | |
| 83 | + | return nil | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // Run dispatches libwayland on one OS-locked goroutine until ctx is done. | |
| 87 | + | func (d *Display) Run(ctx context.Context) error { | |
| 88 | + | runtime.LockOSThread() | |
| 89 | + | defer runtime.UnlockOSThread() | |
| 90 | + | for { | |
| 91 | + | select { | |
| 92 | + | case <-ctx.Done(): | |
| 93 | + | wlDisplayDestroyClients(d.c) | |
| 94 | + | wlDisplayDestroy(d.c) | |
| 95 | + | return nil | |
| 96 | + | case fn := <-d.calls: | |
| 97 | + | fn() | |
| 98 | + | default: | |
| 99 | + | } | |
| 100 | + | if wlEventLoopDispatch(d.loop, 5) < 0 { | |
| 101 | + | return errors.New("purego-libwayland: wl_event_loop_dispatch failed") | |
| 102 | + | } | |
| 103 | + | wlDisplayFlushClients(d.c) | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // Do runs fn on the display goroutine and waits for it. | |
| 108 | + | func (d *Display) Do(fn func()) { | |
| 109 | + | done := make(chan struct{}) | |
| 110 | + | d.calls <- func() { fn(); close(done) } | |
| 111 | + | <-done | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // LiveResources reports resources not yet destroyed. Display goroutine only. | |
| 115 | + | func LiveResources() int { return len(live.resources) } | |
| 116 | + | ||
| 117 | + | func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) { | |
| 118 | + | rc := wlResourceCreate(c.c, iface.c, version, id) | |
| 119 | + | if rc == 0 { | |
| 120 | + | return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name) | |
| 121 | + | } | |
| 122 | + | r := &Resource{c: rc, iface: iface, handler: h} | |
| 123 | + | live.resources[rc] = r | |
| 124 | + | wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy) | |
| 125 | + | return r, nil | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | func (r *Resource) ID() uint32 { return wlResourceGetID(r.c) } | |
| 129 | + | func (r *Resource) Version() int32 { return wlResourceGetVersion(r.c) } | |
| 130 | + | func (r *Resource) Client() Client { return Client{wlResourceGetClient(r.c)} } | |
| 131 | + | func (r *Resource) Destroy() { wlResourceDestroy(r.c) } | |
| 132 | + | func (r *Resource) Iface() *Interface { return r.iface } | |
| 133 | + | ||
| 134 | + | // PostEvent sends an event. String and array arguments are not supported yet. | |
| 135 | + | func (r *Resource) PostEvent(opcode uint32, args ...Arg) { | |
| 136 | + | var p unsafe.Pointer | |
| 137 | + | if len(args) > 0 { | |
| 138 | + | p = unsafe.Pointer(&args[0]) | |
| 139 | + | } | |
| 140 | + | wlResourcePostEventArr(r.c, opcode, p) | |
| 141 | + | runtime.KeepAlive(args) | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | // implMarker is a non-nil implementation pointer. libwayland only passes it | |
| 145 | + | // back to our dispatcher. | |
| 146 | + | func implMarker() uintptr { | |
| 147 | + | if marker == 0 { | |
| 148 | + | _, marker = tables.alloc(8) | |
| 149 | + | } | |
| 150 | + | return marker | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | var marker uintptr | |
| 154 | + | ||
| 155 | + | // --- C callbacks (display goroutine) --- | |
| 156 | + | ||
| 157 | + | func dispatch(_ uintptr, target uintptr, opcode uint32, msg unsafe.Pointer, args unsafe.Pointer) int32 { | |
| 158 | + | r := live.resources[target] | |
| 159 | + | if r == nil || r.handler == nil { | |
| 160 | + | return 0 | |
| 161 | + | } | |
| 162 | + | n := argCount(msgSignature(msg)) | |
| 163 | + | var a []Arg | |
| 164 | + | if n > 0 { | |
| 165 | + | a = append([]Arg(nil), unsafe.Slice((*Arg)(args), n)...) | |
| 166 | + | } | |
| 167 | + | r.handler(r, opcode, a) | |
| 168 | + | return 0 | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func destroyed(res uintptr) { | |
| 172 | + | r := live.resources[res] | |
| 173 | + | delete(live.resources, res) | |
| 174 | + | if r != nil && r.OnDestroy != nil { | |
| 175 | + | r.OnDestroy() | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | func bind(client uintptr, data uintptr, version uint32, id uint32) { | |
| 180 | + | if fn := live.binds[data]; fn != nil { | |
| 181 | + | fn(Client{client}, version, id) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // msgSignature reads wl_message.signature. | |
| 186 | + | func msgSignature(msg unsafe.Pointer) string { | |
| 187 | + | p := *(*unsafe.Pointer)(unsafe.Add(msg, 8)) | |
| 188 | + | n := 0 | |
| 189 | + | for *(*byte)(unsafe.Add(p, n)) != 0 { | |
| 190 | + | n++ | |
| 191 | + | } | |
| 192 | + | return string(unsafe.Slice((*byte)(p), n)) | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | // argCount counts arguments in a libwayland signature ("?oii", "2n", ...). | |
| 196 | + | func argCount(sig string) int { | |
| 197 | + | n := 0 | |
| 198 | + | for _, ch := range sig { | |
| 199 | + | if ch != '?' && (ch < '0' || ch > '9') { | |
| 200 | + | n++ | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | return n | |
| 204 | + | } | |
| @@ -0,0 +1,204 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 6 | + | "fmt" | |||
| 7 | + | "runtime" | |||
| 8 | + | "unsafe" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // Arg is one raw union wl_argument value (int, uint, fixed, object pointer, | |||
| 12 | + | // new_id, or fd). | |||
| 13 | + | type Arg uint64 | |||
| 14 | + | ||||
| 15 | + | func Uint(v uint32) Arg { return Arg(v) } | |||
| 16 | + | func Int(v int32) Arg { return Arg(uint32(v)) } | |||
| 17 | + | func Object(r *Resource) Arg { return Arg(r.c) } | |||
| 18 | + | func (a Arg) Uint() uint32 { return uint32(a) } | |||
| 19 | + | func (a Arg) Int() int32 { return int32(uint32(a)) } | |||
| 20 | + | func (a Arg) NewID() uint32 { return uint32(a) } | |||
| 21 | + | func (a Arg) Resource() *Resource { return live.resources[uintptr(a)] } | |||
| 22 | + | ||||
| 23 | + | // Handler receives every request sent to a resource. | |||
| 24 | + | type Handler func(r *Resource, opcode uint32, args []Arg) | |||
| 25 | + | ||||
| 26 | + | // BindFunc runs when a client binds a global. | |||
| 27 | + | type BindFunc func(c Client, version, id uint32) | |||
| 28 | + | ||||
| 29 | + | // Client is a connected wl_client. | |||
| 30 | + | type Client struct{ c uintptr } | |||
| 31 | + | ||||
| 32 | + | // Resource is a server-side wl_resource. | |||
| 33 | + | type Resource struct { | |||
| 34 | + | c uintptr | |||
| 35 | + | iface *Interface | |||
| 36 | + | handler Handler | |||
| 37 | + | OnDestroy func() | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // live holds Go state reachable from C callbacks. It is only touched on the | |||
| 41 | + | // display goroutine, so it needs no lock. | |||
| 42 | + | var live = struct { | |||
| 43 | + | resources map[uintptr]*Resource | |||
| 44 | + | binds map[uintptr]BindFunc | |||
| 45 | + | nextBind uintptr | |||
| 46 | + | }{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}} | |||
| 47 | + | ||||
| 48 | + | // Display owns a wl_display. All methods except Do must run on the goroutine | |||
| 49 | + | // that calls Run, or before Run starts. | |||
| 50 | + | type Display struct { | |||
| 51 | + | c uintptr | |||
| 52 | + | loop uintptr | |||
| 53 | + | calls chan func() | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func NewDisplay() (*Display, error) { | |||
| 57 | + | if err := load(); err != nil { | |||
| 58 | + | return nil, err | |||
| 59 | + | } | |||
| 60 | + | c := wlDisplayCreate() | |||
| 61 | + | if c == 0 { | |||
| 62 | + | return nil, errors.New("purego-libwayland: wl_display_create failed") | |||
| 63 | + | } | |||
| 64 | + | return &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func())}, nil | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // AddSocketFD serves clients on an already bound, listening unix socket. | |||
| 68 | + | func (d *Display) AddSocketFD(fd int) error { | |||
| 69 | + | if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 { | |||
| 70 | + | return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd) | |||
| 71 | + | } | |||
| 72 | + | return nil | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error { | |||
| 76 | + | live.nextBind++ | |||
| 77 | + | token := live.nextBind | |||
| 78 | + | live.binds[token] = bindFn | |||
| 79 | + | if wlGlobalCreate(d.c, iface.c, version, token, cbBind) == 0 { | |||
| 80 | + | delete(live.binds, token) | |||
| 81 | + | return fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name) | |||
| 82 | + | } | |||
| 83 | + | return nil | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // Run dispatches libwayland on one OS-locked goroutine until ctx is done. | |||
| 87 | + | func (d *Display) Run(ctx context.Context) error { | |||
| 88 | + | runtime.LockOSThread() | |||
| 89 | + | defer runtime.UnlockOSThread() | |||
| 90 | + | for { | |||
| 91 | + | select { | |||
| 92 | + | case <-ctx.Done(): | |||
| 93 | + | wlDisplayDestroyClients(d.c) | |||
| 94 | + | wlDisplayDestroy(d.c) | |||
| 95 | + | return nil | |||
| 96 | + | case fn := <-d.calls: | |||
| 97 | + | fn() | |||
| 98 | + | default: | |||
| 99 | + | } | |||
| 100 | + | if wlEventLoopDispatch(d.loop, 5) < 0 { | |||
| 101 | + | return errors.New("purego-libwayland: wl_event_loop_dispatch failed") | |||
| 102 | + | } | |||
| 103 | + | wlDisplayFlushClients(d.c) | |||
| 104 | + | } | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // Do runs fn on the display goroutine and waits for it. | |||
| 108 | + | func (d *Display) Do(fn func()) { | |||
| 109 | + | done := make(chan struct{}) | |||
| 110 | + | d.calls <- func() { fn(); close(done) } | |||
| 111 | + | <-done | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // LiveResources reports resources not yet destroyed. Display goroutine only. | |||
| 115 | + | func LiveResources() int { return len(live.resources) } | |||
| 116 | + | ||||
| 117 | + | func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) { | |||
| 118 | + | rc := wlResourceCreate(c.c, iface.c, version, id) | |||
| 119 | + | if rc == 0 { | |||
| 120 | + | return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name) | |||
| 121 | + | } | |||
| 122 | + | r := &Resource{c: rc, iface: iface, handler: h} | |||
| 123 | + | live.resources[rc] = r | |||
| 124 | + | wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy) | |||
| 125 | + | return r, nil | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | func (r *Resource) ID() uint32 { return wlResourceGetID(r.c) } | |||
| 129 | + | func (r *Resource) Version() int32 { return wlResourceGetVersion(r.c) } | |||
| 130 | + | func (r *Resource) Client() Client { return Client{wlResourceGetClient(r.c)} } | |||
| 131 | + | func (r *Resource) Destroy() { wlResourceDestroy(r.c) } | |||
| 132 | + | func (r *Resource) Iface() *Interface { return r.iface } | |||
| 133 | + | ||||
| 134 | + | // PostEvent sends an event. String and array arguments are not supported yet. | |||
| 135 | + | func (r *Resource) PostEvent(opcode uint32, args ...Arg) { | |||
| 136 | + | var p unsafe.Pointer | |||
| 137 | + | if len(args) > 0 { | |||
| 138 | + | p = unsafe.Pointer(&args[0]) | |||
| 139 | + | } | |||
| 140 | + | wlResourcePostEventArr(r.c, opcode, p) | |||
| 141 | + | runtime.KeepAlive(args) | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | // implMarker is a non-nil implementation pointer. libwayland only passes it | |||
| 145 | + | // back to our dispatcher. | |||
| 146 | + | func implMarker() uintptr { | |||
| 147 | + | if marker == 0 { | |||
| 148 | + | _, marker = tables.alloc(8) | |||
| 149 | + | } | |||
| 150 | + | return marker | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | var marker uintptr | |||
| 154 | + | ||||
| 155 | + | // --- C callbacks (display goroutine) --- | |||
| 156 | + | ||||
| 157 | + | func dispatch(_ uintptr, target uintptr, opcode uint32, msg unsafe.Pointer, args unsafe.Pointer) int32 { | |||
| 158 | + | r := live.resources[target] | |||
| 159 | + | if r == nil || r.handler == nil { | |||
| 160 | + | return 0 | |||
| 161 | + | } | |||
| 162 | + | n := argCount(msgSignature(msg)) | |||
| 163 | + | var a []Arg | |||
| 164 | + | if n > 0 { | |||
| 165 | + | a = append([]Arg(nil), unsafe.Slice((*Arg)(args), n)...) | |||
| 166 | + | } | |||
| 167 | + | r.handler(r, opcode, a) | |||
| 168 | + | return 0 | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func destroyed(res uintptr) { | |||
| 172 | + | r := live.resources[res] | |||
| 173 | + | delete(live.resources, res) | |||
| 174 | + | if r != nil && r.OnDestroy != nil { | |||
| 175 | + | r.OnDestroy() | |||
| 176 | + | } | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | func bind(client uintptr, data uintptr, version uint32, id uint32) { | |||
| 180 | + | if fn := live.binds[data]; fn != nil { | |||
| 181 | + | fn(Client{client}, version, id) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // msgSignature reads wl_message.signature. | |||
| 186 | + | func msgSignature(msg unsafe.Pointer) string { | |||
| 187 | + | p := *(*unsafe.Pointer)(unsafe.Add(msg, 8)) | |||
| 188 | + | n := 0 | |||
| 189 | + | for *(*byte)(unsafe.Add(p, n)) != 0 { | |||
| 190 | + | n++ | |||
| 191 | + | } | |||
| 192 | + | return string(unsafe.Slice((*byte)(p), n)) | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | // argCount counts arguments in a libwayland signature ("?oii", "2n", ...). | |||
| 196 | + | func argCount(sig string) int { | |||
| 197 | + | n := 0 | |||
| 198 | + | for _, ch := range sig { | |||
| 199 | + | if ch != '?' && (ch < '0' || ch > '9') { | |||
| 200 | + | n++ | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | return n | |||
| 204 | + | } | |||
A server/interface.go +92 -0
| @@ -0,0 +1,92 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | // Message describes one request or event. Signature uses the libwayland | |
| 4 | + | // format ("n", "?oii", ...). Types holds one entry per argument; entries are | |
| 5 | + | // non-nil only for typed new_id and object arguments. | |
| 6 | + | type Message struct { | |
| 7 | + | Name string | |
| 8 | + | Signature string | |
| 9 | + | Types []*Interface | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | // Interface describes a protocol interface. Build it once with | |
| 13 | + | // NewInterfaces; its C table lives for the process lifetime. | |
| 14 | + | type Interface struct { | |
| 15 | + | Name string | |
| 16 | + | Version int32 | |
| 17 | + | Requests []Message | |
| 18 | + | Events []Message | |
| 19 | + | ||
| 20 | + | c uintptr // struct wl_interface* | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // C layout (LP64): | |
| 24 | + | // | |
| 25 | + | // struct wl_message { const char *name; const char *signature; const struct wl_interface **types; } // 24 bytes | |
| 26 | + | // struct wl_interface { const char *name; int version; int method_count; const struct wl_message *methods; | |
| 27 | + | // int event_count; const struct wl_message *events; } // 40 bytes | |
| 28 | + | const ( | |
| 29 | + | sizeMessage = 24 | |
| 30 | + | sizeInterface = 40 | |
| 31 | + | ) | |
| 32 | + | ||
| 33 | + | var tables *arena | |
| 34 | + | ||
| 35 | + | // NewInterfaces writes C tables for a set of interfaces that may reference | |
| 36 | + | // each other. It must be called before any interface is used. | |
| 37 | + | func NewInterfaces(ifaces ...*Interface) error { | |
| 38 | + | if err := load(); err != nil { | |
| 39 | + | return err | |
| 40 | + | } | |
| 41 | + | if tables == nil { | |
| 42 | + | a, err := newArena(1 << 20) | |
| 43 | + | if err != nil { | |
| 44 | + | return err | |
| 45 | + | } | |
| 46 | + | tables = a | |
| 47 | + | } | |
| 48 | + | a := tables | |
| 49 | + | // Pass 1: reserve every wl_interface so cross references resolve. | |
| 50 | + | offs := make([]int, len(ifaces)) | |
| 51 | + | for i, it := range ifaces { | |
| 52 | + | offs[i], it.c = a.alloc(sizeInterface) | |
| 53 | + | } | |
| 54 | + | // Pass 2: fill messages and headers. | |
| 55 | + | for i, it := range ifaces { | |
| 56 | + | o := offs[i] | |
| 57 | + | a.putPtr(o, a.cstring(it.Name)) | |
| 58 | + | a.putI32(o+8, it.Version) | |
| 59 | + | a.putI32(o+12, int32(len(it.Requests))) | |
| 60 | + | a.putPtr(o+16, writeMessages(a, it.Requests)) | |
| 61 | + | a.putI32(o+24, int32(len(it.Events))) | |
| 62 | + | a.putPtr(o+32, writeMessages(a, it.Events)) | |
| 63 | + | } | |
| 64 | + | return nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func writeMessages(a *arena, msgs []Message) uintptr { | |
| 68 | + | if len(msgs) == 0 { | |
| 69 | + | return 0 | |
| 70 | + | } | |
| 71 | + | off, addr := a.alloc(sizeMessage * len(msgs)) | |
| 72 | + | for i, m := range msgs { | |
| 73 | + | var types uintptr | |
| 74 | + | if len(m.Types) > 0 { | |
| 75 | + | toff, taddr := a.alloc(8 * len(m.Types)) | |
| 76 | + | for j, t := range m.Types { | |
| 77 | + | if t != nil { | |
| 78 | + | if t.c == 0 { | |
| 79 | + | panic("purego-libwayland: interface " + t.Name + " referenced before NewInterfaces") | |
| 80 | + | } | |
| 81 | + | a.putPtr(toff+8*j, t.c) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | types = taddr | |
| 85 | + | } | |
| 86 | + | mo := off + i*sizeMessage | |
| 87 | + | a.putPtr(mo, a.cstring(m.Name)) | |
| 88 | + | a.putPtr(mo+8, a.cstring(m.Signature)) | |
| 89 | + | a.putPtr(mo+16, types) | |
| 90 | + | } | |
| 91 | + | return addr | |
| 92 | + | } | |
| @@ -0,0 +1,92 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | // Message describes one request or event. Signature uses the libwayland | |||
| 4 | + | // format ("n", "?oii", ...). Types holds one entry per argument; entries are | |||
| 5 | + | // non-nil only for typed new_id and object arguments. | |||
| 6 | + | type Message struct { | |||
| 7 | + | Name string | |||
| 8 | + | Signature string | |||
| 9 | + | Types []*Interface | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | // Interface describes a protocol interface. Build it once with | |||
| 13 | + | // NewInterfaces; its C table lives for the process lifetime. | |||
| 14 | + | type Interface struct { | |||
| 15 | + | Name string | |||
| 16 | + | Version int32 | |||
| 17 | + | Requests []Message | |||
| 18 | + | Events []Message | |||
| 19 | + | ||||
| 20 | + | c uintptr // struct wl_interface* | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // C layout (LP64): | |||
| 24 | + | // | |||
| 25 | + | // struct wl_message { const char *name; const char *signature; const struct wl_interface **types; } // 24 bytes | |||
| 26 | + | // struct wl_interface { const char *name; int version; int method_count; const struct wl_message *methods; | |||
| 27 | + | // int event_count; const struct wl_message *events; } // 40 bytes | |||
| 28 | + | const ( | |||
| 29 | + | sizeMessage = 24 | |||
| 30 | + | sizeInterface = 40 | |||
| 31 | + | ) | |||
| 32 | + | ||||
| 33 | + | var tables *arena | |||
| 34 | + | ||||
| 35 | + | // NewInterfaces writes C tables for a set of interfaces that may reference | |||
| 36 | + | // each other. It must be called before any interface is used. | |||
| 37 | + | func NewInterfaces(ifaces ...*Interface) error { | |||
| 38 | + | if err := load(); err != nil { | |||
| 39 | + | return err | |||
| 40 | + | } | |||
| 41 | + | if tables == nil { | |||
| 42 | + | a, err := newArena(1 << 20) | |||
| 43 | + | if err != nil { | |||
| 44 | + | return err | |||
| 45 | + | } | |||
| 46 | + | tables = a | |||
| 47 | + | } | |||
| 48 | + | a := tables | |||
| 49 | + | // Pass 1: reserve every wl_interface so cross references resolve. | |||
| 50 | + | offs := make([]int, len(ifaces)) | |||
| 51 | + | for i, it := range ifaces { | |||
| 52 | + | offs[i], it.c = a.alloc(sizeInterface) | |||
| 53 | + | } | |||
| 54 | + | // Pass 2: fill messages and headers. | |||
| 55 | + | for i, it := range ifaces { | |||
| 56 | + | o := offs[i] | |||
| 57 | + | a.putPtr(o, a.cstring(it.Name)) | |||
| 58 | + | a.putI32(o+8, it.Version) | |||
| 59 | + | a.putI32(o+12, int32(len(it.Requests))) | |||
| 60 | + | a.putPtr(o+16, writeMessages(a, it.Requests)) | |||
| 61 | + | a.putI32(o+24, int32(len(it.Events))) | |||
| 62 | + | a.putPtr(o+32, writeMessages(a, it.Events)) | |||
| 63 | + | } | |||
| 64 | + | return nil | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func writeMessages(a *arena, msgs []Message) uintptr { | |||
| 68 | + | if len(msgs) == 0 { | |||
| 69 | + | return 0 | |||
| 70 | + | } | |||
| 71 | + | off, addr := a.alloc(sizeMessage * len(msgs)) | |||
| 72 | + | for i, m := range msgs { | |||
| 73 | + | var types uintptr | |||
| 74 | + | if len(m.Types) > 0 { | |||
| 75 | + | toff, taddr := a.alloc(8 * len(m.Types)) | |||
| 76 | + | for j, t := range m.Types { | |||
| 77 | + | if t != nil { | |||
| 78 | + | if t.c == 0 { | |||
| 79 | + | panic("purego-libwayland: interface " + t.Name + " referenced before NewInterfaces") | |||
| 80 | + | } | |||
| 81 | + | a.putPtr(toff+8*j, t.c) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | types = taddr | |||
| 85 | + | } | |||
| 86 | + | mo := off + i*sizeMessage | |||
| 87 | + | a.putPtr(mo, a.cstring(m.Name)) | |||
| 88 | + | a.putPtr(mo+8, a.cstring(m.Signature)) | |||
| 89 | + | a.putPtr(mo+16, types) | |||
| 90 | + | } | |||
| 91 | + | return addr | |||
| 92 | + | } | |||
A server/lib.go +79 -0
| @@ -0,0 +1,79 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "sync" | |
| 6 | + | "unsafe" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/purego" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // libwayland-server entry points. Only non-varargs functions are bound. | |
| 12 | + | var ( | |
| 13 | + | wlDisplayCreate func() uintptr | |
| 14 | + | wlDisplayDestroy func(display uintptr) | |
| 15 | + | wlDisplayDestroyClients func(display uintptr) | |
| 16 | + | wlDisplayAddSocketFD func(display uintptr, fd int32) int32 | |
| 17 | + | wlDisplayGetEventLoop func(display uintptr) uintptr | |
| 18 | + | wlDisplayFlushClients func(display uintptr) | |
| 19 | + | wlEventLoopDispatch func(loop uintptr, timeout int32) int32 | |
| 20 | + | wlEventLoopGetFD func(loop uintptr) int32 | |
| 21 | + | wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr | |
| 22 | + | wlResourceCreate func(client, iface uintptr, version int32, id uint32) uintptr | |
| 23 | + | wlResourceSetDispatcher func(resource, dispatcher, impl, data, destroy uintptr) | |
| 24 | + | wlResourcePostEventArr func(resource uintptr, opcode uint32, args unsafe.Pointer) | |
| 25 | + | wlResourcePostError func(resource uintptr, code uint32, msg uintptr) | |
| 26 | + | wlResourceDestroy func(resource uintptr) | |
| 27 | + | wlResourceGetID func(resource uintptr) uint32 | |
| 28 | + | wlResourceGetClient func(resource uintptr) uintptr | |
| 29 | + | wlResourceGetVersion func(resource uintptr) int32 | |
| 30 | + | ) | |
| 31 | + | ||
| 32 | + | var ( | |
| 33 | + | loadOnce sync.Once | |
| 34 | + | loadErr error | |
| 35 | + | ||
| 36 | + | // Shared C callbacks. purego callbacks are never freed, so there is one | |
| 37 | + | // of each for the whole process. | |
| 38 | + | cbDispatcher uintptr | |
| 39 | + | cbDestroy uintptr | |
| 40 | + | cbBind uintptr | |
| 41 | + | ) | |
| 42 | + | ||
| 43 | + | func load() error { | |
| 44 | + | loadOnce.Do(func() { | |
| 45 | + | lib, err := purego.Dlopen("libwayland-server.so.0", purego.RTLD_NOW|purego.RTLD_GLOBAL) | |
| 46 | + | if err != nil { | |
| 47 | + | loadErr = fmt.Errorf("purego-libwayland: open libwayland-server.so.0: %w", err) | |
| 48 | + | return | |
| 49 | + | } | |
| 50 | + | defer func() { | |
| 51 | + | if r := recover(); r != nil { | |
| 52 | + | loadErr = fmt.Errorf("purego-libwayland: %v", r) | |
| 53 | + | } | |
| 54 | + | }() | |
| 55 | + | reg := func(fptr any, name string) { purego.RegisterLibFunc(fptr, lib, name) } | |
| 56 | + | reg(&wlDisplayCreate, "wl_display_create") | |
| 57 | + | reg(&wlDisplayDestroy, "wl_display_destroy") | |
| 58 | + | reg(&wlDisplayDestroyClients, "wl_display_destroy_clients") | |
| 59 | + | reg(&wlDisplayAddSocketFD, "wl_display_add_socket_fd") | |
| 60 | + | reg(&wlDisplayGetEventLoop, "wl_display_get_event_loop") | |
| 61 | + | reg(&wlDisplayFlushClients, "wl_display_flush_clients") | |
| 62 | + | reg(&wlEventLoopDispatch, "wl_event_loop_dispatch") | |
| 63 | + | reg(&wlEventLoopGetFD, "wl_event_loop_get_fd") | |
| 64 | + | reg(&wlGlobalCreate, "wl_global_create") | |
| 65 | + | reg(&wlResourceCreate, "wl_resource_create") | |
| 66 | + | reg(&wlResourceSetDispatcher, "wl_resource_set_dispatcher") | |
| 67 | + | reg(&wlResourcePostEventArr, "wl_resource_post_event_array") | |
| 68 | + | reg(&wlResourcePostError, "wl_resource_post_error") | |
| 69 | + | reg(&wlResourceDestroy, "wl_resource_destroy") | |
| 70 | + | reg(&wlResourceGetID, "wl_resource_get_id") | |
| 71 | + | reg(&wlResourceGetClient, "wl_resource_get_client") | |
| 72 | + | reg(&wlResourceGetVersion, "wl_resource_get_version") | |
| 73 | + | ||
| 74 | + | cbDispatcher = purego.NewCallback(dispatch) | |
| 75 | + | cbDestroy = purego.NewCallback(destroyed) | |
| 76 | + | cbBind = purego.NewCallback(bind) | |
| 77 | + | }) | |
| 78 | + | return loadErr | |
| 79 | + | } | |
| @@ -0,0 +1,79 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "sync" | |||
| 6 | + | "unsafe" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/purego" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // libwayland-server entry points. Only non-varargs functions are bound. | |||
| 12 | + | var ( | |||
| 13 | + | wlDisplayCreate func() uintptr | |||
| 14 | + | wlDisplayDestroy func(display uintptr) | |||
| 15 | + | wlDisplayDestroyClients func(display uintptr) | |||
| 16 | + | wlDisplayAddSocketFD func(display uintptr, fd int32) int32 | |||
| 17 | + | wlDisplayGetEventLoop func(display uintptr) uintptr | |||
| 18 | + | wlDisplayFlushClients func(display uintptr) | |||
| 19 | + | wlEventLoopDispatch func(loop uintptr, timeout int32) int32 | |||
| 20 | + | wlEventLoopGetFD func(loop uintptr) int32 | |||
| 21 | + | wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr | |||
| 22 | + | wlResourceCreate func(client, iface uintptr, version int32, id uint32) uintptr | |||
| 23 | + | wlResourceSetDispatcher func(resource, dispatcher, impl, data, destroy uintptr) | |||
| 24 | + | wlResourcePostEventArr func(resource uintptr, opcode uint32, args unsafe.Pointer) | |||
| 25 | + | wlResourcePostError func(resource uintptr, code uint32, msg uintptr) | |||
| 26 | + | wlResourceDestroy func(resource uintptr) | |||
| 27 | + | wlResourceGetID func(resource uintptr) uint32 | |||
| 28 | + | wlResourceGetClient func(resource uintptr) uintptr | |||
| 29 | + | wlResourceGetVersion func(resource uintptr) int32 | |||
| 30 | + | ) | |||
| 31 | + | ||||
| 32 | + | var ( | |||
| 33 | + | loadOnce sync.Once | |||
| 34 | + | loadErr error | |||
| 35 | + | ||||
| 36 | + | // Shared C callbacks. purego callbacks are never freed, so there is one | |||
| 37 | + | // of each for the whole process. | |||
| 38 | + | cbDispatcher uintptr | |||
| 39 | + | cbDestroy uintptr | |||
| 40 | + | cbBind uintptr | |||
| 41 | + | ) | |||
| 42 | + | ||||
| 43 | + | func load() error { | |||
| 44 | + | loadOnce.Do(func() { | |||
| 45 | + | lib, err := purego.Dlopen("libwayland-server.so.0", purego.RTLD_NOW|purego.RTLD_GLOBAL) | |||
| 46 | + | if err != nil { | |||
| 47 | + | loadErr = fmt.Errorf("purego-libwayland: open libwayland-server.so.0: %w", err) | |||
| 48 | + | return | |||
| 49 | + | } | |||
| 50 | + | defer func() { | |||
| 51 | + | if r := recover(); r != nil { | |||
| 52 | + | loadErr = fmt.Errorf("purego-libwayland: %v", r) | |||
| 53 | + | } | |||
| 54 | + | }() | |||
| 55 | + | reg := func(fptr any, name string) { purego.RegisterLibFunc(fptr, lib, name) } | |||
| 56 | + | reg(&wlDisplayCreate, "wl_display_create") | |||
| 57 | + | reg(&wlDisplayDestroy, "wl_display_destroy") | |||
| 58 | + | reg(&wlDisplayDestroyClients, "wl_display_destroy_clients") | |||
| 59 | + | reg(&wlDisplayAddSocketFD, "wl_display_add_socket_fd") | |||
| 60 | + | reg(&wlDisplayGetEventLoop, "wl_display_get_event_loop") | |||
| 61 | + | reg(&wlDisplayFlushClients, "wl_display_flush_clients") | |||
| 62 | + | reg(&wlEventLoopDispatch, "wl_event_loop_dispatch") | |||
| 63 | + | reg(&wlEventLoopGetFD, "wl_event_loop_get_fd") | |||
| 64 | + | reg(&wlGlobalCreate, "wl_global_create") | |||
| 65 | + | reg(&wlResourceCreate, "wl_resource_create") | |||
| 66 | + | reg(&wlResourceSetDispatcher, "wl_resource_set_dispatcher") | |||
| 67 | + | reg(&wlResourcePostEventArr, "wl_resource_post_event_array") | |||
| 68 | + | reg(&wlResourcePostError, "wl_resource_post_error") | |||
| 69 | + | reg(&wlResourceDestroy, "wl_resource_destroy") | |||
| 70 | + | reg(&wlResourceGetID, "wl_resource_get_id") | |||
| 71 | + | reg(&wlResourceGetClient, "wl_resource_get_client") | |||
| 72 | + | reg(&wlResourceGetVersion, "wl_resource_get_version") | |||
| 73 | + | ||||
| 74 | + | cbDispatcher = purego.NewCallback(dispatch) | |||
| 75 | + | cbDestroy = purego.NewCallback(destroyed) | |||
| 76 | + | cbBind = purego.NewCallback(bind) | |||
| 77 | + | }) | |||
| 78 | + | return loadErr | |||
| 79 | + | } | |||