Skip to content

Explain how to add an extension module #1350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 46 commits into from
Sep 11, 2024
Merged
Changes from 13 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
086ded3
explain how to add modules
picnixz Jul 13, 2024
96fb998
Update extension-modules.rst
picnixz Jul 13, 2024
5f8797c
Update extension-modules.rst
picnixz Jul 13, 2024
e5d41f8
Update extension-modules.rst
picnixz Jul 13, 2024
b740114
Update extension-modules.rst
picnixz Jul 13, 2024
c74df67
Update extension-modules.rst
picnixz Jul 13, 2024
e645869
Update extension-modules.rst
picnixz Jul 14, 2024
5a9a57c
Update extension-modules.rst
picnixz Jul 14, 2024
26a18eb
Update extension-modules.rst
picnixz Jul 14, 2024
1b4d73f
Update extension-modules.rst
picnixz Jul 14, 2024
94086dd
Address Hugo's feedback
picnixz Jul 14, 2024
9a78a3b
Update extension-modules.rst
picnixz Jul 14, 2024
e22c278
Update extension-modules.rst
picnixz Jul 14, 2024
1f51497
improvements
picnixz Jul 15, 2024
bdf09e5
fixup! sphinx
picnixz Jul 15, 2024
ef7cf3e
fixup! indents
picnixz Jul 15, 2024
1a405ba
fixup! warnings
picnixz Jul 15, 2024
e39cbc2
improve sections
picnixz Jul 15, 2024
35f207f
fix markup
picnixz Jul 15, 2024
316b00d
improve titles
picnixz Jul 15, 2024
523dece
improve presentation
picnixz Jul 15, 2024
d1cdd1d
fixup! markup
picnixz Jul 15, 2024
defb31e
simplify snippets
picnixz Jul 15, 2024
6213438
improvements
picnixz Jul 15, 2024
f6e5d55
improvements
picnixz Jul 15, 2024
d1a1ed5
some rewordings and cleanups
picnixz Jul 15, 2024
86e3e54
simplify wording
picnixz Jul 16, 2024
65f62e7
address Erlend's review
picnixz Jul 17, 2024
4b7c7d8
fix indents?
picnixz Jul 17, 2024
7abb6f1
add ref to clinic everywhere when needed
picnixz Jul 17, 2024
da9b58b
fix typos
picnixz Jul 17, 2024
783e6db
address encukou's review
picnixz Jul 18, 2024
8906ebd
improve the page flow
picnixz Jul 18, 2024
128c81c
use sentence case
picnixz Jul 18, 2024
7d6c8d6
add podman tip
picnixz Jul 18, 2024
01c25bc
address rest of the review
picnixz Jul 18, 2024
56910fb
address Alyssa's review
picnixz Jul 18, 2024
3ee1cea
add details
picnixz Jul 18, 2024
3d235f4
address review
picnixz Aug 4, 2024
7b0b234
Make it easier to update the required ubuntu version
picnixz Aug 4, 2024
7fa94bf
Merge remote-tracking branch 'upstream/main' into add-extensions-tuto…
picnixz Aug 4, 2024
ec48fdb
fixup!
picnixz Aug 4, 2024
1843e3d
fixup!
picnixz Aug 4, 2024
18c4d91
improve comment
picnixz Aug 14, 2024
8224bbd
use double quotes instead of single quotes
picnixz Aug 14, 2024
2f63053
Address Carol's review.
picnixz Sep 11, 2024
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
331 changes: 327 additions & 4 deletions developer-workflow/extension-modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,336 @@
Standard library extension modules
==================================

In this section, we could explain how to write a CPython extension with the C language, but the topic can take a complete book.
In this section, we could explain how to write a CPython extension with the
C language, but the topic can take a complete book. We will however explain
how to add a new extension module to the standard library, for instance, a
module responsible for accelerating some parts of the library.

For this reason, we prefer to give you some links where you can read a very good documentation.

Read the following references:
For writing a CPython extension itself, we prefer to give you some links
where you can read good documentation:

* https://docs.python.org/dev/c-api/
* https://docs.python.org/dev/extending/
* :pep:`399`
* https://pythonextensionpatterns.readthedocs.io/en/latest/

Adding an extension module to CPython
-------------------------------------

Assume that the standard library contains the module :mod:`!foo`
together with some :func:`!foo.bar` function:

.. code-block:: python

def bar():
return "Hello World!"

Instead of using the Python implementation of :func:`!foo.bar`, we want to
use its corresponding C implementation exposed as the :mod:`!_foo` module.
Ideally, we want to modify ``foo.py`` as follows:

.. code-block:: python

try:
# use the C implementation if possible
from _foo import bar
except ImportError:
# fallback to the pure Python implementation
def bar():
return "Hello World!"

Some modules in the standard library are implemented both in C and in Python,
such as :mod:`decimal` or :mod:`io`, and the C implementation is expected
to improve performance when available (such modules are commonly referred
to as *accelerator modules*). In our example, we need to:

- determine where to place the extension module;
- determine which files to modify in order to compile the project;
- determine which ``Makefile`` rules to invoke at the end.

Usually, accelerator modules are added in the :cpy-file:`Modules` directory of
the CPython project. If more than one file is needed for the extension
module, it is convenient to create a sub-directory in :cpy-file:`Modules`, and
place the files inside it.

For our extension, we will create the following files:

- ``Modules/foo/foomodule.h`` -- the shared prototypes for our mini-project.
- ``Modules/foo/foomodule.c`` -- the actual module's implementation.
- ``Modules/foo/helper.c`` -- some helper's implementation.

.. note::

If ``Modules/foo/foomodule.c`` contains some Argument Clinic directives,
the corresponding header file is written to ``Modules/foo/clinic/foomodule.c.h``.

For simplicity, we assume that the extension module does not rely on external dependencies
and is not a frozen module. The following code snippets illustrate the possible contents of
the above files:

.. code-block:: c

// Modules/foo/foomodule.h

#ifndef FOO_FOOMODULE_H
#define FOO_FOOMODULE_H

#include "Python.h"

typedef struct {
/* ... */
} foomodule_state;

static inline foomodule_state *
get_foomodule_state(PyObject *module)
{
void *state = PyModule_GetState(module);
assert(state != NULL);
return (foomodule_state *)state;
}

/* Helper implemented somewhere else. */
extern PyObject *_Py_fast_bar();

#endif // FOO_FOOMODULE_H

The actual implementation of the module is in the corresponding ``.c`` file:

.. code-block:: c

// Modules/foo/foomodule.c

#include "foomodule.h"
#include "clinic/foomodule.c.h"

/* Functions for the module's state */
static int
foomodule_exec(PyObject *module)
{
// imports, static attributes, exported classes, etc
return 0;
}

static int
foomodule_traverse(PyObject *m, visitproc visit, void *arg)
{
foomodule_state *st = get_foomodule_state(m);
// call Py_VISIT() on the state attributes
return 0;
}

static int
foomodule_clear(PyObject *m)
{
foomodule_state *st = get_foomodule_state(m);
// call Py_CLEAR() on the state attributes
return 0;
}

static void
foomodule_free(void *m) {
(void)foomodule_clear((PyObject *)m);
}

/* Implementation of publicly exported functions */

/*[clinic input]
module foo
[clinic start generated code]*/
/*[clinic end generated code: output=... input=...]*/

/*[clinic input]
foo.bar -> object

[clinic start generated code]*/
static PyObject *
foo_bar_impl(PyObject *module)
/*[clinic end generated code: output=... input=...]*/
{
return _Py_fast_bar();
}

/* Exported module's data */

static PyMethodDef foomodule_methods[] = {
// the following macro is available in 'Modules/foo/clinic/foomodule.c.h'
// after running 'make clinic'
FOO_BAR_METHODDEF
{NULL, NULL}
};

static struct PyModuleDef_Slot foomodule_slots[] = {
{Py_mod_exec, foomodule_exec}, // 'foomodule_exec' may be NULL if the state is trivial
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0, NULL},
};

static struct PyModuleDef foomodule = {
PyModuleDef_HEAD_INIT,
.m_name = "_foo", // name to use in 'import' statements
.m_doc = "some doc", // or NULL if not needed
.m_size = sizeof(foomodule_state),
.m_methods = foomodule_methods,
.m_slots = foomodule_slots,
.m_traverse = foomodule_traverse, // or NULL if the state is trivial
.m_clear = foomodule_clear, // or NULL if the state is trivial
.m_free = foomodule_free, // or NULL if the state is trivial
};

PyMODINIT_FUNC
PyInit_foo(void)
{
return PyModuleDef_Init(&_foomodule);
}

In a separate file, we would have the implementation of ``_Py_fast_bar``:

.. code-block:: c

// Modules/foo/helper.c

#include "foomodule.h"

PyObject *_Py_fast_bar() {
return PyUnicode_FromString("Hello World!");
}

.. tip::

Do not forget that symbols exported by ``libpython`` must start
with ``Py`` or ``_Py``, which is verified via ``make smelly``.

One could imagine having more ``.h`` files, or no ``helper.c`` file if it is
not needed. Here, we wanted to illustrate a simple example without making it
too trivial.

Make the CPython project compile
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Now that we have our files, we need to update the :cpy-file:`Makefile.pre.in` file.
First, define the following the variables:

.. code-block:: makefile

FOO_H = Modules/foo/foomodule.h
FOO_OBJS = Modules/foo/foomodule.o Modules/foo/helper.o

and place them in the **Modules** section where other pre-defined objects live such
as ``MODULE_OBJS`` and ``IO_OBJS``. Then, add the following rule in the section for
**Special rules for object files**:

.. code-block:: makefile

$(FOO_OBJS): $(FOO_H)

and the following rule in the section for **Module dependencies and platform-specific files**:

.. code-block:: makefile

MODULE_FOO_DEPS=$(srcdir)/Modules/foo/foomodule.h

.. note::

The ``FOO_OBJS`` and ``FOO_H`` are not necessarily needed and the rule
``$(FOO_OBJS): $(FOO_H)`` could be hard-coded. Using Makefile variables
is generally better if multiple files need to be compiled.

Finally, we need to modify the configuration for Windows platforms:

- Open :cpy-file:`PC/config.c` and add the prototype:

.. code-block:: c

extern PyObject* PyInit_foo(void);

and the entry ``{"foo", PyInit_foo}`` to ``_PyImport_Inittab``.

- Open :cpy-file:`PCbuild/pythoncore.vcxproj` and add the following line to
the ``<ItemGroup>`` containing the other ``..\Modules\*.h`` files:

.. code-block:: xml

<ClInclude Include="..\Modules\foo\foomodule.h" />

In addition, add the following lines to the ``<ItemGroup>``
containing the the other ``..\Modules\*.c`` files:

.. code-block:: xml

<ClCompile Include="..\Modules\foo\foomodule.c" />
<ClCompile Include="..\Modules\foo\helper.c" />

- Open :cpy-file:`PCbuild/pythoncore.vcxproj.filters` and add the following line to
the ``ItemGroup`` containing the the other ``..\Modules\*.h`` files:

.. code-block:: xml

<ClInclude Include="..\Modules\foo\foomodule.h">
<Filter>Modules\foo</Filter>
</ClInclude>

In addition, add the following lines to the ``ItemGroup`` containing
the the other ``..\Modules\*.c`` files:

.. code-block:: xml

<ClCompile Include="..\Modules\foo\foomodule.c">
<Filter>Modules\foo</Filter>
</ClCompile>
<ClCompile Include="..\Modules\foo\helper.c">
<Filter>Modules\foo</Filter>
</ClCompile>

Observe that ``.h`` files use ``<ClInclude ...>`` whereas ``.c`` files
use ``<ClCompile ...>`` tags.

Compile the CPython project
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Now that everything is in place, it remains to compile the project:

.. code-block:: shell

make regen-configure
make regen-all
make regen-stdlib-module-names

- The ``make regen-configure`` step regenerates the configure script.

- The ``make regen-all`` is responsible for running Arguments Clinic,
regenerating global objects, etc. It is useful to run when you do not
know which files should be updated.

- The ``regen-stdlib-module-names`` updates the standard module names,
making ``_foo`` discoverable and importable via ``import _foo``.

You can now compile the entire project by running the following commands:

.. code-block:: shell

./configure --with-pydebug
make

.. tip:: Use ``make -j12`` to speed-up compilation if you have enough CPU cores.

Troubleshooting
^^^^^^^^^^^^^^^

This section addresses common issues that you may face when following this tutorial.

``make regen-configure`` does not work!
.......................................

Since this rule requires Docker to be running and a Docker instance,
the following can be done on Linux platforms (``systemctl``-based):

.. code-block:: shell

systemctl status docker # is the docker service running?
sudo systemctl start docker # start it if not!
sudo systemctl restart docker # or restart it!

If Docker complains about missing permissions, this Stack Overflow post
could be useful in solving the issue: `How to fix docker: permission denied
<https://stackoverflow.com/q/48957195/9579194>`_.
Loading