pythondllctypes

Automatically encode/decode ctypes argument and return value


I have a shared library exposing a function:

const char* foo(const char* s);

To call it from Python using ctypes, I can do the following:

import ctypes

foo = ctypes.cdll.LoadLibrary('foo.so')
foo.foo.argtypes = [ctypes.c_char_p]
foo.foo.restype = ctypes.c_char_p

print(foo.foo('Hello World!'.encode('utf8')).decode('utf8'))

Is there a way to embed the encode/decode('utf8') inside the type itself? I want to do the following:

foo.foo.argtypes = [c_utf8_p]
foo.foo.restype = c_utf8_p

print(foo.foo('Hello World!'))

I was able to handle the argtypes with following:

class c_utf8_p:
    @classmethod
    def from_param(cls, obj: str):
        return obj.encode('utf8')

But cannot figure out the return type. restype docs say assigning it to anything that is not a ctypes type is deprecated, plus truncates value to int which destroys 64-bit pointers. I would like to avoid using errcheck since logically I am not checking any errors, plus it would require setting two attributes for every function.


Solution

  • There is not a documented way to do what you want with .restype. I think you have two choices:

    1. Use .errcheck, since it allows access to the actual return value and can return a modified version. An advantage is it is only setting an extra attribute per function, vs. #2...

    2. Wrapper function. It would be needed for each function that needed post-processing.

    Examples:

    // test.c - cl /LD test.c
    #include <stdlib.h>
    #include <string.h>
    
    __declspec(dllexport)
    const char* foo(const char* s) {
        char* ret = malloc(strlen(s) * 2 + 1);
        strcpy(ret, s);
        strcat(ret, s);
        return ret;
    }
    
    __declspec(dllexport)
    void free_foo(const char* s) {
        free((void*)s);
    }
    

    Using .errcheck:

    import ctypes as ct
    
    class c_utf8_p:
        @classmethod
        def from_param(cls, obj):
            return obj.encode()
    
    def decode_bytes(result, func, args):
        s = ct.string_at(result).decode()
        dll.free_foo(result)
        return s
    
    dll = ct.CDLL('./test')
    foo = dll.foo
    dll.foo.argtypes = c_utf8_p,
    foo.restype = ct.POINTER(ct.c_char)
    foo.errcheck = decode_bytes
    free_foo = dll.free_foo
    free_foo.argtypes = ct.POINTER(ct.c_char),
    free_foo.restype = None
    
    s = foo('Hello World!')
    print(type(s), s)
    

    Output:

    <class 'str'> Hello World!Hello World!
    

    Using a wrapper:

    import ctypes as ct
    
    dll = ct.CDLL('./test')
    _foo = dll.foo
    _foo.argtypes = ct.c_char_p,
    _foo.restype = ct.POINTER(ct.c_char)
    dll.free_foo.argtypes = ct.POINTER(ct.c_char),
    dll.free_foo.restype = None
    
    def foo(s):
        p = _foo(s.encode())
        s = ct.string_at(p).decode()
        dll.free_foo(p)
        return s
    
    s = foo('Hello World!')
    print(type(s), s)
    

    Output:

    <class 'str'> Hello World!Hello World!