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

feat(rust): add webassembly example #4325

Merged
merged 18 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion rust_dev_preview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,6 @@ members = [
"test-utils",
"testing",
"tls",
"transcribestreaming"
"transcribestreaming",
"webassembly"
]
5 changes: 5 additions & 0 deletions rust_dev_preview/webassembly/.cargo/config.toml
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"]
42 changes: 42 additions & 0 deletions rust_dev_preview/webassembly/Cargo.toml
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",
]
56 changes: 56 additions & 0 deletions rust_dev_preview/webassembly/README.md
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
282 changes: 282 additions & 0 deletions rust_dev_preview/webassembly/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
/*
DavidSouther marked this conversation as resolved.
Show resolved Hide resolved
* 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 {
DavidSouther marked this conversation as resolved.
Show resolved Hide resolved

/// 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)
})
}
}
2 changes: 2 additions & 0 deletions rust_dev_preview/webassembly/www/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
Loading