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..001ec21dce51 --- /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. +pub 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 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 [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 + /// 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`][Self::add_child]. If you don't need any + /// leaf functions, use [`empty`][Self::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`][Self::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