From 3ae22e26800b8c46ac6b70720b155100c7a91306 Mon Sep 17 00:00:00 2001 From: chesedo Date: Wed, 23 Nov 2022 12:18:49 +0200 Subject: [PATCH 1/4] feat: app codegen model --- Cargo.lock | 1 + codegen/Cargo.toml | 1 + codegen/src/lib.rs | 1 + codegen/src/next/mod.rs | 126 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 codegen/src/next/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 014ded2a6..ef6ccb1a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5726,6 +5726,7 @@ dependencies = [ name = "shuttle-codegen" version = "0.7.0" dependencies = [ + "http 0.2.8", "pretty_assertions", "proc-macro-error", "proc-macro2 1.0.43", diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index df1bb4d26..fb831981c 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -10,6 +10,7 @@ description = "Proc-macro code generator for the shuttle.rs service" proc-macro = true [dependencies] +http = "0.2.8" proc-macro-error = "1.0.4" proc-macro2 = "1.0.43" quote = "1.0.21" diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 3541c3c41..c96d2f96c 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -1,4 +1,5 @@ mod main; +mod next; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs new file mode 100644 index 000000000..5af617d58 --- /dev/null +++ b/codegen/src/next/mod.rs @@ -0,0 +1,126 @@ +use http::Method; +use quote::{quote, ToTokens}; +use syn::{Ident, LitStr}; + +struct Endpoint { + route: LitStr, + method: Method, + function: Ident, +} + +impl ToTokens for Endpoint { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { + route, + method, + function, + } = self; + + let method = match *method { + Method::GET => quote!(get), + Method::POST => quote!(post), + Method::DELETE => quote!(delete), + Method::PUT => quote!(put), + Method::OPTIONS => quote!(options), + Method::CONNECT => quote!(connect), + Method::HEAD => quote!(head), + Method::TRACE => quote!(trace), + Method::PATCH => quote!(patch), + _ => quote!(extension), + }; + + let route = quote!(.route(#route, #method(#function))); + + route.to_tokens(tokens); + } +} + +struct App { + endpoints: Vec, +} + +impl ToTokens for App { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { endpoints } = self; + + let app = quote!( + async fn app(request: Request) -> Response + where + B: HttpBody + Send + 'static, + { + let mut router = Router::new() + #(#endpoints)* + .into_service(); + + let response = router.call(request).await.unwrap(); + + response + } + ); + + app.to_tokens(tokens); + } +} + +#[cfg(test)] +mod tests { + use http::Method; + use pretty_assertions::assert_eq; + use quote::quote; + use syn::parse_quote; + + use crate::next::App; + + use super::Endpoint; + + #[test] + fn endpoint_to_token() { + let endpoint = Endpoint { + route: parse_quote!("/hello"), + method: Method::GET, + function: parse_quote!(hello), + }; + + let actual = quote!(#endpoint); + let expected = quote!(.route("/hello", get(hello))); + + assert_eq!(actual.to_string(), expected.to_string()); + } + + #[test] + fn app_to_token() { + let app = App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: Method::GET, + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: Method::POST, + function: parse_quote!(goodbye), + }, + ], + }; + + let actual = quote!(#app); + let expected = quote!( + async fn app(request: Request) -> Response + where + B: HttpBody + Send + 'static, + { + let mut router = Router::new() + .route("/hello", get(hello)) + .route("/goodbye", post(goodbye)) + .into_service(); + + let response = router.call(request).await.unwrap(); + + response + } + ); + + assert_eq!(actual.to_string(), expected.to_string()); + } +} From ffb604ed06874aa6f12b032532204e58251139a1 Mon Sep 17 00:00:00 2001 From: chesedo Date: Wed, 23 Nov 2022 12:37:26 +0200 Subject: [PATCH 2/4] refactor: qualify all namespaces --- codegen/src/next/mod.rs | 24 ++++++++++-------- tmp/axum-wasm/src/lib.rs | 54 +++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs index 5af617d58..4fa03813b 100644 --- a/codegen/src/next/mod.rs +++ b/codegen/src/next/mod.rs @@ -29,7 +29,7 @@ impl ToTokens for Endpoint { _ => quote!(extension), }; - let route = quote!(.route(#route, #method(#function))); + let route = quote!(.route(#route, axum::routing::#method(#function))); route.to_tokens(tokens); } @@ -44,11 +44,13 @@ impl ToTokens for App { let Self { endpoints } = self; let app = quote!( - async fn app(request: Request) -> Response + async fn app(request: http::Request) -> axum::response::Response where - B: HttpBody + Send + 'static, + B: axum::body::HttpBody + Send + 'static, { - let mut router = Router::new() + use tower_service::Service; + + let mut router = axum::Router::new() #(#endpoints)* .into_service(); @@ -82,7 +84,7 @@ mod tests { }; let actual = quote!(#endpoint); - let expected = quote!(.route("/hello", get(hello))); + let expected = quote!(.route("/hello", axum::routing::get(hello))); assert_eq!(actual.to_string(), expected.to_string()); } @@ -106,13 +108,15 @@ mod tests { let actual = quote!(#app); let expected = quote!( - async fn app(request: Request) -> Response + async fn app(request: http::Request) -> axum::response::Response where - B: HttpBody + Send + 'static, + B: axum::body::HttpBody + Send + 'static, { - let mut router = Router::new() - .route("/hello", get(hello)) - .route("/goodbye", post(goodbye)) + use tower_service::Service; + + let mut router = axum::Router::new() + .route("/hello", axum::routing::get(hello)) + .route("/goodbye", axum::routing::post(goodbye)) .into_service(); let response = router.call(request).await.unwrap(); diff --git a/tmp/axum-wasm/src/lib.rs b/tmp/axum-wasm/src/lib.rs index e0a8b6542..00c3778c2 100644 --- a/tmp/axum-wasm/src/lib.rs +++ b/tmp/axum-wasm/src/lib.rs @@ -1,30 +1,19 @@ -use axum::body::{Body, HttpBody}; -use axum::{response::Response, routing::get, Router}; -use futures_executor::block_on; -use http::Request; -use shuttle_common::wasm::{RequestWrapper, ResponseWrapper}; -use std::fs::File; -use std::io::BufReader; -use std::io::{Read, Write}; -use std::os::wasi::prelude::*; -use tower_service::Service; - -extern crate rmp_serde as rmps; - -pub fn handle_request(req: Request) -> Response +pub fn handle_request(req: http::Request) -> axum::response::Response where - B: HttpBody + Send + 'static, + B: axum::body::HttpBody + Send + 'static, { - block_on(app(req)) + futures_executor::block_on(app(req)) } -async fn app(request: Request) -> Response +async fn app(request: http::Request) -> axum::response::Response where - B: HttpBody + Send + 'static, + B: axum::body::HttpBody + Send + 'static, { - let mut router = Router::new() - .route("/hello", get(hello)) - .route("/goodbye", get(goodbye)) + use tower_service::Service; + + let mut router = axum::Router::new() + .route("/hello", axum::routing::get(hello)) + .route("/goodbye", axum::routing::get(goodbye)) .into_service(); let response = router.call(request).await.unwrap(); @@ -42,19 +31,26 @@ async fn goodbye() -> &'static str { #[no_mangle] #[allow(non_snake_case)] -pub extern "C" fn __SHUTTLE_Axum_call(fd_3: RawFd, fd_4: RawFd) { +pub extern "C" fn __SHUTTLE_Axum_call( + fd_3: std::os::wasi::prelude::RawFd, + fd_4: std::os::wasi::prelude::RawFd, +) { + use axum::body::HttpBody; + use std::io::{Read, Write}; + use std::os::wasi::io::FromRawFd; + println!("inner handler awoken; interacting with fd={fd_3},{fd_4}"); // file descriptor 3 for reading and writing http parts - let mut parts_fd = unsafe { File::from_raw_fd(fd_3) }; + let mut parts_fd = unsafe { std::fs::File::from_raw_fd(fd_3) }; - let reader = BufReader::new(&mut parts_fd); + let reader = std::io::BufReader::new(&mut parts_fd); // deserialize request parts from rust messagepack - let wrapper: RequestWrapper = rmps::from_read(reader).unwrap(); + let wrapper: shuttle_common::wasm::RequestWrapper = rmp_serde::from_read(reader).unwrap(); // file descriptor 4 for reading and writing http body - let mut body_fd = unsafe { File::from_raw_fd(fd_4) }; + let mut body_fd = unsafe { std::fs::File::from_raw_fd(fd_4) }; // read body from host let mut body_buf = Vec::new(); @@ -68,7 +64,7 @@ pub extern "C" fn __SHUTTLE_Axum_call(fd_3: RawFd, fd_4: RawFd) { } } - let request: Request = wrapper + let request: http::Request = wrapper .into_request_builder() .body(body_buf.into()) .unwrap(); @@ -79,13 +75,13 @@ pub extern "C" fn __SHUTTLE_Axum_call(fd_3: RawFd, fd_4: RawFd) { let (parts, mut body) = res.into_parts(); // wrap and serialize response parts as rmp - let response_parts = ResponseWrapper::from(parts).into_rmp(); + let response_parts = shuttle_common::wasm::ResponseWrapper::from(parts).into_rmp(); // write response parts parts_fd.write_all(&response_parts).unwrap(); // write body if there is one - if let Some(body) = block_on(body.data()) { + if let Some(body) = futures_executor::block_on(body.data()) { body_fd.write_all(body.unwrap().as_ref()).unwrap(); } // signal to the reader that end of file has been reached From 143b69031470b1106f73b2196b586ebdf3ac540c Mon Sep 17 00:00:00 2001 From: chesedo Date: Wed, 23 Nov 2022 12:55:38 +0200 Subject: [PATCH 3/4] feat: low-level wasi export fn --- codegen/src/next/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 3 deletions(-) diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs index 4fa03813b..0361ea6af 100644 --- a/codegen/src/next/mod.rs +++ b/codegen/src/next/mod.rs @@ -35,7 +35,7 @@ impl ToTokens for Endpoint { } } -struct App { +pub(crate) struct App { endpoints: Vec, } @@ -44,7 +44,7 @@ impl ToTokens for App { let Self { endpoints } = self; let app = quote!( - async fn app(request: http::Request) -> axum::response::Response + async fn __app(request: http::Request) -> axum::response::Response where B: axum::body::HttpBody + Send + 'static, { @@ -64,6 +64,71 @@ impl ToTokens for App { } } +pub(crate) fn wasi_bindings(app: App) -> proc_macro2::TokenStream { + quote!( + #app + + #[no_mangle] + #[allow(non_snake_case)] + pub extern "C" fn __SHUTTLE_Axum_call( + fd_3: std::os::wasi::prelude::RawFd, + fd_4: std::os::wasi::prelude::RawFd, + ) { + use axum::body::HttpBody; + use std::io::{Read, Write}; + use std::os::wasi::io::FromRawFd; + + println!("inner handler awoken; interacting with fd={fd_3},{fd_4}"); + + // file descriptor 3 for reading and writing http parts + let mut parts_fd = unsafe { std::fs::File::from_raw_fd(fd_3) }; + + let reader = std::io::BufReader::new(&mut parts_fd); + + // deserialize request parts from rust messagepack + let wrapper: shuttle_common::wasm::RequestWrapper = rmp_serde::from_read(reader).unwrap(); + + // file descriptor 4 for reading and writing http body + let mut body_fd = unsafe { std::fs::File::from_raw_fd(fd_4) }; + + // read body from host + let mut body_buf = Vec::new(); + let mut c_buf: [u8; 1] = [0; 1]; + loop { + body_fd.read(&mut c_buf).unwrap(); + if c_buf[0] == 0 { + break; + } else { + body_buf.push(c_buf[0]); + } + } + + let request: http::Request = wrapper + .into_request_builder() + .body(body_buf.into()) + .unwrap(); + + println!("inner router received request: {:?}", &request); + let res = futures_executor::block_on(__app(request)); + + let (parts, mut body) = res.into_parts(); + + // wrap and serialize response parts as rmp + let response_parts = shuttle_common::wasm::ResponseWrapper::from(parts).into_rmp(); + + // write response parts + parts_fd.write_all(&response_parts).unwrap(); + + // write body if there is one + if let Some(body) = futures_executor::block_on(body.data()) { + body_fd.write_all(body.unwrap().as_ref()).unwrap(); + } + // signal to the reader that end of file has been reached + body_fd.write(&[0]).unwrap(); + } + ) +} + #[cfg(test)] mod tests { use http::Method; @@ -108,7 +173,7 @@ mod tests { let actual = quote!(#app); let expected = quote!( - async fn app(request: http::Request) -> axum::response::Response + async fn __app(request: http::Request) -> axum::response::Response where B: axum::body::HttpBody + Send + 'static, { From 6e8af7205155a08c6fc18d8361e2c4257aef2495 Mon Sep 17 00:00:00 2001 From: chesedo Date: Wed, 23 Nov 2022 15:32:55 +0200 Subject: [PATCH 4/4] refactor: restrict to supported axum methods --- Cargo.lock | 1 - codegen/Cargo.toml | 1 - codegen/src/next/mod.rs | 31 ++++++++++++++----------------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef6ccb1a5..014ded2a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5726,7 +5726,6 @@ dependencies = [ name = "shuttle-codegen" version = "0.7.0" dependencies = [ - "http 0.2.8", "pretty_assertions", "proc-macro-error", "proc-macro2 1.0.43", diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index fb831981c..df1bb4d26 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -10,7 +10,6 @@ description = "Proc-macro code generator for the shuttle.rs service" proc-macro = true [dependencies] -http = "0.2.8" proc-macro-error = "1.0.4" proc-macro2 = "1.0.43" quote = "1.0.21" diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs index 0361ea6af..f996cfed3 100644 --- a/codegen/src/next/mod.rs +++ b/codegen/src/next/mod.rs @@ -1,10 +1,10 @@ -use http::Method; +use proc_macro_error::emit_error; use quote::{quote, ToTokens}; use syn::{Ident, LitStr}; struct Endpoint { route: LitStr, - method: Method, + method: Ident, function: Ident, } @@ -16,17 +16,15 @@ impl ToTokens for Endpoint { function, } = self; - let method = match *method { - Method::GET => quote!(get), - Method::POST => quote!(post), - Method::DELETE => quote!(delete), - Method::PUT => quote!(put), - Method::OPTIONS => quote!(options), - Method::CONNECT => quote!(connect), - Method::HEAD => quote!(head), - Method::TRACE => quote!(trace), - Method::PATCH => quote!(patch), - _ => quote!(extension), + match method.to_string().as_str() { + "get" | "post" | "delete" | "put" | "options" | "head" | "trace" | "patch" => {} + _ => { + emit_error!( + method, + "method is not supported"; + hint = "Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch`" + ) + } }; let route = quote!(.route(#route, axum::routing::#method(#function))); @@ -131,7 +129,6 @@ pub(crate) fn wasi_bindings(app: App) -> proc_macro2::TokenStream { #[cfg(test)] mod tests { - use http::Method; use pretty_assertions::assert_eq; use quote::quote; use syn::parse_quote; @@ -144,7 +141,7 @@ mod tests { fn endpoint_to_token() { let endpoint = Endpoint { route: parse_quote!("/hello"), - method: Method::GET, + method: parse_quote!(get), function: parse_quote!(hello), }; @@ -160,12 +157,12 @@ mod tests { endpoints: vec![ Endpoint { route: parse_quote!("/hello"), - method: Method::GET, + method: parse_quote!(get), function: parse_quote!(hello), }, Endpoint { route: parse_quote!("/goodbye"), - method: Method::POST, + method: parse_quote!(post), function: parse_quote!(goodbye), }, ],