Skip to content

Commit 676863e

Browse files
Add qiskit.capi module (#15711)
* Add `qiskit.capi` module This adds a basic `qiskit.capi` module for Python-space interaction with the C API. The two initial functions included are `get_include` and `get_lib`, to get the package include directory and shared-object library containing the C-API exported symbols, respectively. Note that while the included header files _are_ complete information to build against the C API, the file returned from `get_lib` is not generally useful as part of a _build_ system, but can be used to interact with the C API live through a Python session. I hope to follow this patch with two more: * provide a second "mode" for the packaged header files that allows building safe Python extension modules against the C API. This would mean that all the C API function names will be `#define`'d to function pointers looked up in a static table stored inside a `PyCapsule` that is initialised as part of `_accelerate`'s initialisation. * generate a `ctypes` wrapper file for the C API as part of the `_accelerate` (`pyext`) build script, which would expose all of the C API functions directly to Python space, potentially allowing them to be jitted through Numba, or allowing direct tests of C API functionality (without the manually-written file currently in our test suite). Co-authored-by: Max Rossmannek <max@rossmannek.de> * Vendor `generated-files` handling from `setuptools-rust` We need PyO3/setuptools-rust#574 (or something akin to it) to handle the movement of the generated files from the build script to the output. This is not yet ready for merge, and is highly unlikely to be ready in time for the Qiskit 2.4.0 release (or at least 2.4.0rc1), so to avoid blocking ourselves, we vendor in a monkey-patching reimplementation of the code temporarily. The allowed range of `setuptools-rust` is _probably_ larger than the hard pin, but the monkey-patch is so invasive and v1.12.0 is sufficiently old (August 2025) that it's not worth the risk. There are no meaningful licensing concerns since the vendored code here was entirely written by me both here and in the `setuptools-rust` patch; there is no cross-contamination from that repository and no code inclusion from it. This commit should be reverted as soon as we can swap to a safe released version of `setuptools-rust`. * Remove pylint suppressions * Fix typos * Comment on hard-pin of `setuptools-rust` --------- Co-authored-by: Max Rossmannek <max@rossmannek.de>
1 parent d8513b5 commit 676863e

16 files changed

Lines changed: 293 additions & 8 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ __pycache__/
1818

1919
# C extensions
2020
*.so
21+
# Generated C header files included in package.
22+
/qiskit/capi/include
2123

2224
# Distribution / packaging
2325
.Python

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bindgen/src/lib.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ pub static GENERATED_FILE: &str = "qiskit.h";
1919
pub static PYTHON_BINDING_FEATURE: &str = "python_binding";
2020
pub static PYTHON_BINDING_DEFINE: &str = "QISKIT_C_PYTHON_INTERFACE";
2121

22+
pub static COPYRIGHT: &str = "\
23+
// This code is part of Qiskit.
24+
//
25+
// (C) Copyright IBM 2026
26+
//
27+
// This code is licensed under the Apache License, Version 2.0. You may
28+
// obtain a copy of this license in the LICENSE.txt file in the root directory
29+
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
30+
//
31+
// Any modifications or derivative works of this code must retain this
32+
// copyright notice, and modified files need to carry a notice indicating
33+
// that they have been altered from the originals.
34+
";
35+
2236
pub static INCLUDE_GUARD: &str = "QISKIT_H";
2337
/// Crates that contain definitions of objects that are exposed through the C API.
2438
pub static QISKIT_PUBLIC_API_CRATES: &[&str] =
@@ -100,6 +114,14 @@ fn manual_include_files() -> anyhow::Result<Vec<PathBuf>> {
100114

101115
/// Get the Qiskit configuration
102116
fn get_config() -> anyhow::Result<cbindgen::Config> {
117+
// `Python.h` is required to be the first file included because it reserves the right to define
118+
// preprocessor macros that affect standard-library includes. This causes it to be ahead of our
119+
// include guard, but `Python.h` has its own, so we should be fine.
120+
let header = Some(format!(
121+
"{}\n{}",
122+
COPYRIGHT,
123+
guarded_python_import(PYTHON_BINDING_DEFINE)
124+
));
103125
let enumeration = cbindgen::EnumConfig {
104126
prefix_with_name: true,
105127
..Default::default()
@@ -158,10 +180,7 @@ fn get_config() -> anyhow::Result<cbindgen::Config> {
158180
})
159181
.collect::<Result<Vec<_>, _>>()?;
160182
Ok(cbindgen::Config {
161-
// `Python.h` is required to be the first file included because it reserves the right to
162-
// define preprocessor macros that affect standard-library includes. This causes it to be
163-
// ahead of our include guard, but `Python.h` has its own, so we should be fine.
164-
header: Some(guarded_python_import(PYTHON_BINDING_DEFINE)),
183+
header,
165184
language: cbindgen::Language::C,
166185
include_version: true,
167186
include_guard: Some(INCLUDE_GUARD.into()),

crates/pyext/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version.workspace = true
44
edition.workspace = true
55
rust-version.workspace = true
66
license.workspace = true
7+
build = "build.rs"
78

89
[lib]
910
name = "qiskit_pyext"
@@ -22,6 +23,10 @@ workspace = true
2223
default = ["pyo3/extension-module"]
2324
cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates", "qiskit-accelerate/cache_pygates", "qiskit-transpiler/cache_pygates", "qiskit-cext/cache_pygates", "qiskit-qpy/cache_pygates"]
2425

26+
[build-dependencies]
27+
anyhow.workspace = true
28+
qiskit-bindgen.workspace = true
29+
2530
[dependencies]
2631
pyo3.workspace = true
2732
qiskit-accelerate.workspace = true

crates/pyext/build.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// This code is part of Qiskit.
2+
//
3+
// (C) Copyright IBM 2026
4+
//
5+
// This code is licensed under the Apache License, Version 2.0. You may
6+
// obtain a copy of this license in the LICENSE.txt file in the root directory
7+
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8+
//
9+
// Any modifications or derivative works of this code must retain this
10+
// copyright notice, and modified files need to carry a notice indicating
11+
// that they have been altered from the originals.
12+
13+
use anyhow::anyhow;
14+
use std::path::Path;
15+
16+
#[allow(clippy::print_stdout)] // We're a build script - we're _supposed_ to print to stdout.
17+
fn main() -> anyhow::Result<()> {
18+
let cext_path = {
19+
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
20+
path.pop();
21+
path.push("cext");
22+
path
23+
};
24+
println!(
25+
"cargo::rerun-if-changed={}",
26+
cext_path
27+
.to_str()
28+
.ok_or_else(|| anyhow!("cext path isn't unicode"))?
29+
);
30+
let out_path = {
31+
let out_dir = std::env::var("OUT_DIR").expect("cargo should set this for build scripts");
32+
let mut path = Path::new(&out_dir).to_path_buf();
33+
path.push("include");
34+
path
35+
};
36+
let bindings = qiskit_bindgen::generate_bindings(&cext_path)?;
37+
// We install the headers into our `OUT_DIR`, then we configure `setuptools-rust` to pick them
38+
// up from there and put them into the Python package.
39+
qiskit_bindgen::install_c_headers(&bindings, &out_path)?;
40+
Ok(())
41+
}

docs/apidoc/capi.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.. _qiskit-capi:
2+
3+
.. automodule:: qiskit.capi
4+
:no-members:
5+
:no-inherited-members:
6+
:no-special-members:

docs/apidoc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Other:
7575
.. toctree::
7676
:maxdepth: 1
7777

78+
capi
7879
compiler
7980
exceptions
8081
root

pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
# This is duplicated as `dependency-groups.build` for convenience.
33
requires = [
44
"setuptools>=77.0",
5-
"setuptools-rust",
5+
# This is hard-pinned to permit the awful monkeypatching in `setup.py`.
6+
# It should be removed when we can use a released version of
7+
# https://github.com/PyO3/setuptools-rust/pull/574
8+
"setuptools-rust==1.12.0",
69
]
710
build-backend = "setuptools.build_meta"
811

@@ -184,7 +187,7 @@ default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultScheduli
184187
# process. If you intended that, use `project.optional-dependencies` instead.
185188
[dependency-groups]
186189
# Sync with `build-system.requires`.
187-
build = ["setuptools>=77.0", "setuptools-rust"]
190+
build = ["setuptools>=77.0", "setuptools-rust==1.12.0"]
188191
# We use quite tight pins on pylint and astroid because they have a habit of adding new
189192
# on-by-default lints that are harder to deal with.
190193
lint = [

qiskit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142

143143

144144
from qiskit.exceptions import QiskitError, MissingOptionalLibraryError
145+
import qiskit.capi
145146

146147
# The main qiskit operators
147148
from qiskit.circuit import ClassicalRegister

qiskit/capi/__init__.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2026.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
r"""
14+
================================================
15+
C API interface from Python (:mod:`qiskit.capi`)
16+
================================================
17+
18+
.. currentmodule:: qiskit.capi
19+
20+
This module provides Python-space interactions with Qiskit's public C API.
21+
22+
For documentation on the C API itself, see `Qiskit C API
23+
<https://quantum.cloud.ibm.com/docs/api/qiskit-c/>`__.
24+
25+
Build-system interaction
26+
========================
27+
28+
The Python package :mod:`qiskit` contains all of the Qiskit C API header files, and a compiled
29+
shared-object library that includes all of the C-API functions. You can access the locations of
30+
these two objects with the functions :func:`get_include` and :func:`get_lib` respectively.
31+
32+
.. warning::
33+
You typically *should not* link directly against the output of :func:`get_lib`, unless you know
34+
what you are doing. In particular, directly linking against this object is not a safe way to
35+
build a distributable Python extension module that uses Qiskit's C API.
36+
37+
However, if you understand all the caveats of direct linking, you can use the function to get
38+
the location of the library.
39+
40+
.. autofunction:: get_include
41+
.. autofunction:: get_lib
42+
"""
43+
44+
__all__ = ["get_include", "get_lib"]
45+
46+
from pathlib import Path
47+
48+
import qiskit._accelerate
49+
50+
51+
def get_include() -> str:
52+
"""Get the directory containing the ``qiskit.h`` C header file and the internal
53+
``qiskit/*.h`` auxiliary files.
54+
55+
When using Qiskit as a build dependency, you typically want to include this directory on the
56+
include search path of your compiler, such as:
57+
58+
.. code-block:: bash
59+
60+
qiskit_include=$(python -c 'import qiskit.capi; print(qiskit.capi.get_include())')
61+
gcc -I "$qiskit_include" my_bin.c -o my_bin
62+
63+
The location of this directory within the Qiskit package data is not fixed, and may change
64+
between Qiskit versions. You should always use this function to retrieve the directory.
65+
66+
Returns:
67+
an absolute path to the package include-files directory.
68+
"""
69+
return str(Path(__file__).parent.absolute() / "include")
70+
71+
72+
def get_lib() -> str:
73+
"""Get the path to a shared-object library that contains all the C-API exported symbols.
74+
75+
.. warning::
76+
You typically *should not* link directly against this object. In particular, directly
77+
linking against this object is not a safe way to build a Python extension module that uses
78+
Qiskit's C API.
79+
80+
You can, if you choose, use :mod:`ctypes` to access the C API symbols contained in this object,
81+
though beware that the C-API types declared in the header file are not interchangeable with the
82+
Python objects that correspond to them.
83+
84+
The location and name of this file within the Qiskit package data is not fixed, and may change
85+
between Qiskit versions.
86+
87+
Returns:
88+
an absolute path to the shared-object library containing the C-API exported symbols.
89+
"""
90+
return str(Path(qiskit._accelerate.__file__).absolute())

0 commit comments

Comments
 (0)