0

So I'm learning how to make Python modules in C and today I wanted to learn how to create a custom type. I wrote a module with one class named "Car" which stores a name, year and maxGas. Problem is, when I try to create an instance of this class in Python the name property contains random characters instead of the given name.

#include <Python.h>
#include <stdio.h> //printf debugging
#include <stdlib.h> //free
#include "structmember.h"

typedef struct {
    PyObject_HEAD
    char* name; //allocated and freed by Python
    int year;
    float maxGas;
} cars_Car;

static int Car_init(cars_Car* self, PyObject* args, PyObject* kwargs){
    /*
    //Method #1
    char* tmp;
    //                                v- Note the &
    if(!PyArg_ParseTuple(args, "sif", &tmp, &self->year, &self->maxGas)){
        return -1;
    }
    self->name = (char*)malloc( sizeof(char) * (strlen(tmp) + 1) );
    sprintf(self->name, "%s", tmp);
    printf("Given name: %s\n", self->name);
    //Works
    */

    //Method #2
    //                                v- Note the &
    if(!PyArg_ParseTuple(args, "sif", &self->name, &self->year, &self->maxGas)){
        return -1;
    }
    printf("Given name: %s\n", self->name); // Works, but repr() and name/getName() are broken
    return 0;
}

static void Car_dealloc(cars_Car* self){
    //free(self->name); //For Method #2 in init()
    Py_TYPE(self)->tp_free((PyObject*)self);
}

static PyObject* Car_repr(cars_Car* self){
    return PyUnicode_FromFormat("<Car name=\"%s\" year=%d tankSize=%SL>",
        self->name, self->year, Py_BuildValue("f", self->maxGas) /* %f not supported, so we convert it to a Python string */ );
}

static PyObject* Car_new(PyTypeObject* type, PyObject* args, PyObject* kwargs){
    cars_Car* self;
    
    self = (cars_Car*)type->tp_alloc(type, 0);
    self->name = NULL;
    self->year = 0;
    self->maxGas = 0.0f;
    
    return (PyObject*)self;
}

/* Methods */

static PyObject* Car_getName(cars_Car* self, PyObject* args){
    return Py_BuildValue("s", self->name);
}

static PyObject* Car_getYear(cars_Car* self, PyObject* args){
    return Py_BuildValue("i", self->year);
}

static PyObject* Car_getTankSize(cars_Car* self, PyObject* args){
    return Py_BuildValue("f", self->maxGas);
}

static PyMethodDef Car_methods[] = {
    {"getName", (PyCFunction)Car_getName, METH_NOARGS,
     "Get the car's name.",
    },
    {"getYear", (PyCFunction)Car_getYear, METH_NOARGS,
     "Get the car's year.",
    },
    {"getTankSize", (PyCFunction)Car_getTankSize, METH_NOARGS,
     "Get the car's tank size.",
    },
    {NULL}
};

static struct PyMemberDef Car_members[] = {
    {"name", T_STRING, offsetof(cars_Car, name), READONLY, "Car name"},
    {"year", T_INT, offsetof(cars_Car, year), READONLY, "Car year"},
    {"maxGas", T_FLOAT, offsetof(cars_Car, maxGas), READONLY, "Car maxGas"},
    {NULL}
};

static PyTypeObject cars_CarType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "cars.Car",             /* tp_name */
    sizeof(cars_Car), /* tp_basicsize */
    0,                         /* tp_itemsize */
    (destructor)Car_dealloc,   /* tp_dealloc */
    0,                         /* tp_print */
    0,                         /* tp_getattr */
    0,                         /* tp_setattr */
    0,                         /* tp_reserved */
    (reprfunc)Car_repr,        /* tp_repr */
    0,                         /* tp_as_number */
    0,                         /* tp_as_sequence */
    0,                         /* tp_as_mapping */
    0,                         /* tp_hash  */
    0,                         /* tp_call */
    0,                         /* tp_str */
    0,                         /* tp_getattro */
    0,                         /* tp_setattro */
    0,                         /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,        /* tp_flags */
    "Cars objects",            /* tp_doc */
    0,                         /* tp_traverse */
    0,                         /* tp_clear */
    0,                         /* tp_richcompare */
    0,                         /* tp_weaklistoffset */
    0,                         /* tp_iter */
    0,                         /* tp_iternext */
    Car_methods,               /* tp_methods */
    Car_members,               /* tp_members */
    NULL,                      /* tp_getset */
    NULL,                      /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    (initproc)Car_init,        /* tp_init */
    0,                         /* tp_alloc */
    Car_new,                   /* tp_new */
    0,                         /* tp_free */
    0                          /* tp_is_gc */
};

static PyModuleDef modcars = {
    PyModuleDef_HEAD_INIT,
    "cars",
    "Example module that creates an extension type.",
    -1,
    NULL, NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_Cars(void) {
    PyObject* m;

    cars_CarType.tp_new = PyType_GenericNew;
    if (PyType_Ready(&cars_CarType) < 0)
        return NULL;

    m = PyModule_Create(&modcars);
    if (m == NULL)
        return NULL;

    Py_INCREF(&cars_CarType);
    PyModule_AddObject(m, "Car", (PyObject *)&cars_CarType);
    return m;
}

As you can see, I mentioned 2 different methods in the Car_init function. When using the first method, I get the following result:

>>> import Cars
>>> x = Cars.Car("The Big Car", 2020, 4.2)
Given name: The Big Car #printf() works
>>> x
<Car name="????" year=2020 tankSize=4.199999809265137L #??? - bunch of random characters

I'm not sure what's causing this, although my guess is that my code is probably writing to some memory address where it's not supposed to. It also looks like there's a floating point precision error.
Interestingly when I directly call getName() or try to access the name attribute, it works.

>>> Cars.Car("The Big Car", 2020, 4.2).name
'The Big Car'
>>> Cars.Car("The Big Car", 2020, 4.2).getName()
'The Big Car'
>>> repr(Cars.Car("The Big Car", 2020, 4.2))
'<Car name="The Big Car" year=2020 tankSize=4.199999809265137L>'

If I switch to the second method everything works fine. I'm not sure why using a temporary variable helps. Also my code doesn't write anything to name, except when initialising it. Everywhere else it's just being read. I also thought about the garbage collector, maybe it's freeing name?

1 Answers1

0

The documentation for the s argument says

A pointer to an existing string is stored in the character pointer variable whose address you pass

Implicitly this pointer is only valid while the existing object is valid (i.e. possibly only until the args tuple is freed).

For your first example that's fine - you allocate new memory with malloc and copy from it. You do that during your initial function call so you know the args tuple and all its contents still exists. You need to remember to free the memory of course.

For your second example you just take a pointer to some memory owned by something else. Keeping a pointer to the memory doesn't do anything to tell Python to keep the memory valid. Therefore the memory is valid during Car_init but most likely invalid shortly after that.

DavidW
  • 29,336
  • 6
  • 55
  • 86