@@ -5,6 +5,7 @@ package windows
55import (
66 "errors"
77 "sync"
8+ "sync/atomic"
89 "syscall"
910 "time"
1011 "unsafe"
7273 procPostThreadMessageW = user32 .NewProc ("PostThreadMessageW" )
7374 procGetCurrentThreadID = kernel32 .NewProc ("GetCurrentThreadId" )
7475
75- activeKeyboardHook * KeyboardHook
76+ // activeKeyboardHook is the hook keyboardHookProc dispatches to. It is
77+ // atomic because the two sides never share a lock: run stores it under h.mu
78+ // on the hook goroutine, and the hook procedure loads it on whichever
79+ // thread Windows delivers the key event on.
80+ activeKeyboardHook atomic.Pointer [KeyboardHook ]
81+
82+ // keyboardHookProcPtr is the WH_KEYBOARD_LL procedure pointer
83+ // SetWindowsHookExW is handed, allocated on first use and never again —
84+ // for the reason monitorEnumProcPtr in win32.go carries in full: a callback
85+ // slot is never freed and the process gets a fixed 2000 of them.
86+ //
87+ // StartKeyboardHook runs on every EventTap.Enable cycle, so a callback
88+ // allocated there was spent on every mode activation. One procedure serves
89+ // every hook because it carries no per-hook state: it reads
90+ // activeKeyboardHook, which is whichever hook is currently installed.
91+ keyboardHookProcPtr = sync .OnceValue (func () uintptr {
92+ return syscall .NewCallback (keyboardHookProc )
93+ })
7694)
7795
7896var (
@@ -169,55 +187,58 @@ func (h *KeyboardHook) Stop() {
169187 }
170188}
171189
172- func (h * KeyboardHook ) run () {
173- defer close (h .doneCh )
190+ // keyboardHookProc is the WH_KEYBOARD_LL procedure Windows calls for every key
191+ // event, for whichever hook is currently installed.
192+ //
193+ // lParam is typed unsafe.Pointer (not uintptr) so the KBDLLHOOKSTRUCT
194+ // dereference is a Pointer->*T conversion, which keeps go vet's unsafeptr
195+ // check happy. syscall.NewCallback accepts pointer-kind parameters.
196+ func keyboardHookProc (code int , wParam uintptr , lParam unsafe.Pointer ) uintptr {
197+ if code < 0 {
198+ ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
174199
175- // lParam is typed unsafe.Pointer (not uintptr) so the KBDLLHOOKSTRUCT
176- // dereference is a Pointer->*T conversion, which keeps go vet's unsafeptr
177- // check happy. syscall.NewCallback accepts pointer-kind parameters.
178- hookProc := syscall .NewCallback (func (code int , wParam uintptr , lParam unsafe.Pointer ) uintptr {
179- if code < 0 {
180- ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
200+ return ret
201+ }
181202
182- return ret
183- }
203+ current := activeKeyboardHook .Load ()
204+ if current == nil || current .callback == nil {
205+ ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
184206
185- current := activeKeyboardHook
186- if current == nil || current .callback == nil {
187- ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
207+ return ret
208+ }
188209
189- return ret
190- }
210+ kbd := (* kbdLLHookStruct )(lParam )
191211
192- kbd := (* kbdLLHookStruct )(lParam )
212+ // Keys this process injected come back through this hook. Handing one
213+ // to the callback would re-enter the mode handler from the hook
214+ // thread, and the down/up pair a modified scroll holds reads as the
215+ // user tapping that modifier — latching a sticky modifier nobody
216+ // pressed. Only Neru's own injection is skipped (neruInjectedTag);
217+ // another tool's synthetic input is still seen.
218+ if kbd .dwExtraInfo == neruInjectedTag {
219+ ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
193220
194- // Keys this process injected come back through this hook. Handing one
195- // to the callback would re-enter the mode handler from the hook
196- // thread, and the down/up pair a modified scroll holds reads as the
197- // user tapping that modifier — latching a sticky modifier nobody
198- // pressed. Only Neru's own injection is skipped (neruInjectedTag);
199- // another tool's synthetic input is still seen.
200- if kbd .dwExtraInfo == neruInjectedTag {
201- ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
221+ return ret
222+ }
202223
203- return ret
204- }
224+ isUp := wParam == wmKeyUp || wParam == wmSysKeyUp || kbd .flags & llkhfUp != 0
205225
206- isUp := wParam == wmKeyUp || wParam == wmSysKeyUp || kbd .flags & llkhfUp != 0
226+ key := hookKeyName (kbd .vkCode , isUp )
227+ if key != "" && current .callback (key , isUp ) {
228+ return 1
229+ }
207230
208- key := hookKeyName (kbd .vkCode , isUp )
209- if key != "" && current .callback (key , isUp ) {
210- return 1
211- }
231+ ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
212232
213- ret , _ , _ := procCallNextHookEx .Call (0 , uintptr (code ), wParam , uintptr (lParam ))
233+ return ret
234+ }
214235
215- return ret
216- } )
236+ func ( h * KeyboardHook ) run () {
237+ defer close ( h . doneCh )
217238
218239 handle , _ , _ := procSetWindowsHookExW .Call (
219240 whKeyboardLL ,
220- hookProc ,
241+ keyboardHookProcPtr () ,
221242 moduleHandle (),
222243 0 ,
223244 )
@@ -233,7 +254,7 @@ func (h *KeyboardHook) run() {
233254 h .hook = handle
234255 threadID , _ , _ := procGetCurrentThreadID .Call ()
235256 h .threadID = uint32 (threadID )
236- activeKeyboardHook = h
257+ activeKeyboardHook . Store ( h )
237258 h .mu .Unlock ()
238259
239260 // Signal successful install so StartKeyboardHook can return the hook.
@@ -246,9 +267,10 @@ func (h *KeyboardHook) run() {
246267 h .hook = 0
247268 }
248269
249- if activeKeyboardHook == h {
250- activeKeyboardHook = nil
251- }
270+ // Only this hook's own registration is cleared: a later hook may
271+ // already have installed itself, and clearing unconditionally would
272+ // leave that one receiving key events with nowhere to deliver them.
273+ activeKeyboardHook .CompareAndSwap (h , nil )
252274 h .mu .Unlock ()
253275 }()
254276
0 commit comments