Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support wasm32-unknown-unknown architecture #458

Merged
merged 4 commits into from
Feb 29, 2024

Conversation

gelendir
Copy link
Contributor

@gelendir gelendir commented Feb 14, 2024

Description

Modifications needed to compile rspotify for a WebAssembly runtime. (i.e. cargo build --target wasm32-unknown-unknown)

Motivation and Context

I want to build an app that helps me better manage music when using spotify to DJ. I'd like to use rust + WASM to build it, however none of the maintained spotify crates support WASM.

@shiguma127 made an attempt in #293, but the PR was closed. This PR improves on what he started.

This is my most "complex" rust endeavor so far and I would be happy to get feedback on better ways of refactoring or rearchitecturing.

The code is now littered with a bunch of architecture conditionals that might be hard to maintain. I'm open to trying better alternatives if the maintainers don't like this.

One idea would be to make it easier to use the rspotify-http traits. One could implement the BaseHttpClient trait outside of this crate and then build a SpotifyClient<CustomHttpClient>. Feature flags would need to be refactored since this crate forces choosing a http client through the client-reqwest or client-ureq features.

Dependencies

  • wasm-pack for building and running tests
  • NodeJS for running tests

The other dependencies are managed through Cargo.toml

Type of change

  • New feature (non-breaking change which adds functionality)

How has this been tested?

The #[wasm_bindgen_test] attribute has been added to all tests. They all pass when running wasm-pack test --node. The attribute doesn't conflict with #[test] (but it does add an extra dev-dependency)

wasm-pack depends on a WASM runtime to run tests. The 3 currently supported are:

  • NodeJS
  • Chrome
  • Firefox

NodeJS is the default, so that's what's configured in the CI job

Client ID and Client secret

Some of the tests depend on environment variables RSPOTIFY_CLIENT_ID and RSPOTIFY_CLIENT_SECRET. However, WASM does not support the concept of environment variables. As a workaround the variables are imported at compile time only when running tests.

Is this change properly documented?

I wasn't sure if wasm support warrants documentation or not. I'm happy to add documentation as part of this PR

@@ -67,6 +70,17 @@ impl Default for ReqwestClient {
}
}

#[cfg(target_arch = "wasm32")]
impl Default for ReqwestClient {
fn default() -> Self {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reqwest doesn't implement the timeout function when built for wasm, hence the need for a separate implementation.

Copy link
Contributor Author

@gelendir gelendir Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stumped me the most. The async runtime in wasm doesn't support Send so it had to be removed from all trait bounds in stream.rs.

At first I tried to extract the trait bounds into "aliases" with the goal of annotating them with #[cfg...] but didn't find a way of making it work. There's trait_alias that might help, but it's available only on nightly.

@@ -442,11 +442,13 @@ impl OAuth {
}

#[cfg(test)]
mod test {
pub mod test {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub is needed so that wasm-pack can find the tests.

};

use maybe_async::maybe_async;

#[cfg(target_arch="wasm32")]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to remove a warning on unused import when building for other architectures

tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1.11.0", features = ["rt", "macros"] }
Copy link
Contributor Author

@gelendir gelendir Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking tokio isn't needed for running wasm tests, but to removing it would have made the #[cfg... attributes on tests more complex


[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1.11.0", features = ["rt", "macros"] }
dotenvy_macro = { version = "0.15.7" }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used to read .env when running wasm tests

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it meanenv:var in the standard library won't work in the wasm32 architecture?

Shouldn't we add a #[cfg...] flag for it, because it's wasm32 specific, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, std::env::var won't work on wasm32-unknown-unknown.Wasm is designed to run in a web browser which is isolated from the host OS.

From my understanding of cargo the #[cfg...] on line 63 creates a block section, so it should already be imported only for wasm. It doesn't seem to get built when running cargo build --tests:

% cargo build --tests
   Compiling wasm-bindgen-backend v0.2.91
   Compiling tokio v1.35.1
   Compiling futures-util v0.3.30
   Compiling serde v1.0.195
   Compiling thiserror v1.0.56
   Compiling strum v0.26.1
   Compiling enum_dispatch v0.3.12
   Compiling async-stream v0.3.5
   Compiling wasm-bindgen-macro-support v0.2.91
   Compiling wasm-bindgen-macro v0.2.91
   Compiling wasm-bindgen v0.2.91
   Compiling futures-executor v0.3.30
   Compiling serde_json v1.0.111
   Compiling serde_urlencoded v0.7.1
   Compiling chrono v0.4.31
   Compiling futures v0.3.30
   Compiling js-sys v0.3.68
   Compiling console_error_panic_hook v0.1.7
   Compiling tokio-util v0.7.10
   Compiling tokio-native-tls v0.3.1
   Compiling tokio-socks v0.5.1
   Compiling h2 v0.3.23
   Compiling rspotify-model v0.12.0 (/Users/gregory/rust/rspotify/rspotify-model)
   Compiling wasm-bindgen-futures v0.4.41
   Compiling wasm-bindgen-test v0.3.41
   Compiling hyper v0.14.28
   Compiling hyper-tls v0.5.0
   Compiling reqwest v0.11.23
   Compiling rspotify-http v0.12.0 (/Users/gregory/rust/rspotify/rspotify-http)
   Compiling rspotify v0.12.0 (/Users/gregory/rust/rspotify)
    Finished dev [unoptimized + debuginfo] target(s) in 6.82s
    ```

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For comparison, here's the output with wasm:

% cargo build --tests --target wasm32-unknown-unknown
   Compiling proc-macro2 v1.0.76
   Compiling unicode-ident v1.0.12
   Compiling wasm-bindgen-shared v0.2.91
   Compiling bumpalo v3.14.0
   Compiling log v0.4.20
   Compiling autocfg v1.1.0
   Compiling wasm-bindgen v0.2.91
   Compiling cfg-if v1.0.0
   Compiling serde v1.0.195
   Compiling version_check v0.9.4
   Compiling pin-project-lite v0.2.13
   Compiling typenum v1.17.0
   Compiling futures-core v0.3.30
   Compiling itoa v1.0.10
   Compiling tinyvec_macros v0.1.1
   Compiling futures-sink v0.3.30
   Compiling tinyvec v1.6.0
   Compiling syn v1.0.109
   Compiling futures-channel v0.3.30
   Compiling percent-encoding v2.3.1
   Compiling futures-task v0.3.30
   Compiling rustversion v1.0.14
   Compiling pin-utils v0.1.0
   Compiling memchr v2.7.1
   Compiling serde_json v1.0.111
   Compiling generic-array v0.14.7
   Compiling ryu v1.0.16
   Compiling slab v0.4.9
   Compiling futures-io v0.3.30
   Compiling num-traits v0.2.17
   Compiling form_urlencoded v1.2.1
   Compiling unicode-bidi v0.3.14
   Compiling thiserror v1.0.56
   Compiling fnv v1.0.7
   Compiling bytes v1.5.0
   Compiling quote v1.0.35
   Compiling unicode-normalization v0.1.22
   Compiling async-trait v0.1.77
   Compiling heck v0.4.1
   Compiling syn v2.0.48
   Compiling http v0.2.11
   Compiling tower-service v0.3.2
   Compiling idna v0.5.0
   Compiling base64 v0.21.7
   Compiling env_filter v0.1.0
   Compiling dotenvy v0.15.7
   Compiling rspotify-macros v0.12.0 (/Users/gregory/rust/rspotify/rspotify-macros)
   Compiling scoped-tls v1.0.1
   Compiling env_logger v0.11.1
   Compiling url v2.5.0
   Compiling block-buffer v0.10.4
   Compiling crypto-common v0.1.6
   Compiling digest v0.10.7
   Compiling sha2 v0.10.8
   Compiling wasm-bindgen-backend v0.2.91
   Compiling maybe-async v0.2.7
   Compiling dotenvy_macro v0.15.7
   Compiling wasm-bindgen-macro-support v0.2.91
   Compiling serde_derive v1.0.195
   Compiling futures-macro v0.3.30
   Compiling thiserror-impl v1.0.56
   Compiling strum_macros v0.26.1
   Compiling async-stream-impl v0.3.5
   Compiling enum_dispatch v0.3.12
   Compiling wasm-bindgen-test-macro v0.3.41
   Compiling tokio-macros v2.2.0
   Compiling async-stream v0.3.5
   Compiling wasm-bindgen-macro v0.2.91
   Compiling futures-util v0.3.30
   Compiling tokio v1.35.1
   Compiling strum v0.26.1
   Compiling js-sys v0.3.68
   Compiling console_error_panic_hook v0.1.7
   Compiling futures-executor v0.3.30
   Compiling futures v0.3.30
   Compiling wasm-bindgen-futures v0.4.41
   Compiling web-sys v0.3.67
   Compiling getrandom v0.2.12
   Compiling serde_urlencoded v0.7.1
   Compiling chrono v0.4.31
   Compiling wasm-bindgen-test v0.3.41
   Compiling rspotify-model v0.12.0 (/Users/gregory/rust/rspotify/rspotify-model)
   Compiling reqwest v0.11.23
   Compiling rspotify-http v0.12.0 (/Users/gregory/rust/rspotify/rspotify-http)
   Compiling rspotify v0.12.0 (/Users/gregory/rust/rspotify)
    Finished dev [unoptimized + debuginfo] target(s) in 9.77s

@@ -4,6 +4,8 @@
use super::{BaseHttpClient, Form, Headers, Query};

use std::convert::TryInto;

#[cfg(not(target_arch = "wasm32"))]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to remove a warning on unused imports

}

#[cfg(target_arch = "wasm32")]
fn get_access_tokens() -> (Option<String>, Option<String>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with wasm32 architecture, does it mean wasm32 doesn't support env::var function, so we need another macro to obtain the environment variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, wasm32-unknown-unknown doesn't support accessing OS environment variables. As a work-around the tokens are imported at compile time. I extracted the code into a function to make it easier to manage the difference between architectures.


[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1.11.0", features = ["rt", "macros"] }
dotenvy_macro = { version = "0.15.7" }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it meanenv:var in the standard library won't work in the wasm32 architecture?

Shouldn't we add a #[cfg...] flag for it, because it's wasm32 specific, right?

@@ -109,7 +123,8 @@ impl ReqwestClient {
}
}

#[async_impl]
#[cfg_attr(target_arch = "wasm32", async_impl(?Send))]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering is there any way to encapsulate these cfg_attr flags, because they are duplicated, and we might need to do the same thing for other architecture in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering the same thing. I found in the rust docs that the #![cfg_attr] form applies to an entire file, but that doesn't help much here. Another idea would be to abstract this within a custom macro.

I've never written custom macros yet but could experiment if you think it's a good idea.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom macros is not an ideal idea for me, it will also increase complexity and mental effort to maintain the codebase.

@@ -0,0 +1,62 @@
//! Asynchronous implementation of automatic pagination requests.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a separate stream module for wasm32 architecture, is there anything the existing stream module is unable to satisfy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like my previous comment dissapeared, I explained it here: #458 (comment)

Basically I didn't find a way of encapsulating the Send trait bounds on the function signature behind a #[cfg...]

@@ -456,6 +458,7 @@ mod test {
}

#[test]
#[wasm_bindgen_test]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A naive question, what does wasm_bindgen_test mean? Do we need this macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regualar #[test] isn't compatible with wasm. Excerpt from the wasm-bindgen-test README:

The normal #[test] cannot be used and will not work. Eventually it's intended that the #[wasm_bindgen_test] attribute could gain arguments like "run in a browser" or something like a minimum Node version.

I believe the macro exports the tests into a separate WASM module to make it easier to run in a browser.

@ramsayleung
Copy link
Owner

Thanks for your contribution, everything looks good to me now, is it ready to merge?

Furthermore, are you planning to add a section in the CHANGELOG to illustrate the support for wasm?

@gelendir
Copy link
Contributor Author

Good to hear ! Yes I can add some docs and notes to the changelog. It's getting late over here but will push the docs to this PR tomorrow ?

@gelendir
Copy link
Contributor Author

Documentation and CHANGELOG have been added. I didn't figureout how you manage version increments, so I guessed that this would be consifered a new feature and added version 1.3.0.

If the docs look good then I don't have any other changes planned and you can merge.

@gelendir gelendir requested a review from ramsayleung February 28, 2024 19:40
@ramsayleung
Copy link
Owner

LGTM, but some CI tasks failed.

@gelendir
Copy link
Contributor Author

It seems to be a transient error with the spotify API, the test received a HTTP 504. I just tried running the CI on my repo and everything passes: https://github.com/gelendir/rspotify/actions/runs/8091405577

Could you try re-running the CI ?

@ramsayleung ramsayleung merged commit 0849ff3 into ramsayleung:master Feb 29, 2024
6 checks passed
@ramsayleung
Copy link
Owner

Merged :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants