├── book ├── .gitignore ├── src │ ├── motivation.md │ ├── images │ │ └── norpc-stack.png │ ├── SUMMARY.md │ ├── architecture.md │ ├── runtime.md │ ├── compiler.md │ ├── previous-work.md │ └── message-passsing.md └── book.toml ├── Cargo.toml ├── .github ├── actions │ └── build │ │ └── Dockerfile └── workflows │ ├── ci.yml │ └── book.yml ├── examples ├── src │ ├── lib.rs │ ├── no_runtime.rs │ ├── panic.rs │ ├── async_std_runtime.rs │ ├── hello_world.rs │ ├── client_drop.rs │ ├── rate_limit.rs │ ├── kvstore.rs │ └── concurrency.rs └── Cargo.toml ├── .gitignore ├── norpc-macros ├── Cargo.toml └── src │ ├── lib.rs │ └── generator.rs ├── benchmark ├── noop-tarpc │ ├── Cargo.toml │ └── benches │ │ └── noop_tarpc.rs └── noop │ ├── Cargo.toml │ └── benches │ └── noop.rs ├── norpc ├── Cargo.toml └── src │ ├── lib.rs │ └── runtime │ └── mod.rs ├── LICENSE └── README.md /book/.gitignore: -------------------------------------------------------------------------------- 1 | book -------------------------------------------------------------------------------- /book/src/motivation.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | In this chapter, 4 | I will tell you why I started this project. -------------------------------------------------------------------------------- /book/src/images/norpc-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akiradeveloper/norpc/HEAD/book/src/images/norpc-stack.png -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Akira Hayakawa"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "norpc" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "norpc", 4 | "norpc-macros", 5 | "examples", 6 | "benchmark/noop", 7 | "benchmark/noop-tarpc", 8 | ] -------------------------------------------------------------------------------- /.github/actions/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | RUN cargo install mdbook --no-default-features --features output --vers "^0.3.5" 3 | CMD ["mdbook", "build", "book"] -------------------------------------------------------------------------------- /examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod async_std_runtime; 2 | mod client_drop; 3 | mod concurrency; 4 | mod hello_world; 5 | mod kvstore; 6 | mod no_runtime; 7 | mod panic; 8 | mod rate_limit; 9 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Motivation](motivation.md) 4 | - [Message Passing](message-passsing.md) 5 | - [Previous Work](previous-work.md) 6 | - [Architecture](architecture.md) 7 | - [Compiler](compiler.md) 8 | - [Runtime](runtime.md) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'book' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: test 14 | run: cargo test 15 | - name: bench 16 | run: cargo bench -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk -------------------------------------------------------------------------------- /norpc-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "norpc-macros" 3 | authors = ["Akira Hayakawa "] 4 | version = "0.9.1" 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Code generator for norpc" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | itertools = "0.10" 14 | proc-macro2 = "1" 15 | quote = "1" 16 | syn = { version = "1", features = ["full"] } -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | norpc = { path = "../norpc", features = ["runtime", "tokio-executor", "async-std-executor"] } 10 | 11 | async-std = { version = "*" } 12 | futures = "*" 13 | rand = "0.8" 14 | tokio = { version = "*", features = ["full"] } 15 | tower = { version = "*", features = ["full"] } -------------------------------------------------------------------------------- /benchmark/noop-tarpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noop-tarpc" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | tarpc = { version = "0.27", features = ["tokio1"] } 10 | tokio = { version = "*", features = ["full"] } 11 | 12 | [dev-dependencies] 13 | criterion = { version = "0.3.5", features = ["async_tokio"] } 14 | 15 | [[bench]] 16 | name = "noop_tarpc" 17 | harness = false -------------------------------------------------------------------------------- /examples/src/no_runtime.rs: -------------------------------------------------------------------------------- 1 | /// This test is to prove the the code generation is self-contained. 2 | 3 | #[norpc::service] 4 | trait Add { 5 | fn add(x: u64, y: u64) -> u64; 6 | } 7 | struct AddApp; 8 | #[norpc::async_trait] 9 | impl Add for AddApp { 10 | async fn add(&self, x: u64, y: u64) -> u64 { 11 | x + y 12 | } 13 | } 14 | #[tokio::test] 15 | async fn test_no_runtime() { 16 | let mut cli = AddClient::new(AddService::new(AddApp)); 17 | let r = cli.add(1, 2).await; 18 | assert_eq!(r, 3); 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | on: 3 | push: 4 | branches: 5 | - book 6 | paths: 7 | - "book/src/**" 8 | - "book/book.toml" 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Build 16 | uses: ./.github/actions/build 17 | - name: Deploy 18 | uses: peaceiris/actions-gh-pages@v3 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | PUBLISH_BRANCH: gh-pages 22 | PUBLISH_DIR: ./book/book -------------------------------------------------------------------------------- /examples/src/panic.rs: -------------------------------------------------------------------------------- 1 | #[norpc::service] 2 | trait Panic { 3 | fn panic(); 4 | } 5 | struct App; 6 | #[norpc::async_trait] 7 | impl Panic for App { 8 | async fn panic(&self) { 9 | panic!("I am panicked!"); 10 | } 11 | } 12 | #[tokio::test] 13 | #[should_panic] 14 | async fn test_panic() { 15 | use norpc::runtime::*; 16 | 17 | let app = App; 18 | let service = PanicService::new(app); 19 | let (chan, server) = ServerBuilder::new(service).build(); 20 | ::tokio::spawn(server.serve(TokioExecutor)); 21 | 22 | let mut cli = PanicClient::new(chan); 23 | cli.panic().await; 24 | } 25 | -------------------------------------------------------------------------------- /book/src/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ![](images/norpc-stack.png) 4 | 5 | norpc utilizes Tower ecosystem. 6 | 7 | The core of the Tower ecosystem is an abstraction called `Service` which is like a function from request to response. 8 | The ecosystem has many decorator stacks to add new behavior to an existing `Service`. 9 | 10 | In the diagram, client requests are coming from the top-left of the stacks and flow down to the bottom-right. 11 | The client and server is connected by async channel driven by some async runtime (like Tokio) so there is no overhead for 12 | the serialization and copying because the messages just "move". 13 | 14 | -------------------------------------------------------------------------------- /benchmark/noop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noop-bench" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | norpc = { path = "../../norpc", features = ["runtime", "tokio-executor"] } 10 | 11 | futures = "*" 12 | flume = "*" 13 | tokio = { version = "*", features = ["full"] } 14 | tower = { version = "*", features = ["full"] } 15 | 16 | [dev-dependencies] 17 | # https://github.com/bheisler/criterion.rs/blob/master/book/src/user_guide/benchmarking_async.md 18 | criterion = { version = "0.3.5", features = ["async_tokio"] } 19 | 20 | [[bench]] 21 | name = "noop" 22 | harness = false -------------------------------------------------------------------------------- /examples/src/async_std_runtime.rs: -------------------------------------------------------------------------------- 1 | #[norpc::service] 2 | trait HelloWorld { 3 | fn hello(s: String) -> String; 4 | } 5 | struct HelloWorldApp; 6 | #[norpc::async_trait] 7 | impl HelloWorld for HelloWorldApp { 8 | async fn hello(&self, s: String) -> String { 9 | format!("Hello, {}", s) 10 | } 11 | } 12 | #[test] 13 | fn test_async_std_runtime() { 14 | async_std::task::block_on(async { 15 | use norpc::runtime::*; 16 | let app = HelloWorldApp; 17 | let builder = ServerBuilder::new(HelloWorldService::new(app)); 18 | let (chan, server) = builder.build(); 19 | ::async_std::task::spawn(server.serve(AsyncStdExecutor)); 20 | let mut cli = HelloWorldClient::new(chan); 21 | assert_eq!(cli.hello("World".to_owned()).await, "Hello, World"); 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /norpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "norpc" 3 | version = "0.9.1" 4 | authors = ["Akira Hayakawa "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Framework for in-process microservices" 8 | repository = "https://github.com/akiradeveloper/norpc" 9 | readme = "../README.md" 10 | categories = ["concurrency"] 11 | keywords = ["rpc", "microservices", "async"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | norpc-macros = { path = "../norpc-macros", version = "0.9.1" } 17 | 18 | anyhow = "1" 19 | async-trait = "0.1" 20 | flume = "0.10" 21 | futures = "0.3" 22 | tower-service = "0.3" 23 | 24 | tokio = { version = "1", features = ["sync", "rt"], optional = true } 25 | async-std = { version = "1", optional = true } 26 | 27 | [dev-dependencies] 28 | tokio-test = "0.4" 29 | 30 | [features] 31 | runtime = [] 32 | tokio-executor = ["tokio"] 33 | async-std-executor = ["async-std"] 34 | 35 | [package.metadata.docs.rs] 36 | all-features = true 37 | rustdoc-args = ["--cfg", "docsrs"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Akira Hayakawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/src/hello_world.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | #[norpc::service] 4 | trait HelloWorld { 5 | fn hello(s: String) -> String; 6 | } 7 | struct HelloWorldApp; 8 | #[norpc::async_trait] 9 | impl HelloWorld for HelloWorldApp { 10 | async fn hello(&self, s: String) -> String { 11 | format!("Hello, {}", s) 12 | } 13 | } 14 | #[tokio::test(flavor = "multi_thread")] 15 | async fn test_hello_world() { 16 | use norpc::runtime::*; 17 | let app = HelloWorldApp; 18 | let builder = ServerBuilder::new(HelloWorldService::new(app)); 19 | let (chan, server) = builder.build(); 20 | ::tokio::spawn(server.serve(TokioExecutor)); 21 | let mut cli = HelloWorldClient::new(chan); 22 | assert_eq!(cli.hello("World".to_owned()).await, "Hello, World"); 23 | } 24 | 25 | #[norpc::service(?Send)] 26 | trait HelloWorldLocal { 27 | // Rc is !Send 28 | fn hello(s: Rc) -> Rc; 29 | } 30 | struct HelloWorldLocalApp; 31 | #[norpc::async_trait(?Send)] 32 | impl HelloWorldLocal for HelloWorldLocalApp { 33 | async fn hello(&self, s: Rc) -> Rc { 34 | format!("Hello, {}", s).into() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /book/src/runtime.md: -------------------------------------------------------------------------------- 1 | # Runtime 2 | 3 | ![](images/norpc-stack.png) 4 | 5 | The yellow and blue parts are called "norpc runtime". 6 | 7 | As mentioned in the earlier section, 8 | the code generated by the compiler (in red) are runtime-agnostic. 9 | 10 | To run the service on a runtime, you need to implement 11 | the trait generated by the compiler. 12 | 13 | ```rust 14 | struct HelloWorldApp; 15 | #[async_trait::async_trait] 16 | impl HelloWorld for HelloWorldApp { 17 | async fn hello(&self, s: String) -> String { 18 | format!("Hello, {}", s) 19 | } 20 | } 21 | ``` 22 | 23 | And then you can build a server and a channel. 24 | After spawning the server on a async runtime, 25 | you can send requests to the server through the channel. 26 | Note that the server is async runtime agnostic so you can 27 | choose any async runtime to execute the server. 28 | 29 | ```rust 30 | use norpc::runtime::*; 31 | let app = HelloWorldApp; 32 | let builder = ServerBuilder::new(HelloWorldService::new(app)); 33 | let (chan, server) = builder.build(); 34 | 35 | tokio::spawn(server.serve(TokioExecutor)); 36 | 37 | let mut cli = HelloWorldClient::new(chan); 38 | assert_eq!(cli.hello("World".to_owned()).await, "Hello, World"); 39 | ``` -------------------------------------------------------------------------------- /book/src/compiler.md: -------------------------------------------------------------------------------- 1 | # Compiler 2 | 3 | ![](images/norpc-stack.png) 4 | 5 | The stacks in red are generated by the compiler which is implemented 6 | by the procedural macros. 7 | 8 | You can define your service by `norpc::service` macro which generates 9 | code in compile time. 10 | The generated code includes message type, client type 11 | and server-side `Service` type. 12 | 13 | ```rust 14 | #[norpc::service] 15 | trait HelloWorld { 16 | fn hello(s: String) -> String; 17 | } 18 | ``` 19 | 20 | Note that these types are runtime-agnostic which means 21 | you can write your own norpc runtime to run these codes for a specific case. 22 | 23 | ## More Examples 24 | 25 | ### Option or Result 26 | 27 | You can return `Option` or `Result` types to 28 | propagate some failure back to the client. 29 | 30 | ```rust 31 | #[norpc::service] 32 | trait YourService { 33 | fn read(id: u64) -> Option; 34 | fn write(id: u64, b: Bytes) -> Result; 35 | } 36 | ``` 37 | 38 | ### Non-Send 39 | 40 | You can generate non-Send service by add `?Send` parameter to `norpc::service` macro. 41 | 42 | This is useful when you want to run the service in pinned thread. 43 | 44 | ```rust 45 | #[norpc::service(?Send)] 46 | trait YourService { 47 | // Rc is !Send 48 | fn echo(s: Rc) -> Rc; 49 | } 50 | ``` -------------------------------------------------------------------------------- /norpc/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(docsrs, feature(doc_cfg))] 2 | 3 | //! norpc is a library to implement in-process microservices. 4 | //! 5 | //! ``` 6 | //! #[norpc::service] 7 | //! trait HelloWorld { 8 | //! fn hello(s: String) -> String; 9 | //! } 10 | //! struct HelloWorldApp; 11 | //! #[async_trait::async_trait] 12 | //! impl HelloWorld for HelloWorldApp { 13 | //! async fn hello(&self, s: String) -> String { 14 | //! format!("Hello, {}", s) 15 | //! } 16 | //! } 17 | //! let rep = tokio_test::block_on(async { 18 | //! use norpc::runtime::*; 19 | //! let app = HelloWorldApp; 20 | //! let svc = HelloWorldService::new(app); 21 | //! let (chan, server) = ServerBuilder::new(svc).build(); 22 | //! ::tokio::spawn(server.serve(TokioExecutor)); 23 | //! let mut cli = HelloWorldClient::new(chan); 24 | //! cli.hello("World".to_owned()).await 25 | //! }); 26 | //! assert_eq!(rep, "Hello, World"); 27 | //! ``` 28 | 29 | #[doc(hidden)] 30 | pub use async_trait::async_trait; 31 | #[doc(hidden)] 32 | pub use futures::future::poll_fn; 33 | #[doc(hidden)] 34 | pub use tower_service::Service; 35 | 36 | /// Macro for code-generation. 37 | pub use norpc_macros::service; 38 | 39 | #[cfg(feature = "runtime")] 40 | #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] 41 | /// Runtime implementation. 42 | pub mod runtime; 43 | -------------------------------------------------------------------------------- /examples/src/client_drop.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use tokio::sync::Semaphore; 3 | 4 | #[norpc::service] 5 | trait Loop { 6 | fn inf_loop(); 7 | fn noop(); 8 | } 9 | struct LoopApp { 10 | sem: Semaphore, 11 | } 12 | #[norpc::async_trait] 13 | impl Loop for LoopApp { 14 | async fn inf_loop(&self) { 15 | let _tok = self.sem.acquire().await; 16 | loop { 17 | tokio::time::sleep(Duration::from_millis(1)).await; 18 | } 19 | } 20 | async fn noop(&self) { 21 | let _tok = self.sem.acquire().await; 22 | } 23 | } 24 | 25 | #[tokio::test(flavor = "multi_thread")] 26 | async fn test_client_drop() { 27 | use norpc::runtime::*; 28 | 29 | let app = LoopApp { 30 | sem: Semaphore::new(1), 31 | }; 32 | let builder = ServerBuilder::new(LoopService::new(app)); 33 | let (chan, server) = builder.build(); 34 | ::tokio::spawn(server.serve(TokioExecutor)); 35 | 36 | let mut cli1 = LoopClient::new(chan.clone()); 37 | let hdl1 = ::tokio::spawn(async move { 38 | cli1.inf_loop().await; 39 | }); 40 | 41 | ::tokio::time::sleep(Duration::from_secs(1)).await; 42 | 43 | let mut cli2 = LoopClient::new(chan); 44 | let hdl2 = ::tokio::spawn(async move { 45 | cli2.noop().await; 46 | }); 47 | 48 | // Comment out this line ends up server looping forever. 49 | hdl1.abort(); 50 | 51 | hdl2.await.unwrap(); 52 | } 53 | -------------------------------------------------------------------------------- /benchmark/noop-tarpc/benches/noop_tarpc.rs: -------------------------------------------------------------------------------- 1 | use tarpc::{client, context, server}; 2 | 3 | #[tarpc::service] 4 | trait Noop { 5 | async fn noop() -> (); 6 | } 7 | #[derive(Clone)] 8 | struct NoopApp; 9 | #[tarpc::server] 10 | impl Noop for NoopApp { 11 | async fn noop(self, _: context::Context) -> () {} 12 | } 13 | 14 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 15 | 16 | fn bench_noop_tarpc(c: &mut Criterion) { 17 | let rt = tokio::runtime::Builder::new_multi_thread() 18 | .enable_all() 19 | .build() 20 | .unwrap(); 21 | // This enables tokio::xxx access the default runtime. 22 | let _guard = rt.enter(); 23 | let (tx, rx) = tarpc::transport::channel::unbounded(); 24 | tokio::spawn({ 25 | use tarpc::server::Channel; 26 | let server = server::BaseChannel::new(server::Config::default(), rx); 27 | let service = NoopApp; 28 | server.requests().execute(service.serve()) 29 | }); 30 | let cli = NoopClient::new(client::Config::default(), tx).spawn(); 31 | c.bench_with_input( 32 | BenchmarkId::new("noop request (tarpc)", 1), 33 | &cli, 34 | |b, cli| { 35 | let ctx = context::current(); 36 | b.to_async(&rt).iter(|| async { 37 | cli.noop(ctx).await.unwrap(); 38 | }); 39 | }, 40 | ); 41 | } 42 | 43 | criterion_group!(noop_tarpc, bench_noop_tarpc); 44 | criterion_main!(noop_tarpc); 45 | -------------------------------------------------------------------------------- /examples/src/rate_limit.rs: -------------------------------------------------------------------------------- 1 | use tower::util::BoxCloneService; 2 | use tower::ServiceBuilder; 3 | 4 | const N: usize = 10000; 5 | 6 | #[norpc::service] 7 | trait RateLimit { 8 | fn noop(); 9 | } 10 | 11 | struct RateLimitApp; 12 | #[norpc::async_trait] 13 | impl RateLimit for RateLimitApp { 14 | async fn noop(&self) {} 15 | } 16 | struct ServiceHolder { 17 | chan: BoxCloneService, 18 | } 19 | #[tokio::test(flavor = "multi_thread")] 20 | async fn test_rate_limit() { 21 | use norpc::runtime::*; 22 | let app = RateLimitApp; 23 | let service = RateLimitService::new(app); 24 | let service = ServiceBuilder::new() 25 | .rate_limit(5000, std::time::Duration::from_secs(1)) 26 | .service(service); 27 | let builder = ServerBuilder::new(service); 28 | let (chan, server) = builder.build(); 29 | ::tokio::spawn(server.serve(TokioExecutor)); 30 | let chan = ServiceBuilder::new() 31 | .buffer(1) 32 | .rate_limit(1000, std::time::Duration::from_secs(1)) 33 | .service(chan); 34 | let chan = BoxCloneService::new(chan); 35 | // This move means nothing but to check if holding the boxed service in struct works. 36 | let holder = ServiceHolder { chan }; 37 | let cli = RateLimitClient::new(holder.chan); 38 | for _ in 0..N { 39 | // This can be commented out but to make sure thet the client is cloneable. 40 | let mut cli = cli.clone(); 41 | cli.noop().await; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /book/src/previous-work.md: -------------------------------------------------------------------------------- 1 | # Previous Work 2 | 3 | [Tarpc](https://github.com/google/tarpc) is a previous work in this area. 4 | 5 | You can define your service by trait and the macro `tarpc::service` generates all the rest of the codes. 6 | 7 | ```rust 8 | #[tarpc::service] 9 | pub trait World { 10 | async fn hello(name: String) -> String; 11 | } 12 | ``` 13 | 14 | ## Problems 15 | 16 | However, I found two problems in Tarpc. 17 | 18 | ### 1. Tarpc isn't optimized for in-memory channel 19 | 20 | The goal of tarpc is providing RPC through TCP channel 21 | and the direct competitor is RPC framework like [Tonic](https://github.com/hyperium/tonic) 22 | or [jsonrpc](https://github.com/paritytech/jsonrpc). 23 | 24 | Tarpc only allows to use in-memory channel under the same abstraction 25 | so the implementation isn't optimized for in-memory channel. 26 | 27 | ### 2. Tarpc doesn't use Tower 28 | 29 | [Tower](https://github.com/tower-rs/tower) is a framework like 30 | Scala's [Finagle](https://twitter.github.io/finagle/) 31 | which provides a abstraction called `Service` which is like a function from request to response 32 | and decorator stacks to add more functionality on top of the abstraction. 33 | 34 | If we design a RPC framework from scratch with the current Rust ecosystem, 35 | we will 100% choose to depend on Tower to implement 36 | functionalities like rate-limiting or timeout which is essential in doing RPC. 37 | In fact, Tonic does so. 38 | 39 | However, Tarpc's started a long ago before the current Rust ecosystem is established 40 | and it doesn't use Tower but implements those functionalities by itself. -------------------------------------------------------------------------------- /benchmark/noop/benches/noop.rs: -------------------------------------------------------------------------------- 1 | #[norpc::service] 2 | trait Noop { 3 | fn noop(); 4 | } 5 | 6 | struct NoopApp; 7 | #[norpc::async_trait] 8 | impl Noop for NoopApp { 9 | async fn noop(&self) { 10 | // tokio::time::sleep(std::time::Duration::from_millis(10)).await; 11 | } 12 | } 13 | 14 | use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 15 | 16 | fn bench_noop(c: &mut Criterion) { 17 | use norpc::runtime::*; 18 | let rt = ::tokio::runtime::Builder::new_multi_thread() 19 | .enable_all() 20 | .build() 21 | .unwrap(); 22 | let app = NoopApp; 23 | let service = NoopService::new(app); 24 | let (chan, server) = ServerBuilder::new(service).build(); 25 | rt.spawn(server.serve(TokioExecutor)); 26 | 27 | let cli = NoopClient::new(chan); 28 | c.bench_with_input(BenchmarkId::new("noop request (norpc)", 1), &cli, |b, cli| { 29 | b.to_async(&rt).iter(|| async { 30 | let mut cli = cli.clone(); 31 | cli.noop().await; 32 | }); 33 | }); 34 | } 35 | 36 | fn bench_channel(c: &mut Criterion) { 37 | use futures::stream::StreamExt; 38 | let rt = tokio::runtime::Builder::new_multi_thread() 39 | .enable_all() 40 | .build() 41 | .unwrap(); 42 | let (tx, rx) = flume::unbounded(); 43 | let mut st = rx.into_stream(); 44 | rt.spawn(async move { while let Some(()) = st.next().await {} }); 45 | c.bench_function("noop channel", |b| { 46 | b.to_async(&rt).iter(|| async { 47 | tx.send(()).unwrap(); 48 | }) 49 | }); 50 | } 51 | 52 | criterion_group!(noop, bench_noop, bench_channel); 53 | criterion_main!(noop); 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # norpc = not remote procedure call 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/norpc.svg)](https://crates.io/crates/norpc) 4 | [![documentation](https://docs.rs/norpc/badge.svg)](https://docs.rs/norpc) 5 | ![CI](https://github.com/akiradeveloper/norpc/workflows/CI/badge.svg) 6 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/akiradeveloper/norpc/blob/master/LICENSE) 7 | [![Tokei](https://tokei.rs/b1/github/akiradeveloper/norpc)](https://github.com/akiradeveloper/norpc) 8 | 9 | [Documentation](https://akiradeveloper.github.io/norpc/) 10 | 11 | ## Example 12 | 13 | ```rust 14 | #[norpc::service] 15 | trait HelloWorld { 16 | fn hello(s: String) -> String; 17 | } 18 | struct HelloWorldApp; 19 | #[async_trait::async_trait] 20 | impl HelloWorld for HelloWorldApp { 21 | async fn hello(&self, s: String) -> String { 22 | format!("Hello, {}", s) 23 | } 24 | } 25 | let rep = tokio_test::block_on(async { 26 | use norpc::runtime::*; 27 | let app = HelloWorldApp; 28 | let svc = HelloWorldService::new(app); 29 | let (chan, server) = ServerBuilder::new(svc).build(); 30 | tokio::spawn(server.serve(TokioExecutor)); 31 | let mut cli = HelloWorldClient::new(chan); 32 | cli.hello("World".to_owned()).await 33 | }); 34 | assert_eq!(rep, "Hello, World"); 35 | ``` 36 | 37 | ## Usage 38 | 39 | ``` 40 | norpc = { version = "0.9", features = ["runtime", "tokio-executor"] } 41 | ``` 42 | 43 | - runtime: Use norpc runtime 44 | - tokio-executor: Use tokio as async runtime. 45 | - async-std-executor: Use async-std as async runtime. 46 | 47 | ## Features 48 | 49 | - Support in-process microservices through async channel. 50 | - Async runtime agnostic. 51 | - Support non-`Send` types. 52 | - Support request cancellation from client. 53 | 54 | ## Performance 55 | 56 | norpc is about 2x faster than [google/tarpc](https://github.com/google/tarpc). 57 | 58 | To compare the pure overhead, he benchmark program launches 59 | a no-op server and send requests from the client. 60 | 61 | ``` 62 | noop request (norpc)/1 time: [8.9181 us 8.9571 us 9.0167 us] 63 | noop request (tarpc)/1 time: [15.476 us 15.514 us 15.554 us] 64 | ``` 65 | 66 | ## Author 67 | 68 | Akira Hayakawa (@akiradeveloper) 69 | -------------------------------------------------------------------------------- /examples/src/kvstore.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use tokio::sync::RwLock; 3 | 4 | #[norpc::service] 5 | trait KVStore { 6 | fn read(id: u64) -> Option; 7 | fn write(id: u64, s: String) -> (); 8 | fn write_many(kv: std::collections::HashSet<(u64, std::string::String)>); 9 | fn list() -> Vec<(u64, std::string::String)>; 10 | fn ret_any_tuple() -> (u8, u8); 11 | // We can return a result from app to the client. 12 | fn noop() -> std::result::Result; 13 | } 14 | 15 | struct KVStoreApp { 16 | state: RwLock>, 17 | } 18 | impl KVStoreApp { 19 | fn new() -> Self { 20 | Self { 21 | state: RwLock::new(HashMap::new()), 22 | } 23 | } 24 | } 25 | #[norpc::async_trait] 26 | impl KVStore for KVStoreApp { 27 | async fn read(&self, id: u64) -> Option { 28 | self.state.read().await.get(&id).cloned() 29 | } 30 | async fn write(&self, id: u64, v: String) { 31 | self.state.write().await.insert(id, v); 32 | } 33 | async fn write_many(&self, kv: HashSet<(u64, String)>) { 34 | for (k, v) in kv { 35 | self.state.write().await.insert(k, v); 36 | } 37 | } 38 | async fn list(&self) -> Vec<(u64, String)> { 39 | let mut out = vec![]; 40 | let reader = self.state.read().await; 41 | for (k, v) in reader.iter() { 42 | out.push((*k, v.clone())); 43 | } 44 | out 45 | } 46 | async fn ret_any_tuple(&self) -> (u8, u8) { 47 | (0, 0) 48 | } 49 | async fn noop(&self) -> Result { 50 | Ok(true) 51 | } 52 | } 53 | #[tokio::test(flavor = "multi_thread")] 54 | async fn test_kvstore() { 55 | use norpc::runtime::*; 56 | 57 | let app = KVStoreApp::new(); 58 | let service = KVStoreService::new(app); 59 | let (chan, server) = ServerBuilder::new(service).build(); 60 | ::tokio::spawn(server.serve(TokioExecutor)); 61 | 62 | let mut cli = KVStoreClient::new(chan); 63 | assert_eq!(cli.read(1).await, None); 64 | cli.write(1, "one".to_owned()).await; 65 | assert_eq!(cli.read(1).await, Some("one".to_owned())); 66 | assert_eq!(cli.read(2).await, None); 67 | assert_eq!(cli.read(3).await, None); 68 | 69 | let mut cli2 = cli.clone(); 70 | let mut h = HashSet::new(); 71 | h.insert((2, "two".to_owned())); 72 | h.insert((3, "three".to_owned())); 73 | cli2.write_many(h).await; 74 | assert_eq!(cli2.read(3).await, Some("three".to_owned())); 75 | assert_eq!(cli2.noop().await, Ok(true)); 76 | } 77 | -------------------------------------------------------------------------------- /book/src/message-passsing.md: -------------------------------------------------------------------------------- 1 | # Message Passing 2 | 3 | To share state between async processes, these two possible solutions can be considered 4 | 5 | 1. Shared memory 6 | 2. Message passing 7 | 8 | The benefit of message passing is 9 | the processes are isolated and only communicated using defined messages. 10 | Each process typically holds some resources like storage or connection to external service as a sole owner 11 | and encapsulates the direct access to the resource from other processes. 12 | This makes developing async applications easy because your interest is minimized. 13 | 14 | You can also read this documentation from Tokio. 15 | [https://tokio.rs/tokio/tutorial/channels](https://tokio.rs/tokio/tutorial/channels) 16 | 17 | ## Problem: Boilarplate 18 | 19 | Let's design your async application by message passing. 20 | In this case, you have to define your own message types for request and response by hand 21 | and may have to write some logics that consumes messages from channel or send response to the sender 22 | using oneshot channel. From the Tokio documentation this could be like this: 23 | 24 | ```rust 25 | use tokio::sync::oneshot; 26 | use bytes::Bytes; 27 | 28 | /// Multiple different commands are multiplexed over a single channel. 29 | #[derive(Debug)] 30 | enum Command { 31 | Get { 32 | key: String, 33 | resp: Responder>, 34 | }, 35 | Set { 36 | key: String, 37 | val: Bytes, 38 | resp: Responder<()>, 39 | }, 40 | } 41 | 42 | /// Provided by the requester and used by the manager task to send 43 | /// the command response back to the requester. 44 | type Responder = oneshot::Sender>; 45 | ``` 46 | 47 | ```rust 48 | while let Some(cmd) = rx.recv().await { 49 | match cmd { 50 | Command::Get { key, resp } => { 51 | let res = client.get(&key).await; 52 | // Ignore errors 53 | let _ = resp.send(res); 54 | } 55 | Command::Set { key, val, resp } => { 56 | let res = client.set(&key, val).await; 57 | // Ignore errors 58 | let _ = resp.send(res); 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | However, writing such codes is really tedious. 65 | 66 | ## Solution: Code generation 67 | 68 | The solution is to generate code so you can 69 | focus on the logics rather than the boilarplates. 70 | 71 | With norpc, you can define your in-memory microservice 72 | like this and this will generate all the other tedious codes. 73 | 74 | ```rust 75 | #[norpc::service] 76 | trait YourService { 77 | fn get(key: String) -> Option; 78 | fn set(key: String, val: Bytes); 79 | } 80 | ``` -------------------------------------------------------------------------------- /examples/src/concurrency.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::atomic::AtomicU64; 3 | use std::sync::atomic::Ordering; 4 | use tokio::sync::RwLock; 5 | use tower::ServiceBuilder; 6 | 7 | #[norpc::service] 8 | trait IdAlloc { 9 | fn alloc(name: u64) -> u64; 10 | } 11 | #[norpc::service] 12 | trait IdStore { 13 | fn save(name: u64, id: u64); 14 | fn query(name: u64) -> Option; 15 | } 16 | 17 | type IdStoreClientService = norpc::runtime::Channel; 18 | type IdStoreClientT = IdStoreClient; 19 | struct IdAllocApp { 20 | n: AtomicU64, 21 | id_store_cli: IdStoreClientT, 22 | } 23 | impl IdAllocApp { 24 | fn new(id_store_cli: IdStoreClientT) -> Self { 25 | Self { 26 | n: AtomicU64::new(1), 27 | id_store_cli, 28 | } 29 | } 30 | } 31 | #[norpc::async_trait] 32 | impl IdAlloc for IdAllocApp { 33 | async fn alloc(&self, name: u64) -> u64 { 34 | let sleep_time = rand::random::() % 100; 35 | tokio::time::sleep(std::time::Duration::from_millis(sleep_time)).await; 36 | let id = self.n.fetch_add(1, Ordering::SeqCst); 37 | self.id_store_cli.clone().save(name, id).await; 38 | name 39 | } 40 | } 41 | 42 | struct IdStoreApp { 43 | map: RwLock>, 44 | } 45 | impl IdStoreApp { 46 | fn new() -> Self { 47 | Self { 48 | map: RwLock::new(HashMap::new()), 49 | } 50 | } 51 | } 52 | #[norpc::async_trait] 53 | impl IdStore for IdStoreApp { 54 | async fn save(&self, name: u64, id: u64) { 55 | self.map.write().await.insert(name, id); 56 | } 57 | async fn query(&self, name: u64) -> Option { 58 | self.map.read().await.get(&name).cloned() 59 | } 60 | } 61 | 62 | const N: u64 = 10000; 63 | 64 | #[tokio::test(flavor = "multi_thread")] 65 | async fn test_concurrent_message() { 66 | use norpc::runtime::*; 67 | 68 | let app = IdStoreApp::new(); 69 | let service = IdStoreService::new(app); 70 | let (chan, server) = ServerBuilder::new(service).build(); 71 | ::tokio::spawn(server.serve(TokioExecutor)); 72 | let mut id_store_cli = IdStoreClient::new(chan); 73 | 74 | let app = IdAllocApp::new(id_store_cli.clone()); 75 | let service = IdAllocService::new(app); 76 | let service = ServiceBuilder::new() 77 | // Changing this value will see a different complete time. 78 | .concurrency_limit(100) 79 | .service(service); 80 | let (chan, server) = ServerBuilder::new(service).build(); 81 | ::tokio::spawn(server.serve(TokioExecutor)); 82 | let id_alloc_cli = IdAllocClient::new(chan); 83 | 84 | let mut queue = futures::stream::FuturesUnordered::new(); 85 | for i in 1..=N { 86 | let mut cli = id_alloc_cli.clone(); 87 | let fut = async move { cli.alloc(i).await }; 88 | queue.push(fut); 89 | } 90 | use futures::StreamExt; 91 | let mut n = 0; 92 | let mut diff_cnt = 0; 93 | while let Some(name) = queue.next().await { 94 | n += 1; 95 | let id0 = id_store_cli.query(name).await; 96 | assert!(id0.is_some()); 97 | let id = id0.unwrap(); 98 | if id != name { 99 | diff_cnt += 1; 100 | } 101 | } 102 | assert_eq!(n, N); 103 | assert!(diff_cnt > 0); 104 | } 105 | -------------------------------------------------------------------------------- /norpc-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use proc_macro2::*; 3 | use quote::quote; 4 | use std::str::FromStr; 5 | use syn::parse::{Parse, ParseStream, Result}; 6 | use syn::*; 7 | 8 | mod generator; 9 | 10 | struct Args { 11 | local: bool, 12 | } 13 | 14 | mod kw { 15 | syn::custom_keyword!(Send); 16 | } 17 | 18 | fn try_parse(input: ParseStream) -> Result { 19 | if input.peek(Token![?]) { 20 | input.parse::()?; 21 | input.parse::()?; 22 | Ok(Args { local: true }) 23 | } else { 24 | Ok(Args { local: false }) 25 | } 26 | } 27 | 28 | impl Parse for Args { 29 | fn parse(input: ParseStream) -> Result { 30 | let args: Args = try_parse(input)?; 31 | Ok(args) 32 | } 33 | } 34 | 35 | #[proc_macro_attribute] 36 | pub fn service(args: TokenStream, item: TokenStream) -> TokenStream { 37 | let args = parse_macro_input!(args as Args); 38 | let t = syn::parse::(item).unwrap(); 39 | let svc = parse_service(&t); 40 | let generator = generator::Generator { 41 | no_send: args.local, 42 | }; 43 | let code = generator.generate(svc); 44 | TokenStream::from_str(&code).unwrap() 45 | } 46 | 47 | #[derive(Debug)] 48 | struct Service { 49 | name: String, 50 | functions: Vec, 51 | } 52 | #[derive(Debug)] 53 | struct Function { 54 | name: String, 55 | inputs: Vec, 56 | output: String, 57 | } 58 | #[derive(Debug)] 59 | struct Parameter { 60 | var_name: String, 61 | typ_name: String, 62 | } 63 | 64 | fn parse_service(t: &ItemTrait) -> Service { 65 | let svc_name = { 66 | let x = &t.ident; 67 | quote!(#x).to_string() 68 | }; 69 | let mut functions = vec![]; 70 | for f in &t.items { 71 | functions.push(parse_func(f)); 72 | } 73 | Service { 74 | name: svc_name, 75 | functions, 76 | } 77 | } 78 | fn parse_func(f: &TraitItem) -> Function { 79 | match f { 80 | TraitItem::Method(m) => { 81 | let sig = &m.sig; 82 | 83 | let x = &sig.ident; 84 | let func_name = quote!(#x).to_string(); 85 | 86 | let mut inputs = vec![]; 87 | for input in &sig.inputs { 88 | match input { 89 | FnArg::Typed(p) => { 90 | let var_name = { 91 | let x = &p.pat; 92 | quote!(#x).to_string() 93 | }; 94 | let var_type = { 95 | let ty = &p.ty; 96 | quote!(#ty).to_string() 97 | }; 98 | inputs.push(Parameter { 99 | var_name, 100 | typ_name: var_type, 101 | }); 102 | } 103 | _ => unreachable!(), 104 | } 105 | } 106 | 107 | let output_ty; 108 | match &sig.output { 109 | ReturnType::Type(_, ty) => { 110 | output_ty = quote!(#ty).to_string(); 111 | } 112 | ReturnType::Default => { 113 | output_ty = "()".to_string(); 114 | } 115 | } 116 | Function { 117 | name: func_name, 118 | inputs, 119 | output: output_ty, 120 | } 121 | } 122 | // TODO ignore here to skip comments 123 | _ => unreachable!(), 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /norpc/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use futures::channel::oneshot; 4 | use futures::StreamExt; 5 | use std::collections::HashMap; 6 | use std::sync::atomic::{AtomicU64, Ordering}; 7 | use std::sync::Arc; 8 | 9 | enum CoreRequest { 10 | AppRequest { 11 | inner: X, 12 | tx: oneshot::Sender, 13 | stream_id: u64, 14 | }, 15 | Cancel { 16 | stream_id: u64, 17 | }, 18 | } 19 | 20 | pub struct ServerBuilder { 21 | svc: Svc, 22 | phantom_x: PhantomData, 23 | } 24 | impl + 'static + Send> ServerBuilder 25 | where 26 | X: 'static + Send, 27 | Svc::Future: Send, 28 | Svc::Response: Send, 29 | { 30 | pub fn new(svc: Svc) -> Self { 31 | Self { 32 | svc: svc, 33 | phantom_x: PhantomData, 34 | } 35 | } 36 | pub fn build(self) -> (Channel, Server) { 37 | let (tx, rx) = flume::unbounded(); 38 | let server = Server::new(rx, self.svc); 39 | let chan = Channel::new(tx); 40 | (chan, server) 41 | } 42 | } 43 | 44 | pub struct Channel { 45 | next_id: Arc, 46 | stream_id: u64, 47 | tx: flume::Sender>, 48 | } 49 | impl Channel { 50 | fn new(tx: flume::Sender>) -> Self { 51 | Self { 52 | stream_id: 0, 53 | next_id: Arc::new(AtomicU64::new(1)), 54 | tx: tx, 55 | } 56 | } 57 | } 58 | impl Clone for Channel { 59 | fn clone(&self) -> Self { 60 | let next_id = self.next_id.clone(); 61 | let stream_id = next_id.fetch_add(1, Ordering::SeqCst); 62 | Self { 63 | stream_id, 64 | next_id: next_id, 65 | tx: self.tx.clone(), 66 | } 67 | } 68 | } 69 | impl Drop for Channel { 70 | fn drop(&mut self) { 71 | let cancel_req = CoreRequest::Cancel { 72 | stream_id: self.stream_id, 73 | }; 74 | self.tx.send(cancel_req).ok(); 75 | } 76 | } 77 | impl crate::Service for Channel { 78 | type Response = Y; 79 | type Error = anyhow::Error; 80 | type Future = 81 | std::pin::Pin> + Send>>; 82 | 83 | fn poll_ready( 84 | &mut self, 85 | _: &mut std::task::Context<'_>, 86 | ) -> std::task::Poll> { 87 | Ok(()).into() 88 | } 89 | 90 | fn call(&mut self, req: X) -> Self::Future { 91 | let tx = self.tx.clone(); 92 | let stream_id = self.stream_id; 93 | Box::pin(async move { 94 | let (tx1, rx1) = oneshot::channel::(); 95 | let req = CoreRequest::AppRequest { 96 | inner: req, 97 | tx: tx1, 98 | stream_id, 99 | }; 100 | if tx.send_async(req).await.is_err() { 101 | anyhow::bail!("failed to send a request"); 102 | } 103 | let rep = rx1.await?; 104 | Ok(rep) 105 | }) 106 | } 107 | } 108 | 109 | pub struct Server> { 110 | service: Svc, 111 | rx: flume::Receiver>, 112 | } 113 | impl + 'static + Send> Server 114 | where 115 | X: 'static + Send, 116 | Svc::Future: Send, 117 | Svc::Response: Send, 118 | { 119 | fn new(rx: flume::Receiver>, service: Svc) -> Self { 120 | Self { service, rx: rx } 121 | } 122 | pub async fn serve(mut self, executor: impl futures::task::Spawn) { 123 | use futures::future::AbortHandle; 124 | use futures::task::SpawnExt; 125 | let mut processings: HashMap = HashMap::new(); 126 | let mut req_stream = self.rx.into_stream(); 127 | while let Some(req) = req_stream.next().await { 128 | match req { 129 | CoreRequest::AppRequest { 130 | inner, 131 | tx, 132 | stream_id, 133 | } => { 134 | if let Some(handle) = processings.get(&stream_id) { 135 | handle.abort(); 136 | } 137 | processings.remove(&stream_id); 138 | 139 | // back-pressure 140 | // A call of `poll_fn` here is required because some tower wrapper requires to do so. 141 | crate::poll_fn(|ctx| self.service.poll_ready(ctx)) 142 | .await 143 | .ok(); 144 | 145 | let fut = self.service.call(inner); 146 | let (fut, abort_handle) = futures::future::abortable(async move { 147 | if let Ok(rep) = fut.await { 148 | tx.send(rep).ok(); 149 | } 150 | }); 151 | let fut = async move { 152 | fut.await.ok(); 153 | }; 154 | if let Err(e) = executor.spawn(fut) { 155 | abort_handle.abort(); 156 | } 157 | processings.insert(stream_id, abort_handle); 158 | } 159 | CoreRequest::Cancel { stream_id } => { 160 | if let Some(handle) = processings.get(&stream_id) { 161 | handle.abort(); 162 | } 163 | processings.remove(&stream_id); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | #[cfg(feature = "tokio-executor")] 171 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio-executor")))] 172 | /// Tokio support. 173 | pub struct TokioExecutor; 174 | 175 | #[cfg(feature = "tokio-executor")] 176 | impl futures::task::Spawn for TokioExecutor { 177 | fn spawn_obj( 178 | &self, 179 | future: futures::task::FutureObj<'static, ()>, 180 | ) -> Result<(), futures::task::SpawnError> { 181 | tokio::spawn(future); 182 | Ok(()) 183 | } 184 | } 185 | 186 | #[cfg(feature = "async-std-executor")] 187 | #[cfg_attr(docsrs, doc(cfg(feature = "async-std-executor")))] 188 | /// async-std support. 189 | pub struct AsyncStdExecutor; 190 | 191 | #[cfg(feature = "async-std-executor")] 192 | impl futures::task::Spawn for AsyncStdExecutor { 193 | fn spawn_obj( 194 | &self, 195 | future: futures::task::FutureObj<'static, ()>, 196 | ) -> Result<(), futures::task::SpawnError> { 197 | async_std::task::spawn(future); 198 | Ok(()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /norpc-macros/src/generator.rs: -------------------------------------------------------------------------------- 1 | use super::Service; 2 | 3 | pub struct Generator { 4 | pub no_send: bool, 5 | } 6 | impl Generator { 7 | fn generate_request(&self, svc: &Service) -> String { 8 | let mut variants = vec![]; 9 | for fun in &svc.functions { 10 | let mut params = vec![]; 11 | for param in &fun.inputs { 12 | params.push(param.typ_name.clone()); 13 | } 14 | variants.push(format!("{}({})", fun.name, &itertools::join(params, ","),)); 15 | } 16 | format!( 17 | " 18 | #[allow(non_camel_case_types)] 19 | pub enum {svc_name}Request {{ 20 | {} 21 | }}", 22 | itertools::join(variants, ","), 23 | svc_name = svc.name, 24 | ) 25 | } 26 | fn generate_response(&self, svc: &Service) -> String { 27 | let mut variants = vec![]; 28 | for fun in &svc.functions { 29 | variants.push(format!("{}({})", fun.name, fun.output,)); 30 | } 31 | format!( 32 | " 33 | #[allow(non_camel_case_types)] 34 | pub enum {svc_name}Response {{ 35 | {} 36 | }}", 37 | itertools::join(variants, ","), 38 | svc_name = svc.name, 39 | ) 40 | } 41 | fn generate_client_struct(&self, svc: &Service) -> String { 42 | format!( 43 | " 44 | #[derive(Clone)] 45 | pub struct {svc_name}Client {{ 46 | svc: Svc 47 | }} 48 | ", 49 | svc_name = svc.name, 50 | ) 51 | } 52 | fn generate_server_struct(&self, svc: &Service) -> String { 53 | format!( 54 | " 55 | pub struct {svc_name}Service {{ 56 | app: std::sync::Arc 57 | }} 58 | ", 59 | svc_name = svc.name, 60 | ) 61 | } 62 | fn generate_trait(&self, svc: &Service) -> String { 63 | let mut methods = vec![]; 64 | for fun in &svc.functions { 65 | let mut params = vec!["&self".to_owned()]; 66 | for param in &fun.inputs { 67 | params.push(format!("{}:{}", param.var_name, param.typ_name,)); 68 | } 69 | let params = itertools::join(params, ","); 70 | methods.push(format!( 71 | "async fn {}({}) -> {};", 72 | fun.name, ¶ms, fun.output, 73 | )); 74 | } 75 | format!( 76 | " 77 | #[norpc::async_trait{no_send_marker}] 78 | pub trait {svc_name}: Sync {no_send_constraint} {{ 79 | {} 80 | }} 81 | ", 82 | itertools::join(methods, ""), 83 | svc_name = svc.name, 84 | no_send_marker = if self.no_send { "(?Send)" } else { "" }, 85 | no_send_constraint = if self.no_send { "" } else { "+ Send " }, 86 | ) 87 | } 88 | fn generate_client_impl(&self, svc: &Service) -> String { 89 | let mut methods = vec![]; 90 | for fun in &svc.functions { 91 | let mut params = vec!["&mut self".to_owned()]; 92 | for p in &fun.inputs { 93 | params.push(format!("{}:{}", p.var_name, p.typ_name,)); 94 | } 95 | let params = itertools::join(params, ","); 96 | 97 | let mut req_params = vec![]; 98 | for p in &fun.inputs { 99 | req_params.push(p.var_name.to_owned()); 100 | } 101 | let req_params = itertools::join(req_params, ","); 102 | 103 | let f = format!( 104 | " 105 | pub async fn {fun_name}({params}) -> {output} {{ 106 | norpc::poll_fn(|ctx| self.svc.poll_ready(ctx)).await.ok(); 107 | let rep = self.svc.call({}Request::{fun_name}({req_params})).await; 108 | match rep {{ 109 | Ok({svc_name}Response::{fun_name}(v)) => v, 110 | #[allow(unreachable_patterns)] 111 | _ => unreachable!(), 112 | }} 113 | }} 114 | ", 115 | svc_name = svc.name, 116 | fun_name = fun.name, 117 | params = params, 118 | output = fun.output, 119 | req_params = req_params, 120 | ); 121 | methods.push(f); 122 | } 123 | format!( 124 | " 125 | impl> {svc_name}Client {{ 126 | pub fn new(svc: Svc) -> Self {{ 127 | Self {{ svc }} 128 | }} 129 | {} 130 | }} 131 | ", 132 | itertools::join(methods, ""), 133 | svc_name = svc.name, 134 | ) 135 | } 136 | fn generate_server_impl(&self, svc: &Service) -> String { 137 | let mut match_arms = vec![]; 138 | for fun in &svc.functions { 139 | let mut req_params = vec![]; 140 | for p in &fun.inputs { 141 | req_params.push(p.var_name.to_owned()); 142 | } 143 | let req_params = itertools::join(req_params, ","); 144 | 145 | let a = format!( 146 | " 147 | {svc_name}Request::{fun_name}({req_params}) => {{ 148 | let rep = app.{fun_name}({req_params}).await; 149 | Ok({svc_name}Response::{fun_name}(rep)) 150 | }} 151 | ", 152 | svc_name = svc.name, 153 | fun_name = fun.name, 154 | req_params = req_params, 155 | ); 156 | 157 | match_arms.push(a); 158 | } 159 | 160 | format!( 161 | " 162 | impl {svc_name}Service {{ 163 | pub fn new(app: App) -> Self {{ 164 | Self {{ app: std::sync::Arc::new(app) }} 165 | }} 166 | }} 167 | impl norpc::Service<{svc_name}Request> for {svc_name}Service {{ 168 | type Response = {svc_name}Response; 169 | type Error = (); 170 | type Future = std::pin::Pin> {no_send}>>; 171 | fn poll_ready( 172 | &mut self, 173 | _: &mut std::task::Context<'_>, 174 | ) -> std::task::Poll> {{ 175 | Ok(()).into() 176 | }} 177 | fn call(&mut self, req: {svc_name}Request) -> Self::Future {{ 178 | let app = self.app.clone(); 179 | Box::pin(async move {{ 180 | match req {{ 181 | {} 182 | }} 183 | }}) 184 | }} 185 | }} 186 | ", 187 | itertools::join(match_arms, ","), 188 | svc_name = svc.name, 189 | no_send = if self.no_send { "" } else { "+ Send" }, 190 | ) 191 | } 192 | pub(super) fn generate(&self, svc: Service) -> String { 193 | format!( 194 | "{}{}{}{}{}{}{}", 195 | self.generate_request(&svc), 196 | self.generate_response(&svc), 197 | self.generate_trait(&svc), 198 | self.generate_client_struct(&svc), 199 | self.generate_client_impl(&svc), 200 | self.generate_server_struct(&svc), 201 | self.generate_server_impl(&svc), 202 | ) 203 | } 204 | } 205 | --------------------------------------------------------------------------------