tags:

views:

1223

answers:

3

I need to access a few functions of the win32 library in ruby. I have found extremely sparse information on the Win32API class online, so I'm asking here.

I know that you can do something like this:

function = Win32API.new('user32','MessageBox',['L', 'P', 'P', 'L'],'I')

But I can't seem to be able to call this function with the current win32 bindings:

http://msdn.microsoft.com/en-us/library/bb762108%28VS.85%29.aspx

The problem is in its prototype:

UINT_PTR SHAppBarMessage(      
    DWORD dwMessage,
    PAPPBARDATA pData
);

I will be able to use the win32 ruby bindings to grab the return type and the first parameter, however, the second one expects a structure. The definition of the structure is as follows:

typedef struct _AppBarData {
    DWORD cbSize;
    HWND hWnd;
    UINT uCallbackMessage;
    UINT uEdge;
    RECT rc;
    LPARAM lParam;
} APPBARDATA, *PAPPBARDATA;

I tried to define this api method using both:

api = Win32API.new('shell32','SHAppBarMessage',['L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L'],'I')

and

api = Win32API.new('shell32','SHAppBarMessage',['L', 'LLLLLLLL'],'I')

but the first one segfaults during the "call" method and the second fails to run due to the wrong amount of arguments specified in the "call" method invocation. Is there any way to expose this api function without resorting to creating an external module in C++?

Thanks.

A: 

SHAppBarMessage takes two parameters: a DWORD and a pointer to APPBARDATA,
so it shuold be declared thus:

app_bar_msg = Win32API.new('shell32', 'SHAppBarMessage', ['L', 'P'], 'L')

then called:

msg_id = 1
app_bar_data = "properly initalized binary string" #should have sizeof(APPBARDATA) bytes
app_bar_msg.call(msg_id, app_bar_data)

But I don't know Ruby, so maybe I am mistaken...

robson3.14
A: 

I think you'll have to investigate the String#pack method to get your APPBARDATA struct filled correctly.

See the "Pickaxe" book section on Win32 and Ruby (scroll down to the Win32API class definition).

So as already spotted, you'll use a 'P' argument and you'll pass a properly-packed String (or Strings) into the function.

Alternatively, if you have a little time to investigate, you may want to look at the FFI library, which seems to do everything in a rather more friendly way. I have no direct experience, but try looking at

Mike Woodhouse
+2  A: 

The trick is to use 'P' as a format specifier for all pointer arguments. You'll have to provide a string as the pointed-to area.

Of course you'll have to make sure these strings have the correct expected size otherwise bad things will happen.

You can directly create these strings

# Mostly useful when the area will be totally overwritten
pointed_to_area = "\0" * n

or use the more civilized Array#pack

# Allows you to control how ruby values get encoded in the buffer
pointed_to_area = [1, 2, 3, 4].pack('SsLI')

Hope this helps.


The following works on my XP box with an old ruby 1.8.2:

require 'Win32API'


module Win32
  # This method is only here for test purposes
  # Be careful to use the ascii version
  FindWindow = Win32API.new('user32', 'FindWindowA', ['P', 'P'], 'L')
  def self.findWindow(lpClassName, lpWindowName)
    h = FindWindow.call(lpClassName, lpWindowName)
    raise "FindWindow failed" if h == 0
    h
  end

  # From winddef.h
  RECT = Struct.new(:left, :top, :right, :bottom)
  RECT.class_eval do
    def pack
      [left, top, right, bottom].pack('l4')
    end
    def self.unpack(s)
      new(*s.unpack('l4'))
    end
  end

  # From shellapi.h
  APPBARDATA = Struct.new(:cbSize, :hWnd, :uCallbackMessage, :uEdge, :rc, :lParam)
  APPBARDATA.class_eval do
    def pack
      unless rc.is_a? RECT
        raise ArgumentError, ":rc must be an instance of Win32::RECT, got #{rc.inspect}"
      end
      # DWORD + HWND + UINT + UINT + RECT + LPARAM
      cbSize = 4 + 4 + 4 + 4 + 16 + 4
      [cbSize, hWnd, uCallbackMessage, uEdge, rc.pack, lParam].pack('L2I2a16L')
    end
    def self.unpack(s)
      tmp = self.new(*s.unpack('L2I2a16L'))
      tmp.rc = RECT.unpack(tmp.rc)
      tmp
    end
  end
  SHAppBarMessage = Win32API.new('shell32', 'SHAppBarMessage', ['L', 'P'], 'L')

  # Calls SHAppBarMessage and returns the altered APPBARDATA
  def self.shAppBarMessage(dwMessage, appBarData)
    s = appBarData.pack
    ok = (SHAppBarMessage.call(dwMessage, s) != 0)
    raise "SHAppBarMessage failed" unless ok
    APPBARDATA.unpack(s)
  end

  ABM_NEW              = 0x00000000
  ABM_REMOVE           = 0x00000001
  ABM_QUERYPOS         = 0x00000002
  ABM_SETPOS           = 0x00000003
  ABM_GETSTATE         = 0x00000004
  ABM_GETTASKBARPOS    = 0x00000005
  ABM_ACTIVATE         = 0x00000006
  ABM_GETAUTOHIDEBAR   = 0x00000007
  ABM_SETAUTOHIDEBAR   = 0x00000008
  ABM_WINDOWPOSCHANGED = 0x00000009
  ABM_SETSTATE         = 0x0000000a

  ABE_LEFT   = 0
  ABE_TOP    = 1
  ABE_RIGHT  = 2
  ABE_BOTTOM = 3
end




if __FILE__ == $0
  require 'test/unit'
  class SHAppBarMessageTest < Test::Unit::TestCase
    include Win32

    def test_pack_unpack
      a = APPBARDATA.new(-1, 0, 0, ABE_TOP, RECT.new(1, 2, 3, 4), 0)
      b = APPBARDATA.unpack(a.pack)
      a.cbSize = b.cbSize
      assert_equal(a.values, b.values)
    end
    def test_simple_pos_query
      h = Win32.findWindow("Shell_TrayWnd", nil)
      a = APPBARDATA.new(-1, 0, 0, ABE_TOP, RECT.new(0, 0, 0, 0), 0)
      result = Win32.shAppBarMessage(ABM_GETTASKBARPOS, a)
      assert(result.rc.left < result.rc.right)
      assert(result.rc.top < result.rc.bottom)
      puts result.rc.inspect
    end
  end
end
bltxd