hammerspoon: Coroutines don't work in Hammerspon
At all.
With the following from a coroutine tutorial (https://riptutorial.com/lua/example/11751/create-and-use-a-coroutine):
thread2 = coroutine.create(function()
for n = 1, 5 do
print("honk "..n)
coroutine.yield()
end
end)
as soon as I enter coroutine.resume(thread2), I get:
false NSInvalidArgumentException: NSConcreteAttributedString initWithString:: nil value
(
0 CoreFoundation 0x00007fff4bcb7acd __exceptionPreprocess + 256
1 libobjc.A.dylib 0x00007fff763b9a17 objc_exception_throw + 48
2 CoreFoundation 0x00007fff4bcb78ff +[NSException raise:format:] + 201
3 Foundation 0x00007fff4dec59b6 -[NSConcreteAttributedString initWithString:] + 222
4 Foundation 0x00007fff4dee2392 -[NSConcreteAttributedString initWithString:attributes:] + 27
5 internal.so 0x0000000115fa369e console_printStyledText + 3198
6 LuaSkin 0x000000010de00107 luaD_precall + 5239
7 LuaSkin 0x000000010dde63fd luaV_execute + 142605
8 LuaSkin 0x000000010de06f19 resume + 1065
9 LuaSkin 0x000000010dcd91ac luai_objcttry + 428
10 LuaSkin 0x000000010ddf8ed3 luaD_rawrunprotected + 1155
11 LuaSkin 0x000000010de056ce lua_resume + 4334
12 LuaSkin 0x000000010dd7231a auxresume + 186
13 LuaSkin 0x000000010dd71ce9 luaB_coresume + 121
14 LuaSkin 0x000000010de00107 luaD_precall + 5239
15 LuaSkin 0x000000010dde6a57 luaV_execute + 144231
16 LuaSkin 0x000000010de0415c luaD_call + 236
17 LuaSkin 0x000000010de04532 luaD_callnoyield + 194
18 LuaSkin 0x000000010dd9d86a f_call + 410
19 LuaSkin 0x000000010dcd91ac luai_objcttry + 428
20 LuaSkin 0x000000010ddf8ed3 luaD_rawrunprotected + 1155
21 LuaSkin 0x000000010de0a031 luaD_pcall + 1089
22 LuaSkin 0x000000010dd9c0e8 lua_pcallk + 4872
23 LuaSkin 0x000000010ddf5ffd luaB_xpcall + 189
24 LuaSkin 0x000000010de00107 luaD_precall + 5239
25 LuaSkin 0x000000010dde63fd luaV_execute + 142605
26 LuaSkin 0x000000010de0415c luaD_call + 236
27 LuaSkin 0x000000010de04532 luaD_callnoyield + 194
28 LuaSkin 0x000000010dd9d86a f_call + 410
29 LuaSkin 0x000000010dcd91ac luai_objcttry + 428
30 LuaSkin 0x000000010ddf8ed3 luaD_rawrunprotected + 1155
31 LuaSkin 0x000000010de0a031 luaD_pcall + 1089
32 LuaSkin 0x000000010dd9c0e8 lua_pcallk + 4872
33 LuaSkin 0x000000010dca4de5 -[LuaSkin protectedCallAndTraceback:nresults:] + 709
34 Hammerspoon 0x000000010d63d21b MJLuaRunString + 1147
35 Hammerspoon 0x000000010d631889 -[MJConsoleWindowController run:] + 345
36 Hammerspoon 0x000000010d631d26 -[MJConsoleWindowController tryMessage:] + 1014
37 AppKit 0x00007fff494e8644 -[NSApplication(NSResponder) sendAction:to:from:] + 312
38 AppKit 0x00007fff49552992 -[NSControl sendAction:to:] + 86
39 AppKit 0x00007fff494e468f -[NSTextField textDidEndEditing:] + 924
40 Hammerspoon 0x000000010d636288 -[HSGrowingTextField textDidEndEditing:] + 584
41 CoreFoundation 0x00007fff4bc64346 __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 12
42 CoreFoundation 0x00007fff4bc642c0 ___CFXRegistrationPost_block_invoke + 63
43 CoreFoundation 0x00007fff4bc6422a _CFXRegistrationPost + 404
44 CoreFoundation 0x00007fff4bc6c6d8 ___CFXNotificationPost_block_invoke + 87
45 CoreFoundation 0x00007fff4bbd5064 -[_CFXNotificationRegistrar find:object:observer:enumerator:] + 1642
46 CoreFoundation 0x00007fff4bbd4417 _CFXNotificationPost + 732
47 Foundation 0x00007fff4de5ba7b -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
48 AppKit 0x00007fff495f4efb -[NSTextView(NSPrivate) _giveUpFirstResponder:] + 415
49 AppKit 0x00007fff49521ca6 -[NSTextView doCommandBySelector:] + 194
50 AppKit 0x00007fff49521bba -[NSTextInputContext(NSInputContext_WithCompletion) doCommandBySelector:completionHandler:] + 228
51 AppKit 0x00007fff49516e1a -[NSKeyBindingManager(NSKeyBindingManager_MultiClients) interpretEventAsCommand:forClient:] + 2972
52 AppKit 0x00007fff49be2fdf __84-[NSTextInputContext _handleEvent:options:allowingSyntheticEvent:completionHandler:]_block_invoke_5 + 341
53 AppKit 0x00007fff49be2e75 __84-[NSTextInputContext _handleEvent:options:allowingSyntheticEvent:completionHandler:]_block_invoke_3.784 + 74
54 AppKit 0x00007fff4951df23 -[NSTextInputContext tryHandleEvent_HasMarkedText_withDispatchCondition:dispatchWork:continuation:] + 87
55 AppKit 0x00007fff49be2dfb __84-[NSTextInputContext _handleEvent:options:allowingSyntheticEvent:completionHandler:]_block_invoke.781 + 237
56 HIToolbox 0x00007fff4aec7efb __TSMProcessRawKeyEventWithOptionsAndCompletionHandler_block_invoke_5 + 70
57 HIToolbox 0x00007fff4aec6db9 ___ZL23DispatchEventToHandlersP14EventTargetRecP14OpaqueEventRefP14HandlerCallRec_block_invoke + 110
58 AppKit 0x00007fff49bdd70e __55-[NSTextInputContext handleTSMEvent:completionHandler:]_block_invoke.265 + 575
59 AppKit 0x00007fff49518512 __55-[NSTextInputContext handleTSMEvent:completionHandler:]_block_invoke_2 + 74
60 AppKit 0x00007fff4951849a -[NSTextInputContext tryHandleTSMEvent_HasMarkedText_withDispatchCondition:dispatchWork:continuation:] + 87
61 AppKit 0x00007fff49517c92 -[NSTextInputContext handleTSMEvent:completionHandler:] + 1749
62 AppKit 0x00007fff49517545 _NSTSMEventHandler + 306
63 HIToolbox 0x00007fff4ae5d22e _ZL23DispatchEventToHandlersP14EventTargetRecP14OpaqueEventRefP14HandlerCallRec + 1422
64 HIToolbox 0x00007fff4ae5c5df _ZL30SendEventToEventTargetInternalP14OpaqueEventRefP20OpaqueEventTargetRefP14HandlerCallRec + 371
65 HIToolbox 0x00007fff4ae5c465 SendEventToEventTargetWithOptions + 45
66 HIToolbox 0x00007fff4aec3f8f SendTSMEvent_WithCompletionHandler + 383
67 HIToolbox 0x00007fff4aec43fa __SendUnicodeTextAEToUnicodeDoc_WithCompletionHandler_block_invoke + 387
68 HIToolbox 0x00007fff4aec4268 __SendFilterTextEvent_WithCompletionHandler_block_invoke + 221
69 HIToolbox 0x00007fff4aec3fde SendTSMEvent_WithCompletionHandler + 462
70 HIToolbox 0x00007fff4aec3de3 SendFilterTextEvent_WithCompletionHandler + 225
71 HIToolbox 0x00007fff4aec3aa4 SendUnicodeTextAEToUnicodeDoc_WithCompletionHandler + 280
72 HIToolbox 0x00007fff4aec384e __utDeliverTSMEvent_WithCompletionHandler_block_invoke_2 + 283
73 HIToolbox 0x00007fff4aec36ad __utDeliverTSMEvent_WithCompletionHandler_block_invoke + 355
74 HIToolbox 0x00007fff4aec34cb TSMKeyEvent_WithCompletionHandler + 598
75 HIToolbox 0x00007fff4aec325a __TSMProcessRawKeyEventWithOptionsAndCompletionHandler_block_invoke_4 + 250
76 HIToolbox 0x00007fff4aec3089 __TSMProcessRawKeyEventWithOptionsAndCompletionHandler_block_invoke_3 + 257
77 HIToolbox 0x00007fff4aec2dce __TSMProcessRawKeyEventWithOptionsAndCompletionHandler_block_invoke_2 + 282
78 HIToolbox 0x00007fff4aec2b32 __TSMProcessRawKeyEventWithOptionsAndCompletionHandler_block_invoke + 274
79 HIToolbox 0x00007fff4aec2127 TSMProcessRawKeyEventWithOptionsAndCompletionHandler + 3398
80 AppKit 0x00007fff49be2cd9 __84-[NSTextInputContext _handleEvent:options:allowingSyntheticEvent:completionHandler:]_block_invoke_3.779 + 110
81 AppKit 0x00007fff49be22d6 __204-[NSTextInputContext tryTSMProcessRawKeyEvent_orSubstitution:dispatchCondition:setupForDispatch:furtherCondition:doubleSpaceSubstitutionCondition:doubleSpaceSubstitutionWork:dispatchTSMWork:continuation:]_block_invoke.734 + 115
82 AppKit 0x00007fff49be21c7 -[NSTextInputContext tryTSMProcessRawKeyEvent_orSubstitution:dispatchCondition:setupForDispatch:furtherCondition:doubleSpaceSubstitutionCondition:doubleSpaceSubstitutionWork:dispatchTSMWork:continuation:] + 245
83 AppKit 0x00007fff49be2892 -[NSTextInputContext _handleEvent:options:allowingSyntheticEvent:completionHandler:] + 1286
84 AppKit 0x00007fff49be2086 -[NSTextInputContext _handleEvent:allowingSyntheticEvent:] + 107
85 AppKit 0x00007fff49516004 -[NSView interpretKeyEvents:] + 209
86 AppKit 0x00007fff49515e2d -[NSTextView keyDown:] + 726
87 AppKit 0x00007fff49363367 -[NSWindow(NSEventRouting) _reallySendEvent:isDelayedEvent:] + 6840
88 AppKit 0x00007fff49361667 -[NSWindow(NSEventRouting) sendEvent:] + 478
89 AppKit 0x00007fff49201889 -[NSApplication(NSEvent) sendEvent:] + 2953
90 AppKit 0x00007fff491ef5c0 -[NSApplication run] + 755
91 AppKit 0x00007fff491deac8 NSApplicationMain + 777
92 Hammerspoon 0x000000010d62b3d2 main + 34
93 libdyld.dylib 0x00007fff77b883d5 start + 1
)
I suspected this might happen if the coroutine actually invoked anything that used LuaSkin, but had forgotten that we no longer use lua_pcall anywhere by itself – it’s always invoked indirectly through LuaSkin’s protectedCallAndTraceback:nresults:.
As I’m coming to understand it:
-
Reason: creating a coroutine invokes
lua_newthread, which creates a newlua_Statevariable; when the coroutine is running (i.e.resumeis invoked), it’s the new state’s stack that has things pushed/pulled from it, not the one that LuaSkin was initialized with… -
Fix: Nontrivial. Not only would we need to change every
[LuaSkin shared]line in our C-functions to something like[LuaSkin sharedWithState:L], we’d need to implementlua_lockandlua_unlockso that callbacks could still use[LuaSkin shared](and thus use the initial state created when first initialized/reloaded) to protect the shared global space between the differinglua_State’s.
Which is half-way to making the whole thing support multi-threading (at the Application level, not the psuedo co-routine lua level) anyways (see point 3 at https://stackoverflow.com/a/54046050).
So while the fix might be a "good idea"™, implementing it will be a giant PITA.
Unless I’m missing something obvious (somebody smarter please tell me that I am! Please, please, please!)
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 1
- Comments: 22 (18 by maintainers)
Admittedly this is a sort of band-aid over a framework that should probably (eventually) be refactored now that we’ve realized that one of the core assumptions that went into making it was mistaken, but I think it should work, once we’ve addressed all
dispatch_*blocks that assume the persistence ofskinand don’t bother setting it again…I’ll keep you guys posted and let you know when a fork is ready for testing.
@asmagill I think I’m mostly concerned about all the state in C that will now have multiple Lua environments mutating it.
I agree that the only way to be sure is to try, so maybe if we have a branch with the modifications to LuaSkin and then we need to hammer the HS API from the coroutines and the main thread and see what happens.
If the limitation is only that you cannot call Hammerspoon API functions from coroutines, you may be able to “fix” the problem from the Lua side by implementing asymmetric coroutines on top of symmetric coroutines on top of asymmetric coroutines, placing a trampoline inside of your new fake
coroutine.resume, and wrapping Hammerspoon API functions to make sure they are called from the main lua_thread even when the user tries to use them from inside a coroutine.A sketch of what this would look like, on the library side, which I’ve called co_wrap.lua over here:
On the user side:
This prints
If you remove lines 8 and 9, then you can get output that doesn’t change based on the addresses of coroutines, and you can try running it with and without co_wrap. Over here it produces identical output with and without co_wrap on lua 5.3.5. This isn’t a guarantee that I haven’t written any bugs, of course.
Edit: While this may let users run code that uses coroutines, it won’t be as fast as you might like. If you’d like to go fast, I think the right play is to fix the problem for real and to use some version of luajit.
Ok, no, that’s stupid… we’d just be checking the saved state, which we already know is the main “thread”… let me think some more…
Callbacks should always run on the main lua_State (the lua docs call it a thread, which I’ve sometimes called a “lua thread” to be pedantic, but it’s really primarily a separate isolated lua stack) --the initial lua_State created when the lua engine was initialized – because they are a new code-block triggered by the external event, not the resuming of something a coder suspended.
That’s why I recommend creating the new method
sharedWithState:and applying it only to functions and methods we add, but not within the callback/delegate/event code that handles externally triggered application events. If we go with the first option I outline above, the internal changes to LuaSkin are manageable and there is still only one instance required.As to why its taken so long to find out… well, most of our users aren’t programmers per se, and those that are didn’t start with Lua… coroutines are a fundamental part of the language, and the primary answer to the fact that it’s by default single threaded. I’ve meant to learn about them for a long time, but since (1) potentially long running lua, no matter whether its on 1 “lua thread” or 1000 causes Hammerspoon to block, but with #2304 that might no longer be an issue, and (2) I personally can code relatively quickly in Objective-C and can use a background thread it if I want.
If a true lua developer wanted to do something, they’d expect coroutines to exist and wouldn’t bother without them.
As I have come to understand the problem, the fix outlined as the first option above won’t be difficult, just tedious.
I’m opposed to burying our head in the sand by removing that the library – we won’t be using Lua, then, but a subset of it. Subsets are fine in memory constrained environments, not on a modern desktop.
As a first step, I think adding the following after the NSThread check in the
sharedWithDelegate:method of LuaSkin will at least stop the application crashes, though 90% of our module functions/methods still won’t be usable within a coroutine:I’ll test this in a few hours to be certain.
Hello,
Coroutines are a fundamental control flow construct that ships with the language. Removing them is comparable to removing the
functionkeyword. I only recently learned that Hammerspoon exists, and I am willing to try to fix the mess that currently prevents it from working with coroutines and with moonjit.Ok, after some more pondering…
lua_lockandlua_unlockactually shouldn’t be necessary, if we stay on a single application thread…We’ll still need to make
LuaSkinmultilua_State*compatible for coroutine support (which is also a necessary step for supporting multiple application threads), but since only a single “lua thread” will ever be active at a given instant in a singe application threaded model,lua_lock/unlockdon’t help us.@cmsj, some ideas I’d like your comments on about how to make LuaSkin support different
lua_Statevalues (these both assume addingsharedWithState:(lua_State *L)and changing functions in our modules – but not in delegates or other callbacks – to use it instead of thesharedcontructor):add (readonly?) property to store initial
lua_Stateinto during creation; whensharedcalled, setself._Lto this value; whensharedWithState:Lcalled, setself._Lto theLpassed in. Pros: simple, Cons: may have to callsharedWithState:Lmultiple times within a function if it’s possible the current value ofskin._Lmight have changed – currently (I think) this only affects the proposedhs_yield, but maybe also others, if we ever writeCfunctions which utilize/participate in coroutines; won’t be sufficient if we ever add true multi application thread support.create (and store to avoid re-creating?) new
LuaSkininstance for each newlua_Stateseen. Pros: muti application thread safe (if/when we decide to go there), as long as no singlelua_Stateis allowed to run on more than one thread at a time; Cons: more changes toLuaSkinbecause registered type helpers, etc. have to be moved to class properties; how do we detect whenlua_threadis garbage collected (so we can remove its LuaSkin instance, if we saved it)? How do we handle a reload if multiple instances still exist (if we can attach a __gc to a lua_thread type, this might be a non-issue, but I don’t think we can)Other possibilities I’m missing?
Re multiple application threads – If we do end up adding
hs.yield, and maybe even consider attaching it to a debug hook so it runs after everynlua calls, the extra effort to support multiple application threads becomes less attractive to me. IMHO the times when yielding isn’t sufficient might be better served by moving something into Objective-C itself which is a lot faster and most things become easy (well, easier) to multithread and trigger a callback upon completion.Just my (current) 2 cents… ask me again in a month and I’ll probably think something totally different 😜
Obviously few/none of our users currently utilize coroutines, or we’d have heard something by now, but it is a feature lua promotes, so its only a matter of time…
Possible short term band-aids that I can think of:
remove coroutine library from our implementation of lua (either by modifying lua source, or adding
coroutine = niltohs._coresetup. Cowardly cop-out, and removes the possibility of the one case that does work – using them within a single file/function/block with pure lua.partially implement above fix – modify functions to use
[LuaSkin sharedWithState:L]and throw a lua error ifLdoesn’t ==skin.Lthat says something likeLuaSkin incompatible with coroutines. Pro: should prevent crashes; Con: inconsistent module support within coroutines – older modules (pre LuaSkin) may partially work, if they don’t use LuaSkin to check arguments or manipulate data; newer or updated stuff won’t work at all, since we’ve pretty much standardized on some common patterns that use LuaSkin a lot.continue to hope no one notices. Cons: obvious.
In any case, if we do end up adding #2304,
hs_yieldcan’t be invoked within a coroutine because we haven’t implementedlua_lockandlua_unlock(yet); but I think I can easily make this detect such a situation by checkinglua_isyieldable()so this issue isn’t a show-stopper (though it does remove the need for one of the tests I was considering)More thoughts if I think of them…
Ok, not quite “don’t work at all”…
if the code which creates and uses the coroutine is in one
block, and doesn’t use anything which involvesLuaSkin(i.e. it’s pure lua, not even a single function we’ve added that usescheckArgs:), then it will work:This works (when pasted into the console as one “line” of input):
This crashes (when pasted into the console as one “line” of input) – yeah, it’s a stupid example, but
hs.canCheckForUpdateswas the first thing I found that only uses LuaSkin for a single[skin checkArgs:LS_TBREAK];, about the simplest usage I could think of):edit: made it more clear that blocks should be pasted in as one entry if testing in the console