How to publish a crate, with docs, that depends on features?

So I have a crate (my first!) which requires that a feature be set. Specifically "webgl_1" or "webgl_2"

When I try to publish it to crates.io, the documentation build fails.

Locally - cargo package also fails if I don't pass --no-verify

What is the solution for this? (I don't want it to be a runtime check - prefer it to be a flag the app/consumer sets at compile time, at least for now)

You can set specific features when you publish, like cargo publish --features webgl_2

docs.rs can use metadata that you set in Cargo.toml: About Docs.rs

2 Likes

How does that affect consumers? I'd imagine they can still choose a different feature set?

In other words I think of publish, without --no-verify, as only doing something like... a cargo build for sanity checks - but the consumers that cargo install still only get the full source distribution (i.e. choosing features is up to them)

OK so if I set the features for docs in Cargo.toml, then the docs will be published for whatever code has that specific feature set yeah? No way for users to see docs on all functions regardless of whether or not they are behind a given feature?

1 Like

Correct, the publishing features only affect the verification build. The crate upload is still just source code.

To see docs for all features, you'll need to enable all features there.

Features are meant to be additive, so if yours are mutually exclusive, you might need to rethink that. For instance, you could end up with different features enabled indirectly in different parts of a dependency tree, and cargo will unify those into a single crate build.

I find mutually exclusive features are reasonable so long at they meet the following constraints:

  • No downstream library intended for reuse by others should require a specific one to be enabled.
    • If they contain code that needs to change based on which one is enabled, they can require "at least one" to be enabled, and expose features that forward to the upstream features.
  • Any given application knows which one it wants to enable (or exposes features for the end user to select the appropriate one).

Basically, it works so long as only the root node of the dependency tree takes authority on which one should be enabled.

Of course, this depends on compliance from the authors of downstream libraries; but I'd argue that if such an author finds the need to enable one of the features, then it is probably tightly coupled to a specific application and therefore it is unlikely that their library would be useful for other projects to begin with.

Yeah... I think that's the situation I'm hitting... happy to get another perspective here, this is where I'm at right now:

Background:

Goal is writing a mid-level wrapper around webgl. Think something sortof in the spirit of TWLGL but for Rust. More specifically, the wasm-bindgen/web-sys approach is intentionally to not concern itself with mid-level wrappers, and it only outputs direct bindings to Web API's, even though it's very non-idiomatic. To that end there are projects like gloo which is also in the spirit of what I'm doing.

Constraints:

web-sys exports two different types: WebGlRenderingContext and WebGl2RenderingContext (representing webgl1 and webgl2 respectively). The gotcha is that there are no traits covering their overlap, but there is a huge amount of overlap.

For example, here's how you bind a buffer in webgl1:

impl WebGlRenderingContext {
  pub fn bind_buffer(&self, target: u32, buffer: Option<&WebGlBuffer>)
}

And here's how you do it in webgl2:

impl WebGl2RenderingContext {
  pub fn bind_buffer(&self, target: u32, buffer: Option<&WebGlBuffer>)
}

Same exact signature...

Real-world usage:

If webgl2 is available, there is no reason whatsoever to use webgl1. Additionally, for real-world applications, the choice to use webgl2 will inevitably dictate how a ton of other code is written. Both shaders and core application logic.

While supporting "webgl2 with a webgl1 fallback" is the approach of several libraries, I think this adds unnecessary cruft to any significant application... it just doesn't make sense to me, especially in a strongly typed language (it perhaps makes more sense in JS). To my eye - it makes more sense to me to have separate builds, just like one would have for native iOS vs. Windows - and then perhaps have a landing page that detects which version of webgl is available and redirect to there.

This is of course debatable - but I don't think my approach here is wrong even if it's not universally right.

Solutions:

As far as I can see there's only 3 possible approaches here:

  1. Create a trait that re-defines the shared functionality, and then impl it for both WebGlRenderingContext and WebGl2RenderingContext (and also create separate getters like get_webgl1_context(&self) -> Option<&WebGlRenderingContext> so the original contexts can be gotten explicitly).

  2. Store the context in an enum with two variants and then at runtime match on the variants to call the appropriate code as needed.

  3. Use features to enable one or the other. Exactly one must be enabled. Ultimately a type alias is used at compile time to represent which one it is.

In terms of pros/cons:

  1. (traits) I am still an inexperienced Rust programmer and I'm worried that this will lead me down a road of needing Trait Objects / dynamic dispatch, which I'd rather avoid since it feels like an unnecessary expense here. Also feel guilty about adding methods to a third-party struct so would probably put it behind a "newtype" wrapper which will get a bit ugly. On the plus side I think it solves the problem of generating docs.

  2. (enums) probably the most flexible and easiest to implement the "fallback" approach too - but requires every call site to check at runtime which variant it is. Of course that's an extremely cheap check and nothing to actually consider, but it does make the code a bit silly looking (e.g. consider the above implementation of bind_buffer... the match arms will all be doing the same thing). Maybe I'm the silly one and I shouldn't think about it - so this is the right approach, but it's also unnecessary bloat since really only one context type will be used in a given application.

  3. (features) is the leanest and solves all my problems - and has an advantage of compile-time checks if accidentally calling a function that is only available on one of the contexts... but has the additional problem with generating docs (which is where I'm stuck now).

Any thoughts are appreciated. Thanks!

You can use this to generate all features for the docs:

[package.metadata.docs.rs]
all-features = true

However, I don't think the mutually exclusive trait idea is good: what if one library enables one feature, and a different library enables the other? What if both traits are enabled at the same time?

Why not instead provide a new type which represents the sub-set of all methods available for WebGL1 and WebGL2? Then you can provide functions for converting into this sub-set:

pub struct WebGlContext {
    value: JsValue,
}

impl WebGlContext {
    ... common methods go here ...
}.

impl From<WebGlRenderingContext> for WebGlContext {
    ...
}

impl From<WebGl2RenderingContext> for WebGlContext {
    ....
}

You can also provide methods for going back to WebGlRenderingContext and WebGl2RenderingContext (using dyn_into)

Is there any particular reason for that? Traits are only used if they are imported, and they can be disambiguated with the Trait::foo syntax, so there's no issue with creating traits for third-party types.

1 Like

Good point... tbh I hadn't really thought of that - I assumed any libraries that wrapped this would also delegate that choice to the app, but that's likely either a false or messy assumption...

Ah - I didn't realize Traits are only used if they are imported... that clears up a ton of confusion for me!

So - given that implementing a trait directly on WebGlRenderingContext and WebGl2RenderingContext is fine, why use From/Into ?

I presented it as 4th alternative in addition to the 3 possibilities you listed.

It might be more efficient since it can avoid monomorphization. But it'll only support methods which are exactly the same for WebGlRenderingContext and WebGl2RenderingContext. For other methods you'll need to convert back into the base type.

1 Like

Does this come at the expense of a runtime type check, similar to enums ?

(I get that it might be negligible but just curious how Into/From work under the hood and I actually didn't see any info about that on the docs)

Using from or into in this situation does not do any runtime checks. In fact it has zero runtime cost at all, the "conversion" is done entirely at compile time.

Using dyn_into (to convert to the original type) would have runtime checks (but still no runtime conversion).

1 Like

Think I'll pick this up in the wg-wasm discord channel since continuing it here will really go far off topic... thanks for the leg-up and I see there are lots of ways to skin this cat!

Spent pretty much all day trying different approaches with this... just couldn't get around runtime checks. It's possible I missed something, but if that really is a hard limit, I think compile-time flags and choosing a specific context is actually the best in terms of trade-offs here (just gotta communicate it well).

To reiterate my own feelings on the matter: If:

  • you document that these features are mutually exclusive, and
  • there exists a library that unconditionally enables one of the features

Then, between the two of you, I believe the downstream library is in the wrong.

I do think that unifying the APIs will almost certainly result in a better API in the end... But I don't think it's required, and I don't believe it is something worth breaking your back over if you have better things to do. For now, all you really need to do is warn against improper usage of these features in your documentation.

1 Like

I appreciate the encouragement - pretty easy to get obsessed over something like this...

The frustrating thing is that it feels like Rust provides a ton of tools and there's some combo that should make this work and I'm just missing it :\ so now it's not just about this specific use-case, but feeling like I'm missing some fundamental toolbelt thing about how traits / generics / etc. work.

Here's another playground to demo where I'm stuck in my thinking so far: Rust Playground

This seems to be getting somewhere:

Phew... @Pauan took some time to help me out walking through a few different approaches - turns out there's actually a few solutions (some involve js-sys specific stuff)!

In the end he gave a macro which doesn't require anything external, just pure Rust, and solves the problem beautifully:

2 Likes

Extended the playground to show more use cases: Rust Playground

Specifically:

  • Splitting the traits (helpful for across files)
  • Holding the context in a struct which itself can be either generic or specific

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.