Live data from Hacker News

Can a Rust binary use incompatible versions of the same library?

github.com

81–89 of 89 posts

Re: Can a Rust binary use incompatible versions of the same library?

#81
post #31
post #20

Earlier quoted context omitted.

It's easy to accidentally ship a minimum version requirement that is out of date when you also consistently use lock files pinned to newer versions. The code may silently depend on something introduced in a newer version pulled in by the lock file.

You can have a CI builder using direct-minimal-versions to check this.

Point releases are often bugfix releases, i.e. not api changes but runtime changes. CI won’t help without very specific accompanying tests.

Re: Can a Rust binary use incompatible versions of the same library?

#82

Earlier quoted context omitted.

The idiomatic Rust way to avoid a scenario where we might have different versions of the unique IDs is to reify unique IDs as an actual type (say named UniqueID) rather than just having a tacit understanding that these particular integers are supposed to be unique. This lets the type's owner decide what affordances to give it (probably adding two UniqueIDs together is nonsense, but comparing them for equality definit…

That safety is merely accidental, it only appears if the dangerous state (the atomic counter memory location) is in the same crate as the UniqueNumber type exposed. A pair of crates (one which defines the struct UniqueNumber(u32), and one which produces sequences of them) would still be suffering from this, because the sequencing crate could use v1 of the unique number struct crate, in both its v1 and v2. This of cou…

I feel like the separate crate with UniqueNumber(u32) is a bad idea. There's nothing unique about these, you're gas lighting me. Like a PrimeNumber(u32) that's just a thin wrapper and expects somebody else to ensure they're prime - no, I know the compiler doesn't understand English, but these aren't prime (or unique) numbers, you're bad at your job.

One of my favourite decisions about Rust which could have gone the other way was their choice to define &str (and String) to be actual text. &str is basically &[u8] but with an extra requirement that this is valid UTF-8 text, likewise then String is basically Vec but requiring UTF-8.

I feel like UniqueNumber is the same, if you call yourself UniqueNumber don't just be a thin wrapper around an integer, you need to own that uniqueness problem.

Re: Can a Rust binary use incompatible versions of the same library?

#83
post #31

Earlier quoted context omitted.

You can have a CI builder using direct-minimal-versions to check this.

Point releases are often bugfix releases, i.e. not api changes but runtime changes. CI won’t help without very specific accompanying tests.

I have literally never run into this being a problem in practice. If someone downstream ever did notice, they can just specify a higher minimum version constraint.

Re: Can a Rust binary use incompatible versions of the same library?

#84

Earlier quoted context omitted.

That safety is merely accidental, it only appears if the dangerous state (the atomic counter memory location) is in the same crate as the UniqueNumber type exposed. A pair of crates (one which defines the struct UniqueNumber(u32), and one which produces sequences of them) would still be suffering from this, because the sequencing crate could use v1 of the unique number struct crate, in both its v1 and v2. This of cou…

I feel like the separate crate with UniqueNumber(u32) is a bad idea. There's nothing unique about these, you're gas lighting me. Like a PrimeNumber(u32) that's just a thin wrapper and expects somebody else to ensure they're prime - no, I know the compiler doesn't understand English, but these aren't prime (or unique) numbers, you're bad at your job. One of my favourite decisions about Rust which could have gone the o…

A GUID is just a string or handful of bytes. It’s not reallt globally unique despite the name, unless we actually initialize it correctly…

But yeah I get your point.

Re: Can a Rust binary use incompatible versions of the same library?

#85

For fun, you could add this to Python and I think it would it cover a lot of edge cases? You would need: A function v_tree_install(spec) which installs a versioned pypi package like “foo=3.2” and all its dependencies in its own tree, rather than in site-packages. Another pair of functions v_import and v_from_import to wrap importlib with a name, version, and symbols. These functions know how to find the versioned pac…

I can’t see how it would ever be possible with Python to do this.

You depend on two packages, each with a function that returns a “requests.Request” object. These packages depend on different versions of “requests”.

How would you implement “isinstance(return_value, requests.Request)” on each of these calls?

Or, the indirect case of this: catching a “requests.HttpException” from each of these calls?

Importing the right thing isn’t hard, but doing things with it is the hard bit.

Re: Can a Rust binary use incompatible versions of the same library?

#86
post #85

For fun, you could add this to Python and I think it would it cover a lot of edge cases? You would need: A function v_tree_install(spec) which installs a versioned pypi package like “foo=3.2” and all its dependencies in its own tree, rather than in site-packages. Another pair of functions v_import and v_from_import to wrap importlib with a name, version, and symbols. These functions know how to find the versioned pac…

I can’t see how it would ever be possible with Python to do this. You depend on two packages, each with a function that returns a “requests.Request” object. These packages depend on different versions of “requests”. How would you implement “isinstance(return_value, requests.Request)” on each of these calls? Or, the indirect case of this: catching a “requests.HttpException” from each of these calls? Importing the righ…

I see what you mean. They might as well be two different types:

  from m1 import T1
  from m2 import T2
  from m3 import f

  x = f()
  assert isinstance(x, T1, T2)
Perhaps you could have a v_import that imported all versions of a symbol used throughout your project?

  Ts = v_from_import_all(
    “foo”,
    “T”,
  )
  assert isinstance(x, *Ts)
For static analysis, your type checker could understand what v_import does, how it works, and which symbols will be actually be there at runtime but yes, it’s starting to seem extremely complicated!

What you do with the return_value defines the behaviour you expect from it so to that extent you can rely on that instead of using isinstance:

  x: Union[T1, T2] = f()
  print(x.foo() ** 12.3)
Perhaps some function could build that Union type for you? It would be a pain to make it by hand if you had 50x different third-party dependencies each pulling in a slightly different requests (but which as far as you are concerned all return some small part of that package that are all compatible.)

If you’re importing a module to use it in some way you’re also declaring some kind of version dependency / compatability on it too, so that’s another thing your static analysis could check for you. That would actually be incredibly useful:

1/ Do your dependencies import an older version of requests than you do?

2/ Does it matter, and why? (eg x.foo() only exists in version 4.5 onwards, but m1 imports 4.4.)

Re: Can a Rust binary use incompatible versions of the same library?

#87
post #6

Earlier quoted context omitted.

There's a subtle point there though. When you rely on something that was introduced in x.y.z, stating that your version requirement is x.y.0 is an error that can easily cause downstream breakage.

I’m confused. If you rely on a feature introduced in X.y.z why would you specify X.y.0 to begin with (and not just X.y.z)? In practice, usual rust projects that have not put a ton of work into their dependencies encode X.y.z in Cargo.toml matching the current release at the time they developed the system. So you get at worst an unnecessarily higher version requirement but never a lower one. Moreover, rust semver woul…

My point was not about "x.y.0" (I mispoke), but "x.y" (or "0.x") that causes this problem.

Take a look at any random crates's cargo config, and you'll regularly see dependencies specified as "1" or "0.3" instead of "1.0.119" or "0.3.39". If another crate depends on this crate, and has a more precise version needed (say "=1.0.100" (perhaps due to a bug introduced in "1.0.101", but the included library relies on features introduced in some version after "1.0.100", then your library won't compile. Stating your dependency on "1" instead of "1.0.119" is what caused this problem.

This is not hypothetical - I've run into this quite a few times with crates that over-specify their dependencies interacting with crates that under-specify theirs.

Yes, the solution to this is pretty simple - check minimal versions in CI. That's something I do for most of my stuff, but it's not universal.

Re: Can a Rust binary use incompatible versions of the same library?

#88

Earlier quoted context omitted.

I'm not sure I understand the use case here. Are you asking if you can depend on two versions of the same crate, for a crate that exports a `#[no_mangle]` or `#[export_name]` function? I guess you could slap a `#[used]` attribute on your exported functions, and use their mangled name to call them with dlopen, but that would be unwieldy and guessing the disambiguator used by the compiler error prone to impossible. Oth…

> I'm not sure I understand the use case here. Are you asking if you can depend on two versions of the same crate, for a crate that exports a `#[no_mangle]` or `#[export_name]` function? Yes, exactly. > Other than that, you cannot. so, to the question "Can a Rust binary use incompatible versions of the same library?", then the answer is definitely "no". It's not yes if it cannot cover one of the most basic use cases…

I agree the answer is no in the abstract, but it is not very useful in practice.

Nobody writing Rust needs to cover this "basic use case" you're referring to, so it is the same as people saying "unsafe exists so Rust is no safer than C++". In theory that's true, in practice in 18 months, 604 commits and 60,008 LoC I wrote `unsafe` exactly twice. Once for memory mapping something, once for skipping UTF-8 validation that I'd just done before (I guess I should have benchmarked that one as it is probably premature).

In practice when developing Rust software at a certain scale you will mix and match incompatible library versions in your project, and it will not be an issue. Our project has 44 dependencies with conflicting versions, one of which appears in 4 incompatible versions, and it compiles and runs perfectly fine. In other languages I used (C++, python), this exact same thing has been a problem, and it is not in Rust. This is what the article is referring to

Re: Can a Rust binary use incompatible versions of the same library?

#89
post #85

Earlier quoted context omitted.

I can’t see how it would ever be possible with Python to do this. You depend on two packages, each with a function that returns a “requests.Request” object. These packages depend on different versions of “requests”. How would you implement “isinstance(return_value, requests.Request)” on each of these calls? Or, the indirect case of this: catching a “requests.HttpException” from each of these calls? Importing the righ…

I see what you mean. They might as well be two different types: from m1 import T1 from m2 import T2 from m3 import f x = f() assert isinstance(x, T1, T2) Perhaps you could have a v_import that imported all versions of a symbol used throughout your project? Ts = v_from_import_all( “foo”, “T”, ) assert isinstance(x, *Ts) For static analysis, your type checker could understand what v_import does, how it works, and which…

> They might as well be two different types

The problem is they are different types, which has huge downstream impacts. Not least of all would be subclassing.

The static analysis you’ve described could work in simple and trivial cases, but the problem you’re now trying to solve is “what concrete type is this object”, and in a dynamic language like Python this can only be fully and 100% determined at runtime.

Mypy and the like do a good job, but often rely on protocols rather than concrete classes. There’s also no impact if the types are wrong or ignored, whereas for this a typing mismatch becomes a subtle runtime issue.

Post reply on HN