Skip to content

Instantly share code, notes, and snippets.

@fortune
Last active April 23, 2021 12:10
Show Gist options
  • Save fortune/fd47f908545a46293044be1f985d314e to your computer and use it in GitHub Desktop.
Save fortune/fd47f908545a46293044be1f985d314e to your computer and use it in GitHub Desktop.

Python の ctypes モジュールに関するメモ

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

ctypes の型の構造

c_intc_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 について

c_char_pc_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.ctest.py でやってみた。

gcc -shared -fPIC lib1.c -o lib1.so

として C ライブラリを作成し、Python を起動して

from test import *

とすれば、実行できる環境が整う。

#include <stdint.h>
#include <string.h>
// 文字列を逆順にする関数
//
// s -- NUL 終端文字列
//
int reverse_chars(char *s)
{
int i = 0;
int tail = strlen(s) - 1;
while (i < tail) {
char c = s[tail];
s[tail] = s[i];
s[i] = c;
i++;
tail--;
}
return 0;
}
// バイト配列を逆順にする関数
//
// n -- バイト列の長さ
// b -- バイト配列
//
int reverse_bytes(int n, char *b)
{
int i = 0;
int tail = n - 1;
while (i < tail) {
char c = b[tail];
b[tail] = b[i];
b[i] = c;
i++;
tail--;
}
return 0;
}
// バッファを double 配列と見て初期化する関数
//
// n -- バッファにセットする double の個数
// init_value -- バッファ先頭にセットする double 値
// p -- double 配列としてあつかうバッファへのポインタ
//
// init_value をバッファ先頭にセットし、以降、インクリメントした値をセットしていく。
//
int init_doubles(int n, double init_value, void *p)
{
double *da = (double *)p;
int i = 0;
for (i = 0; i < n; i++) {
da[i] = i + init_value;
}
return 0;
}
// 3次元空間上の点
struct vector3 {
int32_t x;
int32_t y;
int32_t z;
};
// Python の ctypes で構造体のデータをやり取りするためのサンプルデータとなる構造体。
// 特に意味はない。
struct databox {
uint8_t kind;
uint16_t id;
float sampling_rate;
//char name[10];
uint8_t name[10];
struct vector3 point;
};
// Python の ctypes で構造体データを操作するためのサンプル。
//
// struct databox へのポインタ dp と、それにセットするためのデータを Python 側から受け取り、struct databox 構造体へとセットするだけ。
//
void handle_databox(uint8_t kind, uint16_t id, float sampling_rate, char *name, int32_t x, int32_t y, int32_t z, struct databox *dp)
{
dp -> kind = kind;
dp -> id = id;
dp -> sampling_rate = sampling_rate;
for (int i = 0; i < 9; i++) {
dp -> name[i] = name[i];
if (name[i] == 0) break;
}
dp -> name[9] = 0;
(dp -> point).x = x;
(dp -> point).y = y;
(dp -> point).z = z;
}
"""ctypes の使用サンプル。
C ライブラリの関数が使用するワークスペース用メモリを Python 側で確保、保持し、
C 関数を呼び出すたびにそのメモリを渡すようなユースケース、および、C 関数から
複雑なデータ構造を戻り値として受け取るために、構造体や共用体のアドレスを C 関数に
渡し、それに結果をセットしてもらうことを想定したサンプル。
Example
----------
C ライブラリ `lib1.so` がこのモジュールと同じディレクトリに存在するとし、そのディレクトリで
Python を起動した後、次のようにする。::
>> from test import *
これで必要な設定が完了し、関数や変数が使えるようになるので、いろいろ試すことができる。
`lib1.so` は::
$ gcc -shared -fPIC lib1.c -o lib1.so
として作成する。
"""
import ctypes
lib = ctypes.cdll.LoadLibrary('./lib1.so')
# C ライブラリ側の構造体と対応するクラスを定義する。
class Vector3(ctypes.Structure):
_fields_ = [
('x', ctypes.c_int32),
('y', ctypes.c_int32),
('z', ctypes.c_int32)]
class Databox(ctypes.Structure):
_fields_ = [
('kind', ctypes.c_uint8),
('id', ctypes.c_uint16),
('sampling_rate', ctypes.c_float),
('name', ctypes.c_uint8 * 10),
('point', Vector3)]
# C 関数の戻り値と引数の型を宣言
reverse_chars = lib.reverse_chars
reverse_chars.restype = ctypes.c_int
reverse_chars.argtypes = [ctypes.c_char_p]
reverse_bytes = lib.reverse_bytes
reverse_bytes.restype = ctypes.c_int
reverse_bytes.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_byte)]
init_doubles = lib.init_doubles
init_doubles.restype = ctypes.c_int
init_doubles.argtypes = [ctypes.c_int, ctypes.c_double, ctypes.c_void_p]
handle_databox = lib.handle_databox
handle_databox.restype = None
handle_databox.argtypes = [
ctypes.c_uint8,
ctypes.c_uint16,
ctypes.c_float,
ctypes.c_char_p,
ctypes.c_int32,
ctypes.c_int32,
ctypes.c_int32,
ctypes.POINTER(Databox)]
# 要素数 1000 の double 配列を作成するための型を定義しておく。
DoubleArray1000 = ctypes.c_double * 1000
# 要素数 10 の byte 配列を作成するための型を定義しておく。
ByteArray10 = ctypes.c_byte * 10
"""
# C 関数の int reverse_chars(char *s) を試す。
#
# bytes 型の値で C と互換性のある NUL 終端文字列を作成する。
#
s_for_reverse = ctypes.create_string_buffer(b'Hello, World!')
s_for_reverse
# <ctypes.c_char_Array_14 object at 0x7f6680135620>
s_for_reverse.value
# b'Hello, World!'
s_for_reverse.raw
# b'Hello, World!\x00'
reverse_chars(s_for_reverse)
# 0
s_for_reverse.value
# b'!dlroW ,olleH'
s_for_reverse.raw
# b'!dlroW ,olleH\x00'
-------------------------------------
# C 関数の int reverse_bytes(int n, char *b) を試す。
#
# reverse_bytes に in place で逆に並び替えてもらう配列を作成。
#
byte_array = ByteArray10(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4)
for i in range(len(byte_array)):
print(byte_array[i])
# -5
# -4
# -3
# -2
# -1
# 0
# 1
# 2
# 3
# 4
reverse_bytes(10, byte_array)
for i in range(len(byte_array)):
print(byte_array[i])
# 4
# 3
# 2
# 1
# 0
# -1
# -2
# -3
# -4
# -5
-------------------------------------
# C 関数の int init_doubles(int n, double init_value, void *p) を試す。
#
# init_doubles に渡して初期化してもらう double の配列を作成。
#
double_array = DoubleArray1000() # 全要素が 0.0 で初期化される。
# 引数の型を宣言しておいたので、c_double(3.14) としなくても、型変換して渡してくれる。
init_doubles(1000, 3.14, double_array)
for i in range(len(double_array)):
print(double_array[i])
# 3.14
# 4.140000000000001
# 5.140000000000001
# 6.140000000000001
# 7.140000000000001
# 8.14
# 9.14
....
....
# 999.14
# 1000.14
# 1001.14
# 1002.14
# 要素数 1000 の double 配列として使うことを意図したバイト配列
#
byte_array_as_double = ctypes.create_string_buffer(8 * 1000)
# バイトの配列を double 配列として処理させているので、CPU によってはアラインメント制限により
# Bus エラーが発生し、プロセスが落ちるかもしれない。
#
init_doubles(1000, 2.71, byte_array_as_double)
byte_array_as_double.raw
# b"\xaeG\xe1z\x14\xae\x05@..............4\xaeM\x8f@"
import struct
struct.unpack('1000d', byte_array_as_double.value)
# (2.71, 3.71, 4.71, 5.71, 6.71, 7.71, 8.71, ..........., , 1000.71, 1001.71)
--------------------------------
# C 関数
# void handle_databox(
# uint8_t kind,
# uint16_t id,
# float sampling_rate,
# char *name,
# int32_t x,
# int32_t y,
# int32_t z,
# struct databox *dp)
#
# を試す。
# Databox 構造体を作成
databox = Databox()
# databox.id databox.kind databox.name databox.point databox.sampling_rate
handle_databox(1, 3456, 3.14, b'tomita', -10, 100, -3, ctypes.byref(databox))
databox.kind
# 1
databox.id
# 3456
databox.sampling_rate
# 3.140000104904175
bytes(databox.name[0:])
# b'tomita\x00\x00\x00\x00'
databox.point.x, databox.point.y, databox.point.z
# (-10, 100, -3)
"""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment