Can I return a type that is not re-exported?

I'm creating an app using Bevy.

Bevy depends on winit, but does not re-export winit sufficiently.

In the following code, I want to return Option<winit::monitor::MonitorHandle>.

fn current_monitor(
    winit_windows: &WinitWindows,
    bevy_window_entity: Entity,
) -> Option<TODO> {
    winit_windows
        .get_window(bevy_window_entity)
        .and_then(|winit_window| winit_window.current_monitor())
}
  1. Do I have to add winit to my app's dependency table?

  2. If so, do I have to manually maintain compatibility between the versions of winit used by Bevy and my app?

  3. Is there any way to propagate a dependency of a dependency to my app? In other words, if Bevy is using winit 0.28.6, I want to use winit 0.28.6 too.

Yes.

No. Cargo's dependency resolution would take care of that in most cases.

No. Even if Bevy re-exported wininit by mistake, you shouldn't directly depend on transitive dependencies since, at any point, Bevy developers could correct such mistake and pull the rug under your feet.

You should never take your transitive dependencies for granted. If you depend on any library, just add it to your list of dependencies.

As far as I’m aware you do have to manually pick the correct version to match a public dependency of a crate. That is, as far as the major version is concerned; but winit isn’t a post-1.0 API, so there are “relatively” frequent new major versions. (Sub-1.0 the second digit is considered “major” by cargo.)

By convention, the major version of such a dependency may only be changed by bevy if it itself changes major versions, too. Which is why a PR such as this will itself be labeled a breaking change.

So if you change your bevy dependency from – say – 0.11.* to a future 0.12.0, not only its API may have changed, but you may need to update some other dependencies, too, such as winit; and if you use their API, that usage may need to be adjusted to the new version’s in your program, too.

1 Like

Thanks.

If I create a library, should the types I expose using pub use be limited to the types I define in my library?

Strong disagree. Re-exporting the dependent crates is the only way to ensure that end-users use the proper version. I'm no familiar with the API of Bevy and don't know whether it really exposes winit in its API, but if it does, it should re-export it.

That said, as the end user you need to be sure that the re-export is intentional, otherwise it could indeed break in any update.

While choosing the same version of dependencies as Bevy would likely mean that you choose the same actual version, it's not guaranteed, and it can break on any minor update due to other crates imposing version restrictions. This means that the process is extremely manual: on every dependency update you'd have to carefully check the versions of crates that both you and they use, and pin your version to that value. If two crates enforce incompatible transitive dependency versions, you're screwed. By incompatible I mean semver-compatible but different. In that case Cargo won't allow you to satisfy both dependencies at the same time.

1 Like

As a library crate, bevy will never depend on a single version such as 0.28.6. It will always be a range; it could be the range 0.28.6 .. 0.29.0 (for lack of a better notation, i.e. all x such that 0.28.6 <= x < 0.29.0).

To use a compatible winit version, your program, too, should in that example case specify any 0.28.… version. Say it specified 0.28.3 in its Cargo.toml, to make it more interesting. That too stands for the same style of range. The intersection of these ranges would still be the 0.28.6 .. 0.29.0 one. Cargo can then choose any version out of the intersection to fulfill the dependency. (It will either be the one already present in the Cargo.lock, or the latest, and the Cargo.lock can also be updated to use all the latest minor versions with cargo update).

Even though the specified versions e.g. 0.28.3 vs 0.28.6 don’t match exactly, since these are really ranges, there is overlap; and within a major version, cargo never allows any duplicates, so there always will be compatibility.

A reason for requiring something lower than bevy does not really exist though… there’s a good reason for requiring something higher though, as the minimum: you might have never tested the lower version, never checked if it already has all the API, too, that you’re using, etc… and newer is generally better anyways with minor versions.

1 Like

I don’t quite follow why it should be the only way. Re-exporting or not re-exporting public dependencies should be fine either way.

I didn't want to go too much into details, that's why I tried to be brief by saying that the dependency resolution would work in most cases.

But I'm glad that you have clarified in which cases this doesn't hold true :slight_smile:.

Unless some other crate specifies <28.5 because reasons. Happens more often than I'd like.

Well, raise an issue on that crate then, not the user’s fault. The only pattern where that’s legal that I’m aware of is when two crates the belong together have one pin the exact version of the other.[1] Otherwise, it’s going be able to lead to broken builds no matter what.


  1. Typically their versions match, too.

    foo @ 0.42.0 depends on exactly foo_macros_funny_helper_crate @ 0.42.0
    foo @ 0.42.1 depends on exactly foo_macros_funny_helper_crate @ 0.42.1
    foo @ 0.42.2 depends on exactly foo_macros_funny_helper_crate @ 0.42.2
    …

    and this pattern should cause no issues. ↩︎

Because it's a hard guarantee. If I depend on Bevy and its version resolves successfully, I can use the proper version of its dependencies. Anything else is a manual process of trying to get Cargo pretty please to choose a compatible version.

I don't need to pollute my own dependency list with the dependencies of dependencies, I don't need to manually track their version updates, I don't have the possibility of diamond dependency problems, I can't mess up feature selection.

The proper way is either to avoid exposing transitive dependencies entirely, by wrapping them in your API (the best but verbose solution), or to add pub extern crate winit; to your library root.

I think I might have used the wrong words, so let me clarify:

I agree with you. What I was trying to say is that, if a library author has decided not to re-export some types, it's because they don't want these to be part of their public API.

I believe that the library authors know better about the use cases for their libraries, so if they have decided not to re-export something they must have a strong reason behind such decision.

Since this appears to be such a case, that's why I tried to go by with an example where a library author might re-export something by mistake.

Either way, I would still be cautious when dealing with transitive dependencies.

But in that case the transitive dependency should't appear in their API. I may be misunderstanding your issue. Could you link the Bevy code we're talking about?

Yes. As soon as you expose some types by re-exporting them, they become part of your public API, therefore you should be careful about it because, as @steffahn mentioned above, changing a transitive dependency that a library is re-exporting can be a breaking change upstream.

I don't have any example :smile:. I was just talking in a generic manner, based on the OP's first message.

There’s no “pretty please”, this is all established practice and well-defined procedures.

Of course there can be usability advantages to the re-export. As can there in not having all your public dependencies re-appearing in your module structure.

Well, until your usage of those updated public dependencies breaks because their API changed. Then you need to look up the relevant docs, anyways. Of course, it’s less effort in case you didn’t hit any of the changed API surfaces (and as long as you hope they ensure that nothing breaks and changes behavior “silently”, without compile-time or run-time errors…)

In my view, the question of how proper adding a pub extern crate … thing for your public dependencies depends on many factors. One of them might be how popular the crate is, and how frequently it changes major versions. For example I would imagine that you, too, don’t do it for something like serde, and it’s clear that very few crates re-export serde when they publicly depend on it.

As an example, hashbrown has 5 public dependencies [1] and re-exports none of them.


  1. AFAICT ahash, allocator-api2, rayon, rkyv, serde all appear in some API (some behind feature flags) ↩︎

2 Likes

Thanks guys! :smiling_face_with_three_hearts:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.