Skip to content

C API: Add constructors for classical variables, stretches and values - #17396

Open
Noam Hirshberg (noam-hb) wants to merge 1 commit into
mainfrom
noamh/c-api-classic-expr-part-1
Open

Noam Hirshberg (noam-hb) wants to merge 1 commit into
mainfrom
noamh/c-api-classic-expr-part-1

Conversation

@noam-hb

@noam-hb Noam Hirshberg (noam-hb) commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Add C API functions for creating the leaf nodes of classical expressions. This is part 1 for issue #16667 for classical expression constructors for C.

Added functions:

  • QkVar: qk_var_new, qk_var_free.
  • QkStretch: qk_stretch_new, qk_stretch_free.
  • QkValue:
    • qk_value_new_duration
    • qk_value_new_float
    • qk_value_new_uint
    • qk_value_new_biguint
    • qk_value_free

Unlike the proposal in the issue, qk_value_new_[big]uint do take the bit-width too as a parameter. They default to "full width" of the uint or buffer given when you pass bit_width=0 -- this is unlike Python, but more sensible in C, IMO.


AI/LLM disclosure

  • No part of this submission is LLM generated.
  • Some written text was generated by: Claude Opus 5.5
  • Some submitted code was generated by: Claude Opus 5.5

@qiskit-bot

Copy link
Copy Markdown
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

Add C API functions for creating the leaf nodes of classical expressions.
This is part 1 for issue #16667 for classical expression constructors
for C.

= Added functions:

- QkVar: `qk_var_new`, `qk_var_free`.
- QkStretch: `qk_stretch_new`, `qk_stretch_free`.
- QkValue:
  - `qk_value_new_duration`
  - `qk_value_new_float`
  - `qk_value_new_uint`
  - `qk_value_new_biguint`
  - `qk_value_free`

Unlike the proposal in the issue, `qk_value_new_[big]uint` do take
the bit-width too as a parameter. They default to "full width" of the uint
or buffer given when you pass `bit_width=0` -- this is unlike Python, but
more sensible in C, IMO.

= Error Handling:

We return `NULL` from the added functions in these error cases:

- `qk_var_new`/`qk_stretch_new` name parameter is `NULL`
- `qk_var_new` type parameter is `uint` with bit-width zero
- `qk_value_new_uint`/`qk_value_new_biguint` integer does not fit in the
  given `bit_width`

Since these functions return a pointer anyway, we can return `NULL` for
these cases instead of panicking. This lets the caller check and recover,
if they so choose. Still, there are tradeoffs in doing this instead of
panicking -- panicking would be much easier to debug in some cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noam, this API is coming together nicely. I have a few comments.

Comment thread crates/cext/src/classical_expr.rs Resolved
Comment thread crates/cext/src/classical_expr.rs Resolved
#[unsafe(no_mangle)]
pub extern "C" fn qk_value_new_uint(uint: u64, bit_width: u32) -> *mut Value {
let bit_width = match bit_width {
0 => 64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the caller writes qk_value_new_uint(val, 0), the real width is obscured when reading the caller code. Consider returning NULL instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did consider making bit_width=0 an error case. That is also a sensible option to go with, but not what I did.

What is implemented here is interpreting bit_width=0 as "use the full width of the input", which is 64 bits for a uint. The user can still decide to be explicit and pass the bit_width they want every time. The zero case is simply for convenience here.

I think this is fine as is, but if you see more concrete problems with this behavior, do tell.

@davidfcohen David F. Cohen (davidfcohen) Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also fine leaving it as is. Another path you could take is:

qk_value_new_uint(uint: u64)
qk_value_new_uint_width(uint: u64, width: 0)

The second function is your current one. Then qk_value_new_uint could call it. If you're keeping things as is, feel free to resolve this thread.

// SAFETY: Per documentation, the `QkBigUint` is valid.
let biguint_slice = unsafe { biguint.as_slice() };
// NOTE: there is no constructor from `&[u64]`? weird.
let biguint = BigUint::new(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a helper function that builds a BigUint from CBigUint.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice, yeah. It would have to be unsafe though. I added CBigUint::as_slice (unsafe), with the intention to use it like so:

    BigUint::from_slice(unsafe { biguint.as_slice() })

But it turns out BigUint::from_slice takes &[u32] and there is no similar method that takes &[u64], although limbs on 64bit machines are u64, so go figure.

Anyway, I tried implementing it and using it right now by extracting it from this code, but it didn't work out exactly how I wanted: because I use biguint_slice: &[u64] just afterwards to get the number of limbs, so I need it as an intermediate step. You can actually do biguint.iter_u64_digits().len() on a BigUint and it's O(1) because that iter method returns an ExactSizeIterator. Writing iter().len() for a calculation that should be O(1) looks so much like a red flag though that I don't even want to write it.

Implementing it like so actually changes the inferred bit_width metadata (it is inferred only when the caller passes bit_width=0) because creating a BigUint will truncate trailing zero limbs. Just the fact that this is relevant at all might be a good argument for maybe just making bit_width=0 an error case instead.

I will think through this a bit more before deciding what I do here.

.flat_map(|&d| [d as u32, (d >> 32) as u32])
.collect(),
);
let bit_width: u32 = match bit_width {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we obscuring behavior by taking a "best-effort" approach when bit_width is 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention is not so much doing "best-effort" but that bit_width=0 implies "use full width of the buffer" without the caller having to calculate it or remember it.

The question here is whether this is a common and sensible thing that the caller wants. I intuit that basically yes, but I could be wrong on this.

Also see relevant parts of my comments above that also touch on this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment. I trust your judgement on this.

/*
* Test constructing standalone variables and stretches.
*/
static int test_var_stretch_new(void) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should break up this test. The error cases are independently testable. Also, this function name suggests that we're only testing QkStretch, but we're testing QkVar too.

  • stretch success
  • stretch with null name
  • var success
  • var with null name
  • var with uint width 0

Try breaking up the other tests too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will split it to one function for QkVar and one for QkStretch, covering all the cases per each.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with this middle ground. I think it would be convenient for both name = NULL and width = 0 to be tested independently, but I understand why combining them is convenient.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

theme: C API Related to the C API

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

4 participants