views:

1191

answers:

1

I am trying to use Python's ctypes library to access some methods in the scanning library SANE. This is my first experience with ctypes and the first time I have had to deal with C datatypes in over a year so there is a fair learning curve here, but I think even without that this particular declaration would be troublesome:

extern SANE_Status sane_get_devices (const SANE_Device *** device_list, SANE_Bool local_only);

First of all, I've successfully dealt with SANE_Status (an enum) and SANE_Bool (a typedef to c_int). Those were both simple. That first parameter, on the other hand, is causing me all sorts of grief. I'm unfamiliar with the "***" notation to begin with and my tracer bullets so far have yielded nothing more than garbage data. How do I format the input to this function such that I can read back a list of my Python structure-objects? For reference, the C structure being referenced is:

typedef struct
{
    SANE_String_Const name; /* unique device name */
    SANE_String_Const vendor;   /* device vendor string */
    SANE_String_Const model;    /* device model name */
    SANE_String_Const type; /* device type (e.g., "flatbed scanner") */
 }
SANE_Device;

Where SANE_String_Const is defined as a c_char_p.

My Python/ctypes version of this object is:

class SANE_Device(Structure):
    _fields_ = [
        ("name", c_char_p),
        ("vendor", c_char_p),
        ("model", c_char_p),
        ("type", c_char_p)]

Suggestions on what I should pass in such that I can get the expected behavior (a list of structure-objects) out of this? All responses appreciated.

Update 1:

Using the following, I was able to retrieve a correct SANE_Device Python structure:

devices = pointer(pointer(pointer(SANE_Device())))
status = libsane.sane_get_devices(devices, c_int(0))
print status, devices, devices.contents.contents.contents.name

However, 1) yuck and 2) that seems like it would only work if there is a single result. I can't len() on devices.contents.contents or devices.contents.contents.contents. How am I to determine the number of results? The SANE docs specify that "If the function executes successfully, it stores a pointer to a NULL terminated array of pointers to SANE_Device structures in *device_list". Suggestions?

Update 2:

I was able to pass an ten-item array and then access the first element using:

devices = pointer(pointer(pointer((SANE_Device * 10)())))
status = libsane.sane_get_devices(devices, c_int(0))
print status, devices, devices.contents.contents.contents[0].name

However, ten is obviously an arbitrary number and I have no way of determining the actual number of results. Trying to access devices.contents.contents.contents[1].name when only one device is connected causes a segmentation fault. There must be a proper way of dealing with variable-length constructs like these in ctypes.

+4  A: 

A const SANE_Device *** is a three-level pointer: it's a pointer to a pointer to a pointer to a constant SANE_Device. You can use the program cdecl to decipher complicated C/C++ type definitions.

According to the SANE documentation, SANE_get_devices() will store a pointer to a NULL-terminated list of pointers to SANE devices if successful. Thus, the proper way to call it is to declare a variable of type const SANE_Device ** (i.e. a pointer to a pointer to a constant `SANE_Device), and pass in the address of that pointer:

const SANE_Device **device_list;
SANE_get_devices(&device_list, local_only);  // check return value
// Now, device_list[0] points to the first device,
// device_list[1] points to the second device, etc.
// Once you hit a NULL pointer, that's the end of the list:
int num_devices = 0;
while(device_list[num_devices] != NULL)
    num_devices++;
// num_devices now stores the total number of devices

Now, this is how you would call it from C code. I've skimmed the documentation on ctypes, and it appears that you want to use the byref function to pass the argument by reference, and that the value you pass should be a POINTER to a POINTER to a SANE_Device. Note the distinction between pointer and POINTER: the former creates a pointer to an instance, whereas the latter creates a pointer to a type. Thus, I'm guessing the following code will work:

// SANE_Device declared as you had it
devices = POINTER(POINTER(SANE_Device))()  // devices is a NULL pointer to a pointer to a SANE_Device
status = libsane.sane_get_devices(byref(devices), c_int(0))
if status != successful:   // replace this by whatever success is
    print error
else:
    num_devices = 0
    // Convert NULL-terminated C list into Python list
    device_list = []
    while devices[num_devices]:
        device_list.append(devices[num_devices].contents)  // use .contents here since each entry in the C list is itself a pointer
        num_devices += 1
    print device_list

[Edit] I've tested the above code using a very simple placeholder for SANE_get_devices, and it works.

Adam Rosenfield
My goodness this is an extraordinarily thorough answer and, better yet, it works! And better even than that, I'm not sure I ever would have come to this on my own. Thanks Adam!
bouvard
That's a very nice answer Adam.
sstock