'How does ctypes define the class for C structure with function pointer only and init the variable in python?

I'm working on the python with ctypes to call the c so file, but the c file define the structure with function pointer

// mem ==================================================================
typedef struct StdMemFunc
{
    void* (*const malloc)   (unsigned long size);
    void  (*const free)     (void* ptr);
    void* (*const realloc)  (void* ptr, unsigned long size);
    void* (*const calloc)   (unsigned long count, unsigned long size);
    void* (*const set)      (void* ptr, int value, unsigned long num);
    void* (*const copy)     (void* dest, const void* src, unsigned long num);
}*StdMemFunc;

typedef struct StdLib
{
    const uint32_t          version;
    bool  (*const is_version_compatible)    (uint32_t version, uint32_t func_mask);
    void  (*const delay)                    (int32_t milli_sec);
    const StdMemFunc        mem;
}*StdLib;

and mock the function in another file as below

void *std_malloc(unsigned long size)
{
    return malloc(size);
}

void std_free(void *ptr)
{
    free(ptr);
}

void *std_realloc(void *ptr, unsigned long size)
{
    return realloc(ptr, size);
}

void *std_calloc(unsigned long count, unsigned long size)
{
    return calloc(count, size);
}

void *std_memset(void *ptr, int value, unsigned long num)
{
    return memset(ptr, value, num);
}

void *std_memcopy(void *dest, const void *src, unsigned long num)
{
    return memcpy(dest, src, num);
}

struct StdMemFunc mem_func =
{
    .malloc = std_malloc,
    .free = std_free,
    .realloc = std_realloc,
    .calloc = std_calloc,
    .set = std_memset,
    .copy = std_memcopy
};

then the python need to call another method with std_lib as paramater, the std_lib with call mem->malloc() method in C part, so how to define the class in the python with ctypes?

I have tried the below one, but it was not work

class StdMemFunc(Structure):
    _fields_ = [
        ("malloc", ctypes.CFUNCTYPE(c_void_p, c_ulong)),
        ("free", ctypes.CFUNCTYPE(None, c_void_p)),
        ("realloc", ctypes.CFUNCTYPE(c_void_p, c_void_p, c_ulong)),
        ("calloc", ctypes.CFUNCTYPE(c_void_p, c_ulong, c_ulong)),
        ("set", ctypes.CFUNCTYPE(c_void_p, c_void_p, c_int, c_ulong)),
        ("copy", ctypes.CFUNCTYPE(c_void_p, c_void_p, c_ulong))
    ]
class StdLib(Structure):
    _fields_ = [
        ("version", c_uint32),
        ("is_version_compatible", c_bool),
        ("delay", c_void_p),
        ("mem", POINTER(StdMemFunc)),
    ]
libc_std_lib = CDLL('/home/linus/code/galileo/mock_std_lib.so')
std_lib = StdLib()
std_lib.mem.malloc = libc_std_lib.std_malloc

libc_modbus.modbus_create_server_station.argtypes = [POINTER(ModbusNodeDef), c_int, StdLib, PlcDrvAccessor]
libc_modbus.modbus_create_server_station.restype = POINTER(ModbusStation)
libc_modbus.modbus_create_server_station(node_def, node_num, std_lib, plc_drv_accessor)


Solution 1:[1]

It looks like there are two problems here:

  1. The is_version_compatible and delay fields in the StdLib struct are functions, but you are defining them as constants.
  2. You are not instantiating all the fields in the struct, meaning that the program might be trying to dereference a null pointer, as null pointers are the default value for pointer types.

The StdLib struct definition should look something like this:

class StdLib(Structure):
    _fields_ = [
        ("version", c_uint32),
        ("is_version_compatible", CFUNCTYPE(POINTER(c_bool), c_uint32, _uint32)),
        ("delay", CFUNCTYPE(c_void_p, c_int32)),
        ("mem", POINTER(StdMemFunc)),
    ]

For the instantiation, I would do something like this:

libc_std_lib = CDLL('/home/linus/code/galileo/mock_std_lib.so')
std_mem_func = StdMemFunc(
    libc_std_lib.std_malloc,
    libc_std_lib.std_free,
    libc_std_lib.std_realloc,
    libc_std_lib.std_calloc,
    libc_std_lib.std_set,
    libc_std_lib.std_copy
)
std_lib = StdLib(
    1,
    reference_to_is_version_compatible_func,
    reference_to_delay_func,
    std_mem_func
)

Of course, you need to pass the correct params/function references to StdLib. Maybe you will need to mock the is_version_compatible and delay functions as well.

Disclaimer: this is entirely untested, so I don't guarantee it will work.

Solution 2:[2]

The OP's code isn't quite reproducible, but I was able to get the same error message on the following line:

std_lib.mem.malloc = libc_std_lib.std_malloc

If I am following correctly, the OP wants to initialize the C structure with functions that are provided in C, but libc.std_lib.std_malloc isn't wrapped properly to do that. It is a function that wraps a C function that is callable from Python, not C.

ctypes function prototypes can be instantiated a number of ways, and the one that works is:

prototype(func_spec[, paramflags])
Returns a foreign function exported by a shared library. func_spec must be a 2-tuple (name_or_ordinal, library). The first item is the name of the exported function as string, or the ordinal of the exported function as small integer. The second item is the shared library instance.

For example:

std_lib.mem.malloc = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_ulong)(('std_malloc',libc_std_lib))

Here's a working set of files:

test.cpp

#include <stdlib.h>
#include <stdint.h>
#include <memory.h>
#include <stdio.h>

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

extern "C" {

typedef struct StdMemFunc {
    void* (*const malloc)(unsigned long size);
    void (*const free)(void* ptr);
    void* (*const realloc)(void* ptr, unsigned long size);
    void* (*const calloc)(unsigned long count, unsigned long size);
    void* (*const set)(void* ptr, int value, unsigned long num);
    void* (*const copy)(void* dest, const void* src, unsigned long num);
} *StdMemFuncPtr;

typedef struct StdLib {
    const uint32_t          version;
    bool (*const is_version_compatible)(uint32_t version, uint32_t func_mask);
    void (*const delay)(int32_t milli_sec);
    const StdMemFunc        mem;
} *StdLibPtr;

API void* std_malloc(unsigned long size) {
    return malloc(size);
}

API void std_free(void* ptr) {
    free(ptr);
}

API void* std_realloc(void* ptr, unsigned long size) {
    return realloc(ptr, size);
}

API void* std_calloc(unsigned long count, unsigned long size) {
    return calloc(count, size);
}

API void* std_memset(void* ptr, int value, unsigned long num) {
    return memset(ptr, value, num);
}

API void* std_memcopy(void* dest, const void* src, unsigned long num) {
    return memcpy(dest, src, num);
}

// A couple of test functions that accepts the initialized structure
// and calls sum of the function pointers.

API char* testit(StdLib* test) {
    // This is how I debugged this, by comparing the *actual*
    // function pointer value to the one received from Python.
    // Once they matched the code worked.
    printf("%p %p\n", std_malloc, test->mem.malloc);
    char* p = static_cast<char*>(test->mem.malloc(10));
    test->mem.set(p, 'A', 9);
    p[9] = 0;
    return p;
}

API void freeit(StdLib* test, char* p) {
    test->mem.free(p);
}

}

test.py

import ctypes as ct

# prototypes
MALLOC = ct.CFUNCTYPE(ct.c_void_p,ct.c_ulong)
FREE = ct.CFUNCTYPE(None,ct.c_void_p)
REALLOC = ct.CFUNCTYPE(ct.c_void_p, ct.c_void_p, ct.c_ulong)
CALLOC = ct.CFUNCTYPE(ct.c_void_p, ct.c_ulong, ct.c_ulong)
SET = ct.CFUNCTYPE(ct.c_void_p,ct.c_void_p,ct.c_int,ct.c_ulong)
COPY = ct.CFUNCTYPE(ct.c_void_p, ct.c_void_p, ct.c_ulong)

class StdMemFunc(ct.Structure):
    _fields_ = [("malloc", MALLOC),
                ("free", FREE),
                ("realloc", REALLOC),
                ("calloc", CALLOC),
                ("set", SET),
                ("copy", COPY)]

class StdLib(ct.Structure):
    _fields_ = [("version", ct.c_uint32),
                # Note these two fields were function pointers as well.
                # Declared correctly now.
                ("is_version_compatible", ct.CFUNCTYPE(ct.c_bool, ct.c_uint32, ct.c_uint32)),
                ("delay", ct.CFUNCTYPE(None, ct.c_int32)),
                ("mem", StdMemFunc)]

dll = ct.CDLL('./test')
dll.testit.argtypes = ct.POINTER(StdLib),
dll.testit.restype = ct.POINTER(ct.c_char)
dll.freeit.argtypes = ct.POINTER(StdLib), ct.c_char_p
dll.freeit.restype = None

lib = StdLib()
lib.mem.malloc = MALLOC(('std_malloc', dll))
lib.mem.realloc = REALLOC(('std_realloc', dll))
lib.mem.calloc = CALLOC(('std_calloc', dll))
lib.mem.free = FREE(('std_free', dll))
lib.mem.set = SET(('std_memset', dll))
lib.mem.copy = COPY(('std_memcopy', dll))

p = dll.testit(lib)
# One way to access the data in the returned pointer is to slice it to the known length
print(p[:10])
# If known to be null-terminated, can also cast to c_char_p, which expects
# null-terminated data, and extract the value.
print(ct.cast(p,ct.c_char_p).value)
dll.freeit(lib,p)

Output:

b'AAAAAAAAA\x00'
b'AAAAAAAAA'

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jack Taylor
Solution 2 Mark Tolonen