kun
kun

Reputation: 68

call C code from Python via ctypes, use python object list

I want to

  1. write a function sum_list of sum a list in c, named sum_list.c
  2. make the sum_list.c file to sum_list.so
  3. sum_list = ctypes.CDLL('./sum_list.so')
  4. result = sum_list.sum_list([1, 2, 3])

It raise error in step 4:

ctypes.ArgumentError: argument 1: : Don't know how to convert parameter 1

when I write a function in c which add two numbers, it works ok.

so, the point is i don't know how to pass a list (python object) to c.


sum_list.c

#define PY_SSIZE_T_CLEAN
#include <Python.h>

long sum_list(PyObject *list)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;

    n = PyList_Size(list);
    if (n < 0)
        return -1; /* Not a list */
    for (i = 0; i < n; i++) {
        item = PyList_GetItem(list, i); /* Can't fail */
        if (!PyLong_Check(item)) continue; /* Skip non-integers */
        value = PyLong_AsLong(item);
        if (value == -1 && PyErr_Occurred())
            /* Integer too big to fit in a C long, bail out */
            return -1;
        total += value;
    }
    return total;
}

python code

from ctypes import CDLL

sum_list = CDLL('./sum_list.so')

l = [1, 2, 3]
sum_list.sum_list(l)

I expect the var result in my python code is 6.

Upvotes: 4

Views: 3228

Answers (3)

Mark Tolonen
Mark Tolonen

Reputation: 177674

As the other answer mentioned, you can use ctypes.py_object to represent an actual Python object, but you must also remember when calling Python C APIs you must not release the GIL (global interpreter lock). Use PyDLL instead of CDLL to accomplish this.

Here's a fully working example that works for sequences in general:

test.c

#include <Python.h>

#ifdef _WIN32
#   define API __declspec(dllexport)
#else
    define API
#endif

API long sum_list(PyObject* o)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject* list;
    PyObject* item;

    list = PySequence_Fast(o,"not a sequence");  // New reference!  Must DECREF eventually.
    if(list == NULL)
        return -1;
    n = PySequence_Fast_GET_SIZE(list);
    for (i = 0; i < PySequence_Fast_GET_SIZE(list); i++) {
        item = PySequence_Fast_GET_ITEM(list, i);
        if (!PyLong_Check(item))
        {
            PyErr_SetString(PyExc_TypeError,"sequence contains non-integer");
            Py_DECREF(list);
            return -1;
        }
        value = PyLong_AsLong(item);
        if (value == -1 && PyErr_Occurred())
        {
            Py_DECREF(list);
            return -1;
        }
        total += value;
    }
    Py_DECREF(list);
    return total;
}

test.py

from ctypes import *

dll = PyDLL('test')
dll.sum_list.restype = c_long
dll.sum_list.argtypes = py_object,

print(dll.sum_list((1,2,3,4,5)))
try:
    print(dll.sum_list(7))
except TypeError as e:
    print(e)
try:
    print(dll.sum_list((1,2,'a',4,5)))
except TypeError as e:
    print(e)
try:
    print(dll.sum_list((1,2,0x80000000,4,5)))
except OverflowError as e:
    print(e)
try:
    print(dll.sum_list((1,2,-0x80000001,4,5)))
except OverflowError as e:
    print(e)

Output:

15
not a sequence
sequence contains non-integer
Python int too large to convert to C long
Python int too large to convert to C long

Upvotes: 2

The ctypes library is meant for calling functions in pure C libraries. The example that you found is for a function that would go into a Python extension module, and be usable without ctypes. Ctypes actually has a type for PyObject *, namely ctypes.py_object. You need to wrap the list in this - be careful to ensure that it will stay alive!

In any case you must almost always provide the correct prototype of the function using the restype and argtypes.

Here's a working example:

import ctypes

sum_list = ctypes.PyDLL('./sum_list.so')
sum_list.restype = ctypes.c_long
sum_list.argtypes = [ ctypes.py_object ]

l = [1, 2, 3]
print(sum_list.sum_list(ctypes.py_object(l)))

Note, as noticed by Mark, you must use PyDLL here instead, to ensure that the GIL is not released since the function does not acquire it!

Upvotes: 6

code_dredd
code_dredd

Reputation: 6085

I've up-voted @Antii's answer, but still wanted to add a short, complete, and self-contained example.

First, ctypes is to interact with C code that was written without Python interoperability in mind. Generally, this means code that does not #include <Python.h> and so on. For example:

/**
 * Sample function to call from Python 3
 * Build:
 *  $ gcc -shared -o libmath.so -fPIC math.c
 *
 * See https://stackoverflow.com/a/14884166/4594973
 *
 **/
int add(int x, int y) {
    return x + y;
}

While the code above is trivial, you need to make sure that more complicated pieces of code are compiled properly. For example, if you're using a C++ comnpiler and/or are using features specific to C++ (e.g. classes, method overloading, etc), then you must expose a C-compatible interface when building the library, as C++'s name mangling will make it next to impossible to find the function after the fact.

After building the C code above, use this Python script to call it:

#!/usr/bin/env python3

import ctypes

cmath = ctypes.CDLL('./libmath.so')
cmath.restype = ctypes.c_int
cmath.argtypes = [ctypes.c_int, ctypes.c_int]

x = 4
y = 5
r = cmath.add(x, y)

print(f'{x} + {y} = {r}')

Running the samples from my terminal:

$ gcc -shared -o libmath.so -fPIC math.c
$ python3 math.py
4 + 5 = 9

Upvotes: 2

Related Questions