lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]

I am a contributor to Hammerspoon, which uses an embeded lua 5.3 interpreter to allow users to programatically access a fair amount of the macOS API for things like window management, hoteys, etc. and I've been looking at ways to make it seem more responsive when the lua interpreter is actually doing something slow...

A little background before I get to my example and questions (I hope this makes sense):

As I understand it, when things like a timer fires and we want to invoke a lua callback, what happens is an event is queued for the macOS runloop and when the runloop isn't doing anything else, it checks its queue and pops an event off and handles it. In the case of one of our timers, the block attached to the queued event uses `lua_pcall` to call a stored function and the lua interpreter executes the lua function.

There is also a conosle where we can type things in and have them executed by the lua interpreter (much like invoking lua from the command line interactively).

So... I hit upon this hairbrained idea to "mimic" the macOS application runloop for a short period in a C function added to a lua module, defined like this:

          static int hs_yield(lua_State *L) {
              NSTimeInterval interval = (lua_type(L, 1) == LUA_TNUMBER) ? 0.001 : lua_tonumber(L, 1) ;
              NSDate         *date    = [[NSDate date] dateByAddingTimeInterval:interval] ;

              // a melding of code from gnustep's implementation of NSApplication's run and runUntilDate: methods
              // this allows acting on events (hs.eventtap) and keys (hs.hotkey) as well as timers, etc.
              BOOL   mayDoMore = YES ;
              while (mayDoMore) {
                  NSEvent *e = [NSApp nextEventMatchingMask:NSAnyEventMask
                                                    dequeue:YES] ;
                  if (e) [NSApp sendEvent:e] ;

                  mayDoMore = !([date timeIntervalSinceNow] <= 0.0) ;

              return 0 ;

So if I do the following in the Hammerspoon console (pasted in as one large chunk, which means its treated as a single string and also executed within a lua_pcall:

          -- creates a grey rectangular box on the screen with the tet "**" in it
          cv ={ x = 100, y = 100, h = 100, w = 100 }:show()
          cv[#cv + 1] = { type = "rectangle", fillColor = { white = .1 } }
          cv[#cv + 1] = { type = "text", text = "**", textSize = 75 }

          st = false
          bl = 0
          dd = 0

          -- every second, update the number displayed in the grey box
          xx = hs.timer.doEvery(1, function()
              dd = dd + 1
              cv[2].text = tostring(dd)

          -- after 30 seconds, stop while loop below and clean up
          yy = hs.timer.doAfter(30, function()
              st = true   -- stop runaway while
              xx:stop()   -- stop timer updating canvas
              cv:delete() -- delete canvas

          zz = os.time()
          while (not st) do
              bl = bl + 1
              hs_yield(n) -- defaults to 0.001 if n is nil
          print(bl, dd, os.time() - zz)

And this works as I had hoped -- the canvas is updated each second for 30 seconds and bl ends up being somewhere around 1478052.

My question to the list: is this safe, or am I setting things up for a subtle catastrophe at some point?

As I understand it, what's happening is that the lua interpreter is executing the while loop, and from inside the while loop, pauses for 0.001 seconds to see if an event is queued for the macOS application run loop; when there is, if the event has a lua function attached, it is invoked (from one of the timers) within a new lua_pcall, but before the original lua_pcall, which is executing the while loop, has finished.

Is this safe, since each chunk of lua code is within its own lua_pcall? Are there lua commands I should make sure that hs_yield is never invoked within (haven't tried a for command iterator function yet, for example)? Are there additional sanity checks I should make to ensure the integrity of the lua state and stack?

Figured I should ask here before releasing this into the core Hammerspoon code and let our users start banging on it.

Thanks in advance for any insight or suggestions!