【Python】【Windows】PythonからWindows DLLを呼び出そうとしたら案外大変だった話

2022年8月7日

「大変だったのでムカつきがてら記事にする」シリーズも、第3弾になりました。
仕事でネット上の情報、知識に助けられることも多いいおぶろぐですが、

お前ら本当に自分でやってみて確認したのか?
コピペ
記事ちゃうんかこれ?

と文句付けたい記事も多いですね…。

まあ、いおぶろぐが「やりたいこと」が、そういう記事で済まない「一般的でないこと」なのが原因であるのがいけないような気もしますが…。

今回は、Windowsサーバ上で、PythonからWindows DLLを呼び出す話です。

前提

  1. すでにC#のWebアプリは動いている
  2. 新規機能追加として、Python3を使ったWebAPIを作る(サーバはWindows)
  3. Pythonでも 1.の既存アプリで使っているユーザ認証用DLLを使うべき
  4. ただしそのDLLは10年近く使われており、ソースどころか仕様書もよくわからない
  5. そのDLLを 1.の既存アプリが呼び出して使う部分のC#ソースは(当然)存在する

やったこと1

  1. ctypesを使って呼び出してみる
  2. “function not found" と言われる
  3. じゃあDLLの中にそんな関数があるのか調べよう。とDUMPBINを使ってみる
  4. DUMPBIN /EXPORTS しても、情報が表示されない
  5. DUMPBIN /CLRHEADER したら、情報が表示された

やったこと1の解説

「python dll」とかでGoogle検索して出てくるのは 「DLLの呼び出しには ctypes を使え」という記事ばかり。
なのでその通りにやってみると、"function not found" のエラーメッセージが。

“function not found" なので、こちらの呼び出し方(function名の指定方法とか)が間違っているのかもしれない。DLLの中にどんな関数名があるのか調べてみよう。
でもどうやって調べるの?
→ Visual Studio から使える “DUMPBIN" ってコマンドがあるらしい。

ってことで早速、Visual Studio の Developer Command Prompt より

DUMPBIN /EXPORTS (DLLパス)

としてみましたが…表示されない。
DUMPBIN って、いわゆるC言語で作られたDLLの情報を表示してくれるらしい。
逆に言えば、C#などでビルドされた、いわゆるCOMのDLLの情報は表示されない。
つまりこのDLLはC#ビルドのDLLなのでは?

というわけで今度は

DUMPBIN /CLRHEADER (DLLパス)

してみる。
CLRとは “Common Language Runtime" のことで…つまりは .NET のことですな(ざっくり過ぎ)。

“clr Header:" 以下に情報が表示されました。
このDLLはC#(or .NET の何か)でビルドされたものであることが確定しました。

なぜそれが重要かというと、ctypes では、C#ビルドのDLLを呼び出すことができないからなんです…。

やったこと2

  1. Python3.8未満をWindowsにインストール
  2. pythonnet を pip install する
  3. clr(pythonnet)を使ってDLL呼び出し出来た

やったこと2の解説

C#でビルドされたDLLは、pythonから “ctypes" では呼び出せない。
→ pythonnet ってやつをインストールすれば出来るらしい。ので、python上で

pip install pythonnet

する。
ただし、pythonnet は python3.8 以上ではインストールできないようです。ですのでいおぶろぐも 3.7.9 をインストールしました。

Python3.7.9 で pip install pythonnet した後、ソースを動かしてみると、DLL呼び出し出来ました!
ただ、pythonソース上は “clr" なんですよね、pythonnetって。ちょっとややこしい。

サンプルは以下です。

import clr

clr.AddReference(".\dll\Auth\Person.AuthCheck")
import Person.AuthCheck

dll = Person.AuthCheck.Authentication()

# 認証キー取得
key, errMessage = dll.GetAuthKey("PersonA", "User01", None)
print(key)

# 認証キー検証
res, errMessage = dll.ChkAuthKey(key, "PersonA", "User01", None)
print(res == 0)

そして注意点。
clr.AddReference() の引数にDLLのパスを指定する必要がありますが、末尾の “.dll" を付与してはいけないようです。(3行目)
その後の import には、対象DLLをVisualStudioで参照設定した場合に、C#側「メタデータより」に表示される namespace の値を設定します。途中にピリオドが入っていたりしてもそのまま。(4行目)

インスタンス化する場合は、namespace名.class名 としましょう。(6行目)
また、ref がある場合は、タプルで受けます(引数はNoneとする)。(9、13行目)

代替案

ひとつだけ問題なのは、pythonnetが2022年半ばを過ぎてもpython3.8以降でインストールすら出来ない状態だということです。

すでにpythonバージョンが3.10である現在、dllを起動するためだけに、古いpythonを使い続けるのはセキュリティ的に問題があると思いますし、上司やお客様の合意が得られるとは思えません。

VisualStudioを持っているのであれば、dllをラップするexeを作り、それをpythonから、

subprocess.run() 

で起動するのが、一番簡単かつ確実である気がします。