It is confusing, and I often get confused by this too.
But I’ll try to explain what’s happening under the hood.
From Micropython perspective, each struct is of a different type, but all structs share in common an internal structure containing a
typedef struct mp_lv_struct_t
That means that every “struct” is, under the hood, really a pointer. It get’s allocated on
make_new_lv_struct, or copied from another struct or from a C pointer. Blob is in fact under the hood the same thing.
What differs different structs form each other and from Blob are the data members that are defined differently for each struct according to their fields and member functions (determined according to a naming convention). In case of Blob, it has only two predefined members
cast, which all other structs share too.
When the binding receives a
void* and needs to convert it to a Python Object, it converts it to a Blob and stores the pointer value in
This is done by
STATIC inline mp_obj_t ptr_to_mp(void *data)
return lv_to_mp_struct(&mp_blob_type, data);
handle_ptr doesn’t point to the handle “python object”. It converts back and forth to the underlying
data member that represents the struct/Blob data.
I think the source of the confusion is that passing a
C_Pointer is already a pointer to a pointer.
p = esp.C_Pointer()
This would call
func with a NULL pointer, as p data is initialized to 0 and casted to
p = esp.C_Pointer()
This would pass a pointer to the C_Pointer struct as func argument.
Here is an example of C_Pointer usage which resembles your use case:
ptr_to_spi = esp.C_Pointer()
ret = esp.spi_bus_add_device(self.spihost, devcfg, ptr_to_spi)
if ret != 0: raise RuntimeError("Failed adding SPI device")
self.spi = ptr_to_spi.ptr_val
spi_bus_add_device is defined like this:
esp_err_t spi_bus_add_device(..., spi_device_handle_t *handle);
typedef struct spi_device_t *spi_device_handle_t;
So as you can see here, we actually pass a pointer to a pointer.
spi_bus_add_device updates the pointer which
handle points to.