1. 背景
PythonからCで作られたライブラリにndarray(2次元)をpointer-to-pointerで渡す必要が出てきた.例えばこんな感じ.
(単ポインタで渡せよという意見はごもっともだが,使いたいライブラリがご丁寧にpointer-to-pointerで作られているのでしゃあなし.)
動作検証もかねて記事を作ることにした.
# python
from ctypes import *
lib = LoadLibrary()
lib.func.argtypes = [POINTER(POINTER(c_float)), c_int, c_int]
# how to pass?
lib.func(???, size_y, size_x)
// C, exported as dll.
void func(float **arrays, y, x){ // 省略 }
2. コントリビューション
本記事では,
- CMakeでDLLの作り方(Linux, Macのみ)
- PythonでのDLLの呼び方
- ndarray to pointer pointer
にわけて解説する.なお,2次元配列を出力するprint2f
を作成およびコールするものとする.
本記事は中上級者向けの記事です.
おそらく生ポを扱うことになるので試す場合は自己責任で.
あと結論だけ欲しい人は3.4だけ見れば良い.
3. 本編
3.1 DLL作成
まずはじめにCMakeであるが,SHAREDを忘れないように.
add_library(libPtP SHARED library.cpp library.h)
次にCコードlibrary.c.ダブルポインタでも配列として扱うことにする.
ほんとはポインタを進める書き方がいいのだろうけどここではこうしておく.
# include <stdio.h>
# include "library.h"
void print2f(float **arrays, int y, int x) {
for(int i=0; i<y; i++) {
for(int j=0; j<x; j++) {
printf("%f ", arrays[i][j]);
}
printf("\n");
}
}
ヘッダーlibrary.h. extern Cを忘れると詰む.
extern "C" {
void print2f(float **, int, int);
}
あとはビルドしてやれば良い.ビルドの仕方については解説しない.
CLionとか使えばビルドボタン一撃である.
3.2 PythonでDLLを呼ぶ
ctypesというDLLを扱うためのライブラリがあるのでこいつを使ってライブラリのロードと関数の呼び出しを行う.
が,ここでPOINTER(POINTER(c_float))をどうするか問題が出てくるので,3.3でどうするか説明する.
import numpy as np
from ctypes import *
lib = cdll.LoadLibrary(path)
arrays = np.zeros((2, 2))
lib.print2f.argtypes = [POINTER(POINTER(c_float)), c_int, c_int]
lib.print2f(arrays, 2, 2) # error
3.3 ndarray to pointer pointer
シングルポインタの場合は特に問題がない.なぜならnp.ctypeslib.as_ctypeがArrayをPointerにしてくれるからだ.
(print1fは一次元配列を出力する関数で,ライブラリに定義されているものとする)
ちなみにここでprint(a)すると,c_float_Array_2
と出るので,c_float_Array_[n]
とPOINTER(c_float)
には互換性があるのだろう.たぶん.
# print1f
import numpy as np
from ctypes import *
a = np.zeros((2, )).astype(np.float32)
a = np.ctypeslib.as_ctypes(a)
lib.print1f.argtypes = [POINTER(c_float), c_int]
lib.print1f(a, 2)
問題はpointer-to-pointerの場合だ.一捻り必要になる.
以下手順をコードで解説する.
# まず出力する配列を作成
arrays = np.array([[1, 2], [3, 4]]).astype(np.float32)
# >> arrays = [[1, 2]
# [3, 4]]
# 次にポインタのリストにしてやる
ptrs = [array.ctypes.data_as(POINTER(c_float)) for array in arrays]
# >> ptrs = [ &arrays[0], &arrays[1] ]
# ポインタリストをctypesに変換する
pp = (POINTER(c_float) * y)(*ptrs)
# >> <__main__.LP_c_float_Array_2 object>
# 出力
lib.print2f.argtypes = [POINTER(POINTER(c_float)), c_int, c_int]
lib.print2f(pp, 2, 2)
3.4 まとめ
コードを乗せておく.
import numpy as np
from ctypes import *
arrays = np.array([[1, 2], [3, 4]]).astype(np.float32)
ptrs = [array.ctypes.data_as(POINTER(c_float)) for array in arrays]
pp = (POINTER(c_float) * y)(*ptrs)
lib.print2f.argtypes = [POINTER(POINTER(c_float)), c_int, c_int]
lib.print2f(pp, 2, 2)
多分もうちょっといい方法があると思う.
4. 環境
C++11
Python 3.5
clang-1000.10.44.4
Mac OS High Sierra