空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

PythonでDLLを使う時の話

はじめに

タイトルの通り、PythonでDLLを使う時の話。

以下注意事項

  • 大雑把なところは、他のブログのほうがちゃんと書いてるんで、僕が書くのは細かいところ(というか、躓いたところ)を何点かだけです。
  • 今回はなんと、ソースコードを実際に動かしていないとかいうクズ仕様(手順自体は間違いなくあっているはず)。
  • 基本的にはWindowsでの話をするけれども、大部分はMacLinuxでも同じ。とは言え、Windows以外で動作確認してないんで、動くかどうかは保証しません。

DLLを作ろう

まずは、適当な方法でDLLを作ろう。
個人的には何で作ってもいいと思うけど今回はマルチプラットフォームを意識してcmakeを使ってみます。
使うソースは以下3つ。

config.hpp

#ifndef CONFIG_HPP
#define CONFIG_HPP

#ifdef Test_EXPORTS
# define DLL_API extern "C" __declspec(dllexport)
#else
# define DLL_API extern "C" __declspec(dllexport)
#endif

#endif CONFIG_HPP

test.hpp

#include "config.hpp"

DLL_API void print(char const* str);
DLL_API int sum(int lhs, int rhs);

test.cpp

#include "test.hpp"
#include <iostream>

DLL_API void print(char const* str) { std::cout << str << std::endl; }
DLL_API int sum(int lhs, int rhs) { return lhs + rhs; }

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)
project(Test)
add_executable(Test SHARED
  ${CMAKE_CURRENT_LIST_DIR}/test.cpp 
  ${CMAKE_CURRENT_LIST_DIR}/test.hpp
  ${CMAKE_CURRENT_LIST_DIR}/config.hpp
)

今回のポイントとしては、DLL_APIマクロに __declspec(dllexport) だけではなく extern "C" も含んでいる所。これがないと、C++の関数として扱われて後述する問題にぶちあたります。
それと、cmakeでDLLを作った経験がない人にとっては馴染みがないと思いますが、cmakeでビルドする場合、謎のWinMainを定義する必要も、(モジュール名)_EXPORTSマクロを自分で定義する必要はありません。cmakeが勝手にやってくれます。

ついでに言うと、ぶっちゃっけヘッダーはadd_executableには不要なんだけど、VisualStdio信者な僕には必須です。

DLLの中身を確認しよう

ここはWindows限定(Linuxとかでもできるかもだけれども、やり方は知らん)。
まず、VisualStudioのコマンドツールを起動しよう。
それからビルドしたDLLに対して以下のコマンドを実行。

$ dumpbin /EXPORT Test.dll

すると、自分で定義した関数の一覧が出るはず。
これのname欄に@Xsdwsda@見たいな関数名以外のシグネチャが含まれていたらおそらく、extern "C"が抜けているのだと思う。
extern "C"なしのC++の関数は呼び出せないという訳ではないのだけれども、今回の説明では扱わないのでとりあえずextern "C" を付けましょう。

Dump of file Test.dll

File Type: DLL

  Section contains the following exports for Test.dll

    00000000 characteristics
    577278AE time date stamp Tue Jun 28 22:16:30 2016
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001020 print
          2    1 00001010 add

  Summary

        1000 .data
        1000 .gfids
        1000 .rdata
        1000 .reloc
        1000 .rsrc
        2000 .text

Pythonからの呼び出し

後は簡単、ctypesモジュールを使えばちゃちゃっとできます。

# coding:utf-8
import ctypes

def main():
    dll = ctypes.cdll.LoadLibrary('Test.dll')
    c_print = dll.print
    c_print.argtypes = [ctypes.c_char_p]
    c_print.restype = None

    c_sum= dll.sum
    c_sum.argtypes = [ctypes.c_int, ctypes.c_int]
    c_sum.restype = ctypes.c_int

    c_print(ctypes.c_char_p('utf-8'.encode('utf-8')))
    print(int(c_sum(ctypes.c_int(10), ctypes.c_int(20)))) 

if __name__ == '__main__':
  main()

これを実行すると出力は以下のようになるはずです.

utf-8
30

まず、LoadLibraryで読み込んで、その後は、dll.(関数名)でdll内の関数オブジェクトを取得することができます。
その後、戻り値(restype)と引数の型(argtypes)を設定しやりましょう。
その時の注意事項は以下の4つ

  • 文字列はencodeでbyte型に変換してからc_char_p型に変換する
  • 引数にポインタ型を使いたいときは ctypes.POINTER(ctypes.何らかの型) という風にしてやりましょう。
  • 同様にアドレスを渡す時もctypes.addressofでアドレスを渡すことができる。
  • たまに、アドレスの型変換をしたい時もあると思うけど、そのときは、ctypes.cast を使いましょう

おわりに

もっと知りたければ、Pythonの公式ドキュメントを見よう。
もし僕に間違いがあっても多分書いてるだろう。
16.16. ctypes — Pythonのための外部関数ライブラリ — Python 3.5.1 ドキュメント