I would like to pass a list of strings to a CFFI extension which expects a char** as input parameter.
Example:
extension.h:
#include <stddef.h>
void sort_strings(char** arr, size_t string_count);
extension.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int cmpstrp(const void *p1, const void *p2) {
return strlen(*(const char**)p1) < strlen(*(const char**)p2);
}
void sort_strings(char** arr, size_t string_count) {
qsort(arr, string_count, sizeof(char*), cmpstrp);
}
// `main` is defined only for testing if `sort_strings` works as expected
int main(void) {
char* string_list[] = {"cat", "penguin", "mouse"};
size_t string_count = sizeof(string_list)/sizeof(string_list[0]);
sort_strings(string_list, string_count);
for (int i = 0; i < string_count; i++) {
printf("%s\n", string_list[i]);
}
}
extension_build.py:
from cffi import FFI
ffibuilder = FFI()
ffibuilder.cdef('void sort_strings(char** arr, size_t string_count);')
ffibuilder.set_source('_extension',
'#include "extension.h"',
sources=['extension.c'],
libraries=[])
if __name__ == "__main__":
ffibuilder.compile(verbose=True)
demo.py:
from _extension.lib import sort_strings
from _extension import ffi
string_list = ["cat", "penguin", "mouse"]
bytes_list = [s.encode("latin1") for s in string_list]
cdata_list = ffi.new("char **", bytes_list)
sort_strings(cdata_list, len(string_list))
# print sorted list
How to test:
python extension_build.py
python demo.py
I have tried passing string_list, bytes_list and cdata_list as first input argument to sort_strings. I get these error messages:
TypeError: initializer for ctype 'char *' must be a cdata pointer, not str
TypeError: initializer for ctype 'char *' must be a cdata pointer, not bytes
TypeError: initializer for ctype 'char *' must be a cdata pointer, not list
How can I pass my list of strings correctly (if possible without copying the list)?
(Just in case my intention is not clear: I'm not asking how to sort a list in Python.)
SOLUTION:
(Based on Armin Rigo's answer.)
It works when demo.py is updated like this:
from _extension.lib import sort_strings
from _extension import ffi
string_list = ["cat", "penguin", "mouse"]
bytes_list = [ffi.new("char[]", s.encode("latin1")) for s in string_list]
pointer = ffi.new("char*[]", bytes_list)
sort_strings(pointer, len(string_list))
for s in pointer:
print(ffi.string(s).decode("latin1"))
You need to replace
with
because
ffi.new("T*")allocates a singleTand returns a pointer to it, whereasffi.new("T[]", length_or_list)allocates an array of multipleT.EDIT:
Ah no, another problem is that you have to allocate each
char *explicitly, so you need:and then this
bytes_listcan be passed directly as argument tosort_string()(or you can passcdata_listas above, but CFFI will do the same thing if you just passbytes_listdirectly).EDIT 2:
Passing
bytes_listorcdata_listis not 100% equivalent: if you make an explicitcdata_listand use it in the call, then you can read from it after the call to learn how the C function changed this C array (as it does in your example). If you passbytes_listinstead, then CFFI converts it to the same C array before the call, but frees the C array immediately after the call.