Skip to content

Commit 55f4eb1

Browse files
Add safe Python-extension module C API headers (#15762)
* Add safe Python-extension module C API headers 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. * Remove `dbg!` call from build script * Remove unneeded `pyo3-build-config` dependence * Fix typos Co-authored-by: Max Rossmannek <21973473+mrossinek@users.noreply.github.com> Co-authored-by: Jake Lishman <jake@binhbar.com> --------- Co-authored-by: Max Rossmannek <21973473+mrossinek@users.noreply.github.com>
1 parent 0551a40 commit 55f4eb1

8 files changed

Lines changed: 269 additions & 27 deletions

File tree

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: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,6 @@ pub static FN_DEPRECATED_WITH_NOTE: &str = "Qk_DEPRECATED_FN_NOTE({})";
6969
pub static CFG_FEATURE_DEFINES: &[(&str, &str)] =
7070
&[(PYTHON_BINDING_FEATURE, PYTHON_BINDING_DEFINE)];
7171

72-
fn guarded_python_import(guard: &str) -> String {
73-
format!(
74-
"\
75-
#ifdef {guard}
76-
#include <Python.h>
77-
#endif"
78-
)
79-
}
80-
8172
#[inline]
8273
fn to_vec_string(slice: &[&str]) -> Vec<String> {
8374
slice.iter().map(|s| String::from(*s)).collect()
@@ -117,14 +108,6 @@ fn manual_include_files() -> anyhow::Result<Vec<PathBuf>> {
117108

118109
/// Get the Qiskit configuration
119110
fn get_config() -> anyhow::Result<cbindgen::Config> {
120-
// `Python.h` is required to be the first file included because it reserves the right to define
121-
// preprocessor macros that affect standard-library includes. This causes it to be ahead of our
122-
// include guard, but `Python.h` has its own, so we should be fine.
123-
let header = Some(format!(
124-
"{}\n{}",
125-
COPYRIGHT,
126-
guarded_python_import(PYTHON_BINDING_DEFINE)
127-
));
128111
// We need to include the `attributes.h` file in all generated files to make sure Doxygen can
129112
// understand the deprecated attributes (even though `qiskit.h` is organised to include it).
130113
let includes = vec![
@@ -173,7 +156,7 @@ fn get_config() -> anyhow::Result<cbindgen::Config> {
173156
.map(|&(cfg, def)| (format!("feature = {cfg}"), String::from(def)))
174157
.collect();
175158
Ok(cbindgen::Config {
176-
header,
159+
header: Some(COPYRIGHT.to_owned()),
177160
language: cbindgen::Language::C,
178161
includes,
179162
include_version: true,

crates/pyext/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ workspace = true
2020
# `libpython` is *not* linked, and if the feature isn't present, then it is. To test the Rust
2121
# crates as standalone binaries, executables, we need `libpython` to be linked in, so we make the
2222
# feature a default, and run `cargo test --no-default-features` to turn it off.
23-
default = ["pyo3/extension-module"]
24-
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"]
23+
cache_pygates = ["qiskit-circuit/cache_pygates", "qiskit-accelerate/cache_pygates", "qiskit-transpiler/cache_pygates", "qiskit-cext/cache_pygates", "qiskit-qpy/cache_pygates"]
2524

2625
[build-dependencies]
2726
anyhow.workspace = true
27+
cbindgen = { workspace = true, features = ["unstable_ir"] }
28+
hashbrown.workspace = true
2829
qiskit-bindgen.workspace = true
30+
qiskit-cext-vtable = { workspace = true, features = ["python_binding"] }
2931

3032
[dependencies]
3133
pyo3.workspace = true

crates/pyext/build.rs

Lines changed: 132 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,147 @@
1010
// copyright notice, and modified files need to carry a notice indicating
1111
// that they have been altered from the originals.
1212

13-
use anyhow::anyhow;
13+
use cbindgen::bindgen::ir;
14+
use hashbrown::HashMap;
15+
use qiskit_cext_vtable::{FUNCTIONS_CIRCUIT, FUNCTIONS_QI, FUNCTIONS_TRANSPILE};
16+
use std::fs;
17+
use std::io::Write;
1418
use std::path::Path;
1519

20+
static WRAPPER_FUNCS: &str = "funcs_py.h";
21+
static GENERATED_FUNCS: &str = "funcs_py_generated.h";
22+
23+
/// Render a given type object into a string representing it in C.
24+
fn render_type_as_c(ty: &ir::Type, config: &cbindgen::Config) -> String {
25+
fn render(ty: &ir::Type, config: &cbindgen::Config, acc: &mut String) {
26+
match ty {
27+
ir::Type::Ptr {
28+
ty,
29+
is_const,
30+
is_nullable: _,
31+
is_ref,
32+
} => {
33+
assert!(!is_ref, "C++ reference-likes not handled");
34+
if *is_const {
35+
acc.push_str("const ");
36+
}
37+
render(ty, config, acc);
38+
acc.push_str(" *");
39+
}
40+
ir::Type::Path(p) => acc.push_str(p.export_name()),
41+
ir::Type::Primitive(ty) => acc.push_str(ty.to_repr_c(config)),
42+
ir::Type::Array(..) => todo!("array types not yet handled"),
43+
ir::Type::FuncPtr {
44+
args,
45+
ret,
46+
is_nullable,
47+
never_return,
48+
} => {
49+
assert!(!is_nullable, "nullability of funcptrs is not handled");
50+
assert!(!never_return, "diverging functions not handled");
51+
render(ret, config, acc);
52+
acc.push_str("(*)(");
53+
let mut args = args.iter();
54+
if let Some((_, first)) = args.next() {
55+
render(first, config, acc);
56+
for (_, arg) in args {
57+
acc.push_str(", ");
58+
render(arg, config, acc)
59+
}
60+
}
61+
acc.push(')');
62+
}
63+
}
64+
}
65+
let mut acc = String::new();
66+
render(ty, config, &mut acc);
67+
acc
68+
}
69+
70+
/// Calculate a mapping of exported function names to C casts to appropriate function-pointer types.
71+
fn functions_as_c_funcptr_casts(bindings: &cbindgen::Bindings) -> HashMap<&str, String> {
72+
let to_funcptr = |func: &ir::Function| {
73+
let to_funcptr_arg = |arg: &ir::FunctionArgument| {
74+
let ir::FunctionArgument {
75+
name: _,
76+
ty,
77+
array_length,
78+
} = arg;
79+
assert!(array_length.is_none(), "array arguments not handled");
80+
(None, ty.clone())
81+
};
82+
ir::Type::FuncPtr {
83+
ret: Box::new(func.ret.clone()),
84+
args: func.args.iter().map(to_funcptr_arg).collect(),
85+
is_nullable: false,
86+
never_return: false,
87+
}
88+
};
89+
let config = &bindings.config;
90+
bindings
91+
.functions
92+
.iter()
93+
.map(|func| {
94+
let funcptr = to_funcptr(func);
95+
(func.path.name(), render_type_as_c(&funcptr, config))
96+
})
97+
.collect()
98+
}
99+
100+
/// Install (overwriting) the Python-extension-specific header files into the given directory.
101+
fn install_py_function_headers(
102+
bindings: &cbindgen::Bindings,
103+
install_path: impl AsRef<Path>,
104+
) -> anyhow::Result<()> {
105+
let mut our_include = Path::new(env!("CARGO_MANIFEST_DIR")).join("include");
106+
our_include.push(qiskit_bindgen::SCOPED_INCLUDE_DIR);
107+
// This directory must already have been constructed by the previous "install" command for the
108+
// regular C headers; if it doesn't, writing out the files will be a mistake because we _should_
109+
// be overwriting an existing file (the wrapper that defines `qk_import`).
110+
let install_path = install_path
111+
.as_ref()
112+
.join(qiskit_bindgen::SCOPED_INCLUDE_DIR);
113+
fs::copy(
114+
our_include.join(WRAPPER_FUNCS),
115+
install_path.join(WRAPPER_FUNCS),
116+
)?;
117+
let mut funcs_header = fs::File::create(install_path.join(GENERATED_FUNCS))?;
118+
writeln!(funcs_header, "{}", qiskit_bindgen::COPYRIGHT)?;
119+
120+
// Now, each function's name is just a preprocessor macro that resolves to a lookup into the
121+
// corresponding table. The names given here need to match with the handwritten include file
122+
// that sets up the slots in `qk_import`.
123+
let vtables = [
124+
("_Qk_API_Circuit", &FUNCTIONS_CIRCUIT),
125+
("_Qk_API_Transpile", &FUNCTIONS_TRANSPILE),
126+
("_Qk_API_QI", &FUNCTIONS_QI),
127+
];
128+
let funcs = functions_as_c_funcptr_casts(bindings);
129+
for (vtable_name, vtable) in vtables {
130+
for export in vtable.exports(0) {
131+
writeln!(
132+
funcs_header,
133+
"#define {} (*({})({}[{}]))",
134+
export.name, funcs[export.name], vtable_name, export.slot
135+
)?;
136+
}
137+
}
138+
Ok(())
139+
}
140+
16141
#[allow(clippy::print_stdout)] // We're a build script - we're _supposed_ to print to stdout.
17142
fn main() -> anyhow::Result<()> {
143+
// Our actual requirements for re-running the build script are if `cext-vtable` changes, but
144+
// since that's a build-time dependency, it's already implicit in Cargo's logic, so we just need
145+
// to issue _any_ re-run command to avoid the default behaviour of rerunning if `qiskit_pyext`
146+
// itself changes.
147+
println!("cargo::rerun-if-changed=build.rs");
18148
let cext_path = {
19149
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
20150
path.pop();
21151
path.push("cext");
22152
path
23153
};
24-
println!(
25-
"cargo::rerun-if-changed={}",
26-
cext_path
27-
.to_str()
28-
.ok_or_else(|| anyhow!("cext path isn't unicode"))?
29-
);
30154
let out_path = {
31155
let out_dir = std::env::var("OUT_DIR").expect("cargo should set this for build scripts");
32156
let mut path = Path::new(&out_dir).to_path_buf();
@@ -37,5 +161,6 @@ fn main() -> anyhow::Result<()> {
37161
// We install the headers into our `OUT_DIR`, then we configure `setuptools-rust` to pick them
38162
// up from there and put them into the Python package.
39163
qiskit_bindgen::install_c_headers(&mut bindings, &out_path)?;
164+
install_py_function_headers(&bindings, &out_path)?;
40165
Ok(())
41166
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
#if !defined(QISKIT_FUNCS_PY_H)
14+
#define QISKIT_FUNCS_PY_H
15+
16+
// We rely on `qiskit.h` to `#include <Python.h>` on our behalf, since it needs to be included
17+
// before all other includes. (Though really, a user should also have included it themselves,
18+
// surely, if they're declaring a Python extension module.)
19+
20+
// Declaring these as `static` in the header file means that there is a separate copy per
21+
// compilation unit. This means that compiled extension modules written in C and using this need
22+
// to have all their Qiskit C API calls in the same file as the module definition. There are ways
23+
// around this we can add in the future.
24+
static void **_Qk_API_Circuit;
25+
static void **_Qk_API_Transpile;
26+
static void **_Qk_API_QI;
27+
28+
/**
29+
* Import the Qiskit C API.
30+
*
31+
* @return 0 on success, or negative on failure. If a failure occurs, the Python exception state
32+
* will be set.
33+
*
34+
* This function must be called before any attempt to use the Qiskit C API within this translation
35+
* unit. You must be attached to a Python interpreter to call this function.
36+
*/
37+
static int qk_import(void) {
38+
PyObject *accelerate = PyImport_ImportModule("qiskit._accelerate");
39+
if (!accelerate)
40+
return -1;
41+
// We don't actually need a handle to `accelerate` ourselves, we just need to have ensured it's
42+
// already been imported.
43+
Py_DECREF(accelerate);
44+
45+
_Qk_API_Circuit = (void **)PyCapsule_Import("qiskit._accelerate.capi.QK_FFI_CIRCUIT", 0);
46+
if (!_Qk_API_Circuit)
47+
return -1;
48+
_Qk_API_Transpile = (void **)PyCapsule_Import("qiskit._accelerate.capi.QK_FFI_TRANSPILE", 0);
49+
if (!_Qk_API_Transpile)
50+
return -1;
51+
_Qk_API_QI = (void **)PyCapsule_Import("qiskit._accelerate.capi.QK_FFI_QI", 0);
52+
if (!_Qk_API_QI)
53+
return -1;
54+
return 0;
55+
}
56+
57+
#include "qiskit/funcs_py_generated.h"
58+
59+
#endif // QISKIT_FUNCS_PY_H

docs/cdoc/config.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
=============
2+
Configuration
3+
=============
4+
5+
The Qiskit C API has minimal configuration.
6+
7+
.. c:macro:: QISKIT_PYTHON_EXTENSION
8+
9+
If defined before including ``qiskit.h``, the header files will define all of symbols in a mode
10+
safe for use with Python extension modules. This means that all API functions will evaluate to
11+
function-pointer dereferences from a lookup table.
12+
13+
This is a user-defined macro and not defined by Qiskit itself.
14+
15+
.. c:function:: int qk_import(void)
16+
17+
Import the Qiskit C API from the Python-space :py:mod:`qiskit` package.
18+
19+
You must call this once per compilation unit, before attempting to call any C API functions.
20+
Failure to do so will typically result in null-pointer dereferences at runtime.
21+
22+
You typically will want to do this inside your ``PyInit_*`` module initialization function. For
23+
example, in ``my_extension.c``:
24+
25+
.. code-block:: c
26+
27+
#define QISKIT_PYTHON_EXTENSION
28+
#include <Python.h>
29+
#include <qiskit.h>
30+
31+
static struct PyModuleDef my_extension_mod = {
32+
.m_base = PyModuleDef_HEAD_INIT,
33+
.m_name = "my_extension",
34+
};
35+
36+
PyMODINIT_FUNC PyInit_my_extension(void) {
37+
if (qk_import() < 0) {
38+
return NULL;
39+
}
40+
return PyModuleDef_Init(&my_extension_mod);
41+
}
42+
43+
This function is only defined when :c:macro:`QISKIT_PYTHON_EXTENSION` was defined prior to
44+
including ``qiskit.h``.
45+
46+
:return: 0 on success, negative on failure.

docs/cdoc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,5 @@ Utilities
8888
.. toctree::
8989
:maxdepth: 1
9090

91+
config
9192
version
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
features_c:
3+
- |
4+
Qiskit now supports building Python extension modules that use Qiskit C API from a version of
5+
Qiskit loaded by Python. These extension modules can then be safely distributed in wheels.
6+
7+
To compile an extension module, you must:
8+
9+
* define the :c:macro:`QISKIT_PYTHON_EXTENSION` macro before ``#include <qiskit.h>``.
10+
* call :c:func:`qk_import` before attempting to use any Qiskit C API functions, once per
11+
compilation unit (typically a single ``.c`` file). Typically this will be in your extension
12+
module's ``PyInit_*`` function.
13+
14+
..
15+
TODO: before the final release of 2.4, insert links to documentation/guides about this.
16+
17+
18+
We intend to make it possible to have multiple compilation units using the Qiskit C API in an
19+
extension module in a later release of Qiskit.
20+
21+
Beware that Qiskit's C API is still unstable; extension modules compiled against Qiskit 2.4 are
22+
not guaranteed to be able to run against Qiskit 2.5 or later. Qiskit will commit to this
23+
stability at a later date.
24+

0 commit comments

Comments
 (0)