Create import library from DLL (with dumpbin and Python)

我輩的にデジャブである。名前はまだない。

メモってないみたいだなぁ。

これ、「必要になった」のか「必要になりそうだった」のかビミョーなところだ。「必要になりかけた」ことは、結局ポシャってしまった。そっちの話は…、まぁ気が向いたら書くわ。

さて。

要するに「DLL が公開している関数を使ってプログラムを作りたい」という「開発者としての願い」と、「DLL は製品の裏方であって、オヌシらが使うようなもんでわないのぞ」というミスマッチの話。

Un*x な shared library も Win な DLL も、開発者にとっては常に二つの見方がある。一つには「ライブラリ機能としての公開」、もう一つが「プログラムを横断する共通機能置き場」である。そしてその二つに対する重きの置き方が Un*x と win ではだいぶ違うのはこれはひとえに、Un*x では「開発環境は持ってて当たり前」なのに対し win では「開発環境なんか持ってないのが当たり前」であることに尽きる。つまりどうしても win では「どーせおまいら開発なんかせんやろが」となりがち。

そういうわけで、たとえ出自が Un*x であろうと、win 向けには「DLL だけが提供される」みたいなことが、ひっじょーに多い。「DLL 以外に何が必要なのぞ?」。普通は「インポートライブラリとヘッダファイル」が欲しい。なくても不可能なわけではないが、あれば数億倍程度には僅かに楽だ。

てわけで。

DLL からインポートライブラリを抽出すること自体は簡単である。まぁこれとか探せばいくらでもどこででも情報が見つかろう。ちと少しは鬱陶しいので、この際なのでスクリプトにしてしまった:

mkimplibfromdll.py
 1 import argparse
 2 import re
 3 import subprocess
 4 import platform
 5 
 6 if __name__ == '__main__':
 7     parser = argparse.ArgumentParser()
 8     parser.add_argument("target")
 9     parser.add_argument("outbasename")
10     parser.add_argument("--machine", default=platform.machine())
11     args = parser.parse_args()
12     
13     rgx = re.compile(r"\d+\s+[0-9A-F]+\s+[0-9A-F]+\s+(.*)\s+=\s+.*$")
14     cmd = ["dumpbin", "-exports", args.target]
15     exports = []
16     for line in subprocess.check_output(cmd).decode("ascii").split("\n"):
17         line = line.strip()
18         m = rgx.match(line)
19         if m:
20             exports.append(m.group(1))
21     
22     defname = args.outbasename + ".def"
23     libname = args.outbasename + ".lib"
24     
25     with open(defname, "w") as fo:
26         fo.write("EXPORTS\n")
27         for f in sorted(exports):
28             fo.write("    " + f + "\n")
29     cmd = [
30         "LIB.EXE",
31         '-DEF:%s' % defname,
32         '-OUT:%s' % libname,
33         "-MACHINE:%s" % args.machine,
34         ]
35     print(" ".join(cmd))
36     subprocess.check_call(cmd)

ソースから使い方はわかるんじゃないかとは思うけれど一応…こんな風に使う:

1 me@host: ~$ python mkimplibfromdll.py some.dll some

この場合 some.lib と some.exp が LIB.EXE コマンドによって生成される。

当たり前だが「開発環境を持ってること」が前提である。Visual Studio など。DUMPBIN.EXE、LIB.EXE はそれらに含まれる。

あとはヘッダファイルがあれば、その DLL を使ってプログラムを作ることは出来るであろう。あれば、の話だけど。要するに「オープンソース」が前提、てことな。


2017-07-12追記:
あれこれだけだったっけ、と微かな違和感があったが、記憶違いではなかったようで。公式ドキュメント:

A minimal .def file must contain the following module-definition statements:

  1. The first statement in the file must be the LIBRARY statement. This statement identifies the .def file as belonging to a DLL. The LIBRARY statement is followed by the name of the DLL. The linker places this name in the DLL’s import library.
  2. The EXPORTS statement lists the names and, optionally, the ordinal values of the functions exported by the DLL. You assign the function an ordinal value by following the function’s name with an at sign (@) and a number. When you specify ordinal values, they must be in the range 1 through N, where N is the number of functions exported by the DLL. If you want to export functions by ordinal, see Exporting Functions from a DLL by Ordinal Rather Than by Name as well as this topic.

ordinal values (序数) については鬱陶しいので説明割愛。「旧式」マイクロソフトの「旧式サイズ削減ソリューション」だが今更こんなもんに依存したいプロジェクトはそうそうない。

「The first statement in the file must be the LIBRARY statement」な。けどこれ、うっそ、そうにゃにょ。確かにおぼろげな記憶でいつも入ってた記憶はないではないけれど、.def ファイルを作って DLL に仕立ててる大方の OSS プロジェクトがこれを省略してる。だから「must be」はどこかでウソになっちまったんだろう。狼少年てヤツか。違うか。

ちぅわけで上で挙げたスクリプトは the first statement として the LIBRARY statement を清く正しく突っ込む改修は当然出来るけれど…、まぁいらんのぢゃない? あったほうが柔軟かもしれんけどさ。と思ったけれど、まぁ次に本当に欲しくなってからまた考え直すのもダルいしね、今やっちまおう。ついでなので outbasename の指定省略可能にしつつ、作った .def を消すオプションも追加しとく:

 1 import argparse
 2 from os.path import splitext, basename
 3 import re
 4 import subprocess
 5 import platform
 6 
 7 if __name__ == '__main__':
 8     parser = argparse.ArgumentParser()
 9     parser.add_argument("target")
10     parser.add_argument("--outbasename", "-b")
11     parser.add_argument("--libraryname", "-l")
12     parser.add_argument("--machine", "-m", default=platform.machine())
13     parser.add_argument("--no-libraryname", "-n", action="store_true")
14     parser.add_argument("--remove-def", "-r", action="store_true")
15     args = parser.parse_args()
16     
17     rgx = re.compile(r"\d+\s+[0-9A-F]+\s+[0-9A-F]+\s+(.*)\s+=\s+.*$")
18     cmd = ["dumpbin", "-exports", args.target]
19     exports = []
20     for line in subprocess.check_output(cmd).decode("ascii").split("\n"):
21         line = line.strip()
22         m = rgx.match(line)
23         if m:
24             exports.append(m.group(1))
25     
26     outbasename = args.outbasename if args.outbasename else splitext(args.target)[0]
27     libraryname = args.libraryname if args.libraryname else basename(outbasename)
28     defname = outbasename + ".def"
29     libname = outbasename + ".lib"
30 
31     if args.remove_def:
32         import atexit
33         import os
34         atexit.register(os.unlink, defname)
35 
36     with open(defname, "w") as fo:
37         if not args.no_libraryname:
38             fo.write("LIBRARY    %s\n" % libraryname)
39         fo.write("EXPORTS\n")
40         for f in sorted(exports):
41             fo.write("    " + f + "\n")
42     cmd = [
43         "LIB.EXE",
44         '-DEF:%s' % defname,
45         '-OUT:%s' % libname,
46         "-MACHINE:%s" % args.machine,
47         ]
48     print(" ".join(cmd))
49     subprocess.check_call(cmd)

最初のバージョンとはほんの少しだけ使い方が違う:

1 me@host: ~$ python mkimplibfromdll.py some-25.dll --outbasename=some

元々 outbasename を必須にしてたのは、多くの場合例にしたように「DLL 本体のファイル名」がバージョン番号付きなことを踏まえてたのね。結構な数のプロジェクトは、「some-25.dll」みたいなバージョニングをしてる。けどインポートライブラリは「some.lib」、こういうことをよくやってる。ので結構 DLL のファイル名と違う(ようにしたい)ことが多いのね。けどまぁ「指定可能」ならそれでいいわけで。


2017-07-21追記
ここで幸い言及しなかったんで、あえて追記はいらんかなぁ、とも思ったんだけれど、やっぱり一応。

このネタ、元はと言えば「ffmpeg の Windows ビルド」のためにやろうとしてた。別にこれでやってもいいはいいんだけど、ただ ffmpeg に関しては、ダウンロードの際に「shared」とともに「dev」も入手すれば事足りる。これに気付いてなかったのね、ワタシ。いらん作業をせんでいいように、「凝視」しましょうね、って教訓である。