Multiplication of two integer numbers is pretty simple. Additional information can be found in the documentation.
In [1]:
from ctypes import cdll
In [2]:
basics = cdll.LoadLibrary('./lib_basics.so')
In [3]:
basics.mul(2, 5)
Out[3]:
See? It's easy
In [4]:
from ctypes import c_int
Say, we need to multiply 3-dimensional vectors
In [5]:
first = (c_int * 3)(1, 2, 3)
We can create an alias for this data type and use it
In [6]:
vector3D = c_int * 3
second = vector3D(4, 5, 6)
In [7]:
c_result = basics.dot(first, second, 3)
python_result = sum(a * b for a, b in zip([1, 2, 3], [4, 5, 6]))
print('C code returned', c_result, 'and Python code returned', python_result)
In [8]:
basics.dot((c_int*1)(2), (c_int*1)(*[3]), 1)
Out[8]:
Following examples will cause errors
In [9]:
try:
vector3D([1, 2, 3])
except:
print('You cannot pass lists')
try:
vector3D(0, 1, 2, 3)
except:
print('Forbidden to provide more elements than it should accept')
Following types can be used to pass arguments to C
functions
ctypes | type | C type Python type |
---|---|---|
c_bool | _Bool | bool (1) |
c_char | char | 1-character bytes object |
c_wchar | wchar_t | 1-character string |
c_byte | char | int |
c_ubyte | unsigned | char int |
c_short | short | int |
c_ushort | unsigned | short int |
c_int | int | int |
c_uint | unsigned | int int |
c_long | long | int |
ctypes | type | C type Python type |
---|---|---|
c_ulong | unsigned long | int |
c_longlong | __int64 or long long | int |
c_ulonglong | unsigned __int64 or unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t or Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char * (NUL terminated) | bytes object or None |
c_wchar_p | wchar_t * (NUL terminated) | string or None |
c_void_p | void * | int or None |
Large numbers (bignum
) are represented as arrays of longs in Python:
{d0, d1, d2, ...}
We can change each one of them, so why not?
Read the documentation about Python
API for C
.
File should be compiled to shared library (dll in Windows).
Makefile for Linux:
FLAGS=-shared
LIBRARIES=-I/usr/include/python3.4
BUILD_LIBRARY=gcc $(FLAGS) $(LIBRARIES)
all:
$(BUILD_LIBRARY) setters.c -o lib_setters.so
In [10]:
from ctypes import cdll, c_long, c_size_t, c_voidp
setters = cdll.LoadLibrary('./lib_setters.so')
def change_long(a, b=0, digit=0):
setters.set_long(c_voidp(id(a)), c_long(b), c_size_t(digit))
Don't forget that Python interpreter will not create new objects for small integers like 0
, so we should avoid assigning new values to such numbers, because they will be changed everywhere they're used
In [11]:
from ctypes import c_long, c_size_t, c_voidp
def change_long(a, b=0, digit=0):
args = (a, b, digit)
if not all(type(a) is int for a in args):
raise TypeError('All parameters should be of type "int", '
'but {} provided'.format(map(type, args)))
if a + 0 is a:
raise ValueError('No way. You don\'t want to break '
'your interpreter, right?')
setters.set_long(c_voidp(id(a)), c_long(b), c_size_t(digit))
Recall that we cannot change values of integers inside the Python functions
In [12]:
def variable_info(text, variable):
print('{:^30}: {:#05x} ({:#x})'.format(text, variable, id(variable)))
def foo(a, new_value):
a = new_value
a = 2**10
variable_info('Before function call', a)
foo(a, 5)
variable_info('After function call', a)
Now forget it and take a look at what we've done
In [13]:
a = 2**10
b = a
variable_info('Before function call', a)
change_long(a, 2, 0)
variable_info('After function call', a)
variable_info('What\'s about b? Here it is', b)
In [14]:
from numpy import array, cross
basis = [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
]
In [15]:
def py_cross(u, v):
return [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0]
]
py_cross(basis[0], basis[1])
Out[15]:
In [16]:
cross(array(basis[0]), array(basis[1]))
Out[16]:
In [17]:
from ctypes import cdll
from numpy import empty_like
c_cross = cdll.LoadLibrary('./lib_cross.so')
u = array(basis[0]).astype('f')
v = array(basis[1]).astype('f')
w = empty_like(u)
def cross_wrapper(u, v, w):
return c_cross.cross(u.ctypes.get_as_parameter(),
v.ctypes.get_as_parameter(),
w.ctypes.get_as_parameter())
cross_wrapper(u, v, w)
print(w)
In [18]:
from numpy.random import rand
BIG_ENOUGH_INTEGER = int(1E5)
vectors_u = rand(BIG_ENOUGH_INTEGER, 3).astype('f')
vectors_v = rand(BIG_ENOUGH_INTEGER, 3).astype('f')
print('Vectors u:', vectors_u)
In [19]:
%%timeit
for i in range(BIG_ENOUGH_INTEGER):
py_cross(vectors_u[i], vectors_v[i])
In [20]:
%%timeit
cross(vectors_u, vectors_v)
In [21]:
%%timeit
vectors_w = empty_like(vectors_u)
for i in range(BIG_ENOUGH_INTEGER):
cross_wrapper(vectors_u[i], vectors_v[i], vectors_w[i])
In [22]:
from numpy import allclose
np_result = cross(vectors_u, vectors_v)
py_result = [py_cross(vectors_u[i], vectors_v[i])
for i in range(BIG_ENOUGH_INTEGER)]
print(allclose(np_result, py_result))
vectors_w = empty_like(vectors_u)
assert sum([cross_wrapper(vectors_u[i], vectors_v[i], vectors_w[i])
for i in range(BIG_ENOUGH_INTEGER)]) == 0
print(allclose(np_result, vectors_w))
It's better to compile with optimization. Also to use -fPIC
flag to avoid following compilation error
relocation against symbol `cross' can not be used when making a shared object
What we get:
gcc -shared -fPIC cross.c -O3 -o lib_cross.so
Numpy arrays are flattened when got as C
arrays.
Also len
operator returns amount of rows of matrix. If you want to get the total amount of elements, you should use size
method.
In [23]:
vectors_w = empty_like(vectors_u)
c_vectors_u = vectors_u.ctypes.get_as_parameter()
c_vectors_v = vectors_v.ctypes.get_as_parameter()
c_vectors_w = vectors_w.ctypes.get_as_parameter()
In [24]:
%%timeit
vectors_w = empty_like(vectors_u)
c_vectors_w = vectors_w.ctypes.get_as_parameter()
c_cross.cross_vectors(c_vectors_u, c_vectors_v, c_vectors_w, len(vectors_u))
In [25]:
c_cross.cross_vectors(c_vectors_u, c_vectors_v, c_vectors_w, len(vectors_u))
print(allclose(np_result, vectors_w))
Are you surprised?