PythonでDLLを使う時の話
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 ドキュメント