win

by: Arvicco url: github.com/arvicco/win

SUMMARY

A collection of Windows API functions predefined for you using FFI. In addition to straightforward (CamelCase) API wrappers, it also strives to provide more Ruby-like snake_case methods that take a minimum of arguments with sensible defaults and have sensible return values (false/true instead of 0/nonzero for test functions, etc).

This is still work in progress, only a small portion of Windows API wrapped so far…

DESCRIPTION

So you want to write a simple program that makes some Windows API function calls. You searched MSDN high and low and you know exactly what functions you need. You just want to put these function calls into your Ruby code without too much pain. You’d love this to be more or less natural extension of your Ruby code, preferably not turning your code base into an ugly spaghetty of CamelCase calls, String/Array pack/unpack gymnastics, buffer/pointer allocations, extracting return values from <in/out> parameters and checking return codes for 0.

You have several options at this point. You can use ‘win32-api’ or ‘ffi’ libraries to connect your ruby code to Windows API and manually define wrapper methods for needed function calls. This is definitely a valid approach, even if it is a bit low-level one: you’ll have to handle (somewhat) gory details of callback announcements, argument preparation, mimicking pointers with Strings, declaring pointers explicitly with FFI and other stuff (like manually assigning about a gazillion obscure Windows constants). As an example, consider the amount of code needed to complete a task as simple as getting unicode title text for the window that you already have handle for (using win32-api):

api = Win32::API.new( 'GetWindowTextW', ['L', 'P', 'I'], 'L', 'user32' )
buffer = "\x00" * 1024  # I just hope it will be enough...
num_chars = api.call(window_handle, buffer, buffer.size)
title = if num_chars == 0
  nil
else
   buffer.force_encoding('utf-16LE').encode('utf-8').rstrip
end

This is how you achieve the same result with ffi:

extend FFI::Library
ffi_lib 'user32'
ffi_convention :stdcall
attach_function :GetWindowTextW, [ :long, :buffer_out, :int ], :long
buffer = FFI::Buffer.new 1024
buffer.put_string(0, "\x00" * 1024)
num_chars = GetWindowTextW(window_handle, buffer, 1024)
title = if num_chars == 0
  nil
else
   buffer.get_bytes(0, num_chars).force_encoding('utf-16LE').encode('utf-8').rstrip
end

As an alternative, you can use ‘windows-pr’ (pure ruby) library that gives you lots of Windows functions pre-defined and sectioned into modules, declares Windows constants and adds some other niceties. Unfortunately this library works only with MRI (not JRuby or other Ruby implementations), and still lacks Ruby look-and-feel for declared functions. It helps you to cut some of the declaration slack though:

title = if GetWindowTextW(window_handle, buffer ="\x00" * 1024 , buffer.size) == 0
  nil
else
   buffer.force_encoding('utf-16LE').encode('utf-8').rstrip
end

But still, it seems like TOO MUCH code for something that should (ideally) look like this:

title = window_text_w(window_handle)

This is an idea behind this library - make Windows API functions easier to use and feel more natural inside Ruby code. Following the principle of least surprise, we define wrapper methods that:

  • Have meaningful Rubyesque names (iconic? and minimized? instead of IsIconic, etc)

  • Require minimum arguments with sensible defaults

  • Return appropriate values explicitly (several return values if necessary)

  • Have sensible returns (false/true instead of 0/nonzero for test functions, nil if function fails, etc)

  • Accept blocks where callback is needed, provide default callback if no block given

  • Are partitioned into appropriate namespaces, so that you can load only the modules you really need

Well, we even keep a backup solution for those diehard Win32 API longtimers who would rather allocate their buffer strings by hand and mess with obscure return codes. If you use original CamelCase method name instead of Rubyesque snake_case one, it will expect those standard parameters you know and love from MSDN, return your zeroes instead of nils and support no other enhancements.

Related Windows API functions are grouped by topic and defined in separate namespaces (modules), that also contain related constants and convenience methods. For example, win/dde.rb file contains only functions related to DDE protocol such as DdeInitialize() as well as constants such as DMLERR_NO_ERROR, APPCLASS_STANDARD, etc. So if you need only DDE-related functions, there is no need to load all the other modules, clogging your namespaces - just require ‘win/dde’ and be done with it.

And if you do not see your favorite Windows API functions among those already defined, it is quite easy to ‘include Win::Library’ into your module and define new ones with ‘function’ macro - it does a lot of heavy lifting for you and can be customized with options and code blocks to give you reusable API wrapper methods with the exact behavior you need.

REQUIREMENTS:

Only works with Ruby 1.9 compatible implementations since it uses some of the most recent features (block arguments given to block, etc…).

FEATURES/PROBLEMS:

This project is quite new, so it may be not suitable for production-quality systems yet. Contributors always welcome!

INSTALLATION

$ gem install win

SYNOPSIS

Using pre-defined Windows API functions:

require 'win/gui/window'

class MyClass
include Win::Gui::Window

fg_window = foreground_window
puts window_text(fg_window)
show_window(fg_window) unless minimized?(fg_window)
...
end

Defining your own Windows API functions:

require 'win/library'

module YourLibModule
  include Win::Library

  # Customizing method behavior -
  # :fails option forces function to return nil if its result is equal to specified error code (0)
  # :snake_name option defines method with given snake_case name
  function :FindWindow, [:pointer, :pointer], :ulong, fails: 0, snake_name: :my_find

  # Customizing even further: your own method extension in attached block
  function :GetWindowText, [ :ulong, :pointer, :int ], :int do |api, handle|
    buffer = FFI::MemoryPointer.new :char, 512
    buffer.put_string(0, "\x00" * 511)
    num_chars = api.call(handle, buffer, 512)
    num_chars == 0 ? nil : buffer.get_bytes(0, num_chars)
  end

end

include YourLibModule

handle = my_find(nil, 'cmd')          # find any shell window
puts handle, window_text(handle)      # print shell window handle and title

PRIOR ART:

This library started as an extension of ideas and code described in excellent book “Scripted GUI Testing with Ruby” by Ian Dees. ‘win32-api’ and ‘windows-pr’ gems by Daniel J. Berger and Park Heesob provided both inspiration and an excellent source for code borrowing. ‘ffi’ gem serves as a solid basis for this library, allowing to use it for multiple Ruby implementations.

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Copyright © 2010 arvicco. See LICENSE for details.