Skip to content

Commit d6c1640

Browse files
committed
WIP: Add safe Python-extension module C API headers
TODO: * write the commit message! * add the release note
1 parent 888c877 commit d6c1640

6 files changed

Lines changed: 200 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.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ cbindgen = "0.29.2"
4949
# it disabled for Rust-only tests to avoid linker errors with it not being loaded. See
5050
# https://pyo3.rs/main/features#extension-module for more.
5151
pyo3 = { version = "0.28.1", features = ["abi3-py310"] }
52+
pyo3-build-config.version = "0.28.1"
5253

5354
# These are our own crates.
5455
qiskit-accelerate = { path = "crates/accelerate" }

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: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,148 @@
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+
dbg!(ty);
27+
match ty {
28+
ir::Type::Ptr {
29+
ty,
30+
is_const,
31+
is_nullable: _,
32+
is_ref,
33+
} => {
34+
assert!(!is_ref, "C++ reference-likes not handled");
35+
if *is_const {
36+
acc.push_str("const ");
37+
}
38+
render(ty, config, acc);
39+
acc.push_str(" *");
40+
}
41+
ir::Type::Path(p) => acc.push_str(p.export_name()),
42+
ir::Type::Primitive(ty) => acc.push_str(ty.to_repr_c(config)),
43+
ir::Type::Array(..) => todo!("array types not yet handled"),
44+
ir::Type::FuncPtr {
45+
args,
46+
ret,
47+
is_nullable,
48+
never_return,
49+
} => {
50+
assert!(!is_nullable, "nullability of funcptrs is not handled");
51+
assert!(!never_return, "diverging functions not handled");
52+
render(ret, config, acc);
53+
acc.push_str("(*)(");
54+
let mut args = args.iter();
55+
if let Some((_, first)) = args.next() {
56+
render(first, config, acc);
57+
for (_, arg) in args {
58+
acc.push_str(", ");
59+
render(arg, config, acc)
60+
}
61+
}
62+
acc.push(')');
63+
}
64+
}
65+
}
66+
let mut acc = String::new();
67+
render(ty, config, &mut acc);
68+
acc
69+
}
70+
71+
/// Calculate a mapping of exported function names to C casts to appropriate function-pointer types.
72+
fn functions_as_c_funcptr_casts(bindings: &cbindgen::Bindings) -> HashMap<&str, String> {
73+
let to_funcptr = |func: &ir::Function| {
74+
let to_funcptr_arg = |arg: &ir::FunctionArgument| {
75+
let ir::FunctionArgument {
76+
name: _,
77+
ty,
78+
array_length,
79+
} = arg;
80+
assert!(array_length.is_none(), "array arguments not handled");
81+
(None, ty.clone())
82+
};
83+
ir::Type::FuncPtr {
84+
ret: Box::new(func.ret.clone()),
85+
args: func.args.iter().map(to_funcptr_arg).collect(),
86+
is_nullable: false,
87+
never_return: false,
88+
}
89+
};
90+
let config = &bindings.config;
91+
bindings
92+
.functions
93+
.iter()
94+
.map(|func| {
95+
let funcptr = to_funcptr(func);
96+
(func.path.name(), render_type_as_c(&funcptr, config))
97+
})
98+
.collect()
99+
}
100+
101+
/// Install (overwriting) the Python-extension-specific header files into the given directory.
102+
fn install_py_function_headers(
103+
bindings: &cbindgen::Bindings,
104+
install_path: impl AsRef<Path>,
105+
) -> anyhow::Result<()> {
106+
let mut our_include = Path::new(env!("CARGO_MANIFEST_DIR")).join("include");
107+
our_include.push(qiskit_bindgen::SCOPED_INCLUDE_DIR);
108+
// This directory must already have been constructed by the previous "install" command for the
109+
// regular C headers; if it doesn't, writing out the files will be a mistake because we _should_
110+
// be overwriting an existing file (the wrapper that defines `qk_import`).
111+
let install_path = install_path
112+
.as_ref()
113+
.join(qiskit_bindgen::SCOPED_INCLUDE_DIR);
114+
fs::copy(
115+
our_include.join(WRAPPER_FUNCS),
116+
install_path.join(WRAPPER_FUNCS),
117+
)?;
118+
let mut funcs_header = fs::File::create(install_path.join(GENERATED_FUNCS))?;
119+
writeln!(funcs_header, "{}", qiskit_bindgen::COPYRIGHT)?;
120+
121+
// Now, each function's name is just a preprocessor macro that resolves to a lookup into the
122+
// corresponding table. The names given here need to match with the handwritten include file
123+
// that sets up the slots in `qk_import`.
124+
let vtables = [
125+
("_Qk_API_Circuit", &FUNCTIONS_CIRCUIT),
126+
("_Qk_API_Transpile", &FUNCTIONS_TRANSPILE),
127+
("_Qk_API_QI", &FUNCTIONS_QI),
128+
];
129+
let funcs = functions_as_c_funcptr_casts(bindings);
130+
for (vtable_name, vtable) in vtables {
131+
for export in vtable.exports(0) {
132+
writeln!(
133+
funcs_header,
134+
"#define {} (*({})({}[{}]))",
135+
export.name, funcs[export.name], vtable_name, export.slot
136+
)?;
137+
}
138+
}
139+
Ok(())
140+
}
141+
16142
#[allow(clippy::print_stdout)] // We're a build script - we're _supposed_ to print to stdout.
17143
fn main() -> anyhow::Result<()> {
144+
// Our actual requirements for re-running the build script are if `cext-vtable` changes, but
145+
// since that's a build-time dependency, it's already implicit in Cargo's logic, so we just need
146+
// to issue _any_ re-run command to avoid the default behaviour of rerunning if `qiskit_pyext`
147+
// itself changes.
148+
println!("cargo::rerun-if-changed=build.rs");
18149
let cext_path = {
19150
let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf();
20151
path.pop();
21152
path.push("cext");
22153
path
23154
};
24-
println!(
25-
"cargo::rerun-if-changed={}",
26-
cext_path
27-
.to_str()
28-
.ok_or_else(|| anyhow!("cext path isn't unicode"))?
29-
);
30155
let out_path = {
31156
let out_dir = std::env::var("OUT_DIR").expect("cargo should set this for build scripts");
32157
let mut path = Path::new(&out_dir).to_path_buf();
@@ -37,5 +162,6 @@ fn main() -> anyhow::Result<()> {
37162
// We install the headers into our `OUT_DIR`, then we configure `setuptools-rust` to pick them
38163
// up from there and put them into the Python package.
39164
qiskit_bindgen::install_c_headers(&mut bindings, &out_path)?;
165+
install_py_function_headers(&bindings, &out_path)?;
40166
Ok(())
41167
}
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 delcaring 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 non-zero 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

0 commit comments

Comments
 (0)