From 93b582ab9e0b41f9a6631ce820a0fee7eae836c6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 5 Mar 2026 12:21:26 +0000 Subject: [PATCH 1/3] Add C API vtable to `pyext` While the `_accelerate.abi3.so` object that ships with Qiskit already exposes all the C API symbols with public `qk_*` names, this can't safely be used by distributable compiled Python extension modules, because they cannot rely even on deferred dynamic linking to the object. Instead, we define what is effectively a set of vtables, where the actual addresses of the functions are written (at runtime) at known offsets to a base pointer. These can then be accessed without the actual involvement of a linker by knowing the base pointer of the vtable, the offset of the desired function and the expected signature. In order for user builds to be forwards compatible (or in other words, for later Qiskit builds to be _backwards_ compatible) from an ABI perspective, the offsets into the vtables must be constant between Qiskit versions. This requires them to be defined statically, without being inferred from other functions; if we try to infer based on the set of functions, there is no way to keep them the same as functions are added without defining an order. The hierarchical `VTable` machinery introduced in this commit is a trade-off between two extreme approaches: 1. use a per-function annotation to set the slot and the vtable 2. use a single global list completely defining the vtable Option 1 has the negative that it's incredibly hard to tell from local information only what the available slots are, and which slot should be next assigned. Option 2 is undesirable because it completely centralises all definitions, which will likely make it very hard to add new C API functions without constantly generating merge conflicts (which is especially important to avoid breaking the merge queue), and likely leads to the functions by sequential offset being in random order (which isn't a technical problem, but is aesthetically unsatisfying!). The hierarchical approach allows C-API additions that touch completely different modules to be independent, while still permitting some locality in slot assignments for related functions, and providing an overview of where the slots are assigned. Each `leaves` node in the hierarchy over-allocates slots for itself to allow some addition of new functions in the future. There is a trade-off between having many `PyCapsule` function pointers and spreading the data across many completely separate pointers (which leaves most room for expansion), and having very few vtables (where we have to leave intermediate gaps for expansion within the hierarchy). The names and groupings are not especially important; they're mostly aesthetic, and subsequent patches will introduce header files and other automated tools that mostly mean that users will not have to worry about the internals of the vtables themselves. This is not included in `cext` itself (despite my earlier attempts to do just that) because doing so would require _all_ use of the vtable specification to involve a complete compilation of Qiskit, likely also including linking in `libpython`. In language-binding generation, we do not need to do that; we only need the string names and the separate assistance from `cbindgen` to parse out the function signatures. --- Cargo.lock | 8 + Cargo.toml | 4 +- crates/bindgen/Cargo.toml | 2 +- crates/cext-vtable/Cargo.toml | 20 ++ crates/cext-vtable/README.md | 15 ++ crates/cext-vtable/src/impl_.rs | 232 +++++++++++++++++++ crates/cext-vtable/src/lib.rs | 392 ++++++++++++++++++++++++++++++++ crates/pyext/Cargo.toml | 1 + crates/pyext/src/capi.rs | 84 +++++++ crates/pyext/src/lib.rs | 3 + qiskit/__init__.py | 1 + 11 files changed, 760 insertions(+), 2 deletions(-) create mode 100644 crates/cext-vtable/Cargo.toml create mode 100644 crates/cext-vtable/README.md create mode 100644 crates/cext-vtable/src/impl_.rs create mode 100644 crates/cext-vtable/src/lib.rs create mode 100644 crates/pyext/src/capi.rs diff --git a/Cargo.lock b/Cargo.lock index d5027ddd7b10..713ae1f3490a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1966,6 +1966,13 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "qiskit-cext-vtable" +version = "2.4.0-dev" +dependencies = [ + "qiskit-cext", +] + [[package]] name = "qiskit-circuit" version = "2.4.0-dev" @@ -2026,6 +2033,7 @@ dependencies = [ "qiskit-accelerate", "qiskit-bindgen", "qiskit-cext", + "qiskit-cext-vtable", "qiskit-circuit", "qiskit-circuit-library", "qiskit-qasm2", diff --git a/Cargo.toml b/Cargo.toml index 06c7a21f194c..5d148581a3aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ num-traits = "0.2" uuid = { version = "1.22", features = ["v4", "fast-rng"], default-features = false } anyhow = "1.0" binrw = "0.15" +cbindgen = "0.29.2" # Most of the crates don't need the feature `extension-module`, since only `qiskit-pyext` builds an # actual C extension (the feature disables linking in `libpython`, which is forbidden in Python @@ -52,12 +53,13 @@ pyo3 = { version = "0.28.1", features = ["abi3-py310"] } # These are our own crates. qiskit-accelerate = { path = "crates/accelerate" } qiskit-bindgen = { path = "crates/bindgen" } +qiskit-cext = { path = "crates/cext" } +qiskit-cext-vtable = { path = "crates/cext-vtable" } qiskit-circuit = { path = "crates/circuit" } qiskit-circuit-library = { path = "crates/circuit_library" } qiskit-qasm2 = { path = "crates/qasm2" } qiskit-qasm3 = { path = "crates/qasm3" } qiskit-qpy = { path = "crates/qpy" } -qiskit-cext = { path = "crates/cext" } qiskit-transpiler = { path = "crates/transpiler" } qiskit-quantum-info = { path = "crates/quantum_info" } qiskit-synthesis = {path = "crates/synthesis" } diff --git a/crates/bindgen/Cargo.toml b/crates/bindgen/Cargo.toml index 51d096c4ef7a..e3166e1cf79a 100644 --- a/crates/bindgen/Cargo.toml +++ b/crates/bindgen/Cargo.toml @@ -14,4 +14,4 @@ name = "qiskit_bindgen" [dependencies] anyhow.workspace = true -cbindgen = "0.29.2" +cbindgen.workspace = true diff --git a/crates/cext-vtable/Cargo.toml b/crates/cext-vtable/Cargo.toml new file mode 100644 index 000000000000..bc61e3d08640 --- /dev/null +++ b/crates/cext-vtable/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "qiskit-cext-vtable" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[lib] +name = "qiskit_cext_vtable" +doctest = false + +[lints] +workspace = true + +[dependencies] +qiskit-cext = { workspace = true, optional = true } + +[features] +addr = ["dep:qiskit-cext"] +python_binding = ["qiskit-cext?/python_binding"] diff --git a/crates/cext-vtable/README.md b/crates/cext-vtable/README.md new file mode 100644 index 000000000000..a8f8d3ea5e2a --- /dev/null +++ b/crates/cext-vtable/README.md @@ -0,0 +1,15 @@ +# `qiskit-cext-vtable` + +This crate defines the machinery to specify ABI-stable vtables of function pointers, and provides +concrete vtables for the functions within `cext`. + +This vtable can be compiled into dependencies on `cext` to include the actual function-pointers and +a complete vtable (when using the `addr` feature, in which case it depends on `cext`), or, if not +using the `addr` feature, then the tables can be built solely in terms of the function names, which +is used by build scripts to generate accessor files. + +This exists as a separate module to `cext` because language-bindings generators typically do not +want to, or _cannot_ compile against `cext` fully. For example, the build script of `pyext` cannot +depend on `cext` itself, because that would trigger a complete second compilation of Qiskit and +require the build script to link against `libpython` simply to run, both of which are highly +problematic for the build proces. diff --git a/crates/cext-vtable/src/impl_.rs b/crates/cext-vtable/src/impl_.rs new file mode 100644 index 000000000000..973294a401b1 --- /dev/null +++ b/crates/cext-vtable/src/impl_.rs @@ -0,0 +1,232 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::sync::LazyLock; + +// This has to be `pub(crate)` so that the `export_fn` macro can find it, but nothing in here is +// supposed to actually be used other than as internal implementation details. +#[doc(hidden)] +pub(crate) mod inner { + #[derive(Copy, Clone, Debug)] + pub struct ExportedFunctionPartial { + pub name: &'static str, + #[cfg(feature = "addr")] + pub addr: usize, + } + + pub fn last_element(path: &str) -> &str { + path.rfind(":") + .map(|index| path.split_at(index + 1).1) + .unwrap_or(path) + } +} + +/// An exported C API function, along with a slot to place it in a function-pointer lookup table. +#[derive(Copy, Clone, Debug)] +pub struct ExportedFunction { + /// The name of the function. + pub name: &'static str, + /// Which slot the function pointer should be assigned to, in its appropriate table. + pub slot: usize, + /// A pointer to the function, type erased to a pointer-width integer. + /// + /// In general, these derive from a type that looks like + /// ``` + /// unsafe extern "C" fn(T0, T1, ...) -> TRet + /// ``` + /// for some number of arguments and some (maybe void) return type. + #[cfg(feature = "addr")] + pub addr: usize, +} + +/// Maximum number of children of any single `ExportedFunctions` object. +/// +/// Upping this causes more static memory use, but it shouldn't be too onerous. You can nest +/// `ExportedFunctions` objects at any depth without trouble. +const MAX_CHILDREN: usize = 8; + +/// A compile-time list of exported functions, including potential subgroups of functions. +/// +/// When creating one of these, you almost certainly want to assign it to a `static` variable; all +/// the data it is supposed to represent is static +pub struct ExportedFunctions { + /// The amount of space reserved for the leaves. It is a panic to reserve less space than + /// required, but it's fine (and encouraged) to reserve as much space as you think you'll expand + /// to, in any given set of `ExportedFunctions`. + leaves_reserve: usize, + /// The calculated total length of reserved space (though there may be internal gaps that aren't + /// technically reserved within it). This is calculated at compile time, mostly for the + /// purposes of causing compile-time errors of this crate if the requested reservations don't + /// fit together properly. + len: usize, + /// The leaf functions owned by this set of `ExportedFunctions`. This has to constructed lazily + /// because the function-pointer values can't (in general) be calculated until the compiled + /// artifact is loaded into a process's memory space. + /// + /// This shouldn't be used directly; use [`get_leaves`] to build it to ensure the `assert` code + /// is called too. + leaves: LazyLock>>, + /// The offsets and references to each child owned by this object. The funky static-sized array + /// of maybe-uninitialized references is to make this all work at compile time. The array is + /// guaranteed to be zero or more `Some` values and all the remainder are `None`. + children: [Option<(usize, &'static ExportedFunctions)>; MAX_CHILDREN], +} +impl ExportedFunctions { + /// Create a new (lazy) list of exported functions. + /// + /// The first argument is how much space to reserve for the leaf nodes. It must be at least as + /// large as the vector of leaves, or this will panic when trying to access the functions. The + /// second is a non-capturing closure that produces a vector of items defined by [`export_fn`} + /// (or `None`). + /// + /// The second argument has to be a lazy closure because the addresses of functions generally + /// aren't set until the fully compiled binary has been loaded up into a process; they can't be + /// set at compile time. + /// + /// You can then append children with [`add_child`]. If you don't need any leaf functions, use + /// [`empty`]. + pub const fn leaves( + reserve: usize, + slots: fn() -> Vec>, + ) -> Self { + Self { + leaves_reserve: reserve, + len: reserve, + leaves: LazyLock::new(slots), + children: [None; MAX_CHILDREN], + } + } + /// Create a new empty list of exported functions. + /// + /// You can then append children with [`add_child`]. + pub const fn empty() -> Self { + Self::leaves(0, Vec::new) + } + + /// Add a group of exported functions as a child of this set. + /// + /// You must add children in offset order, or the compile-time checks on validity will fail. + /// + /// # Panics + /// + /// If there are already `MAX_CHILDREN` children attached to this set of functions, or if the + /// base `offset` is less than the maximum current reservation. + pub const fn add_child(mut self, offset: usize, fns: &'static ExportedFunctions) -> Self { + if offset < self.len { + panic!("offset is less than previously reserved space; don't fill in holes"); + } + let mut i = 0; + while self.children[i].is_some() { + i += 1; + if i == MAX_CHILDREN { + // We'd panic even without this catch, but this just makes sure the dev sees a + // clearer message about what's gone wrong. + panic!("too many children; consider using deeper nesting"); + } + } + // There isn't actually a value to throw away here, but we had to do this little dance with + // the iteration and `replace` to keep things safely `const` + self.children[i].replace((offset, fns)); + self.len = offset + fns.len; + self + } + + /// The total length of the reservation + pub fn len(&self) -> usize { + self.len + } + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + #[inline] + fn get_leaves(&self) -> &[Option] { + let slots = &self.leaves; + assert!(slots.len() <= self.leaves_reserve); + slots + } + + /// Iterate through all the exported functions, filling in their complete slot information from + /// a base offset. + /// + /// The order of iteration is not defined with respect to the slots; they are not guaranteed to + /// be in sorted order. + /// + /// Requiring a `'static` lifetime on `self` is mostly just laziness in defining this (it lets + /// us safely do it with iterator combinators rather than producing a custom "walker" class + /// while avoiding recursive types), but one that shouldn't actually affect use of this, since + /// all `ExportedFunctions` objects are expected to be defined as `static`s. + pub fn exports(&'static self, offset: usize) -> Box> { + Box::new( + self.get_leaves() + .iter() + .enumerate() + .filter_map(move |(i, func)| { + func.as_ref().map(move |func| ExportedFunction { + name: func.name, + slot: offset + i, + #[cfg(feature = "addr")] + addr: func.addr, + }) + }) + .chain( + self.children + .iter() + .filter_map(move |funcs| { + funcs + .as_ref() + .map(move |(inner, funcs)| funcs.exports(offset + inner)) + }) + .flatten(), + ), + ) + } +} + +/// Create an entry in an `ExportedFunctions` table. +/// +/// The first argument to the macro is the path to export, which should resolve to some object +/// declared like +/// ``` +/// #[unsafe(no_mangle)] +/// pub unsafe extern "C" fn qk_my_function() {} +/// ``` +/// (or just `pub extern` - the `unsafe` is not important). +/// +/// If the function is only defined when certain features are active, you can follow the path with a +/// comma-separated list of `feature = "my-feature"` items, such as +/// ``` +/// export_fn!(path::to::qk_my_function, feature = "python_binding", feature = "cool_stuff"); +/// ``` +macro_rules! export_fn { + ($fn:path) => { + Some($crate::impl_::inner::ExportedFunctionPartial { + name: $crate::impl_::inner::last_element(stringify!($fn)), + #[cfg(feature = "addr")] + addr: ($fn as *const ()).addr(), + }) + }; + ($fn:path, $(feature = $feat:tt),+) => {{ + #[cfg(all($(feature = $feat),+))] + let out = $crate::impl_::export_fn!($fn); + #[cfg(not(all($(feature = $feat),+)))] + let out = None::<$crate::impl_::inner::ExportedFunctionPartial>; + out + }}; +} +pub(crate) use export_fn; + +/// Helper module to made exports easier. This should contain everything that modules need to +/// define their exports. +pub(crate) mod prelude { + pub(crate) use super::{ExportedFunctions, export_fn}; +} diff --git a/crates/cext-vtable/src/lib.rs b/crates/cext-vtable/src/lib.rs new file mode 100644 index 000000000000..e4075a2a1f47 --- /dev/null +++ b/crates/cext-vtable/src/lib.rs @@ -0,0 +1,392 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +mod impl_; + +pub use crate::impl_::ExportedFunctions; + +// ================================================================================================= +// WARNING +// +// Remember that the exact slot of a function is PUBLIC API and must remain stable between versions. +// ================================================================================================= + +// We use a (small) number of different tables here so there's more breaks and places to expand. +pub static FUNCTIONS_CIRCUIT: ExportedFunctions = ExportedFunctions::empty() + .add_child(0, &circuit::FUNCTIONS) + .add_child(100, &dag::FUNCTIONS) + .add_child(200, ¶m::FUNCTIONS) + .add_child(250, &circuit_library::FUNCTIONS); +pub static FUNCTIONS_QI: ExportedFunctions = + ExportedFunctions::empty().add_child(0, &sparse_observable::FUNCTIONS); +pub use transpiler::FUNCTIONS as FUNCTIONS_TRANSPILE; + +// Below this line is close to a mirror of the actual `cext` structure. Ideally, all of the +// above exports would be locally within `cext` itself, but that has problems with needing to +// compile `cext` just to run the build script of things like `pyext`, which can end up in compiling +// the entire logic twice and needing to link `libpython` during the build. This form lets us only +// _optionally_ depend on `cext`, which avoids those problems, at the cost of non-locality of the +// code. +// +// The module structure here should largely match what is in `cext`, but use your common sense - if +// `cext` just has a single file to encapsulate a small number of functions, you can inline it. The +// idea is to make it easy to find things without too much additional boilerplate. + +mod circuit { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::circuit::*; + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::leaves(100, || { + vec![ + export_fn!(qk_circuit_new), + export_fn!(qk_quantum_register_new), + export_fn!(qk_quantum_register_free), + export_fn!(qk_classical_register_free), + export_fn!(qk_classical_register_new), + export_fn!(qk_circuit_add_quantum_register), + export_fn!(qk_circuit_add_classical_register), + export_fn!(qk_circuit_copy), + export_fn!(qk_circuit_num_qubits), + export_fn!(qk_circuit_num_clbits), + export_fn!(qk_circuit_free), + export_fn!(qk_circuit_gate), + export_fn!(qk_gate_num_qubits), + export_fn!(qk_gate_num_params), + export_fn!(qk_circuit_measure), + export_fn!(qk_circuit_reset), + export_fn!(qk_circuit_barrier), + export_fn!(qk_circuit_unitary), + export_fn!(qk_circuit_inst_unitary), + export_fn!(qk_circuit_pauli_product_rotation), + export_fn!(qk_circuit_inst_pauli_product_rotation), + export_fn!(qk_circuit_pauli_product_measurement), + export_fn!(qk_circuit_inst_pauli_product_measurement), + export_fn!(qk_circuit_instruction_kind), + export_fn!(qk_circuit_count_ops), + export_fn!(qk_circuit_num_instructions), + export_fn!(qk_circuit_get_instruction), + export_fn!(qk_circuit_instruction_clear), + export_fn!(qk_opcounts_clear), + export_fn!(qk_circuit_to_python, feature = "python_binding"), + export_fn!(qk_circuit_delay), + export_fn!(qk_circuit_to_dag), + export_fn!(qk_circuit_copy_empty_like), + ] + }); +} + +mod circuit_library { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::circuit_library::*; + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(iqp::qk_circuit_library_iqp), + export_fn!(iqp::qk_circuit_library_random_iqp), + export_fn!(quantum_volume::qk_circuit_library_quantum_volume), + export_fn!(suzuki_trotter::qk_circuit_library_suzuki_trotter), + export_fn!(pbc::qk_pauli_product_rotation_clear), + export_fn!(pbc::qk_pauli_product_measurement_clear), + ] + }); +} + +mod dag { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::dag::*; + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::leaves(100, || { + vec![ + export_fn!(qk_dag_new), + export_fn!(qk_dag_add_quantum_register), + export_fn!(qk_dag_add_classical_register), + export_fn!(qk_dag_num_qubits), + export_fn!(qk_dag_num_clbits), + export_fn!(qk_dag_num_op_nodes), + export_fn!(qk_dag_node_type), + export_fn!(qk_dag_qubit_in_node), + export_fn!(qk_dag_qubit_out_node), + export_fn!(qk_dag_clbit_in_node), + export_fn!(qk_dag_clbit_out_node), + export_fn!(qk_dag_wire_node_value), + export_fn!(qk_dag_op_node_num_qubits), + export_fn!(qk_dag_op_node_num_clbits), + export_fn!(qk_dag_op_node_num_params), + export_fn!(qk_dag_op_node_qubits), + export_fn!(qk_dag_op_node_clbits), + export_fn!(qk_dag_apply_gate), + export_fn!(qk_dag_apply_measure), + export_fn!(qk_dag_apply_reset), + export_fn!(qk_dag_apply_barrier), + export_fn!(qk_dag_apply_unitary), + export_fn!(qk_dag_op_node_gate_op), + export_fn!(qk_dag_op_node_unitary), + export_fn!(qk_dag_op_node_kind), + export_fn!(qk_dag_successors), + export_fn!(qk_dag_predecessors), + export_fn!(qk_dag_neighbors_clear), + export_fn!(qk_dag_get_instruction), + export_fn!(qk_dag_compose), + export_fn!(qk_dag_free), + export_fn!(qk_dag_to_circuit), + export_fn!(qk_dag_topological_op_nodes), + export_fn!(qk_dag_substitute_node_with_dag), + export_fn!(qk_dag_copy_empty_like), + export_fn!(qk_dag_to_python, feature = "python_binding"), + export_fn!(qk_dag_borrow_from_python, feature = "python_binding"), + export_fn!(qk_dag_convert_from_python, feature = "python_binding"), + export_fn!(qk_dag_replace_block_with_unitary), + export_fn!(qk_dag_substitute_node_with_unitary), + ] + }); +} + +mod param { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::param::*; + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(qk_param_new_symbol), + export_fn!(qk_param_zero), + export_fn!(qk_param_free), + export_fn!(qk_param_from_double), + export_fn!(qk_param_from_complex), + export_fn!(qk_param_copy), + export_fn!(qk_param_str), + export_fn!(qk_param_add), + export_fn!(qk_param_sub), + export_fn!(qk_param_mul), + export_fn!(qk_param_div), + export_fn!(qk_param_pow), + export_fn!(qk_param_sin), + export_fn!(qk_param_cos), + export_fn!(qk_param_tan), + export_fn!(qk_param_asin), + export_fn!(qk_param_acos), + export_fn!(qk_param_atan), + export_fn!(qk_param_log), + export_fn!(qk_param_exp), + export_fn!(qk_param_abs), + export_fn!(qk_param_sign), + export_fn!(qk_param_neg), + export_fn!(qk_param_conjugate), + export_fn!(qk_param_equal), + export_fn!(qk_param_as_real), + ] + }); +} + +mod sparse_observable { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::sparse_observable::*; + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(qk_obs_zero), + export_fn!(qk_obs_identity), + export_fn!(qk_obs_new), + export_fn!(qk_obs_free), + export_fn!(qk_obs_add_term), + export_fn!(qk_obs_term), + export_fn!(qk_obs_num_terms), + export_fn!(qk_obs_num_qubits), + export_fn!(qk_obs_len), + export_fn!(qk_obs_coeffs), + export_fn!(qk_obs_indices), + export_fn!(qk_obs_boundaries), + export_fn!(qk_obs_bit_terms), + export_fn!(qk_obs_multiply), + export_fn!(qk_obs_multiply_inplace), + export_fn!(qk_obs_add), + export_fn!(qk_obs_add_inplace), + export_fn!(qk_obs_scaled_add), + export_fn!(qk_obs_scaled_add_inplace), + export_fn!(qk_obs_compose), + export_fn!(qk_obs_compose_map), + export_fn!(qk_obs_apply_layout), + export_fn!(qk_obs_canonicalize), + export_fn!(qk_obs_copy), + export_fn!(qk_obs_equal), + export_fn!(qk_obs_str), + export_fn!(qk_str_free), + export_fn!(qk_obsterm_str), + export_fn!(qk_bitterm_label), + export_fn!(qk_obs_to_python, feature = "python_binding"), + ] + }); +} + +mod transpiler { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::transpiler::{neighbors::*, transpile_function::*, transpile_layout::*}; + + pub static TRANSPILE_FUNCTION: ExportedFunctions = ExportedFunctions::leaves(20, || { + vec![ + export_fn!(qk_transpile), + export_fn!(qk_transpiler_default_options), + export_fn!(qk_transpile_stage_init), + export_fn!(qk_transpile_stage_routing), + export_fn!(qk_transpile_stage_optimization), + export_fn!(qk_transpile_stage_translation), + export_fn!(qk_transpile_stage_layout), + ] + }); + pub static NEIGHBORS: ExportedFunctions = ExportedFunctions::leaves(5, || { + vec![ + export_fn!(qk_neighbors_is_all_to_all), + export_fn!(qk_neighbors_from_target), + export_fn!(qk_neighbors_clear), + ] + }); + pub static TRANSPILE_LAYOUT: ExportedFunctions = ExportedFunctions::leaves(15, || { + vec![ + export_fn!(qk_transpile_layout_num_input_qubits), + export_fn!(qk_transpile_layout_num_output_qubits), + export_fn!(qk_transpile_layout_initial_layout), + export_fn!(qk_transpile_layout_output_permutation), + export_fn!(qk_transpile_layout_final_layout), + export_fn!(qk_transpile_layout_generate_from_mapping), + export_fn!(qk_transpile_layout_free), + export_fn!(qk_transpile_layout_to_python, feature = "python_binding"), + ] + }); + + mod target { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::transpiler::target::*; + + static FUNCTIONS_TARGET: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(qk_target_new), + export_fn!(qk_target_num_qubits), + export_fn!(qk_target_dt), + export_fn!(qk_target_set_dt), + export_fn!(qk_target_granularity), + export_fn!(qk_target_set_granularity), + export_fn!(qk_target_min_length), + export_fn!(qk_target_set_min_length), + export_fn!(qk_target_pulse_alignment), + export_fn!(qk_target_set_pulse_alignment), + export_fn!(qk_target_acquire_alignment), + export_fn!(qk_target_set_acquire_alignment), + export_fn!(qk_target_copy), + export_fn!(qk_target_free), + export_fn!(qk_target_add_instruction), + export_fn!(qk_target_update_property), + export_fn!(qk_target_num_instructions), + export_fn!(qk_target_instruction_supported), + export_fn!(qk_target_op_index), + export_fn!(qk_target_op_name), + export_fn!(qk_target_op_num_properties), + export_fn!(qk_target_op_qargs_index), + export_fn!(qk_target_op_qargs), + export_fn!(qk_target_op_props), + export_fn!(qk_target_op_get), + export_fn!(qk_target_op_gate), + export_fn!(qk_target_op_clear), + export_fn!(qk_target_borrow_from_python, feature = "python_binding"), + export_fn!(qk_target_convert_from_python, feature = "python_binding"), + ] + }); + static FUNCTIONS_TARGET_ENTRY: ExportedFunctions = ExportedFunctions::leaves(20, || { + vec![ + export_fn!(qk_target_entry_new), + export_fn!(qk_target_entry_new_measure), + export_fn!(qk_target_entry_new_reset), + export_fn!(qk_target_entry_new_fixed), + export_fn!(qk_target_entry_num_properties), + export_fn!(qk_target_entry_free), + export_fn!(qk_target_entry_add_property), + export_fn!(qk_target_entry_set_name), + ] + }); + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::empty() + .add_child(0, &FUNCTIONS_TARGET) + .add_child(50, &FUNCTIONS_TARGET_ENTRY); + } + + mod passes { + use crate::impl_::prelude::*; + #[cfg(feature = "addr")] + use qiskit_cext::transpiler::passes::*; + + static FUNCTIONS_PASSES: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(elide_permutations::qk_transpiler_pass_elide_permutations), + export_fn!(gate_direction::qk_transpiler_pass_check_gate_direction), + export_fn!(gate_direction::qk_transpiler_pass_gate_direction), + export_fn!(optimize_1q_sequences::qk_transpiler_pass_optimize_1q_sequences), + export_fn!(remove_diagonal_gates_before_measure::qk_transpiler_pass_remove_diagonal_gates_before_measure), + export_fn!(remove_identity_equiv::qk_transpiler_pass_remove_identity_equivalent), + export_fn!(split_2q_unitaries::qk_transpiler_pass_split_2q_unitaries), + ] + }); + static FUNCTIONS_STANDALONE: ExportedFunctions = ExportedFunctions::leaves(50, || { + vec![ + export_fn!(basis_translator::qk_transpiler_pass_standalone_basis_translator), + export_fn!(commutative_cancellation::qk_transpiler_pass_standalone_commutative_cancellation), + export_fn!(consolidate_blocks::qk_transpiler_pass_standalone_consolidate_blocks), + export_fn!(elide_permutations::qk_transpiler_pass_standalone_elide_permutations), + export_fn!(gate_direction::qk_transpiler_pass_standalone_check_gate_direction), + export_fn!(gate_direction::qk_transpiler_pass_standalone_gate_direction), + export_fn!(inverse_cancellation::qk_transpiler_pass_standalone_inverse_cancellation), + export_fn!(optimize_1q_sequences::qk_transpiler_pass_standalone_optimize_1q_sequences), + export_fn!(remove_diagonal_gates_before_measure::qk_transpiler_pass_standalone_remove_diagonal_gates_before_measure), + export_fn!(remove_identity_equiv::qk_transpiler_pass_standalone_remove_identity_equivalent), + export_fn!(sabre_layout::qk_transpiler_pass_standalone_sabre_layout), + export_fn!(split_2q_unitaries::qk_transpiler_pass_standalone_split_2q_unitaries), + export_fn!(unitary_synthesis::qk_transpiler_pass_standalone_unitary_synthesis), + export_fn!(vf2::qk_transpiler_pass_standalone_vf2_layout_average), + export_fn!(vf2::qk_transpiler_pass_standalone_vf2_layout_exact), + ] + }); + static FUNCTIONS_SABRE: ExportedFunctions = ExportedFunctions::leaves(5, || { + vec![export_fn!(sabre_layout::qk_sabre_layout_options_default)] + }); + static FUNCTIONS_VF2: ExportedFunctions = ExportedFunctions::leaves(20, || { + vec![ + export_fn!(vf2::qk_vf2_layout_result_has_match), + export_fn!(vf2::qk_vf2_layout_result_has_improvement), + export_fn!(vf2::qk_vf2_layout_result_map_virtual_qubit), + export_fn!(vf2::qk_vf2_layout_result_free), + export_fn!(vf2::qk_vf2_layout_configuration_new), + export_fn!(vf2::qk_vf2_layout_configuration_free), + export_fn!(vf2::qk_vf2_layout_configuration_set_call_limit), + export_fn!(vf2::qk_vf2_layout_configuration_set_time_limit), + export_fn!(vf2::qk_vf2_layout_configuration_set_max_trials), + export_fn!(vf2::qk_vf2_layout_configuration_set_shuffle_seed), + export_fn!(vf2::qk_vf2_layout_configuration_set_score_initial), + ] + }); + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::empty() + .add_child(0, &FUNCTIONS_PASSES) + .add_child(100, &FUNCTIONS_STANDALONE) + .add_child(200, &FUNCTIONS_SABRE) + .add_child(205, &FUNCTIONS_VF2); + } + + pub static FUNCTIONS: ExportedFunctions = ExportedFunctions::empty() + .add_child(0, &TRANSPILE_FUNCTION) + .add_child(20, &NEIGHBORS) + .add_child(35, &TRANSPILE_LAYOUT) + .add_child(50, &target::FUNCTIONS) + .add_child(150, &passes::FUNCTIONS); +} diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index be43a612287b..a31078b767c3 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -36,6 +36,7 @@ qiskit-qasm2.workspace = true qiskit-qasm3.workspace = true qiskit-qpy.workspace = true qiskit-cext = { workspace = true, features = ["python_binding"] } +qiskit-cext-vtable = { workspace = true, features = ["python_binding", "addr"] } qiskit-transpiler.workspace = true qiskit-quantum-info.workspace = true qiskit-synthesis.workspace = true diff --git a/crates/pyext/src/capi.rs b/crates/pyext/src/capi.rs new file mode 100644 index 000000000000..6edf9f6ed795 --- /dev/null +++ b/crates/pyext/src/capi.rs @@ -0,0 +1,84 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use std::ffi::{CStr, CString, c_void}; +use std::ptr::NonNull; +use std::sync::LazyLock; + +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::{PyCapsule, PyModule}; +use qiskit_cext_vtable::ExportedFunctions; + +struct VTable { + name: &'static str, + table: Vec, +} +impl VTable { + fn new(name: &'static str, base: &'static ExportedFunctions) -> Self { + let mut table = vec![0; base.len()]; + for export in base.exports(0) { + table[export.slot] = export.addr; + } + Self { name, table } + } + + fn attach_pycapsule(&'static self, m: &Bound<'_, PyModule>, modname: &str) -> PyResult<()> { + // We do `[T]::as_ptr().cast_mut()` here rather than `.as_mut_ptr()` to avoid, even + // temporarily, breaking Rust's rules on aliasing mutable references; we _cannot_ retrieve a + // safe `&mut T` pointer out of the static, so we mustn't attempt it. It's fine to hold a + // mutable pointer, though - it's only UB to attempt to mutate through that. + let ptr = NonNull::new(self.table.as_ptr().cast_mut().cast::()) + .expect("slices should be backed by non-null pointers"); + let last_modname = modname + .rsplit_once(".") + .map(|(_, last)| last) + .unwrap_or(modname); + let base_modname = m.name()?; + if base_modname != last_modname { + return Err(PyValueError::new_err(format!( + "internal error: mismatched names between module ('{base_modname}') and requested submodule ('{modname}')" + ))); + } + + let fullname = { + // We need to leak the CString because Python needs the name to last until the end of + // time. + let alloc = CString::new(format!("{}.{}", modname, self.name))?; + // SAFETY: the input to `from_ptr` is a pointer from a `CString` created above. + unsafe { CStr::from_ptr::<'static>(alloc.into_raw()) } + }; + + // SAFETY: the pointer is to static-lifetimed data, and no destructor is necessary (only the + // complete memory-space cleanup on process termination. + let capsule = unsafe { PyCapsule::new_with_pointer(m.py(), ptr, fullname) }?; + m.add(self.name, capsule)?; + Ok(()) + } +} + +static FFI_TRANSPILE: LazyLock = + LazyLock::new(|| VTable::new("QK_FFI_TRANSPILE", &qiskit_cext_vtable::FUNCTIONS_TRANSPILE)); +static FFI_CIRCUIT: LazyLock = + LazyLock::new(|| VTable::new("QK_FFI_CIRCUIT", &qiskit_cext_vtable::FUNCTIONS_CIRCUIT)); +static FFI_QI: LazyLock = + LazyLock::new(|| VTable::new("QK_FFI_QI", &qiskit_cext_vtable::FUNCTIONS_QI)); + +#[pymodule(name = "capi")] +pub fn capi_mod(m: &Bound<'_, PyModule>) -> PyResult<()> { + static MODNAME: &str = "qiskit._accelerate.capi"; + + FFI_TRANSPILE.attach_pycapsule(m, MODNAME)?; + FFI_CIRCUIT.attach_pycapsule(m, MODNAME)?; + FFI_QI.attach_pycapsule(m, MODNAME)?; + Ok(()) +} diff --git a/crates/pyext/src/lib.rs b/crates/pyext/src/lib.rs index 27b8c64c2124..ecf19ef5dee9 100644 --- a/crates/pyext/src/lib.rs +++ b/crates/pyext/src/lib.rs @@ -13,6 +13,8 @@ use pyo3::prelude::*; pub use qiskit_cext::*; +mod capi; + #[inline(always)] #[doc(hidden)] fn add_submodule(m: &Bound, constructor: F, name: &str) -> PyResult<()> @@ -29,6 +31,7 @@ where #[rustfmt::skip] #[pymodule] fn _accelerate(m: &Bound) -> PyResult<()> { + add_submodule(m, capi::capi_mod, "capi")?; add_submodule(m, ::qiskit_transpiler::passes::alap_schedule_analysis_mod, "alap_schedule_analysis")?; add_submodule(m, ::qiskit_transpiler::passes::asap_schedule_analysis_mod, "asap_schedule_analysis")?; add_submodule(m, ::qiskit_transpiler::passes::apply_layout_mod, "apply_layout")?; diff --git a/qiskit/__init__.py b/qiskit/__init__.py index f3f56a24dc3f..99d5a08abc7b 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -57,6 +57,7 @@ sys.modules["qiskit._accelerate.alap_schedule_analysis"] = _accelerate.alap_schedule_analysis sys.modules["qiskit._accelerate.asap_schedule_analysis"] = _accelerate.asap_schedule_analysis sys.modules["qiskit._accelerate.apply_layout"] = _accelerate.apply_layout +sys.modules["qiskit._accelerate.capi"] = _accelerate.capi sys.modules["qiskit._accelerate.circuit"] = _accelerate.circuit sys.modules["qiskit._accelerate.circuit.classical"] = _accelerate.circuit.classical sys.modules["qiskit._accelerate.circuit.classical.expr"] = _accelerate.circuit.classical.expr From 1272172bd9fadc1f95b9c924e18203764845881f Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 17 Mar 2026 18:00:09 +0000 Subject: [PATCH 2/3] Fix documentation cross-references Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> Co-authored-by: Max Rossmannek --- crates/cext-vtable/src/impl_.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/cext-vtable/src/impl_.rs b/crates/cext-vtable/src/impl_.rs index 973294a401b1..1a5659cb15f1 100644 --- a/crates/cext-vtable/src/impl_.rs +++ b/crates/cext-vtable/src/impl_.rs @@ -48,11 +48,11 @@ pub struct ExportedFunction { pub addr: usize, } -/// Maximum number of children of any single `ExportedFunctions` object. +/// Maximum number of children of any single [`ExportedFunctions`] object. /// /// Upping this causes more static memory use, but it shouldn't be too onerous. You can nest -/// `ExportedFunctions` objects at any depth without trouble. -const MAX_CHILDREN: usize = 8; +/// [`ExportedFunctions`] objects at any depth without trouble. +pub const MAX_CHILDREN: usize = 8; /// A compile-time list of exported functions, including potential subgroups of functions. /// @@ -61,19 +61,19 @@ const MAX_CHILDREN: usize = 8; pub struct ExportedFunctions { /// The amount of space reserved for the leaves. It is a panic to reserve less space than /// required, but it's fine (and encouraged) to reserve as much space as you think you'll expand - /// to, in any given set of `ExportedFunctions`. + /// to, in any given set of [ExportedFunctions]. leaves_reserve: usize, /// The calculated total length of reserved space (though there may be internal gaps that aren't /// technically reserved within it). This is calculated at compile time, mostly for the /// purposes of causing compile-time errors of this crate if the requested reservations don't /// fit together properly. len: usize, - /// The leaf functions owned by this set of `ExportedFunctions`. This has to constructed lazily - /// because the function-pointer values can't (in general) be calculated until the compiled - /// artifact is loaded into a process's memory space. + /// The leaf functions owned by this set of [ExportedFunctions]. This has to be constructed + /// lazily because the function-pointer values can't (in general) be calculated until the + /// compiled artifact is loaded into a process's memory space. /// - /// This shouldn't be used directly; use [`get_leaves`] to build it to ensure the `assert` code - /// is called too. + /// This shouldn't be used directly; use [Self::get_leaves] to build it to ensure the `assert` + /// code is called too. leaves: LazyLock>>, /// The offsets and references to each child owned by this object. The funky static-sized array /// of maybe-uninitialized references is to make this all work at compile time. The array is @@ -85,15 +85,15 @@ impl ExportedFunctions { /// /// The first argument is how much space to reserve for the leaf nodes. It must be at least as /// large as the vector of leaves, or this will panic when trying to access the functions. The - /// second is a non-capturing closure that produces a vector of items defined by [`export_fn`} + /// second is a non-capturing closure that produces a vector of items defined by `export_fn` /// (or `None`). /// /// The second argument has to be a lazy closure because the addresses of functions generally /// aren't set until the fully compiled binary has been loaded up into a process; they can't be /// set at compile time. /// - /// You can then append children with [`add_child`]. If you don't need any leaf functions, use - /// [`empty`]. + /// You can then append children with [`add_child`][Self::add_child]. If you don't need any + /// leaf functions, use [`empty`][Self::empty]. pub const fn leaves( reserve: usize, slots: fn() -> Vec>, @@ -107,7 +107,7 @@ impl ExportedFunctions { } /// Create a new empty list of exported functions. /// - /// You can then append children with [`add_child`]. + /// You can then append children with [`add_child`][Self::add_child]. pub const fn empty() -> Self { Self::leaves(0, Vec::new) } @@ -118,7 +118,7 @@ impl ExportedFunctions { /// /// # Panics /// - /// If there are already `MAX_CHILDREN` children attached to this set of functions, or if the + /// If there are already [`MAX_CHILDREN`] children attached to this set of functions, or if the /// base `offset` is less than the maximum current reservation. pub const fn add_child(mut self, offset: usize, fns: &'static ExportedFunctions) -> Self { if offset < self.len { From a2766811c5f02c94a98ebfc0cfb47bdd397f7d65 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 17 Mar 2026 18:03:37 +0000 Subject: [PATCH 3/3] Fix missing cross-ref Co-authored-by: Raynel Sanchez <87539502+raynelfss@users.noreply.github.com> --- crates/cext-vtable/src/impl_.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cext-vtable/src/impl_.rs b/crates/cext-vtable/src/impl_.rs index 1a5659cb15f1..001ec21dce51 100644 --- a/crates/cext-vtable/src/impl_.rs +++ b/crates/cext-vtable/src/impl_.rs @@ -164,7 +164,7 @@ impl ExportedFunctions { /// Requiring a `'static` lifetime on `self` is mostly just laziness in defining this (it lets /// us safely do it with iterator combinators rather than producing a custom "walker" class /// while avoiding recursive types), but one that shouldn't actually affect use of this, since - /// all `ExportedFunctions` objects are expected to be defined as `static`s. + /// all [`ExportedFunctions`] objects are expected to be defined as `static`s. pub fn exports(&'static self, offset: usize) -> Box> { Box::new( self.get_leaves()