Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
- '3.12'
- '3.13'
- '3.14'
- '3.14t'
- 'pypy3.9'
- 'pypy3.10'
steps:
Expand Down
127 changes: 85 additions & 42 deletions radix/_radix.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
# define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
#endif

/* Critical sections are defined in the C API in Python 3.13 and newer */
#if PY_VERSION_HEX < 0x030D0000
# define Py_BEGIN_CRITICAL_SECTION(op) {
# define Py_END_CRITICAL_SECTION() }
#endif

/* Prototypes */
struct _RadixObject;
struct _RadixIterObject;
Expand Down Expand Up @@ -118,8 +124,9 @@ RadixNode_dealloc(RadixNodeObject *self)
static PyObject *
Radix_parent(RadixNodeObject *self, void *closure)
{
PyObject *ret;
PyObject *ret = Py_None;
radix_node_t *node;
Py_BEGIN_CRITICAL_SECTION(self);
node = self->rn;
/* walk up through parent to find parent node with data */
for ( ; ; )
Expand All @@ -130,14 +137,15 @@ Radix_parent(RadixNodeObject *self, void *closure)
if (node->data)
{
ret = node->data;
Py_XINCREF(ret);
return ret;
break;
}
}
else
break;
}
Py_RETURN_NONE;
Py_XINCREF(ret);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm less familiar with the python c api than i once was, but i assume incrementing the ref count to Py_None is either a no-op or (since it's None) so meaningless that it's irrelevant.

Copy link
Contributor Author

@ngoldbaum ngoldbaum Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, it was only necessary to do that on CPython 3.12 and older. Here's the definition of Py_RETURN_NONE in the CPython headers:

/* Macro for returning Py_None from a function.
   * Only treat Py_None as immortal in the limited C API 3.12 and newer. */
  #if defined(Py_LIMITED_API) && Py_LIMITED_API+0 < 0x030c0000
  #  define Py_RETURN_NONE return Py_NewRef(Py_None)
  #else
  #  define Py_RETURN_NONE return Py_None
  #endif

(Py_NewRef is a variant of Py_INCREF that returns the object).

There's also an optimization in the implementation of Py_INCREF itself that skips immortal objects.

I restructured the code in this function to always incref (except for NULL return values on errors) because the critical section macros include braces, so it's not syntactically possible to ensure the critical section is closed in an early return block. Instead, all the code paths need to return after the critical section is closed.

IMO it's not worth worrying about optimizing things like avoiding increfs on immortal objects. We should leave it up to CPython to optimize stuff like that and instead aim to make the code as clear and correct as possible.

Py_END_CRITICAL_SECTION();
return ret;
}
static PyMemberDef RadixNode_members[] = {
{"data", T_OBJECT, offsetof(RadixNodeObject, user_attr), READONLY},
Expand Down Expand Up @@ -361,7 +369,9 @@ Radix_add(RadixObject *self, PyObject *args, PyObject *kw_args)
if ((prefix = args_to_prefix(NULL, addr, packed, packlen, prefixlen)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION((PyObject *)self);
node_obj = create_add_node(self, prefix);
Py_END_CRITICAL_SECTION();
Deref_Prefix(prefix);

return node_obj;
Expand All @@ -383,27 +393,36 @@ Radix_delete(RadixObject *self, PyObject *args, PyObject *kw_args)
char *addr = NULL, *packed = NULL;
long prefixlen = -1;
Py_ssize_t packlen = -1;
PyObject *ret = Py_None;

if (!PyArg_ParseTupleAndKeywords(args, kw_args, "|zlz#:delete", keywords,
&addr, &prefixlen, &packed, &packlen))
return NULL;
if ((prefix = args_to_prefix(&lprefix, addr, packed, packlen, prefixlen)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION(self);
if ((node = radix_search_exact(self->rt, prefix)) == NULL) {
PyErr_SetString(PyExc_KeyError, "no such address");
return NULL;
ret = NULL;
}
if (node->data != NULL) {
node_obj = node->data;
node_obj->rn = NULL;
Py_XDECREF(node_obj);
if (ret != NULL) {
if (node->data != NULL) {
node_obj = node->data;
node_obj->rn = NULL;
Py_XDECREF(node_obj);
}

radix_remove(self->rt, node);
}

radix_remove(self->rt, node);
if (ret != NULL) {
self->gen_id++;
}

self->gen_id++;
Py_INCREF(Py_None);
return Py_None;
Py_XINCREF(ret);
Py_END_CRITICAL_SECTION();
return ret;
}

PyDoc_STRVAR(Radix_search_exact_doc,
Expand All @@ -419,7 +438,7 @@ static PyObject *
Radix_search_exact(RadixObject *self, PyObject *args, PyObject *kw_args)
{
radix_node_t *node;
RadixNodeObject *node_obj;
PyObject *node_obj;
prefix_t lprefix, *prefix;
static char *keywords[] = { "network", "masklen", "packed", NULL };

Expand All @@ -432,15 +451,17 @@ Radix_search_exact(RadixObject *self, PyObject *args, PyObject *kw_args)
return NULL;
if ((prefix = args_to_prefix(&lprefix, addr, packed, packlen, prefixlen)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION(self);
node = radix_search_exact(self->rt, prefix);
if (node == NULL || node->data == NULL) {
Py_INCREF(Py_None);
return Py_None;
node_obj = Py_None;
}
else {
node_obj = (PyObject *)node->data;
}
node_obj = node->data;
Py_XINCREF(node_obj);
return (PyObject *)node_obj;
Py_END_CRITICAL_SECTION();
return node_obj;
}

PyDoc_STRVAR(Radix_search_best_doc,
Expand All @@ -457,7 +478,7 @@ static PyObject *
Radix_search_best(RadixObject *self, PyObject *args, PyObject *kw_args)
{
radix_node_t *node;
RadixNodeObject *node_obj;
PyObject *node_obj;
prefix_t lprefix, *prefix;
static char *keywords[] = { "network", "masklen", "packed", NULL };

Expand All @@ -470,15 +491,17 @@ Radix_search_best(RadixObject *self, PyObject *args, PyObject *kw_args)
return NULL;
if ((prefix = args_to_prefix(&lprefix, addr, packed, packlen, prefixlen)) == NULL)
return NULL;

if ((node = radix_search_best(self->rt, prefix)) == NULL ||
node->data == NULL) {
Py_INCREF(Py_None);
return Py_None;
Py_BEGIN_CRITICAL_SECTION(self);
node = radix_search_best(self->rt, prefix);
if (node == NULL || node->data == NULL) {
node_obj = Py_None;
}
else {
node_obj = (PyObject *)node->data;
}
node_obj = node->data;
Py_XINCREF(node_obj);
return (PyObject *)node_obj;
Py_END_CRITICAL_SECTION();
return node_obj;
}

PyDoc_STRVAR(Radix_search_worst_doc,
Expand All @@ -495,7 +518,7 @@ static PyObject *
Radix_search_worst(RadixObject *self, PyObject *args, PyObject *kw_args)
{
radix_node_t *node;
RadixNodeObject *node_obj;
PyObject *node_obj;
prefix_t lprefix, *prefix;
static char *keywords[] = { "network", "masklen", "packed", NULL };

Expand All @@ -508,15 +531,17 @@ Radix_search_worst(RadixObject *self, PyObject *args, PyObject *kw_args)
return NULL;
if ((prefix = args_to_prefix(&lprefix, addr, packed, packlen, prefixlen)) == NULL)
return NULL;

if ((node = radix_search_worst(self->rt, prefix)) == NULL ||
node->data == NULL) {
Py_INCREF(Py_None);
return Py_None;
Py_BEGIN_CRITICAL_SECTION(self);
node = radix_search_worst(self->rt, prefix);
if (node == NULL || node->data == NULL) {
node_obj = Py_None;
}
else {
node_obj = (PyObject *)node->data;
}
node_obj = node->data;
Py_XINCREF(node_obj);
return (PyObject *)node_obj;
Py_END_CRITICAL_SECTION();
return node_obj;
}

static int
Expand Down Expand Up @@ -555,7 +580,9 @@ Radix_search_covered(RadixObject *self, PyObject *args, PyObject *kw_args)
if ((ret = PyList_New(0)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION(self);
radix_search_covered(self->rt, prefix, add_node_to_list, ret, 1);
Py_END_CRITICAL_SECTION();

return (ret);
}
Expand Down Expand Up @@ -591,7 +618,9 @@ Radix_search_covering(RadixObject *self, PyObject *args, PyObject *kw_args)
return NULL;
}

Py_BEGIN_CRITICAL_SECTION(self);
radix_search_covering(self->rt, prefix, add_node_to_list, ret);
Py_END_CRITICAL_SECTION();

return ret;
}
Expand All @@ -615,10 +644,12 @@ Radix_nodes(RadixObject *self, PyObject *args)
if ((ret = PyList_New(0)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION(self);
RADIX_TREE_WALK(self->rt, node) {
if (node->data != NULL)
PyList_Append(ret, (PyObject *)node->data);
} RADIX_TREE_WALK_END;
Py_END_CRITICAL_SECTION();

return (ret);
}
Expand All @@ -642,12 +673,14 @@ Radix_prefixes(RadixObject *self, PyObject *args)
if ((ret = PyList_New(0)) == NULL)
return NULL;

Py_BEGIN_CRITICAL_SECTION(self);
RADIX_TREE_WALK(self->rt, node) {
if (node->data != NULL) {
PyList_Append(ret,
((RadixNodeObject *)node->data)->prefix);
}
} RADIX_TREE_WALK_END;
Py_END_CRITICAL_SECTION();

return (ret);
}
Expand Down Expand Up @@ -767,19 +800,21 @@ static PyObject *
RadixIter_iternext(RadixIterObject *self)
{
radix_node_t *node;
PyObject *ret;
PyObject *ret = Py_None;

if (self->gen_id != self->parent->gen_id) {
PyErr_SetString(PyExc_RuntimeWarning,
"Radix tree modified during iteration");
return (NULL);
}

Py_BEGIN_CRITICAL_SECTION(self);
again:
if ((node = self->rn) == NULL) {
/* We have walked both trees */
if (self->af == AF_INET6)
return NULL;
if (self->af == AF_INET6) {
ret = NULL;
goto done;
}
/* Otherwise reset and start walk of IPv6 tree */
self->sp = self->iterstack;
self->rn = self->parent->rt->head_ipv6;
Expand All @@ -801,13 +836,17 @@ RadixIter_iternext(RadixIterObject *self)

if (node->prefix == NULL || node->data == NULL)
goto again;
done:
if (ret == Py_None) {
ret = node->data;
}
Py_XINCREF(ret);
Py_END_CRITICAL_SECTION();

ret = node->data;
Py_INCREF(ret);
return (ret);
}

PyDoc_STRVAR(RadixIter_doc,
PyDoc_STRVAR(RadixIter_doc,
"Radix tree iterator");

static PyTypeObject RadixIter_Type = {
Expand Down Expand Up @@ -994,6 +1033,10 @@ static PyObject *module_initialize(void)
m = Py_InitModule3("_radix", radix_methods, module_doc);
#endif

#if Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif

/* Stash the callable constructor for use in Radix.__reduce__ */
d = PyModule_GetDict(m);
radix_constructor = PyDict_GetItemString(d, "Radix");
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from os.path import abspath, dirname, join

# specify the version
version = 'v1.0.5'
version = 'v1.1.0'

here = abspath(dirname(__file__))

Expand Down
Loading
Loading