Skip to content

Add safe Python-extension module C API headers#15762

Merged
raynelfss merged 4 commits intoQiskit:mainfrom
jakelishman:c/pycapsule
Mar 19, 2026
Merged

Add safe Python-extension module C API headers#15762
raynelfss merged 4 commits intoQiskit:mainfrom
jakelishman:c/pycapsule

Conversation

@jakelishman
Copy link
Copy Markdown
Member

@jakelishman jakelishman commented Mar 5, 2026

With the vtables now compiled into the _accelerate object and stored in suitable PyCapsules, the last step to exposing the complete ability to compile Python extension modules is providing header-file support for actually using the result. This support is modelled on NumPy.

We generate alternate versions of the function declarations as part of the pyext build script, which are loaded (instead of the standard function prototypes) when the QISKIT_PYTHON_EXTENSION macro is set prior to the inclusion of qiskit.h. These declarations are all pre-processor macros that resolve to compile-time constant offset lookups into the vtables stored in the PyCapsules, except we cache the internal pointer of each PyCapsule into a compilation-unit-local static. If we didn't have this cache, all function calls would have Python-API overhead and require an attached Python thread state (holding the GIL).

The cache population is done by a new header-only function qk_import defined in (the non-stub version of) funcs_py.h, which then must be called before any C API function. This will almost invariably be done inside the PyInit_* module-initialisation function of the extension.

The cache mechanism introduced in this commit is local to a single translation unit. It is possible to extend this to allow sharing it between different translation units, but since this necessarily requires exposing a non-static symbol out of a library, we will have to take care to do it with a mechanism that allows the user to override the names used.


Depends on:

Close #15572

@jakelishman jakelishman added this to the 2.4.0 milestone Mar 5, 2026
@jakelishman jakelishman requested a review from a team as a code owner March 5, 2026 13:38
@jakelishman jakelishman added on hold Can not fix yet Changelog: Added Add an "Added" entry in the GitHub Release changelog. C API Related to the C API labels Mar 5, 2026
@github-project-automation github-project-automation bot moved this to Ready in Qiskit 2.4 Mar 5, 2026
@qiskit-bot
Copy link
Copy Markdown
Collaborator

One or more of the following people are relevant to this code:

@coveralls
Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 22786547376

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 461 of 557 (82.76%) changed or added relevant lines in 12 files are covered.
  • 14 unchanged lines in 4 files lost coverage.
  • Overall coverage increased (+0.09%) to 87.77%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/bindgen-c/src/main.rs 0 2 0.0%
crates/pyext/src/capi.rs 33 36 91.67%
crates/pyext/build.rs 101 106 95.28%
crates/cext/src/transpiler/target.rs 0 9 0.0%
crates/cext/src/dag.rs 0 14 0.0%
crates/cext-vtable/src/impl_.rs 35 66 53.03%
crates/cext/src/py.rs 0 32 0.0%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/parameter/parameter_expression.rs 1 86.99%
crates/qasm2/src/expr.rs 1 93.82%
crates/qasm2/src/lex.rs 2 92.29%
crates/circuit/src/parameter/symbol_expr.rs 10 73.89%
Totals Coverage Status
Change from base Build 22767384343: 0.09%
Covered Lines: 101352
Relevant Lines: 115474

💛 - Coveralls

Comment on lines +16 to +18
// We rely on `qiskit.h` to `#include <Python.h>` on our behalf, since it needs to be included
// before all other includes. (Though really, a user should also have included it themselves,
// surely, if they're delcaring a Python extension module.)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume Python.h has an include guard, so why not just include it here anyways?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because it has to be included before all other includes - if we put it here, it must be redundant because this isn't a top-level import.

if (!_Qk_API_QI)
return -1;

// TODO: any validity checks on the version of the Qiskit API?
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you explain what kind of checks you're thinking of? Since the vtables and functions are all autogenerated they should be consistent, right? Does this refer to coherence checks in case the generation goes wrong?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, I forgot this was still here. The check I have in mind is a runtime version check; the compiled extension knows what header it's compiled with (it's the version info in there) but at runtime, it's got no way of knowing what version the library is. We're supposed to keep our ABI/API safe and forwards compatible, but just for forwards thinking, I'm thinking we should maybe have a version handshake during the import, so we can insert backwards-compatibility shims if we need to, or more basically just warn if there are known incompatibilities.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I might want to squeeze in a qk_version() function into the release just so we can put it at slot number 0 and have it available in the future, but there's no need to do anything with it in the header file in 2.4.0rc1, I think.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#15831 is the qk_api_version function, though I haven't made the header files for 2.4 check it here. I still would like to get it in to give us the option in the future, though.

@jakelishman
Copy link
Copy Markdown
Member Author

I've just rebased the PR here to make the diff clean, but I need to come back later this evening and write the commit message and the release note.

With the vtables now compiled into the `_accelerate` object and stored
in suitable `PyCapsule`s, the last step to exposing the complete ability
to compile Python extension modules is providing header-file support for
actually using the result.  This support is modelled on NumPy.

We generate alternate versions of the function declarations as part of
the `pyext` build script, which are loaded (instead of the standard
function prototypes) when the `QISKIT_PYTHON_EXTENSION` macro is set
prior to the inclusion of `qiskit.h`.  These declarations are all
pre-processor macros that resolve to compile-time constant offset
lookups into the vtables stored in the `PyCapsule`s, except we cache the
internal pointer of each `PyCapsule` into a compilation-unit-local
`static`.  If we didn't have this cache, _all_ function calls would have
Python-API overhead and require an attached Python thread state (holding
the GIL).

The cache population is done by a new header-only function `qk_import`
defined in (the non-stub version of) `funcs_py.h`, which then must be
called _before_ any C API function.  This will almost invariably be done
inside the `PyInit_*` module-initialisation function of the extension.

The cache mechanism introduced in this commit is local to a single
translation unit.  It is possible to extend this to allow sharing it
between different translation units, but since this necessarily requires
exposing a non-`static` symbol out of a library, we will have to take
care to do it with a mechanism that allows the user to override the
names used.
@jakelishman
Copy link
Copy Markdown
Member Author

Ok, the release note and commit message are now all written, so this should be good to review.

Copy link
Copy Markdown
Member

@mrossinek mrossinek left a comment

Choose a reason for hiding this comment

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

I found mostly typos. Other than that, I think this looks good 👍

}
ir::Type::Path(p) => acc.push_str(p.export_name()),
ir::Type::Primitive(ty) => acc.push_str(ty.to_repr_c(config)),
ir::Type::Array(..) => todo!("array types not yet handled"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How likely are these to come up in the future? Do you want a (or more) tracking issue(s) for this as well as the other not-handled cases below?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Really pretty unlikely I think - minimum-size arrays as function arguments in C are super uncommon. I put in the panic so that if we ever do try it, it's marked as needing extra implementation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Makes sense 👍

Co-authored-by: Max Rossmannek <21973473+mrossinek@users.noreply.github.com>
Co-authored-by: Jake Lishman <jake@binhbar.com>
Copy link
Copy Markdown
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

I again couldn't find anything concerning here. I assume this isn't being used yet as we haven't designed any Python extensions yet but the logic seems to make sense.

From what I could read, based on usage of an extension definition, this module will first import qiskit and use the vtable to generate the functions header file.

That said, I only had one question about a check

ty,
array_length,
} = arg;
assert!(array_length.is_none(), "array arguments not handled");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this supposed to be array_length.is_some() or am I reading this wrong?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The assertion is that the argument isn't an array - if it is, there's a problem because we explicitly don't handle that case. I didn't want the potential bug to pass silently.

@jakelishman
Copy link
Copy Markdown
Member Author

I assume this isn't being used yet as we haven't designed any Python extensions yet but the logic seems to make sense.

It's not used within the Qiskit/qiskit repository itself yet, but I will follow up adding integration tests involving it. There's just more repo admin work to do about that because I'll most likely need/want a more powerful/less wildly over-opinionated command runner than tox.

From what I could read, based on usage of an extension definition, this module will first import qiskit and use the vtable to generate the functions header file.

Well, the header file is generated in the build script of pyext, so before "Qiskit" as a Python-space package actually exists yet. The qk_import function is inlined into downstream extension modules, and that ensures (at import time) that Python-space qiskit is imported, then caches the vtable pointers from the PyCapsule objects stored inside the qiskit._accelerate.capi module object.

Copy link
Copy Markdown
Contributor

@raynelfss raynelfss left a comment

Choose a reason for hiding this comment

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

Thank you for quickly addressing my comments and explaining how things work here. I don't have much more to add, so feel free to merge this after @alexanderivrii takes a look

@raynelfss raynelfss added this pull request to the merge queue Mar 19, 2026
Merged via the queue into Qiskit:main with commit 55f4eb1 Mar 19, 2026
25 checks passed
@github-project-automation github-project-automation bot moved this from Ready to Done in Qiskit 2.4 Mar 19, 2026
@jakelishman jakelishman deleted the c/pycapsule branch March 19, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C API Related to the C API Changelog: Added Add an "Added" entry in the GitHub Release changelog.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Provide a safe way for Python extension modules to use the C API

7 participants