Skip to content

Commit

Permalink
WASM backend (#25)
Browse files Browse the repository at this point in the history
* init wasm client

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wasm32 cfg flags

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* sort deps alphabetically

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* init cross platform by default

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* finish compile

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wasm ci

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* move over cfg statements

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* make fetch

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wasmify logging middleware

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wip

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* add wasm kv logging

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wasm compiles

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* update safety notice

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* requests work!

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* wasm req method

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* split out fetch code to separate block

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* fix all warnings

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>

* finalize header extraction

Signed-off-by: Yoshua Wuyts <yoshuawuyts@gmail.com>
  • Loading branch information
yoshuawuyts authored Aug 7, 2019
1 parent 94d2bc6 commit 00e5351
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 79 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist/
npm-debug.log*
Cargo.lock
.DS_Store
wasm-pack.log
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ rust:

before_script: |
rustup component add rustfmt-preview &&
rustup component add clippy-preview
rustup component add clippy-preview &&
rustup target add wasm32-unknown-unknown
script: |
cargo fmt -- --check &&
cargo clippy -- -D clippy &&
cargo build --verbose &&
cargo check --target wasm32-unknown-unknown &&
cargo test --verbose
cache: cargo
49 changes: 38 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,61 @@ readme = "README.md"
edition = "2018"

[features]
default = ["chttp-client", "middleware-logger"]
default = ["native-client", "middleware-logger"]
native-client = ["chttp-client", "wasm-client"]
hyper-client = ["hyper", "runtime", "runtime-raw", "runtime-tokio" ]
chttp-client = ["chttp"]
wasm-client = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]
middleware-logger = []

[dependencies]
http = "0.1.17"
futures-preview = { version = "0.3.0-alpha.17", features = ["compat", "io-compat"] }
http = "0.1.17"
log = { version = "0.4.7", features = ["kv_unstable"] }
mime = "0.3.13"
mime_guess = "2.0.0-alpha.6"
serde = "1.0.97"
serde_json = "1.0.40"
log = { version = "0.4.7", features = ["kv_unstable"] }
serde_urlencoded = "0.6.1"
url = "2.0.0"
mime_guess = "2.0.0-alpha.6"
mime = "0.3.13"

# Chttp
# chttp-client
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
chttp = { version = "0.5.3", optional = true }

# Hyper deps
# hyper-client
hyper = { version = "0.12.32", optional = true, default-features = false }
hyper-tls = { version = "0.3.2", optional = true }
runtime = { version = "0.3.0-alpha.6", optional = true }
native-tls = { version = "0.2.2", optional = true }
runtime = { version = "0.3.0-alpha.6", optional = true }
runtime-raw = { version = "0.3.0-alpha.4", optional = true }
runtime-tokio = { version = "0.3.0-alpha.5", optional = true }
serde_urlencoded = "0.6.1"

# wasm-client
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.25", optional = true }
wasm-bindgen = { version = "0.2.48", optional = true }
wasm-bindgen-futures = { version = "0.3.25", features = ["futures_0_3"], optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.25"
optional = true
features = [
"AbortSignal",
"Headers",
"ObserverCallback",
"ReferrerPolicy",
"Request",
"RequestCache",
"RequestCredentials",
"RequestInit",
"RequestMode",
"RequestRedirect",
"Response",
"Window",
]

[dev-dependencies]
serde = { version = "1.0.97", features = ["derive"] }
runtime = "0.3.0-alpha.6"
femme = "1.1.0"
runtime = "0.3.0-alpha.6"
serde = { version = "1.0.97", features = ["derive"] }
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,11 @@ $ cargo add surf
```

## Safety
This crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in
100% Safe Rust.
This crate makes use of a single instance of `unsafe` in order to make the WASM
backend work despite the `Send` bounds. This is safe because WASM targets
currently have no access to threads. Once they do we'll be able to drop this
implementation, and use a parked thread instead and move to full multi-threading
in the process too.

## Contributing
Want to join us? Check out our ["Contributing" guide][contributing] and take a
Expand Down
7 changes: 7 additions & 0 deletions examples/browser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use surf;
use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main() {
println!("Hello wasm");
}
10 changes: 5 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::http_client::HttpClient;
use crate::Request;

#[cfg(feature = "chttp-client")]
use crate::http_client::chttp::ChttpClient;
#[cfg(feature = "native-client")]
use super::http_client::native::NativeClient;

/// An HTTP client, capable of creating new `Request`s.
///
Expand All @@ -23,8 +23,8 @@ pub struct Client<C: HttpClient> {
client: C,
}

#[cfg(feature = "chttp-client")]
impl Client<ChttpClient> {
#[cfg(feature = "native-client")]
impl Client<NativeClient> {
/// Create a new instance.
///
/// # Examples
Expand All @@ -37,7 +37,7 @@ impl Client<ChttpClient> {
/// # Ok(()) }
/// ```
pub fn new() -> Self {
Self::with_client(ChttpClient::new())
Self::with_client(NativeClient::new())
}
}

Expand Down
5 changes: 1 addition & 4 deletions src/http_client/chttp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ use futures::future::BoxFuture;

use std::sync::Arc;

/// Curl HTTP Client.
///
/// ## Performance
/// Libcurl is not thread safe, which means unfortunatley we cannot reuse connections or multiplex.
/// Curl-based HTTP Client.
#[derive(Debug)]
pub struct ChttpClient {
client: Arc<chttp::HttpClient>,
Expand Down
10 changes: 8 additions & 2 deletions src/http_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};

#[cfg(feature = "hyper-client")]
#[cfg(all(feature = "hyper-client", not(target_arch = "wasm32")))]
pub(crate) mod hyper;

#[cfg(feature = "chttp-client")]
#[cfg(all(feature = "chttp-client", not(target_arch = "wasm32")))]
pub(crate) mod chttp;

#[cfg(all(feature = "wasm-client", target_arch = "wasm32"))]
pub(crate) mod wasm;

#[cfg(feature = "native-client")]
pub(crate) mod native;

/// An HTTP Request type with a streaming body.
pub type Request = http::Request<Body>;

Expand Down
8 changes: 8 additions & 0 deletions src/http_client/native.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[cfg(all(feature = "chttp-client", not(target_arch = "wasm32")))]
pub(crate) use super::chttp::ChttpClient as NativeClient;

#[cfg(all(
feature = "wasm-client",
target_arch = "wasm32"
))]
pub(crate) use super::wasm::WasmClient as NativeClient;
201 changes: 201 additions & 0 deletions src/http_client/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use super::{Body, HttpClient, Request, Response};

use futures::future::BoxFuture;
use futures::prelude::*;

use std::pin::Pin;
use std::task::{Context, Poll};
use std::io;

/// WebAssembly HTTP Client.
#[derive(Debug)]
pub struct WasmClient {
_priv: (),
}

impl WasmClient {
/// Create a new instance.
pub fn new() -> Self {
Self { _priv: () }
}
}

impl Clone for WasmClient {
fn clone(&self) -> Self {
Self { _priv: () }
}
}

impl HttpClient for WasmClient {
type Error = std::io::Error;

fn send(&self, req: Request) -> BoxFuture<'static, Result<Response, Self::Error>> {
let fut = Box::pin(async move {
let url = format!("{}", req.uri());
let req = fetch::new(req.method().as_str(), &url);
let mut res = req.send().await?;

let body = res.body_bytes();
let mut response = Response::new(Body::from(body));
*response.status_mut() = http::StatusCode::from_u16(res.status()).unwrap();

for (name, value) in res.headers() {
let name: http::header::HeaderName = name.parse().unwrap();
response.headers_mut().insert(name, value.parse().unwrap());
}

Ok(response)
});

Box::pin(InnerFuture { fut })
}
}

// This type e
struct InnerFuture {
fut: Pin<Box<dyn Future<Output = Result<Response, io::Error>> + 'static>>,
}

// This is safe because WASM doesn't have threads yet. Once WASM supports threads we should use a
// thread to park the blocking implementation until it's been completed.
unsafe impl Send for InnerFuture {}

impl Future for InnerFuture {
type Output = Result<Response, io::Error>;

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// This is safe because we're only using this future as a pass-through for the inner
// future, in order to implement `Send`. If it's safe to poll the inner future, it's safe
// to proxy it too.
unsafe { Pin::new_unchecked(&mut self.fut).poll(cx) }
}
}

mod fetch {
use js_sys::{Array, ArrayBuffer, Uint8Array, Reflect};
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::futures_0_3::JsFuture;
use web_sys::window;
use web_sys::RequestInit;

use std::iter::{Iterator, IntoIterator};
use std::io;

/// Create a new fetch request.
pub(crate) fn new(method: impl AsRef<str>, url: impl AsRef<str>) -> Request {
Request::new(method, url)
}

/// An HTTP Fetch Request.
pub(crate) struct Request {
init: RequestInit,
url: String,
}

impl Request {
/// Create a new instance.
pub(crate) fn new(method: impl AsRef<str>, url: impl AsRef<str>) -> Self {
let mut init = web_sys::RequestInit::new();
init.method(method.as_ref());
Self {
init,
url: url.as_ref().to_owned(),
}
}

/// Submit a request
// TODO(yoshuawuyts): turn this into a `Future` impl on `Request` instead.
pub(crate) async fn send(self) -> Result<Response, io::Error> {
// Send the request.
let window = window().expect("A global window object could not be found");
let request = web_sys::Request::new_with_str_and_init(&self.url, &self.init).unwrap();
let promise = window.fetch_with_request(&request);
let resp = JsFuture::from(promise).await.unwrap();
debug_assert!(resp.is_instance_of::<web_sys::Response>());
let res: web_sys::Response = resp.dyn_into().unwrap();

// Get the request body.
let promise = res.array_buffer().unwrap();
let resp = JsFuture::from(promise).await.unwrap();
debug_assert!(resp.is_instance_of::<js_sys::ArrayBuffer>());
let buf: ArrayBuffer = resp.dyn_into().unwrap();
let slice = Uint8Array::new(&buf);
let mut body: Vec<u8> = vec![0; slice.length() as usize];
slice.copy_to(&mut body);

Ok(Response::new(res, body))
}
}

/// An HTTP Fetch Response.
pub(crate) struct Response {
res: web_sys::Response,
body: Option<Vec<u8>>,
}

impl Response {
fn new(res: web_sys::Response, body: Vec<u8>) -> Self {
Self {
res,
body: Some(body),
}
}

/// Access the HTTP headers.
pub(crate) fn headers(&self) -> Headers {
Headers {
headers: self.res.headers()
}
}

/// Get the request body as a byte vector.
///
/// Returns an empty vector if the body has already been consumed.
pub(crate) fn body_bytes(&mut self) -> Vec<u8> {
self.body.take().unwrap_or_else(|| vec![])
}

/// Get the HTTP return status code.
pub(crate) fn status(&self) -> u16 {
self.res.status()
}
}

/// HTTP Headers.
pub(crate) struct Headers {
headers: web_sys::Headers,
}

impl IntoIterator for Headers {
type Item = (String, String);
type IntoIter = HeadersIter;

fn into_iter(self) -> Self::IntoIter {
HeadersIter {
iter: js_sys::try_iter(&self.headers).unwrap().unwrap(),
}
}
}

/// HTTP Headers Iterator.
pub(crate) struct HeadersIter {
iter: js_sys::IntoIter,
}

impl Iterator for HeadersIter {
type Item = (String, String);

fn next(&mut self) -> Option<Self::Item> {
let pair = self.iter.next()?;

let array: Array = pair.unwrap().into();
let vals = array.values();

let prop = String::from("value").into();
let key = Reflect::get(&vals.next().unwrap(), &prop).unwrap();
let value = Reflect::get(&vals.next().unwrap(), &prop).unwrap();

Some((key.as_string().to_owned().unwrap(), value.as_string().to_owned().unwrap()))
}
}
}
Loading

0 comments on commit 00e5351

Please sign in to comment.