📜 ⬆️ ⬇️

Generalized programming techniques in Rust: how we translated Exonum from Iron to actix-web

Rust Ecosystem is still not fully settled. It often has new libraries that are noticeably better than its predecessors, and previously popular frameworks are becoming obsolete. This is exactly what happened with the Iron web framework, which we used when developing Exonum.

Actix-web was chosen to replace Iron. Then I will tell you how we have ported the existing code to a new solution, using generalized programming techniques.


Image of ulleo PD
')

How we used Iron


In Exonum, the Iron framework was used without any abstractions. We installed handlers for certain resources, request parameters were obtained by parsing URLs with auxiliary methods, and the result was returned simply as a string.

It looked like this:

fn set_blocks_response(self, router: &mut Router) { let blocks = move |req: &mut Request| -> IronResult<Response> { let count: usize = self.required_param(req, "count")?; let latest: Option<u64> = self.optional_param(req, "latest")?; let skip_empty_blocks: bool = self.optional_param(req, "skip_empty_blocks")? .unwrap_or(false); let info = self.blocks(count, latest.map(Height), skip_empty_blocks)?; self.ok_response(&::serde_json::to_value(info).unwrap()) }; router.get("/v1/blocks", blocks, "blocks"); } 

In addition, some middleware add-ons in the form of CORS headers were used. To combine all the handlers into a single API, we used mount.

Why had to refuse him


Iron was a good “workhorse” with a lot of extras. However, it was written in those days when projects such as futures and tokio did not exist.

The Iron architecture provides for synchronous processing of requests, so it was easy to fit on the blades with a large number of simultaneously open connections. For Iron to become scalable, it should be made asynchronous. For this it was necessary to rethink and rewrite the entire framework, but the developers gradually abandoned the work on it.

Why we switched to actix-web


This is a popular framework that ranks high on TechEmpower benchmarks . At the same time, unlike Iron, it is actively developing. Actix-web has a well-designed API and high-quality implementation based on the actix actor framework. Requests are processed asynchronously by the thread pool, and if processing causes panic, the actor is automatically restarted.

Of course, actix-web had drawbacks, for example, it contained a large amount of unsafe code. But later he was rewritten to Safe Rust, which solved this problem.

The transition to actix solved the problem with the stability of the work. Iron-backend could drop a large number of connections. In general, the new API is a simpler, more productive and unified solution. It will be easier for users and developers to use the software interface, and its speed will increase.

What do we want from a web framework


It was important for us not just to change Iron to actix-web, but to make the groundwork for the future - to work out a new API architecture for abstracting from a specific web framework. This will allow to create handlers, almost without thinking about web-specificity, and transfer them to any backend. This can be done by writing a frontend that would operate on basic types and types.

To understand what this frontend looks like, let's define what any HTTP API is:


If you analyze all the layers of abstraction, it turns out that any HTTP request is just a function call:

 fn request(context: &ServiceContext, query: Query) -> Result<Response, ServiceError> 

Everything else can be considered extensions of this basic entity. Thus, in order to abstract from a specific implementation of a web framework, we need to write handlers in a style similar to the example above.

Endpoint for generalized processing of HTTP requests

You can go the simplest and most straightforward way and declare a type Endpoint,
Describing the implementation of specific queries:

 // ,   GET .      //    ,      . //         . trait Endpoint: Sync + Send + 'static { type Request: DeserializeOwned + 'static; type Response: Serialize + 'static; fn handle(&self, context: &Context, request: Self::Request) -> Result<Self::Response, io::Error>; } 

After that, you will need to implement this handler in a specific framework. Let's say for actix-web it looks like this:

 //    actix-web.  ,   , //  `Endpoint`   . type FutureResponse = actix_web::FutureResponse<HttpResponse, actix_web::Error>; // «»    actix-web.      //   .     , //     . type RawHandler = dyn Fn(HttpRequest<Context>) -> FutureResponse + 'static + Send + Sync; //   ,     ,     . #[derive(Clone)] struct RequestHandler { ///  . pub name: String, /// HTTP . pub method: actix_web::http::Method, ///  .  ,       . pub inner: Arc<RawHandler>, } 

Structures can be used to pass request parameters through context. Actix-web is able to automatically de-serialize parameters using serde. For example, a = 15 & b = hello is deserialized into a structure of the following form:

 #[derive(Deserialize)] struct SimpleQuery { a: i32, b: String, } 

This agrees well with the associated Request type from the Endpoint type.

Now we’ll write an adapter that "wraps" the concrete Endpoint implementation in RequstHandler for actix-web. Please note that in the process information about Request and Response types is lost. This technique is called type erasure. Its task is to turn static dispatching into dynamic.

 impl RequestHandler { fn from_endpoint<E: Endpoint>(name: &str, endpoint: E) -> RequestHandler { let index = move |request: HttpRequest<Context>| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<E::Request>| query.into_inner()) .and_then(|query| endpoint.handle(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: name.to_owned(), method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } 

At this stage, you can add handlers for POST requests and stop, since we have created a type that abstracts implementation details. However, it is still not very ergonomic.

Type problems

When writing a handler, a lot of auxiliary code is generated:

 //    . struct ElementCountEndpoint { elements: Rc<RefCell<Vec<Something>>>, } //   Endpoint. impl Endpoint for ElementCountEndpoint { type Request = (); type Result = usize; fn handle(&self, context: &Context, _request: ()) -> Result<usize, io::Error> { Ok(self.elements.borrow().len()) } } //    . let endpoint = ElementCountEndpoint::new(elements.clone()); let handler = RequestHandler::from_endpoint("/v1/element_count", endpoint); actix_backend.endpoint(handler); 

Ideally, you want to be able to pass a normal closure as a handler, reducing the amount of syntactic noise by an order of magnitude:

 let elements = elements.clone(); actix_backend.endpoint("/v1/elements_count", move || {   Ok(elements.borrow().len()) }); 

How to do this, I will discuss further.

Easy immersion in generalized programming


We need to implement the ability to automatically generate an adapter that implements an Endpoint with the correct associated types. Only a closure with an HTTP request handler will be sent to the input.

Arguments and the result of the closure can be of different types, so you will have to work with overloading methods here. Rust does not support overloading directly, but allows it to be emulated using the Into and From types.

In addition, the return type of the closure does not have to match the return value of the Endpoint implementation. To manipulate this type, it must be extracted from the type of received circuit.

Fetching Types from Fn Type

In Rust, each closure has a unique type that cannot be explicitly written in the program. For manipulation of closures, there is a type Fn. It contains the signature of the function with the types of the arguments and the return value, but retrieving them separately is not so easy.

The basic idea is to use a supporting structure of the following form:

 ///       F: Fn(A) -> B. struct SimpleExtractor<A, B, F> {   //   .   inner: F,   _a: PhantomData<A>,   _b: PhantomData<B>, } 

We are forced to use PhantomData, since Rust requires that all the generalization parameters be in the definition of the structure. However, the specific type of closure or function F is not generalized (although it implements a generalized type of Fn). The type parameters A and B are not directly used in it.

It is this restriction of the Rust type system that does not allow using a simpler strategy - to implement the Endpoint type for closures directly:

 impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { type Request = A; type Response = B; fn handle(&self, context: &Context, request: A) -> Result<B, io::Error> { // ... } } 

The compiler in this case returns an error:

 error[E0207]: the type parameter `A` is not constrained by the impl trait, self type, or predicates --> src/main.rs:10:6 | 10 | impl<A, B, F> Endpoint for F where F: Fn(&Context, A) -> B { | ^ unconstrained type parameter 

The supporting structure of SimpleExtractor makes it possible to describe the From conversion. It allows you to save any function and extract the types of its arguments:

 impl<A, B, F> From<F> for SimpleExtractor<A, B, F> where F: Fn(&Context, A) -> B, A: DeserializeOwned, B: Serialize, { fn from(inner: F) -> Self { SimpleExtractor { inner, _a: PhantomData, _b: PhantomData, } } } 

The following code compiles successfully:

 #[derive(Deserialize)] struct Query { a: i32, b: String, }; //   . fn my_handler(_: &Context, q: Query) -> String { format!("{} has {} apples.", qb, qa) } let fn_extractor = SimpleExtractor::from(my_handler); //  . let c = 15; let my_closure = |_: &Context, q: Query| -> String { format!("{} has {} apples, but Alice has {}", qb, qa, c) }; let closure_extractor = SimpleExtractor::from(my_closure); 

Specialization and Marker Types

Now we have a function with explicitly parameterized argument types, suitable for use instead of Endpoint. For example, we can easily implement the conversion from SimpleExtractor to RequestHandler. Still, this is not a complete solution. You also need to somehow distinguish between GET request handlers and POST requests at the type level (and synchronous handlers from asynchronous). So-called marker types will help us with this.

To begin with, we will rewrite SimpleExtractor so that it can distinguish between synchronous and asynchronous results. At the same time we realize the type From for each of the cases. Please note that types can be implemented for specific variants of generalized structures.

 ///   HTTP-. pub struct With<Q, I, R, F> { ///  -. pub handler: F, ///     . _query_type: PhantomData<Q>, ///   . _item_type: PhantomData<I>, ///  ,  . ///  ,       . _result_type: PhantomData<R>, } //   ,   . impl<Q, I, F> From<F> for With<Q, I, Result<I>, F> where F: Fn(&ServiceApiState, Q) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } //     . impl<Q, I, F> From<F> for With<Q, I, FutureResult<I>, F> where F: Fn(&ServiceApiState, Q) -> FutureResult<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

Now we need to declare the structure in which to combine the request handler with its name and type:

 #[derive(Debug)] pub struct NamedWith<Q, I, R, F, K> {   ///  .   pub name: String,   ///    .   pub inner: With<Q, I, R, F>,   ///  .   _kind: PhantomData<K>, } 

After that, you can declare several empty structures that will act as marker types. Markers allow you to implement for each of the handlers your conversion code to the previously described RequestHandler.

 /// ,    .  HTTP   GET-. pub struct Immutable; /// ,   .  HTTP   POST, PUT, UPDATE ///    ,        POST. pub struct Mutable; 

Now we can define four different implementations of the type From For all combinations of template parameters R and K (the return value of the handler and the type of request).

 //     get . impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state(); let future = Query::from_request(&request, &()) .map(|query: Query<Q>| query.into_inner()) .and_then(|query| handler(context, query).map_err(From::from)) .and_then(|value| Ok(HttpResponse::Ok().json(value))) .into_future(); Box::new(future) }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } //     post . impl<Q, I, F> From<NamedWith<Q, I, Result<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> Result<I> + 'static + Send + Sync + Clone, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, Result<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } //     get . impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Immutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Immutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let context = request.state().clone(); let handler = handler.clone(); Query::from_request(&request, &()) .map(move |query: Query<Q>| query.into_inner()) .into_future() .and_then(move |query| handler(&context, query).map_err(From::from)) .map(|value| HttpResponse::Ok().json(value)) .responder() }; Self { name: f.name, method: actix_web::http::Method::GET, inner: Arc::from(index) as Arc<RawHandler>, } } } //     post . impl<Q, I, F> From<NamedWith<Q, I, FutureResult<I>, F, Mutable>> for RequestHandler where F: Fn(&ServiceApiState, Q) -> FutureResult<I> + 'static + Clone + Send + Sync, Q: DeserializeOwned + 'static, I: Serialize + 'static, { fn from(f: NamedWith<Q, I, FutureResult<I>, F, Mutable>) -> Self { let handler = f.inner.handler; let index = move |request: HttpRequest| -> FutureResponse { let handler = handler.clone(); let context = request.state().clone(); request .json() .from_err() .and_then(move |query: Q| { handler(&context, query) .map(|value| HttpResponse::Ok().json(value)) .map_err(From::from) }) .responder() }; Self { name: f.name, method: actix_web::http::Method::POST, inner: Arc::from(index) as Arc<RawHandler>, } } } 

"Facade" for backend

Now for all this it remains to write the "facade", which would take the circuit and add them to the corresponding backend. In our case, there is only one backend - actix-web - but behind the facade you can hide any additional implementations, for example, the generator of Swagger-specifications.

 pub struct ServiceApiScope { actix_backend: actix::ApiBuilder, } impl ServiceApiScope { ///    Immutable    . pub fn endpoint<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where //     ,      : Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, //  ,          //  NamedWith  RequestHandler. RequestHandler: From<NamedWith<Q, I, R, F, Immutable>>, { self.actix_backend.endpoint(name, endpoint); self } ///    Mutable . pub fn endpoint_mut<Q, I, R, F, E>(&mut self, name: &'static str, endpoint: E) -> &mut Self where Q: DeserializeOwned + 'static, I: Serialize + 'static, F: Fn(&ServiceApiState, Q) -> R + 'static + Clone, E: Into<With<Q, I, R, F>>, RequestHandler: From<NamedWith<Q, I, R, F, Mutable>>, { self.actix_backend.endpoint_mut(name, endpoint); self } 

Notice how the types of the query parameters, the type of its result, and the synchronicity / asynchrony of the handler are derived automatically from its signature. Additionally, you must explicitly specify the name of the request, as well as its type

Disadvantages approach


This approach still has its drawbacks. In particular, endpoint and endpoint_mut should know the specifics of the implementation of specific backends . This does not allow us to add backends on the fly, but this functionality is rarely needed.

Another problem is that you cannot define a specialization for a handler without additional arguments. In other words, if we write the following code, it will not compile because it conflicts with the existing generalized implementation:

 impl<(), I, F> From<F> for With<(), I, Result<I>, F> where F: Fn(&ServiceApiState) -> Result<I>, { fn from(handler: F) -> Self { Self { handler, _query_type: PhantomData, _item_type: PhantomData, _result_type: PhantomData, } } } 

As a result, requests that do not have parameters must still accept the JSON string null, which is deserialized into (). This problem could be solved by specialization in the C ++ style, but for now it is available only in the nightly-version of the compiler and it is not clear when it “stabilizes”.

Similarly, the type of return value cannot be specialized. Even if the request does not imply it, it will always give JSON with null.

Decoding the URL query in GET requests also imposes some unobvious restrictions on the type of parameters, but these are already features of the serde-urlencoded implementation.

Conclusion


Thus, we have implemented an API that allows you to simply and clearly create handlers, almost without thinking about web specifics. Later, they can be transferred to any backends or even use several backends at the same time.

Source: https://habr.com/ru/post/429450/


All Articles