diff --git a/.github/workflows/branch-protection.yml b/.github/workflows/branch-protection.yml index a0502f8be4f8..84ffacec7a8f 100644 --- a/.github/workflows/branch-protection.yml +++ b/.github/workflows/branch-protection.yml @@ -53,6 +53,7 @@ jobs: - miri - qpy - neko + - pyo3-ffi # This job needs to trigger even if its predecessors failed and return the same status. By # default, GitHub Actions will "skip" the job if a requirement failed, but that counts as a # "success" state, which is not acceptable for a branch-protection rule. We don't use `always` @@ -84,6 +85,13 @@ jobs: python-version: ${{ needs.config.outputs.python-old }} runner: ${{ needs.config.outputs.runner-linux-x86_64 }} + pyo3-ffi: + name: pyo3-ffi + needs: config + uses: ./.github/workflows/pyo3-ffi.yml + with: + runner: ${{ needs.config.outputs.runner-linux-x86_64 }} + test-linux: name: Unit Python / Linux / ${{ matrix.config.id }} - ${{ matrix.runner }} needs: config diff --git a/.github/workflows/pyo3-ffi.yml b/.github/workflows/pyo3-ffi.yml new file mode 100644 index 000000000000..945b1a6c9c6a --- /dev/null +++ b/.github/workflows/pyo3-ffi.yml @@ -0,0 +1,28 @@ +--- +name: qiskit-pyo3-ffi +on: + workflow_call: + inputs: + runner: + description: Runner image to use. + type: string + required: true + +jobs: + check: + name: Checks + runs-on: ${{ inputs.runner }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v6 + - name: Prepare package + run: make qiskit-pyo3-ffi + - name: Package + working-directory: dist/rust/qiskit-pyo3-ffi + run: cargo package + - name: Lint + working-directory: dist/rust/qiskit-pyo3-ffi + run: cargo clippy + - name: Docs + working-directory: dist/rust/qiskit-pyo3-ffi + run: cargo doc --no-deps diff --git a/Cargo.lock b/Cargo.lock index 16faf816f562..63ee4fb7cbb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2263,6 +2263,7 @@ dependencies = [ "anyhow", "cbindgen", "hashbrown 0.15.5", + "qiskit-cext-vtable", "regex", ] diff --git a/Makefile b/Makefile index a84ae77eaa6e..6ae0b8b9216a 100644 --- a/Makefile +++ b/Makefile @@ -184,6 +184,11 @@ ctest: cheader build-clib-dev ccoverage: C_LIB_RUSTC_FLAGS=-Cinstrument-coverage ccoverage: ctest +.PHONY: qiskit-pyo3-ffi +qiskit-pyo3-ffi: + rm -rf dist/rust/qiskit-pyo3-ffi + cargo run -p qiskit-bindgen-cli -- generate-pyo3 --cext-path crates/cext --output-path dist/rust/qiskit-pyo3-ffi + .PHONY: cclean cclean: rm -rf $(C_DIR_OUT) $(C_DIR_TEST_BUILD) $(C_INCLUDE_FILES_ABS_GENERATED) diff --git a/crates/bindgen-cli/README.md b/crates/bindgen-cli/README.md index b40cd2337df7..e7adb74679e0 100644 --- a/crates/bindgen-cli/README.md +++ b/crates/bindgen-cli/README.md @@ -25,6 +25,16 @@ The `-c` (`--cext-path`) argument specifies the location of the `cext` crate sou internal calls to `cbindgen`. The `-o` (`--output-path`) argument specifies where to place the files. + +## Produce the `qiskit-pyo3-ffi` crate for distribution + +Use the `generate-pyo3` subcommand, such as + +```bash +cargo run -p qiskit-bindgen-cli -- generate-pyo3 -c crates/cext -o dist/rust/qiskit-pyo3-ffi +``` + + ## Linting the current vtable slots Use the `lint-slots` subcommand, such as diff --git a/crates/bindgen-cli/src/main.rs b/crates/bindgen-cli/src/main.rs index f34594d7be04..af18003d3279 100644 --- a/crates/bindgen-cli/src/main.rs +++ b/crates/bindgen-cli/src/main.rs @@ -44,6 +44,14 @@ enum Command { #[arg(short, long)] cext_path: PathBuf, }, + GeneratePyo3 { + /// Path to the `cext` sources to generate headers for. + #[arg(short, long)] + cext_path: PathBuf, + /// Path to write the output crate. + #[arg(short, long)] + output_path: PathBuf, + }, } fn main() -> anyhow::Result<()> { @@ -61,5 +69,13 @@ fn main() -> anyhow::Result<()> { let bindings = qiskit_bindgen::generate_bindings(cext_path)?; lint::lint(&bindings, &SlotsLists::ours())?.map_err(|fails| anyhow!(fails.explain())) } + Command::GeneratePyo3 { + cext_path, + output_path, + } => { + let bindings = qiskit_bindgen::generate_bindings(cext_path)?; + qiskit_bindgen::install_rust_pyo3_ffi(&bindings, output_path)?; + Ok(()) + } } } diff --git a/crates/bindgen/Cargo.toml b/crates/bindgen/Cargo.toml index 6699ac9b976c..97a225f0263c 100644 --- a/crates/bindgen/Cargo.toml +++ b/crates/bindgen/Cargo.toml @@ -13,6 +13,7 @@ workspace = true name = "qiskit_bindgen" [dependencies] +qiskit-cext-vtable = { workspace = true, features = ["python_binding"] } anyhow.workspace = true cbindgen = { workspace = true, features = ["unstable_ir"] } hashbrown.workspace = true diff --git a/crates/bindgen/README.md b/crates/bindgen/README.md index a07e09d71083..b3ccb6cd0990 100644 --- a/crates/bindgen/README.md +++ b/crates/bindgen/README.md @@ -5,7 +5,8 @@ library produced by that crate. This is an internal library only used as part of the build and distribution process of Qiskit. This crate owns all parts of the stand-alone header-file generation logic, including the -custom-written include files, and the installation logic. +custom-written include files (the `include` directory), the Rust template crate (the `pyo3-ffi` +directory), and the installation logic of both. ## Usage diff --git a/crates/bindgen/pyo3-ffi/.cargo/config.toml b/crates/bindgen/pyo3-ffi/.cargo/config.toml new file mode 100644 index 000000000000..c38851e26968 --- /dev/null +++ b/crates/bindgen/pyo3-ffi/.cargo/config.toml @@ -0,0 +1,5 @@ +[build] +# Share the `target` with the root of the repository as a developer convenience, so using +# `rust-analyzer` or `clippy` within the template crate will not need to keep separate compilations +# of `pyo3`; we can just re-use Qiskit's. +target-dir = "../../../target" diff --git a/crates/bindgen/pyo3-ffi/Cargo.lock b/crates/bindgen/pyo3-ffi/Cargo.lock new file mode 100644 index 000000000000..f1ef2f51da09 --- /dev/null +++ b/crates/bindgen/pyo3-ffi/Cargo.lock @@ -0,0 +1,158 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "qiskit-pyo3-ffi" +version = "0.1.0" +dependencies = [ + "num-complex", + "pyo3", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/crates/bindgen/pyo3-ffi/Cargo.toml b/crates/bindgen/pyo3-ffi/Cargo.toml new file mode 100644 index 000000000000..28a7f0c1e8ee --- /dev/null +++ b/crates/bindgen/pyo3-ffi/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "qiskit-pyo3-ffi" +version = "0.1.0" +edition = "2021" +# This doesn't need to be the same as Qiskit itself. +rust-version = "1.80" +license = "Apache-2.0" +homepage = "https://www.ibm.com/quantum/qiskit" +repository = "https://github.com/Qiskit/qiskit" +description = "Raw bindings to the Qiskit C FFI accessed through the Python-space 'qiskit' package" +# These files are Qiskit developer convenience, and don't have a meaning for the packaged crate. +exclude = [".cargo"] + +# Forcibly exclude this from any containing workspace. +[workspace] + +[lib] +name = "qiskit_pyo3_ffi" + +[dependencies] +# We don't inherit from the workspace because this crate is intended for distribution to others to +# use as a dependency. For PyO3 in particular, we don't want to hard-pin the required version. +pyo3.version = ">=0.22,<=0.28" +num-complex.version = "0.4" diff --git a/crates/bindgen/pyo3-ffi/src/ffi.rs b/crates/bindgen/pyo3-ffi/src/ffi.rs new file mode 100644 index 000000000000..7580b4662e64 --- /dev/null +++ b/crates/bindgen/pyo3-ffi/src/ffi.rs @@ -0,0 +1,19 @@ +// 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. + +// This is a dummy file that's overwritten by the crate-generation script. It's just here as a base +// test that the macro works, and to suppress clippy/rust-analyzer warnings while editing the +// template crate. + +use crate::declare_fn; + +declare_fn!(crate::QK_FFI_CIRCUIT[0]; qk_api_version() -> u32); diff --git a/crates/bindgen/pyo3-ffi/src/lib.rs b/crates/bindgen/pyo3-ffi/src/lib.rs new file mode 100644 index 000000000000..13e030d52b03 --- /dev/null +++ b/crates/bindgen/pyo3-ffi/src/lib.rs @@ -0,0 +1,222 @@ +// 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. + +//! Rust bindings to the raw Qiskit C API, accessed through the Python-space `qiskit` package. +//! +//! This is a low-level, mostly automatically generated, crate. All the functions (with the +//! exception of [`qk_import`]) are unsafe, since they rely on [`qk_import`] having been called +//! (successfully!) before any of them are valid. +//! +//! You should also consult the [documentation of the Qiskit C API][qiskit-capi], which is the +//! canonical version of the C API documentation. All the functions, structs, and enums listed here +//! are derived directly from the same source as the C API documentation. +//! +//! You may find the [Qiskit C API documentation on creating Python extension modules in +//! C][extend-qiskit-with-c] useful as a guide to using the C API. +//! +//! [qiskit-capi]: https://quantum.cloud.ibm.com/docs/api/qiskit-c +//! [extend-qiskit-with-c]: https://quantum.cloud.ibm.com/docs/guides/c-extension-for-python +//! +//! # Export conventions +//! +//! ## Enums +//! +//! C does not have namespacing, so in the raw C API, all enum values are prefixed with the name +//! of the enum (such as +//! [`QkExitCode_Success`](https://quantum.cloud.ibm.com/docs/api/qiskit-c/qk-exit-code)). In Rust, +//! we instead export them using natural namespacing, such as [`QkExitCode::Success`]. +//! +//! All Qiskit C API enums are simple integer representations, so ours derive the standard-library +//! derivations (`Clone`, `Copy`, etc). +//! +//! ## Structs +//! +//! Fully defined C structs are declared in the natural way one would expect from C: all fields are +//! `pub`, all fields have their documented names, and the struct is `#[repr(C)]`. The structs do +//! not derive anything other than `Debug`. +//! +//! Opaque structs are defined as unconstructable invariant pinned objects, [as suggested by the +//! Rustonomicon](https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs). +//! +//! ### Exceptions +//! +//! `QkComplex64` is exposed directly as [`num_complex::Complex64`], which it is necessarily ABI +//! compatible with (in order to interoperate with C/C++ correctly), and is how the object is +//! actually defined by Qiskit too. +//! +//! ## Functions +//! +//! All C API functions are exposed directly using the same naming conventions and call signatures +//! as stated in the C API documentation. +//! +//! *Note*: the functions exposed by [`qiskit-pyo3-ffi`] are not the raw `extern "C"` functions of +//! the Qiskit C API; they are `#[inline] extern "Rust"` wrappers around suitable function-pointer +//! dereferencing. +//! +//! ### Exceptions +//! +//! [`qk_import`] is not a native-library C API function (just as it isn't in the C header file +//! either), and requires a PyO3 [`Python`] token to call. +//! +//! +//! # Safety +//! +//! All function calls (other than of [`qk_import`] itself) are *invalid* until [`qk_import`] has +//! been called successfully. You will typically want to do this in your Python module +//! initialization. +//! +//! Note also that the "safety" documentation of individual functions (and of the usage patterns of +//! structs) is incomplete; you should consult the [Qiskit C API documentation][qiskit-capi] for the +//! relevant function for all the pre- and post-conditions. + +// The complete version of this module is autogenerated. +mod ffi; + +use pyo3::prelude::*; +use pyo3::types::PyCapsule; +use std::ffi::{CString, c_void}; +use std::ptr::NonNull; +use std::sync::OnceLock; + +pub use ffi::*; + +/// Simple wrapper around a pointer we know should be safe to share between threads. +#[derive(Clone, Copy, Debug)] +#[repr(transparent)] +struct VTablePtr(NonNull<*mut c_void>); +impl std::ops::Deref for VTablePtr { + type Target = NonNull<*mut c_void>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +// Raw pointers aren't `Send` or `Sync`, but this is more of a lint to prevent data structures that +// contain them from accidentally auto-deriving `Send + Sync`. In our case, we trust that neither +// Qiskit nor Python will invalidate the pointers stored in the `PyCapsule`s at any point (and we +// leak a reference to each capsule to make sure the capsules themselves stick around). +unsafe impl Send for VTablePtr {} +unsafe impl Sync for VTablePtr {} + +static CAPI_MODULE_NAME: &str = "qiskit._accelerate.capi"; + +static QK_FFI_CIRCUIT: OnceLock = OnceLock::new(); +static QK_FFI_TRANSPILE: OnceLock = OnceLock::new(); +static QK_FFI_QI: OnceLock = OnceLock::new(); + +/// Prepare the Qiskit C API for use. +/// +/// This *must* be called before any raw FFI functions are called. All Qiskit C API functions +/// accessed through this crate are invalid before this has succeeded. +/// +/// In general, you should call this during the Python module initialization function of any +/// submodule you create, such as: +/// +/// ```rust,ignore +/// #[pyo3::pymodule] +/// mod my_module { +/// use pyo3::prelude::*; +/// use qiskit_pyo3_ffi as ffi; +/// +/// #[pymodule_init] +/// fn init(m: Bound<'_, PyModule>) -> PyResult<()> { +/// ffi::qk_import(m.py())?; +/// Ok(()) +/// } +/// } +/// ``` +/// +/// It is safe to call this function more than once. +pub fn qk_import(py: Python) -> PyResult<()> { + let capsules = [ + (&QK_FFI_CIRCUIT, "QK_FFI_CIRCUIT"), + (&QK_FFI_TRANSPILE, "QK_FFI_TRANSPILE"), + (&QK_FFI_QI, "QK_FFI_QI"), + ]; + for (lock, name) in capsules { + // This is a lazy approximation of `get_or_try_init`, which isn't stable in Rust 1.80. If + // more than one thread is attempting to do `qk_import` simultaneously, then the caller is + // doing something very weird, but it shouldn't be a problem for us because Python is + // responsible for making sure the import system is thread safe. + // + // We don't care about duplicating work because a) it's fairly cheap work and there's a + // clear single-threaded program point to call it at (module import), and b) the + // `PyCapsule`s _must_ have the same pointer in at all times even if accessed multiple times + // or all the vtable lookups even within a single thread would be broken. + if lock.get().is_none() { + // We don't use `PyCapsule::import` so we can deliberately leak the Python reference to + // it, making it immortal. + let capsule = py + .import(CAPI_MODULE_NAME)? + .getattr(name)? + .cast_into::()?; + let fullname = CString::new(format!("{CAPI_MODULE_NAME}.{name}")) + .expect("our static values should not contain nul"); + let ptr = capsule + .pointer_checked(Some(&fullname))? + .cast::<*mut c_void>(); + // We don't care about the error of another thread beating us to set the value; the + // value should be the same from every calculation or we've got worse problems. + if lock.set(VTablePtr(ptr)).is_ok() { + // Make the capsule immortal by leaking a reference. In theory this shouldn't + // actually matter, because Qiskit shouldn't ever be invalidating its capsules, but + // in that case this has no effect so it's safe anyway. (This also doesn't prevent + // somebody from overwriting the pointer in the capsule, but there's not much we can + // do about that if it happens and we won't see the update anyway.) + std::mem::forget(capsule); + } + } + } + Ok(()) +} + +/// Declare a C API function in a given `vtable` at a given `offset`. The syntax is +/// ``` +/// declare_fn!(vtable[offset]; fn_name(arg_list) -> return_type); +/// ``` +/// where the second argument looks like a Rust function signature. For example: +/// ``` +/// declare_fn!(QK_FFI_CIRCUIT[5]; qk_circuit_new(num_qubits: u32, num_clbits: u32) -> *mut QkCircuit); +/// ``` +/// +/// All the calls to this are autogenerated. +macro_rules! declare_fn { + ( + $vtable:path[$offset:expr]; + $fn_name:ident ( $($arg_name:ident : $arg_ty:ty),* ) $(-> $ret_ty:ty)? + ) => { + /// See docs for + #[doc = concat!("`", stringify!($fn_name), "`")] + /// in the Qiskit C API for full information. + /// + /// # Safety + /// + /// `qk_import` must have been called and returned successfully within the process before + /// this function is called. Typically this should be done in a PyO3 module initializer. + /// + /// See the [Qiskit C API documentation](https://quantum.cloud.ibm.com/docs/api/qiskit-c) + /// for any other safety requirements. + #[inline(always)] + pub unsafe fn $fn_name( $($arg_name: $arg_ty),* ) $( -> $ret_ty )? { + // SAFETY: per documentation, caller is reponsible for ensuring that `qk_import` was + // called before any API functions. + let __vtable = unsafe { $vtable.get().unwrap_unchecked() }; + // SAFETY: per `qk_import` and Qiskit documentation, the stored pointer is valid for + // all vtable accesses to Qiskit C API functions. + let __fptr = unsafe { __vtable.add($offset) }.as_ptr(); + let __fptr = __fptr as *const extern "C" fn($($arg_ty),*) $( -> $ret_ty )?; + // SAFETY: per `qk_import` and Qiskit documentation, the function pointer is valid and + // has a known type. + unsafe { (*__fptr)($($arg_name),*) } + } + } +} +pub(crate) use declare_fn; diff --git a/crates/bindgen/src/lib.rs b/crates/bindgen/src/lib.rs index c495d6322c3f..df068004b87e 100644 --- a/crates/bindgen/src/lib.rs +++ b/crates/bindgen/src/lib.rs @@ -16,6 +16,7 @@ pub mod simple_ir; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; +use std::process::Command; pub const CBINDGEN_ATTRIBUTE_NAME: &str = "qk-vtable-rules"; pub const CBINDGEN_SKIP: &str = "no-export"; @@ -281,3 +282,58 @@ pub fn install_c_headers( } Ok(()) } + +fn rust_pyo3_ffi_root() -> PathBuf { + [env!("CARGO_MANIFEST_DIR"), "pyo3-ffi"].iter().collect() +} + +fn rust_pyo3_ffi_files() -> anyhow::Result> { + let root = rust_pyo3_ffi_root(); + let output = Command::new("cargo") + .arg("package") + .arg("--list") // Only list the files in the output, don't do any packaging. + .arg("--allow-dirty") // Permit `git` to be dirty, since this isn't a true packaging op. + .current_dir(&root) + .output()?; + anyhow::ensure!( + output.status.success(), + "'cargo package' listing failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let mut out = Vec::new(); + // Technically the paths are probably `OsString`, but that's just fiddly to handle properly. We + // can fix it if we need to. + for line in String::from_utf8(output.stdout)?.lines() { + let path = line.parse::()?; + // Some of the files listed by `cargo package` are ones that would be generated by the + // packaging command, so if they don't exist, don't add them. + if root.join(&path).is_file() { + out.push(path); + } + } + Ok(out) +} + +pub fn install_rust_pyo3_ffi( + bindings: &cbindgen::Bindings, + install_path: impl AsRef, +) -> anyhow::Result<()> { + let mut items = render::rust::Items::default(); + items.add_from_cbindgen(bindings)?; + + let install_path = install_path.as_ref(); + fs::create_dir_all(install_path)?; + + // First copy over the template repository verbatim... + let crate_root = rust_pyo3_ffi_root(); + for file in rust_pyo3_ffi_files()? { + if let Some(parent) = file.parent() { + fs::create_dir_all(install_path.join(parent))?; + } + fs::copy(crate_root.join(&file), install_path.join(&file))?; + } + + // ... and then overwrite the generated file. + items.export(fs::File::create(install_path.join("src").join("ffi.rs"))?)?; + Ok(()) +} diff --git a/crates/bindgen/src/render/mod.rs b/crates/bindgen/src/render/mod.rs index e7233a370c85..e315bf621e57 100644 --- a/crates/bindgen/src/render/mod.rs +++ b/crates/bindgen/src/render/mod.rs @@ -12,3 +12,4 @@ pub mod c; pub mod ctypes; +pub mod rust; diff --git a/crates/bindgen/src/render/rust.rs b/crates/bindgen/src/render/rust.rs new file mode 100644 index 000000000000..f3154afb51c8 --- /dev/null +++ b/crates/bindgen/src/render/rust.rs @@ -0,0 +1,448 @@ +// 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 crate::simple_ir; +use anyhow::anyhow; +use cbindgen::bindgen::ir; + +pub const FN_MACRO: &str = "declare_fn"; + +#[derive(Clone, Copy, Debug)] +pub enum Primitive { + PyObject, + Complex64, + Bool, + I8, + I16, + I32, + I64, + I128, + Isize, + U8, + U16, + U32, + U64, + U128, + Usize, + F32, + F64, + /// The Rust 'char'. + Char32, + /// The C 'char', not the Rust one. + Char, + SChar, + UChar, + Short, + UShort, + Int, + UInt, + Long, + ULong, + LongLong, + ULongLong, + Void, +} +impl Primitive { + /// A fully-qualified identifier for the type name. + pub const fn qualname(&self) -> &'static str { + /// Helper wrapper around `stringify` that triggers a compiler error if the path is not + /// valid. This is just a self-documenting test that there aren't typos in the arms + /// (because we're unlikely to have API coverage of all of them). + macro_rules! valid_type { + ($x:path) => {{ + let _: $x; + stringify!($x) + }}; + } + + match self { + Self::Bool => valid_type!(bool), + Self::I8 => valid_type!(i8), + Self::I16 => valid_type!(i16), + Self::I32 => valid_type!(i32), + Self::I64 => valid_type!(i64), + Self::I128 => valid_type!(i128), + Self::Isize => valid_type!(isize), + Self::U8 => valid_type!(u8), + Self::U16 => valid_type!(u16), + Self::U32 => valid_type!(u32), + Self::U64 => valid_type!(u64), + Self::U128 => valid_type!(u128), + Self::Usize => valid_type!(usize), + Self::F32 => valid_type!(f32), + Self::F64 => valid_type!(f64), + Self::Char32 => valid_type!(char), + Self::Void => valid_type!(::std::ffi::c_void), + Self::Char => valid_type!(::std::ffi::c_char), + Self::SChar => valid_type!(::std::ffi::c_schar), + Self::UChar => valid_type!(::std::ffi::c_uchar), + Self::Short => valid_type!(::std::ffi::c_short), + Self::UShort => valid_type!(::std::ffi::c_ushort), + Self::Int => valid_type!(::std::ffi::c_int), + Self::UInt => valid_type!(::std::ffi::c_uint), + Self::Long => valid_type!(::std::ffi::c_long), + Self::ULong => valid_type!(::std::ffi::c_ulong), + Self::LongLong => valid_type!(::std::ffi::c_longlong), + Self::ULongLong => valid_type!(::std::ffi::c_ulonglong), + // Can't use `valid_type!` on these because we don't depend on these crates here. + Self::PyObject => "::pyo3::ffi::PyObject", + Self::Complex64 => "::num_complex::Complex64", + } + } + + /// Convert to an equivalent `unsigned` type, if there is one. + pub const fn to_unsigned(self) -> Option { + match self { + Self::PyObject + | Self::Complex64 + | Self::Void + | Self::Bool + | Self::F32 + | Self::F64 + | Self::Char32 => None, + Self::Char | Self::SChar | Self::UChar => Some(Self::UChar), + Self::I8 | Self::U8 => Some(Self::U8), + Self::I16 | Self::U16 => Some(Self::U16), + Self::I32 | Self::U32 => Some(Self::U32), + Self::I64 | Self::U64 => Some(Self::U64), + Self::I128 | Self::U128 => Some(Self::U128), + Self::Isize | Self::Usize => Some(Self::Usize), + Self::Short | Self::UShort => Some(Self::UShort), + Self::Int | Self::UInt => Some(Self::UInt), + Self::Long | Self::ULong => Some(Self::ULong), + Self::LongLong | Self::ULongLong => Some(Self::ULongLong), + } + } + + pub const fn from_cbindgen_intkind(kind: ir::IntKind, signed: bool) -> Self { + let signed_ty = match kind { + ir::IntKind::Short => Self::Short, + ir::IntKind::Int => Self::Int, + ir::IntKind::Long => Self::Long, + ir::IntKind::LongLong => Self::LongLong, + ir::IntKind::SizeT => Self::Isize, + ir::IntKind::Size => Self::Isize, + ir::IntKind::B8 => Self::I8, + ir::IntKind::B16 => Self::I16, + ir::IntKind::B32 => Self::I32, + ir::IntKind::B64 => Self::I64, + }; + if signed { + signed_ty + } else { + signed_ty + .to_unsigned() + .expect("all integer types have unsigned variants") + } + } + + pub fn try_from_cbindgen_primitive(ty: &ir::PrimitiveType) -> anyhow::Result { + match ty { + ir::PrimitiveType::Void => Ok(Self::Void), + ir::PrimitiveType::Bool => Ok(Self::Bool), + ir::PrimitiveType::Char => Ok(Self::Char), + ir::PrimitiveType::SChar => Ok(Self::SChar), + ir::PrimitiveType::UChar => Ok(Self::UChar), + ir::PrimitiveType::Char32 => Ok(Self::Char32), + ir::PrimitiveType::Float => Ok(Self::F32), + ir::PrimitiveType::Double => Ok(Self::F64), + ir::PrimitiveType::VaList => Err(anyhow!("variadic arguments not handled")), // come on. + ir::PrimitiveType::PtrDiffT => Ok(Self::Isize), + ir::PrimitiveType::Integer { + zeroable: _, + signed, + kind, + } => Ok(Self::from_cbindgen_intkind(*kind, *signed)), + } + } +} + +mod parse { + use super::Primitive; + use crate::simple_ir::{self, PtrKind, TypeKind}; + use anyhow::bail; + use cbindgen::bindgen::ir; + + pub fn r#type(mut ty: &ir::Type) -> anyhow::Result> { + let mut ptrs = Vec::new(); + let base = loop { + match ty { + ir::Type::Ptr { + ty: inner, + is_const, + .. + } => { + ptrs.push(if *is_const { + PtrKind::Const + } else { + PtrKind::Mut + }); + ty = inner; + } + ir::Type::Path(p) => { + break match p.export_name() { + "PyObject" => TypeKind::Builtin(Primitive::PyObject), + "QkComplex64" => TypeKind::Builtin(Primitive::Complex64), + name => TypeKind::Custom(name.to_owned()), + }; + } + ir::Type::Primitive(ty) => { + break TypeKind::Builtin(Primitive::try_from_cbindgen_primitive(ty)?); + } + ir::Type::Array(..) => bail!("array types not yet handled"), + ir::Type::FuncPtr { .. } => bail!("funcptrs not yet handled"), + } + }; + Ok(simple_ir::Type { ptrs, base }) + } + + pub fn function(func: &ir::Function) -> anyhow::Result> { + let name = func.path.name().to_owned(); + let args = func + .args + .iter() + .map(|arg| { + if arg.array_length.is_some() { + bail!("function array arguments not handled yet"); + } + Ok(simple_ir::FunctionArg { + name: arg.name.clone(), + ty: r#type(&arg.ty)?, + }) + }) + .collect::>>()?; + // `void` in return position is modelled by `()`, but by `std::ffi::c_void` when a pointee. + let ret = (func.ret != ir::Type::Primitive(ir::PrimitiveType::Void)) + .then(|| r#type(&func.ret)) + .transpose()?; + Ok(simple_ir::Function { name, args, ret }) + } + + pub fn r#struct(val: &ir::Struct) -> anyhow::Result> { + let fields = val + .fields + .iter() + .map(|field| -> anyhow::Result<_> { + Ok(simple_ir::StructField { + name: field.name.clone(), + ty: r#type(&field.ty)?, + }) + }) + .collect::>()?; + Ok(simple_ir::Struct { + name: val.export_name.clone(), + fields: Some(fields), + }) + } + + /// Extract all objects from a set of `cbindgen::Bindings`, adding them to ourselves. + /// + /// This fails if the bindings contain any unsupported constructs. + pub fn add_items( + items: &mut simple_ir::Items, + bindings: &cbindgen::Bindings, + ) -> anyhow::Result<()> { + for item in bindings.items.iter() { + match item { + ir::ItemContainer::Enum(item) => { + items.enums.push(simple_ir::Enum::try_from_cbindgen(item)?) + } + ir::ItemContainer::OpaqueItem(item) => items + .structs + .push(simple_ir::Struct::opaque(item.export_name.clone())), + ir::ItemContainer::Struct(item) => items.structs.push(r#struct(item)?), + ir::ItemContainer::Constant(_) + | ir::ItemContainer::Static(_) + | ir::ItemContainer::Union(_) + | ir::ItemContainer::Typedef(_) => { + bail!("unhandled item: {item:?}"); + } + } + } + + for func in &bindings.functions { + items.functions.push(function(func)?); + } + + Ok(()) + } +} + +mod export { + use super::{FN_MACRO, Primitive}; + use crate::simple_ir::{self, PtrKind, TypeKind}; + use anyhow::anyhow; + use hashbrown::HashMap; + + fn render_type(ty: &simple_ir::Type, out: &mut String) { + for ptr in ty.ptrs.iter().rev() { + match ptr { + PtrKind::Mut => out.push_str("*mut "), + PtrKind::Const => out.push_str("*const "), + } + } + match &ty.base { + TypeKind::Builtin(ty) => out.push_str(ty.qualname()), + TypeKind::Custom(name) => out.push_str(name), + } + } + + fn r#enum(val: &simple_ir::Enum) -> String { + let repr = Primitive::from_cbindgen_intkind(val.repr.kind, val.repr.signed).qualname(); + let name = &val.name; + let mut out = format!( + " +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[repr({repr})] +pub enum {name} {{ +" + ); + for variant in &val.variants { + out.push_str(" "); + out.push_str(&variant.name); + out.push_str(" = "); + out.push_str(&variant.value); + out.push_str(",\n"); + } + out.push('}'); + out + } + + fn function( + val: &simple_ir::Function, + macro_name: &str, + vtable: &str, + offset: usize, + ) -> String { + // TODO: this function doesn't actually define the function at all; we delegate that to a + // macro definition in the _receiving_ crate. This is done so that the complex Rust code of + // the function is written in an actual Rust file as opposed to being in a string literal + // inside this file, but we probably want a better story for that in the future. + let name = &val.name; + let mut out = format!("{macro_name}!({vtable}[{offset}]; {name}("); + let mut counter = 0usize..; + let mut next_arg = || format!("__arg_{}", counter.next().expect("functionally infinite")); + let mut args = val.args.as_slice(); + if let Some(first) = args.split_off_first() { + out.push_str(&first.name.clone().unwrap_or_else(&mut next_arg)); + out.push_str(": "); + render_type(&first.ty, &mut out); + } + for arg in args { + out.push_str(", "); + out.push_str(&arg.name.clone().unwrap_or_else(&mut next_arg)); + out.push_str(": "); + render_type(&arg.ty, &mut out); + } + out.push(')'); + if let Some(ret) = val.ret.as_ref() { + out.push_str(" -> "); + render_type(ret, &mut out); + } + out.push_str(");"); + out + } + + fn r#struct(val: &simple_ir::Struct) -> String { + let name = &val.name; + let Some(fields) = val.fields.as_deref() else { + // In the absence of Rust-stable `extern type`, this is the Nomicon-approved way of + // defining an opaque type: + // https://doc.rust-lang.org/nomicon/ffi.html#representing-opaque-structs + return format!( + " +#[repr(C)] +pub struct {name} {{ + _private: ::core::marker::PhantomData<(*mut u8, ::core::marker::PhantomPinned)>, +}}" + ); + }; + let mut out = format!( + " +#[derive(Debug)] +#[repr(C)] +pub struct {name} {{" + ); + for field in fields { + out.push_str("\n pub "); + out.push_str(&field.name); + out.push_str(": "); + render_type(&field.ty, &mut out); + out.push(','); + } + out.push_str("\n}"); + out + } + + pub fn items( + items: &simple_ir::Items, + mut out: impl std::io::Write, + ) -> anyhow::Result<()> { + writeln!(out, "{}", crate::copyright_with_line_comments("//"))?; + writeln!( + out, + "\ +// ===================================== +// This file is automatically generated. +// ===================================== + +use crate::{FN_MACRO};" + )?; + for item in &items.enums { + writeln!(out, "{}", r#enum(item))?; + } + for item in &items.structs { + writeln!(out, "{}", r#struct(item))?; + } + writeln!(out)?; + let functions = items + .functions + .iter() + .map(|func| (func.name.clone(), func)) + .collect::>(); + let capsules = [ + ( + "crate::QK_FFI_CIRCUIT", + &qiskit_cext_vtable::FUNCTIONS_CIRCUIT, + ), + ( + "crate::QK_FFI_TRANSPILE", + &qiskit_cext_vtable::FUNCTIONS_TRANSPILE, + ), + ("crate::QK_FFI_QI", &qiskit_cext_vtable::FUNCTIONS_QI), + ]; + for (vtable_name, vtable) in capsules { + for entry in vtable.exports(0) { + let item = functions + .get(entry.name) + .ok_or_else(|| anyhow!("failed to find {} in bindings", entry.name))?; + writeln!( + out, + "{}", + function(item, super::FN_MACRO, vtable_name, entry.slot) + )?; + } + } + Ok(()) + } +} + +pub type Items = simple_ir::Items; +impl Items { + pub fn add_from_cbindgen(&mut self, bindings: &cbindgen::Bindings) -> anyhow::Result<()> { + parse::add_items(self, bindings) + } + + pub fn export(&self, out: impl std::io::Write) -> anyhow::Result<()> { + export::items(self, out) + } +} diff --git a/crates/bindgen/src/simple_ir.rs b/crates/bindgen/src/simple_ir.rs index 9a26fc777bdc..c8464b941f90 100644 --- a/crates/bindgen/src/simple_ir.rs +++ b/crates/bindgen/src/simple_ir.rs @@ -171,3 +171,12 @@ pub struct Items { pub structs: Vec>, pub functions: Vec>, } +impl Default for Items { + fn default() -> Self { + Self { + enums: Default::default(), + structs: Default::default(), + functions: Default::default(), + } + } +}