Subject: Re: bug#78946: 30.1; accept-process-output not allowed in threads on macOS

From: Alan Third <[email-masked]>
Date: Wed, 9 Jul 2025 20:45:38 +0100

On Wed, Jul 09, 2025 at 10:12:17AM +0200, Robert Pluim wrote:
> >>>>> On Mon, 7 Jul 2025 21:47:09 +0100, Alan Third <[email-masked]> said:
> 
>     Alan> OK, the fd_handler thread only exists because we "need" to run the NS
>     Alan> run loop and pselect simultaneously. Obviously we can't do that with a
>     Alan> single thread, so one has to be put into a separate thread. The
>     Alan> fd_handler only communicates with the run loop in as much as it tells
>     Alan> it to stop. All it's actual *results* are handed back via the global
>     Alan> variables select_readfds, select_writefds, etc.
> 
>     Alan> So there's nothing special about running pselect in the main thread
>     Alan> here, it's just not done because the run loop is required at the same
>     Alan> time.
> 
>     Alan> This means that if we decide we don't need to run the run loop (which
>     Alan> is something we used to do) then we could just call thread_select. And
>     Alan> just to confuse matters we have to run thread_select anyway, as that's
>     Alan> how Emacs chooses to run another lisp thread. So you'll see a call
>     Alan> near the top of the ns_select function with a zero timeout and we just
>     Alan> ignore the return value.
> 
> I guess you mean this in ns_select_1, which does a non-blocking
> pselect for 0 seconds on no file descriptors:
> 
>   else
>     {
>       struct timespec t = {0, 0};
>       thread_select (pselect, 0, NULL, NULL, NULL, &t, sigmask);
>     }
> 
> If itʼs there for the emacs thread infrastructure, is there a more
> explicit way of doing whatever it does?

I've no idea, I don't actually know much about how it works. IIRC Eli
told me to add it years ago when I was trying to improve thread
support...

>     Alan> Sub-threads, as you say, can't call the run loop, so for them
>     Alan> ns_select should act pretty much just like a call to pselect.
> 
> OK. But it would be good if we could have a single code path, rather
> than 2.

I think that it makes more sense to just have an early return rather
than go through all the nonsense required for the run loop when we
don't actually use it.

>     Alan> Ultimately, ns_select should look to the caller just like a call to
>     Alan> pselect, or whatever. It should return whatever pselect would return
>     Alan> in that situation, and shouldn't behave differently, although you and
>     Alan> I know it's doing a whole bunch of other stuff behind the caller's
>     Alan> back.
> 
>     Alan> I scare quoted "need" above because the reality is that we don't
>     Alan> actually *need* to run the run loop in ns_select, it's just a
>     Alan> convenient place for it.
> 
>     Alan> The ideal situation would be for the run loop to run in it's own
>     Alan> thread and ns_select could then be replaced with a simple call to
>     Alan> thread_select like some of the other terms.
> 
> Yes. I was a bit surprised when I discovered that weʼre starting and
> stopping the run loop all the time. It makes reasoning about stuff a
> lot harder.
> 
>     Alan> But implementing that comes with it's own set of nightmares.
> 
> Such as? If we put the run loop in its own thread, running forever,
> then the main emacs code (and emacs threads) can call
> pselect/thread_select without having to communicate with the
> fd_handler thread, which we could remove[1]. It would remove a bunch of
> code thatʼs just needed to synchronize between the main thread, the
> run loop, and the fd_handler thread, which is evidently fragile enough
> that we keep futzing with it.

The trouble is that if we move the run loop to it's own thread then
everything else has to synchronise with it as it must be in the main
thread, and all GUI calls must also be in the main thread.

If we take a simple function like ns_clear_frame_area as an example:

    {
      NSRect r = NSMakeRect (x, y, width, height);
      NSView *view = FRAME_NS_VIEW (f);
      struct face *face = FRAME_DEFAULT_FACE (f);
    
      if (!view || !face)
        return;
    
      NSTRACE_WHEN (NSTRACE_GROUP_UPDATES, "ns_clear_frame_area");
    
      r = NSIntersectionRect (r, [view frame]);
      ns_focus (f, &r, 1);
      [[NSColor colorWithUnsignedLong:NS_FACE_BACKGROUND (face)] set];
    
      NSRectFill (r);
    
      ns_unfocus (f);
      return;
    }

[view frame], [NSColor set] and NSRectFill all, I think, need to be
run in the main thread.

Odds are ns_focus and ns_unfocus also need to be largely run in the
main thread.

And this is a simple example, presumably we'll also need to dispatch
from the main thread back to the Emacs thread on occasion.

However this function needs to be run in the "Emacs" thread, so we
need to take most of this function and dispatch it to the run loop to
run it in the main thread.

And we're not allowed to use Obj C blocks for this as GCC doesn't
support them, so this means we need another function, or probably an
object instance method.

Last time I looked at this I think my approach was to create an
"EmacsFrame" class that would handle all the gui work and provide a
minimal interface for the Emacs C code.

Perhaps I'm over-thinking it and it's actually quite straight-forward,
but I'm pretty much on my own with this and have nobody to turn to for
help. Frankly it's all a bit much for me, especially given that the
Mac Port works perfectly already and I'm not really convinced of the
requirement for support of the NS port. It feels like a waste of my
time.

(Cue gasps of outrage from the GNUstep community... Oh no, wait, they
don't really care.)

>     Alan> Another approach I looked at briefly to simplify this is emulating
>     Alan> pselect in the run loop. It can monitor the read fds, but not the
>     Alan> write fds, so I gave up on that.
> 
> Is that because macOS doesnʼt send events for fds becoming writeable?

IIRC, and it's a long time since I looked at this so I could be
misremembering, the NSFileHandle class lets you wrap an FD and then
monitor it in the run loop so it will call a method when the FD has
stuff to read, but there's just no way to do that with writes. I
assume the idea is you should be using the NS file writing stuff and
therefore don't need that functionality.

So, rereading what you wrote, yes.

-- 
Alan Third