1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "runtime"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "unsafe"
13
14 "github.com/bnema/purego"
15 "golang.org/x/sys/unix"
16)
17
18// Handler receives every request sent to a resource. args borrows libwayland's
19// argument array and must not be retained after the handler returns.
20type Handler func(r *Resource, opcode uint32, args []Arg)
21
22// BindFunc runs when a client binds a global.
23type BindFunc func(c Client, version, id uint32)
24
25// Client is a connected wl_client. It is a comparable handle to the C object,
26// usable as a map key until the client is destroyed (see Client.OnDestroy).
27type Client struct{ c uintptr }
28
29// Resource is a server-side wl_resource.
30type Resource struct {
31 c uintptr
32 id uint32
33 version int32
34 client uintptr
35 iface *Interface
36 handler Handler
37 OnDestroy func()
38 gone bool
39 bare bool // created by libwayland, no destroy callback
40
41 // Data holds the owner's state, typically the generated wrapper.
42 Data any
43}
44
45// live holds Go state reachable from C callbacks. It is only touched on the
46// display goroutine, so it needs no lock.
47var live = struct {
48 resources map[uintptr]*Resource
49 binds map[uintptr]BindFunc
50 nextBind uintptr
51 // clientGone maps wl_listener addresses to OnDestroy functions.
52 clientGone map[uintptr]func()
53 // fds maps AddFD tokens to their readable callbacks.
54 fds map[uintptr]func()
55 nextFD uintptr
56}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]func(){}}
57
58// Display owns a wl_display. All methods except Do and Post must run on the
59// goroutine that calls Run, or before Run starts.
60//
61// Resource, bind and client-listener state is shared by the whole process, so
62// only one Display may be running at a time per process.
63//
64// Do and Post wake the loop through an eventfd so calls run at once instead of
65// after the dispatch timeout.
66type Display struct {
67 c uintptr
68 loop uintptr
69 calls chan func()
70 wake int
71 // wakeSrc owns libwayland's duplicate of wake; only removing the source
72 // closes it, destroying the display does not.
73 wakeSrc uintptr
74 stopped chan struct{}
75 state atomic.Uint32
76
77 // postMu guards posts and closed. spare is only used by the display
78 // goroutine: it is the emptied batch swapped back in on the next drain, so
79 // steady-state Post does not allocate.
80 postMu sync.Mutex
81 posts []func()
82 spare []func()
83 // armed is true while an eventfd wake for posts is pending, so a burst of
84 // Posts writes the eventfd once.
85 armed atomic.Bool
86 closed atomic.Bool // set under postMu before the wake fd is closed
87 // wakeWrites counts eventfd writes made for posts (diagnostics and tests).
88 wakeWrites atomic.Uint64
89
90 // sources are the AddFD sources not yet removed (display goroutine only).
91 sources map[*Source]struct{}
92}
93
94// maxDrainRounds bounds how many times one loop iteration re-drains calls and
95// posts that were queued by callbacks, so a self-posting callback cannot starve
96// libwayland's dispatch and client flushing.
97const maxDrainRounds = 8
98
99func NewDisplay() (*Display, error) {
100 if err := load(); err != nil {
101 return nil, err
102 }
103 c := wlDisplayCreate()
104 if c == 0 {
105 return nil, errors.New("purego-libwayland: wl_display_create failed")
106 }
107 wake, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
108 if err != nil {
109 wlDisplayDestroy(c)
110 return nil, fmt.Errorf("purego-libwayland: eventfd: %w", err)
111 }
112 d := &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func(), 64), wake: wake, stopped: make(chan struct{}), sources: map[*Source]struct{}{}}
113 // The callback only drains the counter; Run then executes queued calls.
114 if d.wakeSrc = wlEventLoopAddFD(d.loop, int32(wake), 1 /* WL_EVENT_READABLE */, cbWake, 0); d.wakeSrc == 0 {
115 unix.Close(wake)
116 wlDisplayDestroy(c)
117 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
118 }
119 return d, nil
120}
121
122// AddSocketFD serves clients on an already bound, listening unix socket.
123func (d *Display) AddSocketFD(fd int) error {
124 if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 {
125 return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd)
126 }
127 return nil
128}
129
130func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
131 _, err := d.AddGlobal(iface, version, bindFn)
132 return err
133}
134
135// Global is a global that can be withdrawn, such as a wl_output on hotplug.
136type Global struct{ c uintptr }
137
138// AddGlobal advertises a global and returns a handle to remove it later.
139func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
140 live.nextBind++
141 token := live.nextBind
142 live.binds[token] = bindFn
143 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
144 if c == 0 {
145 delete(live.binds, token)
146 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
147 }
148 return &Global{c: c}, nil
149}
150
151// Remove withdraws the global: clients get wl_registry.global_remove.
152// Existing resources stay valid. A client that binds before it sees the
153// removal still gets its bind function called, so it always receives a
154// resource (the protocol allows binding a just-removed global); the bind
155// function should then send inert state. The global is freed with the
156// display. Display goroutine only; idempotent.
157func (g *Global) Remove() {
158 if g == nil || g.c == 0 {
159 return
160 }
161 wlGlobalRemove(g.c)
162 g.c = 0
163}
164
165// AckGlobalRemove implements wl_fixes.ack_global_remove: libwayland checks
166// that registry announced the removed global name, posting
167// wl_fixes.invalid_ack_remove on fixes otherwise. It reports false when
168// libwayland-server is older than 1.26 and has no support.
169// Display goroutine only.
170func AckGlobalRemove(fixes, registry *Resource, name uint32) bool {
171 if symFixesAckGlobalRemove == 0 {
172 return false
173 }
174 if fixes == nil || fixes.gone || registry == nil || registry.gone {
175 return true
176 }
177 wlFixesHandleAckGlobalRemove(fixes.c, registry.c, name)
178 return true
179}
180
181// Close destroys a display that has not started Run. It is a no-op once Run
182// has started; Run owns destruction in that case. Call before Run starts.
183func (d *Display) Close() {
184 if !d.state.CompareAndSwap(0, 1) {
185 return
186 }
187 d.destroy()
188 close(d.stopped)
189}
190
191// destroy frees the display and the wake-up fds.
192func (d *Display) destroy() {
193 // Stop accepting posts before the wake fd is closed (its number could be
194 // reused), and drop what never ran.
195 d.postMu.Lock()
196 d.closed.Store(true)
197 clear(d.posts)
198 d.posts, d.spare = nil, nil
199 d.postMu.Unlock()
200 for s := range d.sources {
201 s.remove()
202 }
203 clear(d.sources)
204 wlDisplayDestroyClients(d.c)
205 wlEventSourceRemove(d.wakeSrc)
206 wlDisplayDestroy(d.c)
207 unix.Close(d.wake)
208}
209
210// Run dispatches libwayland on one OS-locked goroutine until ctx is done.
211func (d *Display) Run(ctx context.Context) error {
212 if !d.state.CompareAndSwap(0, 2) {
213 return errors.New("purego-libwayland: display already started or closed")
214 }
215 runtime.LockOSThread()
216 defer runtime.UnlockOSThread()
217 defer close(d.stopped)
218 defer d.destroy()
219 // Wait for the event loop's epoll fd in Go's netpoller instead of blocking
220 // in epoll_wait inside C: the goroutine parks, its P is released, and the
221 // runtime's sysmon can sleep deeply while the server is idle. libwayland
222 // is then dispatched without blocking.
223 loopFile, err := d.openLoopFile()
224 if err != nil {
225 return err
226 }
227 defer loopFile.Close() // runs before destroy: the dup must not outlive the epoll fd
228 rc, err := loopFile.SyscallConn()
229 if err != nil {
230 return fmt.Errorf("purego-libwayland: event loop fd: %w", err)
231 }
232 // Cancellation wakes the loop through the eventfd.
233 stop := context.AfterFunc(ctx, d.wakeGuarded)
234 defer stop()
235 ready := func(fd uintptr) bool {
236 // Level-triggered check: the netpoller is edge-triggered, and a
237 // dispatch can leave events (libwayland takes 32 per epoll_wait).
238 fds := [1]unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
239 n, err := unix.Poll(fds[:], 0)
240 return n > 0 || (err != nil && err != unix.EINTR)
241 }
242 consecutiveFailures := 0
243 for {
244 if ctx.Err() != nil {
245 return nil
246 }
247 // Run every queued post and call. Wakes are coalesced, so the eventfd
248 // is not written once per item; it only makes the wait below
249 // return at once when work arrives while the loop is asleep.
250 d.drain()
251 wlDisplayFlushClients(d.c)
252 if err := rc.Read(ready); err != nil {
253 return fmt.Errorf("purego-libwayland: wait for event loop: %w", err)
254 }
255 if ctx.Err() != nil {
256 return nil
257 }
258 if wlEventLoopDispatch(d.loop, 0) < 0 {
259 errno := lastErrno()
260 consecutiveFailures++
261 if consecutiveFailures < 16 {
262 continue
263 }
264 return fmt.Errorf("purego-libwayland: wl_event_loop_dispatch failed %d times in a row: %w", consecutiveFailures, errno)
265 }
266 consecutiveFailures = 0
267 wlDisplayFlushClients(d.c)
268 }
269}
270
271// openLoopFile wraps a duplicate of the event loop's epoll fd for the netpoller.
272func (d *Display) openLoopFile() (*os.File, error) {
273 fd := int(wlEventLoopGetFD(d.loop))
274 if fd < 0 {
275 return nil, errors.New("purego-libwayland: wl_event_loop_get_fd failed")
276 }
277 dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
278 if err != nil {
279 return nil, fmt.Errorf("purego-libwayland: dup event loop fd: %w", err)
280 }
281 if err := unix.SetNonblock(dup, true); err != nil {
282 unix.Close(dup)
283 return nil, fmt.Errorf("purego-libwayland: nonblock event loop fd: %w", err)
284 }
285 return os.NewFile(uintptr(dup), "wl-event-loop"), nil
286}
287
288// drain runs queued posts and Do calls until none are left, at most
289// maxDrainRounds rounds. Posts are taken again after each call is received and
290// before it runs, so a Post that happened before a Do always runs before that
291// Do's function. When the cap is hit the loop is woken again so the next
292// dispatch returns at once.
293func (d *Display) drain() {
294 for range maxDrainRounds {
295 ran := d.runPosts()
296 calls:
297 for {
298 select {
299 case fn := <-d.calls:
300 d.runPosts()
301 fn()
302 ran = true
303 default:
304 break calls
305 }
306 }
307 if !ran {
308 return
309 }
310 }
311 d.armed.Store(true)
312 d.wakeLoop()
313}
314
315// runPosts runs the posts queued so far and reports whether there were any.
316func (d *Display) runPosts() bool {
317 // Clear armed before taking the batch: a Post that lands after the swap
318 // sees armed false and writes the eventfd again.
319 d.armed.Store(false)
320 d.postMu.Lock()
321 batch := d.posts
322 d.posts, d.spare = d.spare, nil
323 d.postMu.Unlock()
324 for _, fn := range batch {
325 fn()
326 }
327 clear(batch)
328 d.spare = batch[:0]
329 return len(batch) > 0
330}
331
332// wakeOne is the eventfd increment. It is read-only, so sharing it between
333// goroutines is safe and avoids a per-call buffer (which escapes under -race).
334var wakeOne = [8]byte{1}
335
336// wakeLoop writes the eventfd on behalf of posts. The caller must hold postMu
337// (or be the display goroutine, which destroy cannot overlap) so the fd cannot
338// be closed, and its number reused, underneath the write.
339func (d *Display) wakeLoop() {
340 d.wakeWrites.Add(1)
341 _, _ = unix.Write(d.wake, wakeOne[:])
342}
343
344// wakeGuarded wakes the loop from any goroutine: it takes postMu and skips the
345// write once the display is closed, so a late caller never writes to a reused
346// fd number.
347func (d *Display) wakeGuarded() {
348 d.postMu.Lock()
349 defer d.postMu.Unlock()
350 if d.closed.Load() {
351 return
352 }
353 _, _ = unix.Write(d.wake, wakeOne[:])
354}
355
356// Post queues fn to run on the display goroutine and returns at once. Unlike
357// Do it never waits, and unlike Do it may be called from any goroutine,
358// including the display goroutine itself.
359//
360// Posts run in the order they were made, and a Post made before a Do (by the
361// same goroutine, or otherwise known to happen before it) runs before that Do's
362// function. A Post made by a posted function or a Do function runs in a later
363// drain round of the same loop iteration (at most 8 rounds; past that the loop
364// is woken again and continues after one libwayland dispatch). Wakes are coalesced:
365// any number of Posts while one wake is pending write the eventfd once.
366//
367// fn must not block: it runs on the goroutine that serves every client. After
368// the display has stopped or been closed, Post silently drops fn, and posts
369// that had not run when the display stopped are dropped too. In steady state
370// Post does not allocate.
371//
372// The queue is unbounded: Post applies no backpressure, so a producer that
373// outpaces the display goroutine grows memory without limit. Callers that can
374// produce faster than the loop runs must bound or coalesce their own posts.
375func (d *Display) Post(fn func()) {
376 if fn == nil {
377 return
378 }
379 d.postMu.Lock()
380 defer d.postMu.Unlock()
381 if d.closed.Load() {
382 return
383 }
384 d.posts = append(d.posts, fn)
385 // Written under postMu so destroy cannot close the fd underneath.
386 if d.armed.CompareAndSwap(false, true) {
387 d.wakeLoop()
388 }
389}
390
391// Source is an fd registered with the display's event loop by AddFD.
392type Source struct {
393 d *Display
394 c uintptr
395 token uintptr
396}
397
398// AddFD calls onReadable on the display goroutine whenever fd is readable. The
399// registration is level-triggered: onReadable must consume the readiness (read
400// the eventfd or timerfd, accept, ...) or it is called again at once. It
401// should not block.
402//
403// onReadable is also called when the fd hangs up or errors (EPOLLHUP,
404// EPOLLERR), which level-triggered polling keeps reporting. The callback should
405// read the fd, and on EOF or error call Remove (it may do so from within the
406// callback), or it is called again at once.
407//
408// libwayland duplicates fd; the caller still owns and must close its own
409// descriptor, and should do so after Remove. Call AddFD only on the display
410// goroutine or before Run starts. Sources not removed are released when the
411// display is destroyed.
412func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
413 if fd < 0 {
414 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
415 }
416 if onReadable == nil {
417 return nil, errors.New("purego-libwayland: AddFD: nil callback")
418 }
419 if d.closed.Load() {
420 return nil, errors.New("purego-libwayland: AddFD: display closed")
421 }
422 live.nextFD++
423 token := live.nextFD
424 live.fds[token] = onReadable
425 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
426 if c == 0 {
427 delete(live.fds, token)
428 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
429 }
430 s := &Source{d: d, c: c, token: token}
431 d.sources[s] = struct{}{}
432 return s, nil
433}
434
435// Remove stops the callbacks and releases libwayland's duplicate of the fd.
436// It may be called from the source's own callback. Display goroutine only;
437// idempotent, and a no-op after the display was destroyed.
438func (s *Source) Remove() {
439 if s == nil || s.c == 0 {
440 return
441 }
442 delete(s.d.sources, s)
443 s.remove()
444}
445
446func (s *Source) remove() {
447 delete(live.fds, s.token)
448 wlEventSourceRemove(s.c)
449 s.c = 0
450}
451
452// Do runs fn on the display goroutine and waits for it. It returns false if
453// the display loop has stopped (fn was not run, or its run was not confirmed).
454// Do must not be called from the display goroutine: it would deadlock.
455func (d *Display) Do(fn func()) bool {
456 done := make(chan struct{})
457 select {
458 case d.calls <- func() { fn(); close(done) }:
459 case <-d.stopped:
460 return false
461 }
462 d.wakeGuarded()
463 select {
464 case <-done:
465 return true
466 case <-d.stopped:
467 // If both are ready, prefer confirmation of a completed call.
468 select {
469 case <-done:
470 return true
471 default:
472 return false
473 }
474 }
475}
476
477// Stopped is closed after Run returns and the display is destroyed.
478func (d *Display) Stopped() <-chan struct{} { return d.stopped }
479
480// LiveResources reports resources not yet destroyed. Display goroutine only.
481func LiveResources() int { return len(live.resources) }
482
483// Credentials are the peer credentials of a client, read from the socket
484// (SO_PEERCRED) when it connected. UID/GID are effective IDs in the server's
485// namespaces; an unmappable ID is the namespace overflow ID. PID may be zero
486// even for a valid client when its PID namespace is not visible to the server.
487type Credentials struct {
488 PID int
489 UID uint32
490 GID uint32
491}
492
493var credScratch [3]int32
494
495var errInvalidClient = errors.New("purego-libwayland: invalid client")
496
497// Credentials returns the peer credentials of the client. It returns an error
498// for an invalid (zero) Client. It does not allocate. Display goroutine only.
499func (c Client) Credentials() (Credentials, error) {
500 if c.c == 0 {
501 return Credentials{}, errInvalidClient
502 }
503 // Package-level scratch (display goroutine only) avoids heap-escaping locals.
504 purego.Syscall6(symClientGetCredentials, c.c, uintptr(unsafe.Pointer(&credScratch[0])), uintptr(unsafe.Pointer(&credScratch[1])), uintptr(unsafe.Pointer(&credScratch[2])), 0, 0)
505 return Credentials{PID: int(credScratch[0]), UID: uint32(credScratch[1]), GID: uint32(credScratch[2])}, nil
506}
507
508// PID is the process ID of the client, from Credentials. It is zero for an
509// invalid client or a peer outside the server's visible PID namespaces.
510// Display goroutine only.
511func (c Client) PID() int {
512 cr, err := c.Credentials()
513 if err != nil {
514 return 0
515 }
516 return cr.PID
517}
518
519func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
520 rc := wlResourceCreate(c.c, iface.c, version, id)
521 if rc == 0 {
522 return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name)
523 }
524 // id 0 asks libwayland to allocate a server-side ID.
525 if id == 0 {
526 id = wlResourceGetID(rc)
527 }
528 r := &Resource{c: rc, id: id, version: version, client: c.c, iface: iface, handler: h}
529 live.resources[rc] = r
530 wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy)
531 return r, nil
532}
533
534func (r *Resource) Alive() bool { return !r.gone }
535func (r *Resource) ID() uint32 {
536 if r.gone {
537 return 0
538 }
539 return r.id
540}
541func (r *Resource) Version() int32 {
542 if r.gone {
543 return 0
544 }
545 return r.version
546}
547func (r *Resource) Client() Client {
548 if r.gone {
549 return Client{}
550 }
551 return Client{r.client}
552}
553func (r *Resource) Iface() *Interface { return r.iface }
554
555// Destroy frees the resource. It is a no-op once the resource is gone, so
556// generated destructor requests and handlers may both call it.
557func (r *Resource) Destroy() {
558 if !r.gone {
559 wlResourceDestroy(r.c)
560 // A bare handle gets no destroy callback to mark it gone.
561 r.gone = r.gone || r.bare
562 }
563}
564
565// eventArgs holds the wl_argument array of the event being posted.
566// libwayland copies it before wl_resource_post_event_array returns.
567// Display goroutine only; post_event_array does not call back into Go, so
568// PostEvent is not re-entrant. No protocol event has more than 8 arguments.
569var eventArgs [16]Arg
570
571// PostEvent sends an event. Arguments must match the event signature; build
572// string and array arguments with a Pinner and unpin it after this returns.
573// Calling it after destruction is safe and does nothing.
574func (r *Resource) PostEvent(opcode uint32, args ...Arg) {
575 if r.gone {
576 return
577 }
578 if len(args) > len(eventArgs) {
579 panic("purego-libwayland: too many event arguments")
580 }
581 var p unsafe.Pointer
582 if len(args) > 0 {
583 copy(eventArgs[:], args)
584 p = unsafe.Pointer(&eventArgs[0])
585 }
586 wlResourcePostEventArr(r.c, opcode, p)
587}
588
589// PostError sends a protocol error and disconnects the client.
590// Calling it after destruction is safe and does nothing.
591func (r *Resource) PostError(code uint32, msg string) {
592 if r.gone {
593 return
594 }
595 // wl_resource_post_error takes a printf format; escape it and pass no
596 // variadic arguments (purego clears AL, as SysV varargs require).
597 b := append([]byte(strings.ReplaceAll(msg, "%", "%%")), 0)
598 var pin runtime.Pinner
599 pin.Pin(&b[0])
600 defer pin.Unpin()
601 wlResourcePostError(r.c, code, uintptr(unsafe.Pointer(&b[0])))
602 runtime.KeepAlive(b)
603}
604
605// implMarker is a non-nil implementation pointer. libwayland only passes it
606// back to our dispatcher.
607func implMarker() uintptr {
608 if marker == 0 {
609 tablesMu.Lock()
610 if err := tables.reserve(8); err != nil {
611 panic(err)
612 }
613 _, marker = tables.alloc(8)
614 tablesMu.Unlock()
615 }
616 return marker
617}
618
619var marker uintptr
620
621// --- C callbacks (display goroutine) ---
622
623func dispatch(_ uintptr, target uintptr, opcode uint32, _ unsafe.Pointer, args unsafe.Pointer) int32 {
624 r := live.resources[target]
625 if r == nil || opcode >= uint32(len(r.iface.Requests)) {
626 return 0
627 }
628 m := &r.iface.Requests[opcode]
629 if r.handler == nil {
630 closeRequestFDs(m.fdPositions, args)
631 return 0
632 }
633 var a []Arg
634 if m.argc > 0 {
635 a = unsafe.Slice((*Arg)(args), m.argc)
636 }
637 r.handler(r, opcode, a)
638 return 0
639}
640
641func destroyed(res uintptr) {
642 r := live.resources[res]
643 delete(live.resources, res)
644 if r == nil {
645 return
646 }
647 r.gone = true
648 if r.OnDestroy != nil {
649 r.OnDestroy()
650 }
651}
652
653// wake drains a Display's eventfd (wl_event_loop_fd_func_t).
654func wake(fd int32, mask uint32, data uintptr) int32 {
655 var buf [8]byte
656 _, _ = unix.Read(int(fd), buf[:])
657 return 0
658}
659
660// fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
661func fdReadable(_ int32, _ uint32, data uintptr) int32 {
662 if fn := live.fds[data]; fn != nil {
663 fn()
664 }
665 return 0
666}
667
668func bind(client uintptr, data uintptr, version uint32, id uint32) {
669 if fn := live.binds[data]; fn != nil {
670 fn(Client{client}, version, id)
671 }
672}
673
674// closeRequestFDs takes ownership of fd arguments when there is no handler.
675func closeRequestFDs(positions []int, args unsafe.Pointer) {
676 for _, i := range positions {
677 _ = unix.Close((*Arg)(unsafe.Add(args, i*8)).Fd())
678 }
679}
680
681// argCount counts arguments in a libwayland signature ("?oii", "2n", ...).
682func argCount(sig string) int {
683 n := 0
684 for _, ch := range sig {
685 if ch != '?' && (ch < '0' || ch > '9') {
686 n++
687 }
688 }
689 return n
690}