DLとWin32APIとFiddle

Rubyには外部のライブラリの呼び出しインターフェイスとしてDLというライブラリが添付されているが、Ruby2.0になってそれは廃止予定になってFiddleが追加された。他にWindows用にはWin32APIもある。このへんをまとめる。

■Ruby1.8時代
Win32APIが独自実装で存在していた。ダイナミックリンクライブラリのインターフェイスとしてDLがあった。標準で添付されていないがDL2があった。

Ruby1.9時代
DLが廃止され、DL2がDLという名前で標準添付になる。互換性は無い。Win32APIは新しいDLを使うラッパライブラリとして書き直されたが、廃止予定になり代わりにDLを直接使えという警告が出る。DLの一部機能を実現するために内部的にFiddleが組み込まれる。

■Ruby2.0時代
DLの機能はFiddleにマージされ、DLが廃止予定となる。DLをrequireするとFiddleを使えという警告が出る。使い方は似ているが互換性は無い。Win32APIは相変わらず残ってはいるものの、DLを使っているので使うと警告が出る。

■総括
バージョンが進むたびに互換性が無くなるという悪夢のような状態である。まあ、嫌がらせでやっているわけでもないし、そうなるには理由があるのだろう。でもバージョンアップに追随できずに消えていったコードがたくさんあるんだろうと思う。DXRubyWikiにも1個あるし。
そういう中でWin32APIは互換性という面で優秀だ。1.8用のコードが1.9でも動く。現状警告は出るが2.0でも動く。DLが無くなったときに一緒に消えるのではなく、Fiddle用に書き直して添付しておいてくれたらちょっとしたAPI呼び出しに使うには便利である。

というわけでちょっと試してみた。なんとなく動く。少なくともWikiにおいてあるbass.rbで再生することはできた。全機能テストしたわけでもないのでバグってることもありえる。

require 'fiddle'

class Win32API
  DLL = {}
  TYPEMAP = {"0" => Fiddle::TYPE_VOID, "S" => Fiddle::TYPE_VOIDP, "I" => Fiddle::TYPE_LONG}
  POINTER_TYPE = Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG_LONG ? 'q*' : 'l!*'

  def initialize(dllname, func, import, export = "0", calltype = :stdcall)
    @proto = [import].join.tr("VPpNnLlIi", "0SSI").sub(/^(.)0*$/, '\1')
    handle = DLL[dllname] ||= Fiddle.dlopen(dllname)
    temp = import.each_char.map{|s|s.tr("VPpNnLlIi", "0SSI")}.map{|v|TYPEMAP[v]}
    @func = Fiddle::Function.new(handle[func], temp, TYPEMAP[export.tr("VPpNnLlIi", "0SSI")], calltype == :stdcall ? Fiddle::Function::STDCALL : Fiddle::Function::DEFAULT)
  rescue Fiddle::DLError => e
    raise LoadError, e.message, e.backtrace
  end

  def call(*args)
    import = @proto.split("")
    args.each_with_index do |x, i|
      args[i], = [x == 0 ? nil : x].pack("p").unpack(POINTER_TYPE) if import[i] == "S"
      args[i], = [x].pack("I").unpack("i") if import[i] == "I"
    end
    ret, = @func.call(*args)
    return ret || 0
  end

  alias Call call
end