0

Let's say I want to write a module called countries that contains information about the countries of the world. I want to write it in C. It will expose a member called countries which is a list of objects of type Country. The Country type has two members: name and capital, both strings. So we could do something like this:

>>> from countries import countries
>>> countries[0].name
Afghanistan
>>> countries[0].capital
Kabul

Following the various scraps of tutorials I was able to find online, I figured out how to create my custom Country type in C. It looks basically like this:

typedef struct {
    PyObject_HEAD
    char *name;
    char *capital;
} Country;

static struct PyMemberDef CountryMembers[] = {
    {"name", T_OBJECT_EX, offsetof(Country, name), 0, "name"},
    {"capital", T_OBJECT_EX, offsetof(Country, capital), 0, "capital"},
    {NULL}
};

static PyTypeObject CountryType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "countries.Country",
    .tp_doc = "Country",
    .tp_basicsize = sizeof(Country),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
    .tp_members = CountryMembers
};

and then in the module init function, call Py_TypeReady and add the type to the module. However, what I cannot figure out how to do is create a new instance of this type, in C, add it to the module, and populate its fields. The closest I can get is this, again in the module init function:

PyObject *afghanistan = PyObject_CallObject((PyObject*) &CountryType, NULL);
PyModule_AddObject(local_module, "afghanistan", (PyObject*) afghanistan);

But I don't know how to fill in the members.

khelwood
  • 55,782
  • 14
  • 81
  • 108
Jack M
  • 4,769
  • 6
  • 43
  • 67
  • `T_OBJECT_EX` and `char *` don't match. Either `T_OBJECT_EX` should be `T_STRING`, or `char *` should be `PyObject *`, depending on how you actually want these objects to work. – user2357112 Dec 03 '21 at 09:29
  • You really ought to write a meaningful constructor, too. – user2357112 Dec 03 '21 at 09:32
  • @user2357112supportsMonica Do I have to? That feels like it should be unnecessary, considering I just want to populate these things with fixed values. I don't intend for anyone to construct objects of type `Country` from Python. – Jack M Dec 03 '21 at 10:21

1 Answers1

1

You just need to cast the PyObject* to a Country* and then you can set the members yourself:

Country *afghanistan = (Country*)PyObject_CallObject((PyObject*) &CountryType, NULL);
afghanistan->name = "Afghanistan";
afghanistan->captial = "Kabul";
PyModule_AddObject(local_module, "afghanistan", (PyObject*) afghanistan);

Obviously if you want name and capital to point to non-static strings then you need to malloc them and free them appropriately in the constructor.


Given that it sounds like you don't want this class to be constructable from Python then you might to properly ban that with Py_TPFLAGS_DISALLOW_INSTANTIATION (on older versions of Python just set tp_new to NULL). You'd then create a C-only "constructor" function:

static Country* MakeCountry(const char* name, const char* capital) {
   Country *c = CountryType.tp_alloc(&CountryType, 0);
   c->name = name;
   c->capital = capital;
   return c;
}

And use that function instead of PyObject_CallObject((PyObject*)&CountryType, NULL).

DavidW
  • 29,336
  • 6
  • 55
  • 86