ctypes
モジュールを利用して C 言語のライブラリをロードし、そこで定義されている C の関数を
呼び出すことができる。
https://docs.python.org/ja/3/library/ctypes.html
# ctypes モジュールが定義しているすべての型、関数をインポート
from ctypes import *
# 標準 C ライブラリをロードする。
libc = cdll.LoadLibrary('libc.so.6')
# time 関数を呼び出す。これは UNIX 時刻を整数で返す。
# None を渡しているので time 関数には NULL ポインタが渡される。
libc.time(None)
# --> 1619154874
# time 関数は、time_t 型へのポインタを引数にとり、それに Unix 時刻をセットしてくれる。
tloc = c_int()
tloc
# --> c_int(0)
libc.time(byref(tloc))
# --> 1619155009
tloc
# --> c_int(1619155009)
tloc.value
# --> 1619155009
C 関数呼び出しにおけるパラメータとして直に使える Python のオブジェクトは、None
, 整数、bytes
, そして文字列だけだ。
None
は C の NULL ポインタに変換され、整数はプラットホームのデフォルトの C int
型に変換され、
bytes
と文字列は、そのデータを含むメモリブロックへのポインタ(char *
または wchar_t *
)に変換される。
ただし、C 関数の argtypes
リストに引数の型を定義しておけば、ある程度、型の変換をしてくれる。
libc.printf(b'I am %d years old.\n', 49)
# --> I am 49 years old.
# --> 19
# Python の浮動小数点数オブジェクトは直に C 関数に渡せないのでエラーになる。
libc.printf(b'PI is %f.\n', 3.14)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2
libc.printf(b'PI is %f.\n', c_float(3.14))
# --> PI is -nan.
# --> 12
libc.printf(b'PI is %f.\n', c_double(3.14))
# --> PI is 3.140000.
# --> 16
c_int
や c_char
, c_float
, c_double
, c_char_p
等々、C と互換性のある型が多数定義されている。
これらの型を呼び出すことで C と互換性のある値をメモリにもち、そのメモリの内容にアクセスするいくつかの
記述子(descriptor)をもつ Python オブジェクトが作成される。
i = c_int()
i
# --> c_int(0)
i.value
# --> 0
i.value = -99
i
# --> c_int(-99)
c_ushort(-1)
# --> c_ushort(65535)
C と互換性のある値を格納したメモリブロックのアドレスは、addressof()
ユーティリティ関数で取得できる。
これは byref
関数で取得できるポインタの値と同じになる。byref
は C 関数へ引数のポインタを渡すとき、
つまり参照渡しのときに使う関数だ。
# c_int オブジェクト中で、C int と互換性のある 100 という値が格納されているメモリブロックの
# アドレスを addressof 関数で取得できる。
i = c_int(100)
addressof(i)
# --> 140141957166568
hex(addressof(i))
# --> '0x7f75579335e8'
byref(i)
# --> <cparam 'P' (0x7f75579335e8)>
c_char_p
と c_wchar_p
は特殊だ。初期化子として bytes
または文字列を渡すと、それらを NUL 終端の
C の文字列に相当する char
配列または wchar_t
配列を contents にもつ Python オブジェクトを生成する。
これらは、C の関数へパラメータとして渡すと contents としてもっている NUL 終端文字列への参照渡しになるが、immutable なので
C 関数側でこれを変更することはできない。なお、初期化子として None
を渡すと参照渡し時に NULL ポインタが渡される。
s = c_char_p(b'Hello')
s
# --> c_char_p(140141926003288)
#
# ['H', 'e', 'l', 'l', 'o', 0x00] という char 配列が存在するメモリブロックのアドレスが
# 140141926003288 だということ。
s.value
# --> b'Hello'
t = c_char_p(140141926003288) # s が持っているのと同じ char 配列を指すようにセットする。
t
# --> c_char_p(140141926003288)
t.value
# --> b'Hello'
# s と t は、同じ char 配列を参照しているわけだが、そのアドレス 140141926003288 を
# 各々別個の変数にもっているわけであり、byref や addresof 関数はその変数のアドレスを
# 返すので、s と t では値が異なっている。
byref(s), byref(t)
# --> (<cparam 'P' (0x7f75579336f8)>, <cparam 'P' (0x7f75579335e8)>)
hex(addressof(s)), hex(addressof(t))
# --> ('0x7f75579336f8', '0x7f75579335e8')
# value 属性を参照すると、その都度 char 配列から bytes オブジェクトを生成するので、False になる。
s.value is s.value
# --> False
>>>
# 途中に 0x00 をもつ bytes オブジェクト。
# これを c_char_p に渡すと、c_char_p は、NUL 終端の文字列を前提とするので途中で切れてしまう。
b = bytes([0x61, 0x62, 0x63, 0x00, 0x64])
b
# --> b'abc\x00d'
s = c_char_p(b)
s
# --> c_char_p(140629891864368)
s.value
# --> b'abc'
# value 属性の値を変更すると、別の char 配列を指すようになる。
m = 'Hello, world'
c_s = c_wchar_p(m)
c_s
# --> c_wchar_p(140629860993488)
c_s.value
# --> 'Hello, world'
c_s.value = 'Hi, there!'
c_s
# --> c_wchar_p(140629891774576)
c_s.value
# -->'Hi, there!'
m
# --> 'Hello, world'
# 10 というアドレスをもつ char 配列を指すように c_char_p オブジェクトを作成する。
s = c_char_p(10)
s
# --> c_char_p(10)
s.value
# Segmentation fault # 10 というアドレスにアクセスしようとしてプロセスが落ちる。
C 関数へポインタを渡すことにより(参照渡し)、Python 側のデータを C 関数で書き換えてもらうことができる。
byref
関数を使い、
i = c_int(100)
libc.hoge(byref(i))
のようにすればいいだけ。ただし、c_char_p
, c_wchar_p
の場合、immutable なオブジェクトを参照しているので
変更は不可能である。そのときは、create_string_buffer
, create_unicode_buffer
関数を使う。
また、NUL 終端の文字列でないようなバイナリのバイト配列を使う場合は、
ByteArray = c_byte * 10
arr = ByteArray()
libc.foo(len(arr), arr)
のようにする。
参照渡しによる Python 側データの変更、構造体の使用など、少々高度な使い方を lib1.c
と test.py
でやってみた。
gcc -shared -fPIC lib1.c -o lib1.so
として C ライブラリを作成し、Python を起動して
from test import *
とすれば、実行できる環境が整う。