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:
-
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).
-
Store the context in an enum with two variants and then at runtime match on the variants to call the appropriate code as needed.
-
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:
-
(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.
-
(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.
-
(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!