Skip to main content

ket/c_api/
execute.rs

1// SPDX-FileCopyrightText: 2026 Evandro Chagas Ribeiro da Rosa <evandro@quantuloop.com>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! C API bindings for quantum execution backends.
6//!
7//! This module defines the FFI-compatible structs ([`CLiveExecution`],
8//! [`CBatchExecution`], [`CNativeGateSet`]) that allow external C/C++ quantum
9//! simulators or hardware drivers to plug into the Libket execution pipeline.
10//!
11//! ## JSON serialization contract
12//!
13//! All data exchanged between Rust and the C callbacks is serialized as JSON:
14//!
15//! - **Rust → C**: gate instructions, Hamiltonians, and gate matrices are
16//!   serialized with [`serde_json`] into null-terminated C strings before
17//!   being passed to the corresponding callback.
18//! - **C → Rust**: JSON strings returned by C callbacks (e.g., sample counts,
19//!   state dumps, native gate sequences) are parsed back with [`serde_json`]
20//!   on the Rust side. If parsing fails, [`KetError::SerdeError`] is returned.
21//!
22//! Heap-allocated strings returned by C callbacks are **not** freed by Rust;
23//! the C backend is responsible for their lifetime (typically they are freed
24//! via [`super::ket_string_delete`] by the caller of the top-level C API).
25
26use std::{
27    ffi::{c_char, CStr, CString},
28    ptr::null,
29};
30
31use crate::{
32    c_api::c_ok,
33    error::KetError,
34    execution::{
35        BatchExecution, DumpData, ExpValueStrategy, GradientStrategy, LiveExecution, NativeGate,
36        NativeGateSet, QuantumExecution, SampleData,
37    },
38    ir::{gate::GateInstruction, hamiltonian::Hamiltonian},
39    matrix::Matrix,
40    process::QPUConfig,
41};
42
43/// FFI-compatible function-pointer table for a *live* (gate-at-a-time) execution backend.
44///
45/// In live mode each gate is dispatched to the hardware or simulator
46/// immediately as it is applied to the process, and measurement results are
47/// available without an explicit `ket_process_execute` call.
48///
49/// Construct a `CLiveExecution` value on the C side, fill in all function
50/// pointers, then call [`ket_quantum_execution_live`] to create the
51/// corresponding [`QuantumExecution`] variant.
52///
53/// All callbacks must return `0` on success or a non-zero libket error code on
54/// failure.
55#[repr(C)]
56#[derive(Debug, Clone)]
57pub struct CLiveExecution {
58    /// Dispatch a single quantum gate to the backend.
59    ///
60    /// `json` is a null-terminated JSON string encoding the gate instruction
61    /// (type, parameters, target qubit, etc.).  Returns `0` on success.
62    pub compute_gate: fn(json: *const c_char) -> i32,
63    /// Dispatch a sequence of pre-translated native gate instructions to the backend.
64    ///
65    /// `json` is a null-terminated JSON string encoding the array of native
66    /// gate instructions (each a `[name, angles, qubits]` triple). Returns `0`
67    /// on success.
68    pub compute_native_gates: fn(json: *const c_char) -> i32,
69
70    /// Perform a single-shot measurement of `len` qubits.
71    ///
72    /// `qubits` points to an array of `len` qubit indices.  On success the
73    /// measurement result is written to `*result` as a bitmask where bit `i`
74    /// corresponds to `qubits[i]`.  Returns `0` on success.
75    pub measure: fn(qubits: *const usize, len: usize, result: &mut u64) -> i32,
76
77    /// Retrieve the full quantum state (probability amplitudes) of `len` qubits.
78    ///
79    /// `qubits` points to an array of `len` qubit indices.  On success
80    /// `*result_json` is set to a heap-allocated null-terminated JSON string
81    /// encoding the state; Rust will parse it and then it is the C side's
82    /// responsibility to manage the lifetime.  Returns `0` on success.
83    pub dump: fn(qubits: *const usize, len: usize, result_json: &mut *const c_char) -> i32,
84
85    /// Perform a repeated measurement sample of `len` qubits over `shots` shots.
86    ///
87    /// `qubits` points to an array of `len` qubit indices.  On success
88    /// `*result_json` is set to a heap-allocated null-terminated JSON string
89    /// with the sample counts.  Returns `0` on success.
90    pub sample:
91        fn(qubits: *const usize, len: usize, shots: usize, result_json: &mut *const c_char) -> i32,
92
93    /// Compute the expectation value of a Hamiltonian on the current quantum state.
94    ///
95    /// `hamiltonian_json` is a null-terminated JSON string encoding the
96    /// Hamiltonian.  On success `*result` is set to the computed expectation
97    /// value.  Returns `0` on success.
98    pub exp_value: fn(hamiltonian_json: *const c_char, result: &mut f64) -> i32,
99}
100
101impl LiveExecution for CLiveExecution {
102    fn compute_gate(&mut self, gate: &GateInstruction) -> Result<(), KetError> {
103        let Ok(json) = serde_json::to_string(gate) else {
104            return Err(KetError::SerdeError);
105        };
106
107        let Ok(json) = CString::new(json) else {
108            return Err(KetError::SerdeError);
109        };
110
111        let err = KetError::from_error_code((self.compute_gate)(json.as_ptr()));
112
113        match err {
114            KetError::Success => Ok(()),
115            err => Err(err),
116        }
117    }
118
119    fn compute_native_gates(&mut self, gates: &[NativeGate]) -> Result<(), KetError> {
120        let Ok(gates) = serde_json::to_string(gates) else {
121            return Err(KetError::SerdeError);
122        };
123
124        let Ok(gates) = CString::new(gates) else {
125            return Err(KetError::SerdeError);
126        };
127
128        let err = KetError::from_error_code((self.compute_native_gates)(gates.as_ptr()));
129        match err {
130            KetError::Success => Ok(()),
131            err => Err(err),
132        }
133    }
134
135    fn measure(&mut self, qubits: &[usize]) -> Result<u64, KetError> {
136        let mut result = 42;
137
138        let err =
139            KetError::from_error_code((self.measure)(qubits.as_ptr(), qubits.len(), &mut result));
140
141        match err {
142            KetError::Success => Ok(result),
143            err => Err(err),
144        }
145    }
146
147    fn sample(&mut self, qubits: &[usize], shots: usize) -> Result<SampleData, KetError> {
148        let mut result_json = null();
149
150        let err = KetError::from_error_code((self.sample)(
151            qubits.as_ptr(),
152            qubits.len(),
153            shots,
154            &mut result_json,
155        ));
156
157        match err {
158            KetError::Success => {
159                let Ok(result_json) = { unsafe { CStr::from_ptr(result_json) } }.to_str() else {
160                    return Err(KetError::SerdeError);
161                };
162
163                match serde_json::from_str::<SampleData>(result_json) {
164                    Ok(sample_date) => Ok(sample_date),
165                    Err(_) => Err(KetError::SerdeError),
166                }
167            }
168            err => Err(err),
169        }
170    }
171
172    fn exp_value(&mut self, hamiltonian: Hamiltonian) -> Result<f64, KetError> {
173        let mut result = f64::NAN;
174
175        let Ok(json) = serde_json::to_string(&hamiltonian) else {
176            return Err(KetError::SerdeError);
177        };
178
179        let Ok(json) = CString::new(json) else {
180            return Err(KetError::SerdeError);
181        };
182
183        let err = KetError::from_error_code((self.exp_value)(json.as_ptr(), &mut result));
184
185        match err {
186            KetError::Success => Ok(result),
187            err => Err(err),
188        }
189    }
190
191    fn dump(&mut self, qubits: &[usize]) -> Result<DumpData, KetError> {
192        let mut result_json = null();
193
194        let err =
195            KetError::from_error_code((self.dump)(qubits.as_ptr(), qubits.len(), &mut result_json));
196
197        match err {
198            KetError::Success => {
199                let Ok(result_json) = { unsafe { CStr::from_ptr(result_json) } }.to_str() else {
200                    return Err(KetError::SerdeError);
201                };
202
203                match serde_json::from_str::<DumpData>(result_json) {
204                    Ok(dump_data) => Ok(dump_data),
205                    Err(_) => Err(KetError::SerdeError),
206                }
207            }
208            err => Err(err),
209        }
210    }
211}
212
213/// FFI-compatible function-pointer table for a *batch* execution backend.
214///
215/// In batch mode the entire compiled circuit is serialized and handed to the
216/// backend in a single call when `ket_process_execute` is invoked, rather than
217/// dispatching gate-by-gate.
218///
219/// Construct a `CBatchExecution` value on the C side, fill in all function
220/// pointers, then call [`ket_quantum_execution_batch`] to create the
221/// corresponding [`QuantumExecution`] variant.
222///
223/// All callbacks must return `0` on success or a non-zero libket error code on
224/// failure.
225#[repr(C)]
226#[derive(Debug, Clone)]
227pub struct CBatchExecution {
228    pub sample: fn(
229        gates_json: *const c_char,
230        qubits_to_sample: *const usize,
231        qubits_to_sample_len: usize,
232        shots: usize,
233        sample_json: &mut *const c_char,
234    ) -> i32,
235
236    pub exp_value: fn(
237        gates_json: *const c_char,
238        hamiltonian_list_json: *const c_char,
239        result: *mut f64,
240    ) -> i32,
241
242    pub sample_native: fn(
243        gates_json: *const c_char,
244        qubits_to_sample: *const usize,
245        qubits_to_sample_len: usize,
246        shots: usize,
247        sample_json: &mut *const c_char,
248    ) -> i32,
249
250    pub exp_value_native: fn(
251        gates_json: *const c_char,
252        hamiltonian_list_json: *const c_char,
253        result: *mut f64,
254    ) -> i32,
255
256    pub gradient: fn(
257        gates_json: *const c_char,
258        hamiltonian_list_json: *const c_char,
259        exp_result: &mut f64,
260        grad_json: &mut *const c_char,
261    ) -> i32,
262}
263
264impl BatchExecution for CBatchExecution {
265    fn sample(
266        &self,
267        gates: &[GateInstruction],
268        qubits_to_sample: &[usize],
269        shots: usize,
270    ) -> Result<SampleData, KetError> {
271        let mut sample_json = std::ptr::null();
272
273        let Ok(gates_json) = serde_json::to_string(gates) else {
274            return Err(KetError::SerdeError);
275        };
276        let Ok(gates_json) = CString::new(gates_json) else {
277            return Err(KetError::SerdeError);
278        };
279
280        let err = KetError::from_error_code((self.sample)(
281            gates_json.as_ptr(),
282            qubits_to_sample.as_ptr(),
283            qubits_to_sample.len(),
284            shots,
285            &mut sample_json,
286        ));
287
288        match err {
289            KetError::Success => {
290                let Ok(result_json) = { unsafe { CStr::from_ptr(sample_json) } }.to_str() else {
291                    return Err(KetError::SerdeError);
292                };
293
294                match serde_json::from_str::<SampleData>(result_json) {
295                    Ok(sample_data) => Ok(sample_data),
296                    Err(_) => Err(KetError::SerdeError),
297                }
298            }
299            err => Err(err),
300        }
301    }
302
303    fn exp_value(
304        &self,
305        gates: &[GateInstruction],
306        hamiltonian_list: &[Hamiltonian],
307    ) -> Result<Vec<f64>, KetError> {
308        let mut result = vec![f64::NAN; hamiltonian_list.len()];
309
310        let Ok(gates_json) = serde_json::to_string(gates) else {
311            return Err(KetError::SerdeError);
312        };
313        let Ok(gates_json) = CString::new(gates_json) else {
314            return Err(KetError::SerdeError);
315        };
316
317        let Ok(ham_json) = serde_json::to_string(&hamiltonian_list) else {
318            return Err(KetError::SerdeError);
319        };
320        let Ok(ham_json) = CString::new(ham_json) else {
321            return Err(KetError::SerdeError);
322        };
323
324        let err = KetError::from_error_code((self.exp_value)(
325            gates_json.as_ptr(),
326            ham_json.as_ptr(),
327            result.as_mut_ptr(),
328        ));
329
330        match err {
331            KetError::Success => Ok(result),
332            err => Err(err),
333        }
334    }
335
336    fn sample_native(
337        &self,
338        gates: &[NativeGate],
339        qubits_to_sample: &[usize],
340        shots: usize,
341    ) -> Result<SampleData, KetError> {
342        let mut sample_json = std::ptr::null();
343
344        let Ok(gates_json) = serde_json::to_string(gates) else {
345            return Err(KetError::SerdeError);
346        };
347        let Ok(gates_json) = CString::new(gates_json) else {
348            return Err(KetError::SerdeError);
349        };
350
351        let err = KetError::from_error_code((self.sample_native)(
352            gates_json.as_ptr(),
353            qubits_to_sample.as_ptr(),
354            qubits_to_sample.len(),
355            shots,
356            &mut sample_json,
357        ));
358
359        match err {
360            KetError::Success => {
361                let Ok(result_json) = { unsafe { CStr::from_ptr(sample_json) } }.to_str() else {
362                    return Err(KetError::SerdeError);
363                };
364
365                match serde_json::from_str::<SampleData>(result_json) {
366                    Ok(sample_data) => Ok(sample_data),
367                    Err(_) => Err(KetError::SerdeError),
368                }
369            }
370            err => Err(err),
371        }
372    }
373
374    fn exp_value_native(
375        &self,
376        gates: &[NativeGate],
377        hamiltonian_list: &[Hamiltonian],
378    ) -> Result<Vec<f64>, KetError> {
379        let mut result = vec![f64::NAN; hamiltonian_list.len()];
380
381        let Ok(gates_json) = serde_json::to_string(gates) else {
382            return Err(KetError::SerdeError);
383        };
384        let Ok(gates_json) = CString::new(gates_json) else {
385            return Err(KetError::SerdeError);
386        };
387
388        let Ok(ham_json) = serde_json::to_string(&hamiltonian_list) else {
389            return Err(KetError::SerdeError);
390        };
391        let Ok(ham_json) = CString::new(ham_json) else {
392            return Err(KetError::SerdeError);
393        };
394
395        let err = KetError::from_error_code((self.exp_value_native)(
396            gates_json.as_ptr(),
397            ham_json.as_ptr(),
398            result.as_mut_ptr(),
399        ));
400
401        match err {
402            KetError::Success => Ok(result),
403            err => Err(err),
404        }
405    }
406
407    fn gradient(
408        &self,
409        gates: &[GateInstruction],
410        hamiltonian: &Hamiltonian,
411    ) -> Result<(f64, Vec<f64>), KetError> {
412        let mut exp_result = 0.0;
413
414        let Ok(gates_json) = serde_json::to_string(gates) else {
415            return Err(KetError::SerdeError);
416        };
417        let Ok(gates_json) = CString::new(gates_json) else {
418            return Err(KetError::SerdeError);
419        };
420
421        let Ok(ham_json) = serde_json::to_string(hamiltonian) else {
422            return Err(KetError::SerdeError);
423        };
424        let Ok(ham_json) = CString::new(ham_json) else {
425            return Err(KetError::SerdeError);
426        };
427
428        let mut grad_json = std::ptr::null();
429
430        let err = KetError::from_error_code((self.gradient)(
431            gates_json.as_ptr(),
432            ham_json.as_ptr(),
433            &mut exp_result,
434            &mut grad_json,
435        ));
436
437        match err {
438            KetError::Success => {
439                let Ok(grad_json) = { unsafe { CStr::from_ptr(grad_json) } }.to_str() else {
440                    return Err(KetError::SerdeError);
441                };
442
443                match serde_json::from_str::<Vec<f64>>(grad_json) {
444                    Ok(grad_json) => Ok((exp_result, grad_json)),
445                    Err(_) => Err(KetError::SerdeError),
446                }
447            }
448            err => Err(err),
449        }
450    }
451}
452
453/// FFI-compatible function-pointer table for translating abstract gates into
454/// backend-native instructions.
455///
456/// The compiler calls these callbacks during the decomposition and routing
457/// passes to convert high-level gate representations into the instruction set
458/// understood by the target hardware or simulator.
459///
460/// Construct a `CNativeGateSet` value on the C side, fill in all function
461/// pointers, then pass it to [`ket_quantum_execution_batch`].  Passing `NULL`
462/// instead uses identity translation (no decomposition).
463///
464/// All callbacks must return `0` on success or a non-zero libket error code on
465/// failure.
466#[repr(C)]
467#[derive(Debug, Clone)]
468pub struct CNativeGateSet {
469    /// Translate a single-qubit gate (given as a 2×2 unitary matrix) into
470    /// native gate instructions.
471    ///
472    /// `gate_json` is a null-terminated JSON string encoding the gate as a
473    /// flat 2×2 complex matrix `[[re,im,re,im],[re,im,re,im]]`.  `target` is
474    /// the target qubit index.  On success `*native_gate_json` is set to a
475    /// heap-allocated null-terminated JSON string encoding the native gate
476    /// sequence.  Returns `0` on success.
477    pub translate:
478        fn(gate_json: *const c_char, target: usize, native_gate_json: &mut *const c_char) -> i32,
479
480    /// Produce the native gate sequence that implements a CNOT between two qubits.
481    ///
482    /// The first argument is the control qubit index and the second is the
483    /// target qubit index.  On success `*native_gate_json` is set to a
484    /// heap-allocated null-terminated JSON string encoding the native CNOT
485    /// decomposition.  Returns `0` on success.
486    pub cnot: fn(control: usize, target: usize, native_gate_json: &mut *const c_char) -> i32,
487}
488
489impl NativeGateSet for CNativeGateSet {
490    fn translate(&self, matrix: &Matrix, target: usize) -> Result<Vec<NativeGate>, KetError> {
491        let matrix_json = [
492            [
493                matrix[0][0].re,
494                matrix[0][0].im,
495                matrix[0][1].re,
496                matrix[0][1].im,
497            ],
498            [
499                matrix[1][0].re,
500                matrix[1][0].im,
501                matrix[1][1].re,
502                matrix[1][1].im,
503            ],
504        ];
505        let Ok(gate) = serde_json::to_string(&matrix_json) else {
506            return Err(KetError::SerdeError);
507        };
508        let Ok(gate) = CString::new(gate) else {
509            return Err(KetError::SerdeError);
510        };
511
512        let mut result_json = null();
513
514        let err =
515            KetError::from_error_code((self.translate)(gate.as_ptr(), target, &mut result_json));
516
517        match err {
518            KetError::Success => {
519                let Ok(result_json) = { unsafe { CStr::from_ptr(result_json) } }.to_str() else {
520                    return Err(KetError::SerdeError);
521                };
522                match serde_json::from_str(result_json) {
523                    Ok(result) => Ok(result),
524                    Err(_) => Err(KetError::SerdeError),
525                }
526            }
527            err => Err(err),
528        }
529    }
530
531    fn cnot(&self, control: usize, target: usize) -> Result<Vec<NativeGate>, KetError> {
532        let mut result_json = null();
533
534        let err = KetError::from_error_code((self.cnot)(control, target, &mut result_json));
535
536        match err {
537            KetError::Success => {
538                let Ok(result_json) = { unsafe { CStr::from_ptr(result_json) } }.to_str() else {
539                    return Err(KetError::SerdeError);
540                };
541                match serde_json::from_str(result_json) {
542                    Ok(result) => Ok(result),
543                    Err(_) => Err(KetError::SerdeError),
544                }
545            }
546            err => Err(err),
547        }
548    }
549}
550
551/// Creates a live-mode [`QuantumExecution`] backed by the provided callback table.
552///
553/// In live mode each gate is dispatched to the backend immediately as it is
554/// applied to the process. The `decompose` flag controls whether multi-qubit
555/// gates are decomposed into single-qubit and two-qubit native gates before
556/// being dispatched.
557///
558/// # Parameters
559/// - `live`: Pointer to a caller-owned [`CLiveExecution`] struct whose
560///   function pointers implement the backend. The struct is cloned; the caller
561///   retains ownership of the original.
562/// - `decompose`: If `true`, enables multi-qubit gate decomposition before
563///   dispatching each gate to the backend.
564/// - `quantum_execution`: Output: receives the pointer to the newly allocated
565///   [`QuantumExecution`] object. Ownership is transferred to the caller.
566///
567/// # Returns
568/// `0` on success, non-zero error code on failure.
569#[no_mangle]
570pub extern "C" fn ket_quantum_execution_live(
571    num_qubits: usize,
572    live: &CLiveExecution,
573    decompose: bool,
574    native_gate_set: *const CNativeGateSet,
575    qpu_config: &mut *mut QPUConfig,
576) -> i32 {
577    let native_gate_set = if native_gate_set.is_null() {
578        None
579    } else {
580        Some(Box::new(unsafe { &*native_gate_set }.clone()) as Box<dyn NativeGateSet>)
581    };
582
583    let execution = QuantumExecution::Live {
584        qpu: Box::new(live.clone()),
585        decompose,
586        native_gate_set,
587    };
588
589    *qpu_config = Box::into_raw(Box::new(QPUConfig {
590        num_qubits,
591        quantum_execution: Some(execution),
592    }));
593
594    c_ok()
595}
596
597/// Creates a batch-mode [`QuantumExecution`] backed by the provided callback tables.
598///
599/// In batch mode the full compiled circuit is handed to the backend in a
600/// single call when `ket_process_execute` is invoked. The `native_gate_set`
601/// callback table is used during the decomposition and routing passes to
602/// translate abstract gates into hardware-native instructions.
603///
604/// # Parameters
605/// - `batch`: Pointer to a caller-owned [`CBatchExecution`] struct. The struct
606///   is cloned; the caller retains ownership of the original.
607/// - `native_gate_set`: Pointer to a caller-owned [`CNativeGateSet`] struct,
608///   or `NULL` to use identity translation (no decomposition into native gates).
609/// - `gradient`: If `true`, enables parameter-shift rule gradient computation
610///   during execution.
611/// - `coupling_graph_json`: A null-terminated JSON string that is either
612///   `null` (no connectivity constraint) or a JSON array of `[usize, usize]`
613///   edge pairs describing the QPU coupling graph used for qubit routing.
614/// - `quantum_execution`: Output: receives the pointer to the newly allocated
615///   [`QuantumExecution`] object. Ownership is transferred to the caller.
616///
617/// # Returns
618/// `0` on success, non-zero error code on failure.
619#[no_mangle]
620pub extern "C" fn ket_quantum_execution_batch(
621    num_qubits: usize,
622    batch: &CBatchExecution,
623    native_gate_set: *const CNativeGateSet,
624    decompose: bool,
625    gradient: bool,
626    coupling_graph_json: *const c_char,
627    exp_value_strategy_json: *const c_char,
628    qpu_config: &mut *mut QPUConfig,
629) -> i32 {
630    let native_gate_set: Option<Box<dyn NativeGateSet>> = unsafe {
631        if native_gate_set.is_null() {
632            None
633        } else {
634            Some(Box::new((*native_gate_set).clone()))
635        }
636    };
637
638    let coupling_graph = from_json!(coupling_graph_json, Option<Vec<(usize, usize)>>);
639    let exp_value = from_json!(exp_value_strategy_json, ExpValueStrategy);
640
641    let execution = QuantumExecution::Batch {
642        qpu: Box::new(batch.clone()),
643        native_gate_set,
644        gradient: if gradient {
645            GradientStrategy::ParameterShiftRule
646        } else {
647            GradientStrategy::None
648        },
649        exp_value,
650        coupling_graph,
651        decompose,
652    };
653
654    *qpu_config = Box::into_raw(Box::new(QPUConfig {
655        num_qubits,
656        quantum_execution: Some(execution),
657    }));
658
659    c_ok()
660}