Skip to content

Breaking change in upstream C API #167

@abrown

Description

@abrown

The upstream OpenVINO C API has once again broken its C ABI, limiting which versions of OpenVINO will work with these Rust bindings. This issue serves as an explainer for users who may run into errors.

Problem

The upstream OpenVINO project releases a C API; this is what the openvino-sys crate interfaces with for using OpenVINO in Rust. In #143, we noticed that an insertion to the middle of ov_element_type_e changed the enum values for items after the insertion. This meant a tensor of one type was actually interpreted as a tensor of another type, potentially an unsafe operation. Fortunately, tests in this repository caught the issue.

This happened again upstream in #28766 (specifically d9c2aee): ov_element_type_e::UNDEFINED and ov_element_type_e::DYNAMIC where merged into a single value. This innocent-seeming refactoring changes all the enum values, ensuring that any users with code compiled against the previous header (C, Rust, etc.) could have incorrect results and possibly buffer security problems.

What does this mean?

The breaking change to ov_element_type appears first in v2025.1.0:

  • users of this crate that want to use OpenVINO v2025.0.0 and prior should use this crate at version 0.8.0
  • users that want to use v2025.1.0 and above must upgrade to the latest version of the crate.

Internally, this means this crate must find some way to protect itself against unwitting ABI changes. It is unfortunate that these breakages occur across minor versions (e.g., 2025.0 to 2025.1), so it is impossible to support a version subset.

Current Solution

The current solution is for the latest version of the crate to dynamically check the OpenVINO library version and fail if it is prior to some version. This is not ideal (we would like to support as many versions as possible) but is safe.

Internally, we do something like:

/// Parse the version string and return true if it is older than 2024.2.
fn is_pre_2024_2_version(version: &str) -> bool {
let mut parts = version.split(['.', '-']);
let year: usize = parts.next().unwrap().parse().unwrap();
let minor: usize = parts.next().unwrap().parse().unwrap();
year < 2024 || (year == 2024 && minor < 2)
}

Future Solution

In the future, we could explore other options:

  • interface with the C++ API directly: using cxx, e.g., one could imagine being immune to more API breakage by relying directly on the C++ API. It is unclear if this is any more stable than
  • integrate more tightly with upstream OpenVINO: unfortunately, upstream developers may not be aware that their changes have downstream effects; moving the Rust bindings into the upstream OpenVINO repository would help them see test failures immediately. This has been discussed in [Question] Moving to OpenVINO Contrib #70 and requires more work to happen.
  • release new Rust bindings for each new upstream release: this crate would begin to release versions such as v2025.2.0, v2025.3.0, etc., to align with the upstream version numbers; there would be no guarantee of compatibility between crate versions, much like upstream today. The unfortunate side effect of this would be more frequent releases of this crate (i.e., more chores!).

I'm interested in other ideas to protect these bindings from this kind of breakage in the future. This problem does come with the territory, though: this crate cannot be stable if upstream is unstable. And I sympathize with upstream maintainers who may want to change APIs when they want, regardless of semantic versioning. For the time being, we'll continue to try to maintain compatibility for as many versions as we can!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions