views:

166

answers:

2

I'm working on a free, simple, hopefully multi-platform hotkey launcher tool, written in Ruby.

Right now I'm struggling with some threading issues of the Windows implementation. The rough proof of concept code shown below already works reliably in my day-to-day use:

HotkeyProcessor#process_keypress (footnote 1) gets called by the C code when a key is pressed and subsequently handles the key event.

Problem

However, when process_keypress starts a thread that does some time consuming work (footnote 2), a subtle flaw shows up: The program spends most of its time in the C extension, waiting for get_message (footnote 3) to return - which by design never happens. Ruby threads aren't run in the meanwhile. Thread execution only happens while Ruby-level code is in control, thus only for the few miliseconds after every key event when #process_keypress is active.

Is continually concurrent execution possible in this context? Thanks for any suggestions!

require 'hotkey_processor'

def slowly_do_stuff
  5.times { |i| sleep(0.1); print "#{i} " }
end

module HotkeyProcessor
  def self.process_keypress(code, down) # footnote 1
    print '. '
    slowly_do_stuff              if code == 65 and down # A pressed
    Thread.new {slowly_do_stuff} if code == 66 and down # B pressed # footnote 2
    true
  end
end
puts 'Press A, B and other keys.'
HotkeyProcessor.start

HotkeyProcessor:

#include "ruby.h"
#include "windows.h"
#include <stdio.h>

int process_keypress_function;
VALUE HotkeyProcessor;
HHOOK keyboard_hook;

LRESULT CALLBACK callback_function(int Code, WPARAM wParam, LPARAM lParam)
{
  PKBDLLHOOKSTRUCT kbd = (PKBDLLHOOKSTRUCT)lParam;

  if (Code < 0 || Qtrue == rb_funcall(HotkeyProcessor, 
                                      process_keypress_function,
                                      2,
                                      INT2NUM(kbd->vkCode), /* code */
                                      (wParam == WM_KEYDOWN || 
                                       wParam == WM_SYSKEYDOWN) ? 
                                         Qtrue : Qfalse)) /* down */
  {
    return CallNextHookEx(keyboard_hook, Code, wParam, lParam);
  }
  else
    return 1;
}

static BOOL get_message(MSG *msg)
{
  return GetMessage(msg, 0, 0, 0);
}

static VALUE start(VALUE self)
{
    HMODULE Module = GetModuleHandle(NULL);
    MSG msg;

    keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, 
                                     (HOOKPROC) callback_function, 
                                     Module, 
                                     0);

    get_message(&msg); /* footnote 3 */

    /* Never get here. Wait forever for get_message() to return
       to keep the program alive and responding to callbacks. */
    return self;
}

static VALUE process_keypress(VALUE self, VALUE code)
{
  return Qnil;
}

void Init_hotkey_processor() {
  HotkeyProcessor = rb_define_module("HotkeyProcessor");
  rb_define_module_function(HotkeyProcessor, 
                            "process_keypress", 
                            process_keypress, 
                            1);
  rb_define_module_function(HotkeyProcessor, 
                            "start", 
                            start, 
                            0);
  process_keypress_function = rb_intern("process_keypress");
}
+1  A: 

I don't fully understand what you are doing, but how about using PeekMessage which does return?

Stephen Nutt
+1  A: 

Start the Windows message loop via 'rb_thread_blocking_region' and wrap 'rb_thread_call_with_gvl' around the Ruby API calls that take place in the callback function.

See here for a working demo: git clone git://github.com/kaysarraute/ruby-win32-callback-threading.git


/* Release the Giant VM Lock (GVL) while calling pump_messages */
rb_thread_blocking_region(pump_messages, 0, 0, 0);


static BOOL pass_key_event_to_ruby(struct KEYEVENT *key_event) {
  /* Ruby API calls go here. */
}

/* In the callback: Reacquire GVL for calls to the Ruby API. */
rb_thread_call_with_gvl(pass_key_event_to_ruby, &key_event);
Kay Sarraute