-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rust): add webassembly example (#4325)
* feat(rust): add webassembly example * chore: include copyright notice * chore: pass credentials from browser * chore: retrieve credentials from browser cookie * chore: use webpack to serve instead * chore: remove additional-ci file * chore: move to top the credentials to be filled * chore: set curly braces around function bodies with void return type * chore: add some comments on outbound http request This explains how we are making a network "proxy" for wasm using the Fetch API (browser). * Update index.html Fix closing head tag * chore: update based on editorial review * Update lib.rs fmt lint --------- Co-authored-by: Eduardo Rodrigues <[email protected]> Co-authored-by: David Souther <[email protected]>
- Loading branch information
1 parent
ecbfe23
commit a0fb259
Showing
14 changed files
with
4,347 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,5 +60,6 @@ members = [ | |
"test-utils", | ||
"testing", | ||
"tls", | ||
"transcribestreaming" | ||
"transcribestreaming", | ||
"webassembly" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[build] | ||
target = "wasm32-unknown-unknown" | ||
|
||
[target.wasm32-unknown-unknown] | ||
rustflags = ["-C", "target-feature=+atomics"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
[package] | ||
name = "aws-wasm" | ||
version = "0.1.0" | ||
authors = ["Eduardo Rodrigues <[email protected]>"] | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[lib] | ||
crate-type = ["cdylib"] | ||
|
||
[dependencies] | ||
aws-config = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", default-features = false } | ||
aws-credential-types = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", features = ["hardcoded-credentials"] } | ||
aws-sdk-lambda = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", default-features = false } | ||
aws-smithy-async = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main" } | ||
aws-smithy-client = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", default-features = false } | ||
aws-smithy-http = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main", features = ["event-stream"] } | ||
aws-smithy-types = { git = "https://github.com/awslabs/aws-sdk-rust", branch = "main" } | ||
async-trait = "0.1.63" | ||
console_error_panic_hook = "0.1.7" | ||
http = "0.2.8" | ||
js-sys = "0.3.60" | ||
serde = { version = "1.0.152", features = ["derive"] } | ||
serde-wasm-bindgen = "0.4.5" | ||
tokio = { version = "1.24.2", features = ["macros", "rt"] } | ||
tower = "0.4.13" | ||
wasm-bindgen = "0.2.83" | ||
wasm-bindgen-futures = "0.4.33" | ||
wasm-timer = "0.2.5" | ||
|
||
[dependencies.web-sys] | ||
version = "0.3.60" | ||
features = [ | ||
"console", | ||
"Headers", | ||
"Request", | ||
"RequestInit", | ||
"RequestMode", | ||
"Response", | ||
"Window", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# AWS SDK for Rust code examples using WebAssembly | ||
|
||
## Purpose | ||
|
||
This example demonstrates how to package in an WebAssembly module that uses the developer preview version of the AWS SDK for Rust. | ||
|
||
## Code examples | ||
|
||
- [Show functions](./src/lib.rs) (ListFunctions) | ||
|
||
## Running the code examples | ||
|
||
### Prerequisites | ||
|
||
- You must have an AWS account, and have configured your default credentials and AWS Region as described in [https://github.com/awslabs/aws-sdk-rust](https://github.com/awslabs/aws-sdk-rust). | ||
|
||
- Install the latest stable version of [Node.js](https://nodejs.org/en/download/). | ||
|
||
- Install [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/#). | ||
|
||
### ⚠ Important | ||
Your must customize your AWS credentials in the file [www/env/credentials.js](./www/env/credentials.js). Otherwise, it will not make the request to the backend. | ||
|
||
### count-functions | ||
|
||
This example lists your Lambda functions and returns the total amount found in a certain Region. | ||
|
||
``` | ||
wasm-pack build --target web --out-dir www/pkg --dev | ||
``` | ||
|
||
From within the [www](./www) directory, run the following command to install project and start serving. | ||
|
||
``` | ||
npm ci | ||
npm start | ||
``` | ||
|
||
Access your page at `http://localhost:3000`. Make your selection and press `Run`: | ||
|
||
- **region** is the Region in which the client is created. | ||
If not supplied, defaults to **us-west-2**. | ||
- **verbose** displays additional information. | ||
|
||
## Resources | ||
|
||
- [AWS SDK for Rust repo](https://github.com/awslabs/aws-sdk-rust) | ||
- [AWS SDK for Rust API Reference Guide](https://awslabs.github.io/aws-sdk-rust/aws_sdk_config/index.html) | ||
|
||
## Contributing | ||
|
||
To propose a new code example to the AWS documentation team, | ||
see [CONTRIBUTING.md](https://github.com/awsdocs/aws-doc-sdk-examples/blob/master/CONTRIBUTING.md). | ||
The team prefers to create code examples that show broad scenarios rather than individual API calls. | ||
|
||
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0. | ||
*/ | ||
|
||
use async_trait::async_trait; | ||
use aws_credential_types::{cache::CredentialsCache, provider::ProvideCredentials, Credentials}; | ||
use aws_sdk_lambda::{Client, Region, PKG_VERSION}; | ||
use aws_smithy_async::rt::sleep::{AsyncSleep, Sleep}; | ||
use aws_smithy_client::erase::DynConnector; | ||
use aws_smithy_http::{body::SdkBody, result::ConnectorError}; | ||
use serde::Deserialize; | ||
use wasm_bindgen::{prelude::*, JsCast}; | ||
|
||
macro_rules! log { | ||
( $( $t:tt )* ) => { | ||
web_sys::console::log_1(&format!( $( $t )* ).into()); | ||
} | ||
} | ||
|
||
#[derive(Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub struct AwsCredentials { | ||
pub access_key_id: String, | ||
pub secret_access_key: String, | ||
pub session_token: Option<String>, | ||
} | ||
|
||
#[wasm_bindgen(module = "env")] | ||
extern "C" { | ||
fn now() -> f64; | ||
|
||
#[wasm_bindgen(js_name = retrieveCredentials)] | ||
fn retrieve_credentials() -> JsValue; | ||
} | ||
|
||
#[wasm_bindgen(start)] | ||
pub fn start() { | ||
std::panic::set_hook(Box::new(console_error_panic_hook::hook)); | ||
log!("initializing module..."); | ||
} | ||
|
||
#[wasm_bindgen] | ||
pub async fn main(region: String, verbose: bool) -> Result<String, String> { | ||
log!(""); | ||
|
||
if verbose { | ||
log!("Lambda client version: {}", PKG_VERSION); | ||
log!("Region: {}", region); | ||
log!(""); | ||
} | ||
|
||
let credentials_provider = static_credential_provider(); | ||
let credentials = credentials_provider.provide_credentials().await.unwrap(); | ||
let access_key = credentials.access_key_id(); | ||
|
||
let shared_config = aws_config::from_env() | ||
.sleep_impl(BrowserSleep) | ||
.region(Region::new(region)) | ||
.credentials_cache(browser_credentials_cache()) | ||
.credentials_provider(credentials_provider) | ||
.http_connector(DynConnector::new(Adapter::new( | ||
verbose, | ||
access_key == "access_key", | ||
))) | ||
.load() | ||
.await; | ||
let client = Client::new(&shared_config); | ||
|
||
let now = std::time::Duration::new(now() as u64, 0); | ||
log!("current date in unix timestamp: {}", now.as_secs()); | ||
|
||
let resp = client | ||
.list_functions() | ||
.send() | ||
.await | ||
.map_err(|e| format!("{:?}", e))?; | ||
let functions = resp.functions().unwrap_or_default(); | ||
|
||
for function in functions { | ||
log!( | ||
"Function Name: {}", | ||
function.function_name().unwrap_or_default() | ||
); | ||
} | ||
let output = functions.len().to_string(); | ||
|
||
Ok(output) | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct BrowserSleep; | ||
impl AsyncSleep for BrowserSleep { | ||
fn sleep(&self, duration: std::time::Duration) -> Sleep { | ||
Sleep::new(Box::pin(async move { | ||
wasm_timer::Delay::new(duration).await.unwrap(); | ||
})) | ||
} | ||
} | ||
|
||
fn static_credential_provider() -> impl ProvideCredentials { | ||
let credentials = serde_wasm_bindgen::from_value::<AwsCredentials>(retrieve_credentials()) | ||
.expect("invalid credentials"); | ||
Credentials::from_keys( | ||
credentials.access_key_id, | ||
credentials.secret_access_key, | ||
credentials.session_token, | ||
) | ||
} | ||
|
||
fn browser_credentials_cache() -> CredentialsCache { | ||
CredentialsCache::lazy_builder() | ||
.sleep(std::sync::Arc::new(BrowserSleep)) | ||
.into_credentials_cache() | ||
} | ||
|
||
/// At this moment, there is no standard mechanism to make an outbound | ||
/// HTTP request from within the guest Wasm module. | ||
/// Eventually that will be defined by the WebAssembly System Interface: | ||
/// https://github.com/WebAssembly/wasi-http | ||
#[async_trait(?Send)] | ||
trait MakeRequestBrowser { | ||
async fn send( | ||
parts: http::request::Parts, | ||
body: SdkBody, | ||
) -> Result<http::Response<SdkBody>, JsValue>; | ||
} | ||
|
||
pub struct BrowserHttpClient {} | ||
|
||
#[async_trait(?Send)] | ||
impl MakeRequestBrowser for BrowserHttpClient { | ||
/// The [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) | ||
/// will be used to actually send the outbound HTTP request. | ||
/// Most of the logic here is around converting from | ||
/// the [http::Request]'s shape to [web_sys::Request]. | ||
async fn send( | ||
parts: http::request::Parts, | ||
body: SdkBody, | ||
) -> Result<http::Response<SdkBody>, JsValue> { | ||
use js_sys::{Array, ArrayBuffer, Reflect, Uint8Array}; | ||
use wasm_bindgen_futures::JsFuture; | ||
|
||
let mut opts = web_sys::RequestInit::new(); | ||
opts.method(parts.method.as_str()); | ||
opts.mode(web_sys::RequestMode::Cors); | ||
|
||
let body_pinned = std::pin::Pin::new(body.bytes().unwrap()); | ||
if body_pinned.len() > 0 { | ||
let uint_8_array = unsafe { Uint8Array::view(&body_pinned) }; | ||
opts.body(Some(&uint_8_array)); | ||
} | ||
|
||
let request = web_sys::Request::new_with_str_and_init(&parts.uri.to_string(), &opts)?; | ||
|
||
for (name, value) in parts | ||
.headers | ||
.iter() | ||
.map(|(n, v)| (n.as_str(), v.to_str().unwrap())) | ||
{ | ||
request.headers().set(name, value)?; | ||
} | ||
|
||
let window = web_sys::window().ok_or("could not get window")?; | ||
let promise = window.fetch_with_request(&request); | ||
let res_web = JsFuture::from(promise).await?; | ||
let res_web: web_sys::Response = res_web.dyn_into().unwrap(); | ||
|
||
let promise_array = res_web.array_buffer()?; | ||
let array = JsFuture::from(promise_array).await?; | ||
let buf: ArrayBuffer = array.dyn_into().unwrap(); | ||
let slice = Uint8Array::new(&buf); | ||
let body = slice.to_vec(); | ||
|
||
let mut builder = http::Response::builder().status(res_web.status()); | ||
for i in js_sys::try_iter(&res_web.headers())?.unwrap() { | ||
let array: Array = i?.into(); | ||
let values = array.values(); | ||
|
||
let prop = String::from("value").into(); | ||
let key = Reflect::get(values.next()?.as_ref(), &prop)? | ||
.as_string() | ||
.unwrap(); | ||
let value = Reflect::get(values.next()?.as_ref(), &prop)? | ||
.as_string() | ||
.unwrap(); | ||
builder = builder.header(&key, &value); | ||
} | ||
let res_body = SdkBody::from(body); | ||
let res = builder.body(res_body).unwrap(); | ||
Ok(res) | ||
} | ||
} | ||
|
||
pub struct MockedHttpClient {} | ||
|
||
#[async_trait(?Send)] | ||
impl MakeRequestBrowser for MockedHttpClient { | ||
async fn send( | ||
_parts: http::request::Parts, | ||
_body: SdkBody, | ||
) -> Result<http::Response<SdkBody>, JsValue> { | ||
let body = "{ | ||
\"Functions\": [ | ||
{ | ||
\"FunctionName\": \"function-name-1\" | ||
}, | ||
{ | ||
\"FunctionName\": \"function-name-2\" | ||
} | ||
], | ||
\"NextMarker\": null | ||
}"; | ||
let builder = http::Response::builder().status(200); | ||
let res = builder.body(SdkBody::from(body)).unwrap(); | ||
Ok(res) | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct Adapter { | ||
verbose: bool, | ||
use_mock: bool, | ||
} | ||
|
||
impl Adapter { | ||
fn new(verbose: bool, use_mock: bool) -> Self { | ||
Self { verbose, use_mock } | ||
} | ||
} | ||
|
||
impl tower::Service<http::Request<SdkBody>> for Adapter { | ||
type Response = http::Response<SdkBody>; | ||
|
||
type Error = ConnectorError; | ||
|
||
#[allow(clippy::type_complexity)] | ||
type Future = std::pin::Pin< | ||
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>, | ||
>; | ||
|
||
fn poll_ready( | ||
&mut self, | ||
_cx: &mut std::task::Context<'_>, | ||
) -> std::task::Poll<Result<(), Self::Error>> { | ||
std::task::Poll::Ready(Ok(())) | ||
} | ||
|
||
fn call(&mut self, req: http::Request<SdkBody>) -> Self::Future { | ||
let (parts, body) = req.into_parts(); | ||
let uri = parts.uri.to_string(); | ||
if self.verbose { | ||
log!("sending request to {}", uri); | ||
log!("http::Request parts: {:?}", parts); | ||
log!("http::Request body: {:?}", body); | ||
log!(""); | ||
} | ||
|
||
let (tx, rx) = tokio::sync::oneshot::channel(); | ||
|
||
log!("begin request..."); | ||
let use_mock = self.use_mock; | ||
wasm_bindgen_futures::spawn_local(async move { | ||
let fut = if use_mock { | ||
MockedHttpClient::send(parts, body) | ||
} else { | ||
BrowserHttpClient::send(parts, body) | ||
}; | ||
let _ = tx.send( | ||
fut.await | ||
.unwrap_or_else(|_| panic!("failure while making request to: {}", uri)), | ||
); | ||
}); | ||
|
||
Box::pin(async move { | ||
let response = rx.await.map_err(|e| ConnectorError::user(Box::new(e)))?; | ||
log!("response received"); | ||
Ok(response) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
Oops, something went wrong.