├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── tests.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MAINTAINERS.md ├── Makefile ├── README.md ├── docs ├── favicon.png ├── getting-started │ ├── architecture.mdx │ ├── concepts.mdx │ ├── overview.mdx │ ├── quick-start.mdx │ └── roadmap.mdx ├── images │ ├── grpc │ │ ├── 128kb.png │ │ ├── 1mb.png │ │ ├── 32byte.png │ │ ├── 512byte.png │ │ ├── multi.png │ │ └── throughput.png │ ├── intro.svg │ ├── lightbackground.png │ └── twirp │ │ ├── 128kb.png │ │ ├── 1mb.png │ │ ├── 32byte.png │ │ ├── 512byte.png │ │ └── throughput.png ├── introduction.mdx ├── logo │ ├── dark.svg │ └── light.svg ├── mint.json ├── performance │ ├── grpc-benchmarks.mdx │ ├── optimizations.mdx │ └── twirp-benchmarks.mdx └── reference │ ├── client-methods.mdx │ ├── overview.mdx │ └── server-methods.mdx ├── examples ├── echo │ └── echo.proto ├── norpc │ └── norpc.proto ├── noservice │ └── noservice.proto ├── pubsub │ └── pubsub.proto ├── simple │ └── simple.proto └── test │ └── test.proto ├── go.mod ├── go.sum ├── pkg └── generator │ ├── generator.go │ ├── headers.go │ ├── imports.go │ ├── server.go │ └── test │ ├── generator_test.go │ ├── server.go │ └── test.frpc.go ├── protoc-gen-go-frpc └── main.go ├── templates ├── base.templ ├── client.templ ├── constants.templ ├── customDecode.templ ├── customEncode.templ ├── customFields.templ ├── headers.templ ├── imports.templ ├── interfaces.templ ├── prebase.templ ├── server.templ └── templates.go └── version ├── current_version └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | golang: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Set up Golang 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version: '1.22' 15 | cache: true 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v6 18 | with: 19 | version: latest 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | 12 | - name: Set up Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.22' 16 | cache: true 17 | 18 | - name: Install Protoc 19 | uses: arduino/setup-protoc@v3 20 | 21 | - name: Install protoc-gen plugin 22 | working-directory: ./protoc-gen-go-frpc 23 | run: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 24 | 25 | - name: Install frpc plugin for protoc-gen 26 | working-directory: ./ 27 | run: go install ./protoc-gen-go-frpc 28 | 29 | - name: Run generator 30 | working-directory: ./examples/test 31 | run: protoc --go-frpc_out=../../pkg/generator test.proto 32 | 33 | - name: Test 34 | run: go test -v ./... 35 | 36 | tests-race: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: '1.22' 46 | cache: true 47 | 48 | - name: Install Protoc 49 | uses: arduino/setup-protoc@v3 50 | 51 | - name: Install protoc-gen plugin 52 | working-directory: ./protoc-gen-go-frpc 53 | run: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 54 | 55 | - name: Install frpc plugin for protoc-gen 56 | working-directory: ./ 57 | run: go install ./protoc-gen-go-frpc 58 | 59 | - name: Run generator 60 | working-directory: ./examples/test 61 | run: protoc --go-frpc_out=../../pkg/generator test.proto 62 | 63 | - name: Test with Race Conditions 64 | run: go test -race -v ./... 65 | timeout-minutes: 15 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.test 3 | .env 4 | .idea/ 5 | .DS_Store 6 | dist/ 7 | vendor -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## fRPC Community Code of Conduct 2 | 3 | fRPC follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Frisbee uses GitHub to manage reviews of pull requests. 4 | 5 | - If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | - If you plan to do something more involved, first discuss your ideas 10 | on our [discord](https://loopholelabs.io/discord). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | - Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | 20 | - Be sure to sign off on the [DCO](https://github.com/probot/dco#how-it-works) 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | - Shivansh Vij @shivanshvij 2 | - Alex Sørlie Glomsaas @supermanifolds 3 | - Felicitas Pojtinger @pojntfx 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: proto 2 | proto: install 3 | protoc --go-frpc_out=./pkg/generator ./examples/test/test.proto 4 | 5 | .PHONY: install 6 | install: 7 | go install ./protoc-gen-go-frpc 8 | 9 | .PHONY: test 10 | test: proto 11 | go test -v ./... 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fRPC-go 2 | 3 | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-brightgreen.svg)](https://www.apache.org/licenses/LICENSE-2.0) 4 | [![Tests](https://github.com/loopholelabs/frpc-go/actions/workflows/tests.yaml/badge.svg)](https://github.com/loopholelabs/frpc-go/actions/workflows/tests.yaml) 5 | [![Discord](https://dcbadge.vercel.app/api/server/JYmFhtdPeu?style=flat)](https://loopholelabs.io/discord) 6 | ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.22-61CFDD.svg) 7 | 8 | 9 | This is the [Go](http://golang.org) implementation of [fRPC](https://frpc.io), a modern RPC framework 10 | designed for high performance and stability, that uses the [frisbee-go](https://github.com/loopholelabs/frisbee-go) messaging framework under the hood. 11 | 12 | ## Usage and Documentation 13 | 14 | Usage instructions and documentation for fRPC is available at [https://frpc.io/](https://frpc.io/). 15 | 16 | fRPC is still in very early _Alpha_. There may be bugs in the library that will be fixed 17 | as the library matures and usage of fRPC grows. One of the major benefits to fRPC is that reading the generated code 18 | is extremely straight forward, making it easy to debug potential issues down the line. 19 | 20 | ### Unsupported Features 21 | 22 | fRPC currently does not support the following features, though they are actively being worked on: 23 | 24 | - `OneOf` Message Types 25 | 26 | Example `Proto3` files can be found [here](/examples). 27 | 28 | ## Contributing 29 | 30 | Bug reports and pull requests are welcome on GitHub at [https://github.com/loopholelabs/frpc-go][gitrepo]. For more 31 | contribution information check 32 | out [the contribution guide](https://github.com/loopholelabs/frpc-go/blob/main/CONTRIBUTING.md). 33 | 34 | ## License 35 | 36 | The Frisbee project is available as open source under the terms of 37 | the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 38 | 39 | ## Code of Conduct 40 | 41 | Everyone interacting in the Frisbee project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 42 | 43 | ## Project Managed By: 44 | 45 | [![https://loopholelabs.io][loopholelabs]](https://loopholelabs.io) 46 | 47 | [gitrepo]: https://github.com/loopholelabs/frpc-go 48 | [loopholelabs]: https://cdn.loopholelabs.io/loopholelabs/LoopholeLabsLogo.svg 49 | [loophomepage]: https://loopholelabs.io 50 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/favicon.png -------------------------------------------------------------------------------- /docs/getting-started/architecture.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | --- 4 | 5 | The architecture of fRPC is based on the standard Server/Client model that a lot of other RPC frameworks are follow. 6 | The idea is that the `Client` makes a connection with the `Server`, and then sends a structured 7 | request. Based on the request type, the `Server` runs a handler that then returns a response or an error. 8 | The `Server` then forwards that response object (or the error) back to the `Client`. 9 | 10 | From the perspective of the `Client`, they have simply called a function and received a response. The fact 11 | that the request is serialized and transmitted to the `Server` is hidden for simplicity. 12 | 13 | To dig into how the underlying architecture of both the `Server` and `Client` work, it is first 14 | important to understand that the underlying [Frisbee](https://github.com/loopholelabs/frisbee-go) protocol does not have any notion of a request 15 | or response. When Frisbee sends a `Packet` of data, it does not wait for a response. This makes 16 | the protocol suitable for a number of use cases (like real-time streaming), but also means that Request/Reply semantics 17 | need to be implemented in the application logic - in this case, the code that fRPC generates. 18 | 19 | # Server Architecture 20 | 21 | The generated fRPC `Server` is based on the RPC `Services` that are defined in the `proto3` 22 | file that is passed to the `protoc` compiler. Developers are responsible for implementing the generated 23 | `Service` interfaces and passing that into the `Server` constructor. 24 | 25 | The `Server` then takes that implementation and creates a `handler table` that maps the request type to the 26 | accompanying function in the provided `Service` implementation. 27 | 28 | When it receives a request, it looks up the request type in the `handler table` and calls the accompanying 29 | function with the deserialized Request object. The function then returns a Response object that 30 | is serialized and sent back to the `Client`. 31 | 32 | # Client Architecture 33 | 34 | The generated fRPC `Client` is also based on the RPC `Services` that are defined in the 35 | `proto3` file that is passed to the `protoc` compiler. Based on the RPC Calls defined in those services, 36 | fRPC generates a number of `Client` helper functions - one for each possible RPC Call. 37 | 38 | As mentioned before, Frisbee does not have any notion of a request or response - this means that we must implement 39 | the ability to wait for a response from the `Server` in the application logic. We need to also be able to map 40 | those incoming responses to the correct ongoing request. 41 | 42 | To achieve this, fRPC Clients make use of an `in-flight` requests table that maps a request ID to a channel 43 | that can be listened to for a response. When an RPC function is called, it generates a request ID, serializes the request 44 | object and sends it to the `Server`. When a response object is received from the `Server`, it is 45 | deserialized and request ID is looked up in the `in-flight` requests table. 46 | 47 | The response is then pushed into the channel associated with the request ID, where it is read by the RPC function 48 | that made the request in the first place. This response unblocks the RPC caller and the response object (or an error) 49 | is returned. 50 | -------------------------------------------------------------------------------- /docs/getting-started/concepts.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Concepts 3 | --- 4 | 5 | fRPC is, at its core, a code generator - one which uses the Frisbee messaging framework as its underlying transport mechanism. It hooks into 6 | the `protoc` compiler and generates an RPC framework that matches the `proto3` spec provided to it. 7 | 8 | Frisbee was designed to allow developers to define their own messaging protocols, while having a library that would handle 9 | all the lower level implementation for them. 10 | 11 | RPC Frameworks 12 | are implementations of the Request/Reply pattern, and so fRPC generates the necessary 13 | Frisbee code to handle that messaging pattern. 14 | 15 | There are three main components to fRPC: 16 | 17 | - The Message Types 18 | - The Client 19 | - The Server 20 | 21 | # Message Types 22 | 23 | One of the challenges with any messaging system is that the messages must be serialized and deserialized into formats that 24 | can be transmitted over the wire. With a code generator like fRPC, that means we need to take your `proto3` 25 | message definitions and generate the accompanying `structs` in Go. We then need to create consistent, performant, 26 | and safe serialization and deserialization functions for those structs. 27 | 28 | To do this, fRPC makes use of the [Polyglot](https://github.com/loopholelabs/polyglot-go) library, which is a high-performance 29 | serialization framework that recycles byte buffers and can serialize and deserialize data with almost no allocations. 30 | This makes serialization and deserialization extremely fast, while also allowing us to minimize the accompanying memory allocations. 31 | 32 | [polyglot-go](https://github.com/loopholelabs/polyglot-go) library type comes with a number of 33 | `encode` and `decode` methods for various types, that fRPC chains together to create the 34 | serialization and deserialization functions for your `proto3` message definitions. 35 | 36 | We're also actively working on a [polyglot-rs](https://github.com/loopholelabs/polyglot-rs) library, which is a Rust 37 | implementation of `Polyglot`, as well as [polyglot-ts](https://github.com/loopholelabs/polyglot-ts) which is a 38 | TypeScript (and Javascript) implementation of `Polyglot`. 39 | 40 | # The Client 41 | 42 | The fRPC Client is a simple wrapper around the `frisbee.Client` type, and contains generated helper 43 | functions for creating and sending requests to an fRPC Server and then returning the accompanying response. 44 | 45 | It's also possible to deviate from those helper functions and access the underlying `frisbee.Client` directly. 46 | This allows you to do things like turn Frisbee off (and thus retrieve the underlying TCP connection). 47 | 48 | # The Server 49 | 50 | The fRPC Server is a simple wrapper around the `frisbee.Server` type, and contains generated helper 51 | functions for handling incoming requests and returning the accompanying response based on the handlers you've passed in 52 | to the constructor. 53 | 54 | Similar to the Client, it's also possible to deviate from those helper functions and access the underlying 55 | `frisbee.Server` directly. This allows you to do things like turn Frisbee off (and thus retrieve the 56 | underlying TCP connection), or write your own middleware functions for incoming or outgoing packets. 57 | 58 | # Accessing Frisbee Directly 59 | 60 | As we've mentioned before, it's possible to access the underlying [Frisbee](https://github.com/loopholelabs/frisbee-go) primitives from both the 61 | client and the server. This is why fRPC is more flexible than other RPC frameworks, and why it's possible to 62 | do things like send a few RPC requests using fRPC and then reuse that underlying TCP connection for something like an 63 | HTTP proxy. 64 | 65 | fRPC generates a `frisbee.HandlerTable` that allows Frisbee to route incoming packets to the correct 66 | handler functions. It's possible to override this table using the `frisbee.Server.SetHandlerTable()` 67 | method (which is exposed in the generated `frpc.Server` type). 68 | 69 | To learn more about how [Frisbee](https://github.com/loopholelabs/frisbee-go) works and how you can leverage it from within the generated fRPC 70 | code, check out the [frisbee-go Github Repository](https://github.com/loopholelabs/frisbee-go). 71 | -------------------------------------------------------------------------------- /docs/getting-started/overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | --- 4 | 5 | **fRPC** (or **Frisbee RPC**), is an RPC Framework (similar to [gRPC](https://grpc.io) or 6 | [Apache Thrift](https://thrift.apache.org/)) that's designed from the ground up to be lightweight, extensible, and extremely performant. 7 | 8 | We built fRPC because we loved the idea of defining our message types in a standardized 9 | [proto3](https://protobuf.dev/programming-guides/proto3/) format and having the [protobuf](https://github.com/protocolbuffers/protobuf) compiler generate all the necessary 10 | glue code for us, but we didn't like the [overhead](https://github.com/boguslaw-wojcik/encoding-benchmarks) of encoding and decoding 11 | messages in the Protobuf format, and wanted a wire protocol that was lighter and faster 12 | than HTTP\/2. 13 | 14 | fRPC offers a few major improvements over existing 15 | RPC frameworks like gRPC: 16 | 17 | - **Speed** - On average fRPC outperforms other RPC frameworks [by 2-4x in an apples-to-apples comparison](/performance/grpc-benchmarks), and is easily able to handle more than **2 million RPCs/second** on a single server 18 | - **Flexibility** - Not only does fRPC allow developers to deviate from the standard request/reply messaging pattern and implement custom patterns alongside their existing RPCs, but developers also have the ability to turn fRPC off and retrieve the underlying TCP connections so they can be reused for something else 19 | - **Familiarity** - Using fRPC feels very familiar to anyone who's used gRPC before, which means that developers can take advantage of the speed and extensibility that fRPC provides without a steep learning curve 20 | 21 | fRPC works by making use of protobuf plugins, and allows developers to use their existing proto3 files to generate a full 22 | RPC Framework that uses Frisbee under the hood. Our goal is to make fRPC a **drop-in 23 | replacement for gRPC** thanks to its generated interfaces matching gRPC's, however we don't support all of the features that 24 | gRPC does yet, most notable being Streaming and OneOf message types. 25 | 26 | # fRPC vs Frisbee 27 | 28 | It's important to note the distinction between fRPC and Frisbee. fRPC uses proto3 files to generate client and server 29 | implementations that use the Frisbee framework under the hood. This is why fRPC is so performant compared to other RPC 30 | frameworks - the Frisbee messaging framework and wire protocol are lightweight and extremely optimized. 31 | 32 | At its core, **Frisbee** is best described as a `bring-your-own-protocol` messaging framework. Our goal was 33 | to make it possible for developers to define their **own** messaging patterns and protocols, and have the actual 34 | lower-level implementations done for them by the library. 35 | 36 | 37 | A simple way to understand this is to think of fRPC as a Request/Reply 38 | protocol, and Frisbee as the low-level implementation of that protocol. With 39 | Frisbee you can implement any protocol or pattern you'd like, but since 40 | Request/Reply is so common fRPC allows you to implement that specific pattern 41 | very quickly and easily. 42 | 43 | 44 | # Getting started with fRPC 45 | 46 | Over the next few pages we'll walk you through the process of getting started with fRPC, 47 | from defining your message types in a proto3 file, to writing your first server and client. 48 | 49 | We'll also introduce the core concepts around Frisbee as well as how you can use the Framework to build your own custom messaging protocols. 50 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | --- 4 | 5 | In this section we'll be going over how you can quickly get started with fRPC, 6 | from defining your message types in a proto3 file, to writing your first server and client. 7 | 8 | We'll be building a simple echo service that will echo back the message it receives, and later on we'll also show how 9 | you can use the Frisbee framework itself to build a more complex PUB\/SUB service. 10 | 11 | # Installation 12 | 13 | To get started with fRPC, you'll need to make sure you have `Go` 14 | and the `protoc` compiler installed. Then, you'll need to install 15 | the `protoc-gen-go-frpc` protoc plugin 16 | which we will use to generate the server and client code. 17 | 18 | ## Prerequisites 19 | 20 | - [Go](https://golang.org) - fRPC works with `Go` version 1.18 or later. For installation instructions see [Go's Getting Started Guide](https://golang.org/doc/install). 21 | - [Protocol Buffer Compiler (protoc)](https://grpc.io/docs/protoc-installation/) - fRPC works with `protoc` version 3. For installation instructions see the [Protoc Getting Started Guide](https://developers.google.com/protoc/docs/getting_started). 22 | 23 | If you're using MacOS and have [Brew](https://brew.sh/) installed, you can use `brew install go` 24 | to install Golang, and `brew install protobuf` to install the protoc compiler. 25 | 26 | ## Install the fRPC Plugin 27 | 28 | To install the `protoc-gen-go-frpc` plugin, you'll first need to make sure that your `$GOBIN` environment variable is set and available in 29 | your system path. See the [Go Environment Variables](https://golang.org/doc/code.html#GOPATH) page 30 | for more information, but in general, you can do this by adding the following to 31 | your `~/.bashrc` file: 32 | 33 | ```sh .bashrc 34 | export GOBIN=$GOPATH/bin 35 | export PATH=$PATH:$GOBIN 36 | ``` 37 | 38 | To install the `protoc-gen-go-frpc` plugin itself, you'll need to run the following command: 39 | 40 | ```bash 41 | $ go install github.com/loopholelabs/frpc-go/protoc-gen-go-frpc@latest 42 | ``` 43 | 44 | This will install the `protoc-gen-go-frpc` plugin into your `$GOBIN` directory 45 | where it will be available for use by the `protoc` compiler. 46 | 47 | You can check that the plugin is installed and available by running the following command: 48 | 49 | ```bash 50 | $ which protoc-gen-go-frpc 51 | /Users//go/bin/protoc-gen-go-frpc # or $GOPATH/bin/protoc-gen-go-frpc 52 | ``` 53 | 54 | # Create a Proto3 File 55 | 56 | Now that we have the prerequisites and the `protoc-gen-go-frpc` plugin installed, 57 | we can start writing our echo service. Let's start by creating a directory to house our project: 58 | 59 | ```bash 60 | $ mkdir -p ~/frpc 61 | $ cd ~/frpc 62 | ``` 63 | 64 | Now we'll create an `echo.proto` file and define our message types: 65 | 66 | ```protobuf echo.proto 67 | syntax = "proto3"; 68 | 69 | option go_package = "/echo"; 70 | 71 | message Request { 72 | string Message = 1; 73 | } 74 | 75 | message Response{ 76 | string Message = 1; 77 | } 78 | ``` 79 | 80 | You can see that we've defined two message types, one for the `Request` and one for the `Response`. 81 | 82 | Next, we will define a new `EchoService` in our `proto3` file. This tells the compiler that we want to generate a server and client for this service. 83 | 84 | ```protobuf echo.proto 85 | syntax = "proto3"; 86 | 87 | option go_package = "/echo"; 88 | 89 | service EchoService { 90 | rpc Echo(Request) returns (Response); 91 | } 92 | 93 | message Request { 94 | string Message = 1; 95 | } 96 | 97 | message Response{ 98 | string Message = 1; 99 | } 100 | ``` 101 | 102 | And with that you should be ready. Next we'll start the `protoc` compiler to generate 103 | our fRPC server and client. 104 | 105 | # Generate the Server and Client 106 | 107 | Let's run the following command to generate the server and client code: 108 | 109 | ```bash 110 | $ protoc --go-frpc_out=. echo.proto 111 | ``` 112 | 113 | This command tells the `protoc` compiler to generate the server and client code for us and 114 | by specifying the `--go-frpc_out` flag, we're implicitly specifying that we want to use the `protoc-gen-go-frpc` plugin. 115 | 116 | If we wanted to be more explicit, we could have run the following command: 117 | 118 | ```bash 119 | $ protoc --plugin=protoc-gen-go-frpc=$GOBIN/protoc-gen-go-frpc --go-frpc_out=. echo.proto 120 | ``` 121 | 122 | These commands should have generated a new folder at `~/frpc/echo`, which 123 | contains an `echo.frpc.go` file containing the server and client code. Within 124 | that file, you'll find the following interface: 125 | 126 | ```go echo.frpc.go 127 | ... 128 | 129 | type EchoService interface { 130 | Echo(context.Context, *Request) (*Response, error) 131 | } 132 | 133 | ... 134 | ``` 135 | 136 | All we have left to do is implement the `EchoService` interface with our server-side logic, 137 | and pass that into the server. The generated library will then be able to handle everything else for us. 138 | 139 | # Setting up the Server 140 | 141 | To set up our server, we simply need to implement the `EchoService` interface and then start 142 | the server. We'll start by creating a new `server/main.go` file in our `~/frpc` directory: 143 | 144 | ```go server/main.go 145 | package main 146 | 147 | import ( 148 | "context" 149 | "frpc/echo" 150 | ) 151 | 152 | type svc struct{} 153 | 154 | func (s *svc) Echo(_ context.Context, req *echo.Request) (*echo.Response, error) { 155 | res := new(echo.Response) 156 | res.Message = req.Message 157 | return res, nil 158 | } 159 | ``` 160 | 161 | As you can see we've created a new struct called `svc` and implemented the `EchoService` interface by 162 | creating a new function called `Echo` which takes a `context.Context` and an `*echo.Request` object. 163 | We aren't really using the context in this example so we just ignore that and instead return an `*echo.Response` object with the 164 | same message as the request. 165 | 166 | Now we can implement the server itself: 167 | 168 | ```go server/server.go 169 | package main 170 | 171 | import ( 172 | "context" 173 | "log" 174 | 175 | "frpc/echo" 176 | ) 177 | 178 | type svc struct{} 179 | 180 | func (s *svc) Echo(_ context.Context, req *echo.Request) (*echo.Response, error) { 181 | log.Printf("Received request %s\n", req.Message) 182 | res := new(echo.Response) 183 | res.Message = req.Message 184 | return res, nil 185 | } 186 | 187 | func main() { 188 | frpcServer, err := echo.NewServer(new(svc), nil, nil) 189 | if err != nil { 190 | panic(err) 191 | } 192 | 193 | err = frpcServer.Start(":8080") 194 | if err != nil { 195 | panic(err) 196 | } 197 | } 198 | ``` 199 | 200 | This additional `main` function runs when the server starts up, and passes in our `svc` struct to the 201 | generated `echo.NewServer()` function. It then binds the server to port `:8080` and starts listening for connections. 202 | 203 | We're passing in `nil` for both the `*tls.Config` and `logging` parameters in the generated `echo.NewServer()` function because 204 | we don't want to use TLS or logging in this example. 205 | 206 | # Setting up the Client 207 | 208 | To set up our client, we don't need to implement any additional logic, but we do need to create a new `client/main.go` file 209 | in our `~/frpc` directory: 210 | 211 | ```go client/main.go 212 | package main 213 | 214 | import ( 215 | "context" 216 | "fmt" 217 | "frpc/echo" 218 | "log" 219 | "os" 220 | "os/signal" 221 | "syscall" 222 | "time" 223 | ) 224 | 225 | func main() { 226 | c, err := echo.NewClient(nil, nil) 227 | if err != nil { 228 | panic(err) 229 | } 230 | 231 | err = c.Connect("127.0.0.1:8080") 232 | if err != nil { 233 | panic(err) 234 | } 235 | } 236 | ``` 237 | 238 | Here, we're creating a new echo client using our generated `echo.NewClient()` function. 239 | Then, we're passing in the address of the server we want to connect to. But we're not actually sending any 240 | requests to the server yet. 241 | 242 | To do that, we can write a simple loop to send a request to the server every second and then print out the response: 243 | 244 | ```go echo/client/client.go 245 | package main 246 | 247 | import ( 248 | "context" 249 | "fmt" 250 | "frpc/echo" 251 | "log" 252 | "os" 253 | "os/signal" 254 | "syscall" 255 | "time" 256 | ) 257 | 258 | func main() { 259 | c, err := echo.NewClient(nil, nil) 260 | if err != nil { 261 | panic(err) 262 | } 263 | 264 | err = c.Connect("127.0.0.1:8080") 265 | if err != nil { 266 | panic(err) 267 | } 268 | defer c.Close() 269 | 270 | stop := make(chan os.Signal, 1) 271 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 272 | 273 | req := echo.NewRequest() 274 | for i := 0; ; i++ { 275 | select { 276 | case <-stop: 277 | err = c.Close() 278 | if err != nil { 279 | panic(err) 280 | } 281 | return 282 | case <-time.After(time.Second): 283 | req.Message = fmt.Sprintf("#%d", i) 284 | log.Printf("Sending Request %s\n", req.Message) 285 | res, err := c.EchoService.Echo(context.Background(), req) 286 | if err != nil { 287 | panic(err) 288 | } 289 | log.Printf("Received Response %s\n", res.Message) 290 | } 291 | } 292 | } 293 | ``` 294 | 295 | The above loop registers a `stop` channel to receive a signal when the user hits `Ctrl+C`, 296 | and then starts sending a request to the server every second. 297 | 298 | And that's it! We've now set up a simple echo client that can send requests to our server and print out the response. 299 | 300 | We were able to use a simple `proto3` file to define our request and response objects, and all we had to do was 301 | implement the `EchoService` interface. Everything else was handled for us by **fRPC**. 302 | 303 | The complete code for this example is available in the [frpc-echo-example](https://github.com/loopholelabs/frpc-echo-example) 304 | repository on [Github](https://github.com/loopholelabs). 305 | 306 | # Next Steps 307 | 308 | Now that we've seen how easy it is to use **fRPC**, we recommend you check out our [benchmarks](/performance/grpc-benchmarks) pages 309 | to learn more about how **fRPC** fares against other RPC frameworks. 310 | 311 | If you want to learn more about how **fRPC** works under the hood, you can check out our [fRPC Concepts](/getting-started/concepts) page, or our [technical reference](/reference/overview). 312 | 313 | Finally, if you'd like to learn how to use [Frisbee](https://github.com/loopholelabs/frisbee-go) (the underlying transport mechanism for fRPC) 314 | to implement your **own messaging protocol** that's fast and performant, you can check out the [frisbee-go Github repository](https://github.com/loopholelabs/frisbee-go). 315 | 316 | If you need any help or have any feedback about Frisbee or fRPC, please to check out our [Discord Community](https://loopholelabs.io/discord)! 317 | Our team would love to hear your thoughts and understand how you're planning on using fRPC, and we're always happy to help! 318 | -------------------------------------------------------------------------------- /docs/getting-started/roadmap.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Roadmap 3 | --- 4 | 5 | We're constantly building new features into fRPC to improve its functionality and expand its ease of use. 6 | We'd love to hear your feedback on these as well as any other ideas you have! 7 | Also, if you have any questions, please feel free to join our [Discord Server](https://loopholelabs.io/discord) where 8 | you can ask questions and get help from other fRPC developers. 9 | 10 | ## Currently In Development 11 | 12 | These are the features that are actively being developed right now. 13 | 14 | ### Load Balancing Support 15 | 16 | One of gRPC's most important features is load balancing. This is a feature that allows you to distribute your requests 17 | across multiple servers, and allows you to scale your application to handle more requests. Currently, fRPC does not 18 | have load balancing built-in, however it is already possible to do application-level load balancing on top of a normal 19 | fRPC client. 20 | 21 | Our goal is to have a built-in solution that Developers can use off the shelf, though, and this feature 22 | is being actively developed. We're expecting to have it available in the coming months. 23 | 24 | ### Retry Support 25 | 26 | Sometimes requests fail, whether it's because of a network issue or because of a server issue. Currently, this sort 27 | of failure will result in an fRPC client reporting an error - instead, we'd like to be able to retry the request 28 | a number of times before giving up. 29 | 30 | It's currently possible to do this at the application level, but our goal is to have a built-in solution that Developers \ 31 | can use off the shelf, though, and this feature is being actively developed. We're expecting to have it available in the coming months. 32 | 33 | # Planned Features 34 | 35 | These are the features that we've planned to work on in the coming months. If any of these are important to you please 36 | let us know! We use community feedback to plan our roadmap, and we also encourage contributors to submit their ideas 37 | to the [Discord Server](https://loopholelabs.io/discord) so that we can discuss them with the community. 38 | 39 | ### OneOf Message Types 40 | 41 | A recent addition to the proto3 syntax, `OneOf` message types allow developers to specify that 42 | only a single field from a set can be used at a time. 43 | 44 | This feature is also actively being developed and should be available in the coming months. 45 | 46 | ### JS/TS Support 47 | 48 | Currently, fRPC is only compatible with `Golang` but we plan to add support for JS and TS in the near future. 49 | We know it's important to have an RPC framework that can be used across language boundaries so this is a big priority for us. 50 | 51 | We've already begun the work required to add support for JS and TS by porting the [polyglot-go](https://github.com/loopholelabs/polyglot-go) 52 | framework to [Typescript](https://github.com/loopholelabs/polyglot-ts). This means we are already able to serialize and 53 | deserialize RPCs in Typescript. 54 | 55 | ### Rust Support 56 | 57 | Currently, fRPC is only compatible with `Golang` but we plan to add support for Rust in the near future. 58 | 59 | We've already begun the work required to add support for Rust by porting the [polyglot-go](https://github.com/loopholelabs/polyglot-go) 60 | framework to [Rust](https://github.com/loopholelabs/polyglot-rs). This means we are already able to serialize and 61 | deserialize RPCs in Rust. 62 | 63 | ### Websocket Support 64 | 65 | We would love for developers to be able to use fRPC over websockets. This would allow fRPC clients to in the browser and 66 | communicate directly with fRPC servers. 67 | -------------------------------------------------------------------------------- /docs/images/grpc/128kb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/128kb.png -------------------------------------------------------------------------------- /docs/images/grpc/1mb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/1mb.png -------------------------------------------------------------------------------- /docs/images/grpc/32byte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/32byte.png -------------------------------------------------------------------------------- /docs/images/grpc/512byte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/512byte.png -------------------------------------------------------------------------------- /docs/images/grpc/multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/multi.png -------------------------------------------------------------------------------- /docs/images/grpc/throughput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/grpc/throughput.png -------------------------------------------------------------------------------- /docs/images/lightbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/lightbackground.png -------------------------------------------------------------------------------- /docs/images/twirp/128kb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/twirp/128kb.png -------------------------------------------------------------------------------- /docs/images/twirp/1mb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/twirp/1mb.png -------------------------------------------------------------------------------- /docs/images/twirp/32byte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/twirp/32byte.png -------------------------------------------------------------------------------- /docs/images/twirp/512byte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/twirp/512byte.png -------------------------------------------------------------------------------- /docs/images/twirp/throughput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopholelabs/frpc-go/03eadf833bbf20677d19555e80cd296e2983fdb4/docs/images/twirp/throughput.png -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "fRPC Documentation" 3 | sidebarTitle: "Introduction" 4 | mode: "wide" 5 | --- 6 | 7 | fRPC is a **proto3-compatible** RPC Framework 8 | that's designed from the ground up to be **lightweight, extensible, and extremely 9 | performant**. On average fRPC outperforms other RPC frameworks [by 2-4x in an apples-to-apples 10 | comparison](/performance/grpc-benchmarks), and is easily able to handle more than 11 | 2 million RPCs/second on a single server. 12 | 13 | Welcome to fRPC 14 | 15 | 16 | 21 | Quickly get up and running with fRPC by following our getting started guide. 22 | 23 | 24 | Take a look at some unique fRPC concepts and how it differs from other 25 | frameworks. 26 | 27 | 28 | Check out our planned technical roadmap to see how we'll be improving fRPC 29 | in the future. 30 | 31 | 36 | Take a look at our technical docs to dig into the details of fRPC and how 37 | you can use it. 38 | 39 | 40 | 41 | 42 | 47 | The footer of each page contains an "Edit on Github" link. Please feel free 48 | to contribute by making a pull request! 49 | 50 | 55 | The **#Frisbee** Channel in our Discord server is a great place to get help 56 | with all things Frisbee and fRPC. 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/mint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fRPC", 3 | "logo": { 4 | "light": "/logo/light.svg", 5 | "dark": "/logo/dark.svg" 6 | }, 7 | "favicon": "/favicon.png", 8 | "colors": { 9 | "primary": "#AB1CFC", 10 | "light": "#C854FF", 11 | "dark": "#9015D9", 12 | "ultraLight": "#F8EEFE", 13 | "ultraDark": "#6A06A5", 14 | "background": { 15 | "dark": "#121212" 16 | }, 17 | "anchors": { 18 | "from": "#7C34FF", 19 | "to": "#FE9195" 20 | } 21 | }, 22 | "topbarCtaButton": { 23 | "type": "github", 24 | "url": "https://github.com/loopholelabs/frpc-go" 25 | }, 26 | "anchors": [ 27 | { 28 | "name": "Discord", 29 | "icon": "discord", 30 | "url": "https://loopholelabs.io/discord" 31 | }, 32 | { 33 | "name": "GitHub", 34 | "icon": "github", 35 | "url": "https://github.com/loopholelabs/frpc-go" 36 | } 37 | ], 38 | "navigation": [ 39 | { 40 | "group": "Welcome", 41 | "pages": ["introduction"] 42 | }, 43 | { 44 | "group": "Getting Started", 45 | "pages": [ 46 | "getting-started/overview", 47 | "getting-started/quick-start", 48 | "getting-started/concepts", 49 | "getting-started/architecture", 50 | "getting-started/roadmap" 51 | ] 52 | }, 53 | { 54 | "group": "Performance", 55 | "pages": [ 56 | "performance/optimizations", 57 | "performance/grpc-benchmarks", 58 | "performance/twirp-benchmarks" 59 | ] 60 | }, 61 | { 62 | "group": "Reference", 63 | "pages": [ 64 | "reference/overview", 65 | "reference/client-methods", 66 | "reference/server-methods" 67 | ] 68 | } 69 | ], 70 | "footerSocials": { 71 | "discord": "https://loopholelabs.io/discord", 72 | "github": "https://github.com/loopholelabs/frpc-go" 73 | }, 74 | "backgroundImage": "/images/lightbackground.png", 75 | "analytics": { 76 | "posthog": { 77 | "apiKey": "phc_WfWunxGOdcI7urnQA4XolSTSisVIh9NI38jTbg2TshT", 78 | "apiHost": "https://data.frpc.io" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/performance/grpc-benchmarks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: gRPC Benchmarks 3 | --- 4 | 5 | We can't just claim that fRPC is faster than a battle-tested tool like gRPC without backing it up with an apples-to-apples comparison. 6 | These benchmarks are publicly available at [https://github.com/loopholelabs/frpc-go-benchmarks](https://github.com/loopholelabs/frpc-go-benchmarks), and we encourage you to run them for yourselves. 7 | 8 | To make sure our benchmark is fair, we'll be using the exact same proto3 file as the input for both fRPC and gRPC. 9 | Moreover, we'll be using **the exact same service implementation for both the gRPC and fRPC** servers - the generated service interfaces in fRPC are designed to look the same as in gRPC, 10 | so using the same service implementation is extremely simple. 11 | 12 | ```protobuf Benchmark Proto3 File 13 | option go_package = "/"; 14 | 15 | service BenchmarkService { 16 | rpc Benchmark(Request) returns (Response); 17 | } 18 | 19 | message Request { 20 | string Message = 1; 21 | } 22 | 23 | message Response{ 24 | string Message = 1; 25 | } 26 | ``` 27 | 28 | We'll be running a number of different benchmarks with an increasing number of concurrent clients to show off both the throughput and the scalability of fRPC when compared to gRPC. 29 | 30 | The following benchmarks were performed on a bare metal host running Debian 11, 2x AMD EPYC 7643 48-Core CPUs, and 256GB of DDR4 memory. The benchmarks were performed over a local network to avoid inconsistencies due to network latency. 31 | 32 | # Client Throughput 33 | 34 | For our first set of benchmarks we'll have a number of concurrently connected clients and each client will make RPCs to the fRPC or 35 | gRPC server using a randomly generated fixed-sized message, and then wait for a response before repeating. 36 | 37 | In each of our benchmark runs we're increasing the number of concurrently connected clients and we're measuring the average throughput of each client to see how well fRPC and gRPC scale. We're also 38 | running a number of separate benchmarks, each with an increasing message size. 39 | 40 | ## 32-Byte Messages 41 | 42 | ![32-byte messages](/images/grpc/32byte.png) 43 | 44 | Starting with **32-byte** messages, it's obvious from the graph above that fRPC consistently outperforms and outscales gRPC - 45 | often by more than 2x. In the case of 8192 connected clients, fRPC's throughput is still 112 RPCs/second while gRPC drops to only 29. 46 | 47 | That means that clients using fRPC get almost 4x more throughput than gRPC using the same services and the same proto3 files. 48 | 49 | With 32-byte messages and 112 RPCs/second for fRPC that means our total throughput is about 3584B/s per client. With 8192 50 | clients that means our total throughput is about 28MB/s. for gRPC our total throughput is about 7.25MB/s. 51 | 52 | ## 512-Byte Messages 53 | 54 | ![512-byte messages](/images/grpc/512byte.png) 55 | 56 | Moving to the slightly larger **512-byte** messages, we can see the total throughput seems to drop for each individual client, but 57 | fRPC is still comfortably 2-3x faster than gRPC is. In the case of 8192 connected clients, fRPC's throughput is still 98 RPCs/second while gRPC drops to only 29. 58 | 59 | With 512-byte messages and 98 RPCs/second for fRPC that means our total throughput is about 49KB/s per client. With 8192 60 | clients that means our total throughput is about 392MB/s. for gRPC our total throughput is about 116MB/s. 61 | 62 | ## 128-KB Messages 63 | 64 | ![128-KB messages](/images/grpc/128kb.png) 65 | 66 | Now we're moving to the next larger message size, **128-KB**. Total throughput drops as expected for each client, but fRPC is still 67 | easily 3-4x faster than gRPC. In the case of 100 connected clients, fRPC's throughput is 192 RPCs/second while gRPC drops to only 65. 68 | 69 | With 128KB messages and 192 RPCs/second for fRPC that means our total throughput is about 24MB/s per client. 70 | With 100 clients that means our total throughput is about 2.34GB/s. For gRPC our total throughput is only about 0.8GB/s. 71 | 72 | ## 1-MB Messages 73 | 74 | ![1-MB messages](/images/grpc/1mb.png) 75 | 76 | With the next largest message size, **1MB**, it's clear that we're starting to become bottlenecked by the bare metal host we're using to benchmark. 77 | 78 | Still, fRPC keeps its lead with a 3-4x improvement over gRPC, and in the case of 100 connected clients fRPC's throughput 79 | per client is about 37MB/s. With 100 clients that means our total throughput is about 3.6GB/s. For gRPC our total throughput is only about 1.7GB/s. 80 | 81 | # Server Throughput 82 | 83 | Now let's look at how fRPC servers scale as we increase the number of connected clients. 84 | For this benchmark, we're going to make it so that each client repeatedly sends 10 concurrent RPCs in order to 85 | saturate the underlying TCP connections and the accompanying RPC server. 86 | 87 | ![server throughput](/images/grpc/throughput.png) 88 | 89 | As before, we can see that fRPC consistently outperforms gRPC - but as we increase the number of clients it's also 90 | clear that fRPC does not get as slowed down as the gRPC server does. It's able to handle **more than 91 | 2,000,000 RPCs/second** and the bottleneck actually seems to be our bare metal host as opposed to fRPC. 92 | 93 | In the case where we have 8192 connected clients, we can see that the gRPC server is able to handle just less than 500,000 RPCs/second - whereas **fRPC can easily handle more than 4x that**. 94 | 95 | # Multi-Threaded Performance 96 | 97 | By default, fRPC creates a new goroutine for each incoming RPC. This is a very similar approach to the one used by gRPC, 98 | and is a good choice for high-throughput applications where handling the RPC can be a blocking operation (like querying a remote 99 | database). 100 | 101 | However, fRPC can also be configured to create a single goroutine to handle all the RPCs from 102 | each incoming connection. This is a good choice for applications that require very low latency and where the 103 | handlers are not blocking operations (such as metrics streaming). 104 | 105 | The benchmarks above were all run with the single-goroutine option enabled, because our 106 | BenchmarkService implementation is a simple `Echo` service that does little to no processing and does 107 | not block. 108 | 109 | It's also important, however, to benchmark an application where the RPCs are blocking operations - and for those we'll go back 110 | to fRPCs default behavior to create a new goroutine to handle each incoming RPC. 111 | 112 | Our blocking operation for the following benchmark is a simple `time.Sleep` call that sleeps for exactly 50 Microseconds. 113 | 114 | ![Multi-threaded Throughput](/images/grpc/multi.png) 115 | 116 | The trend above is very similar to the single-threaded benchmarks above - fRPC is still leading gRPC in throughput, but it's also clear that boht gRPC and fRPC have suffered a performance penalty. For gRPC this is likely because the RPCs are now doing "work" and are blocking operations, but 117 | for fRPC it's a mixture of the added computation as well as the multi-threaded behaviour. 118 | 119 | In the case where we have 8192 connected clients, we can see that the performance of fRPC has dropped from 2,000,000 RPCs/second to about 1,600,000 RPCs/second, and gRPC has dropped from 500,000 RPCs/second to about 400,000 RPCs/second. 120 | 121 | These benchmarks show off just a small portion of fRPCs capabilities, and we encourage everyone to run 122 | these for themselves. We also have [benchmarks comparing fRPCs messaging format with protobuf and other serialization frameworks](https://github.com/loopholelabs/polyglot-go-benchmarks). 123 | -------------------------------------------------------------------------------- /docs/performance/optimizations.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Optimizations 3 | --- 4 | 5 | With **Zero Allocations** in the hot-path and an 8-Byte Packet Header, the network overhead of Frisbee is significantly 6 | lower than that of existing protocols like gRPC (performance comparisons available in our [gRPC Benchmarks](/performance/grpc-benchmarks)) 7 | which use HTTP/2 under the hood. 8 | 9 | This, combined with the substantial performance gains over [protobufs](https://github.com/protocolbuffers/protobuf) that come with using 10 | [polyglot](http://github.com/loopholelabs/polyglot-go) for serialization and deserialization, makes fRPC a great choice for high-performance, high-scalability, and high-reliability RPC frameworks. 11 | 12 | We originally designed Frisbee for our own messaging needs at [Loophole Labs](https://loopholelabs.io), where we needed 13 | to send both large and small amounts of data in a latency-sensitive manner. We also needed it to be massively scalable, 14 | able to handle thousands of concurrent connections and able to send millions of messages. 15 | 16 | To fulfill these requirements we spent a lot of time architecting the Frisbee data path to be extremely fast and extremely efficient. 17 | 18 | # Data Path Optimizations 19 | 20 | Our optimizations began with the `Packet` package, which efficiently recycles the byte buffers 21 | that are used throughout Frisbee to hold interstitial data. These make use of the [polyglot](http://github.com/loopholelabs/polyglot-go) library 22 | to implement `Encoding` and `Decoding` packages, which read and write directly from the `Packet.packet` 23 | structure. By recycling the `Packet.packet` structures throughout Frisbee, we can significantly reduce 24 | the number of allocations in the encoding and decoding functions. 25 | 26 | Most of our other optimizations center around our network I/O. Actually reading and writing data from a TCP socket 27 | is extremely slow, and so Frisbee makes an effort to maximize the amount of data that we read or write to a TCP socket 28 | while avoiding any additional latency. 29 | 30 | All these optimizations - as well as Frisbee's architecture, make it feasible to use Frisbee (as well as fRPC) 31 | in both latency-sensitive applications like in real-time streaming, as well as high-throughput applications like PUB/SUB systems. 32 | 33 | # Scheduling Optimizations 34 | 35 | By default, fRPC creates a new goroutine for each incoming RPC. This is a very similar approach to the one used by gRPC, 36 | and is a good choice for high-throughput applications where handling the RPC can be a blocking operation (like querying a remote 37 | database). 38 | 39 | However, fRPC can also be configured to create a single goroutine to handle all the RPCs from 40 | each incoming connection. This is a good choice for applications that require very low latency and where the handlers are not blocking operations (such as metrics streaming). 41 | 42 | 43 | In our benchmarks we've tested both approaches, though it should be noted that 44 | the single-goroutine approach is not as efficient as the multi-goroutine 45 | approach when the blocking time of the RPC handler is high. 46 | 47 | 48 | # Why TCP? 49 | 50 | Many of the recently released wire protocols like [Wireguard](https://www.wireguard.com/) and [QUIC](https://datatracker.ietf.org/doc/html/rfc9000) 51 | use UDP under the hood instead of TCP. Unlike TCP, UDP is an unreliable transport mechanism and provides no guarantees 52 | on packet delivery. 53 | 54 | There are benefits to using UDP and implementing packet delivery mechanisms on top of it - QUIC, for example, uses 55 | UDP to solve the [head-of-line blocking](https://calendar.perfplanet.com/2020/head-of-line-blocking-in-quic-and-http-3-the-details/) 56 | problem. 57 | 58 | For Frisbee, however, we wanted to make use of the existing performance optimizations that networking software and hardware 59 | have for TCP traffic, and we wanted the strong guarantees around packet delivery that TCP already provides. 60 | 61 | 62 | It's important to note that while Frisbee and fRPC were designed to be used 63 | with TCP connections, there's no reason developers can't use other transports. 64 | As long as the transport fulfills the 'net.Conn' interface, it will work as 65 | expected with Frisbee and fRPC. 66 | 67 | -------------------------------------------------------------------------------- /docs/performance/twirp-benchmarks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Twirp Benchmarks 3 | --- 4 | 5 | As with our other benchmarks, this will be as close to an apples-to-apples comparison as possible. 6 | These benchmarks are publicly available at [https://github.com/loopholelabs/frpc-go-benchmarks](https://github.com/loopholelabs/frpc-go-benchmarks), and we encourage you to run them for yourselves. 7 | 8 | To make sure our benchmark is fair, we'll be using the exact same proto3 file as the input for both fRPC and Twirp. 9 | Moreover, we'll be using **the exact same service implementation for both the Twirp and fRPC** servers - Twirp uses protobufs for serialization and its interface is very similar to gRPC. Because fRPC was designed 10 | to feel familiar to the gRPC interface, using the same service implementation is extremely simple. 11 | 12 | ```protobuf Benchmark Proto3 File 13 | option go_package = "/"; 14 | 15 | service BenchmarkService { 16 | rpc Benchmark(Request) returns (Response); 17 | } 18 | 19 | message Request { 20 | string Message = 1; 21 | } 22 | 23 | message Response{ 24 | string Message = 1; 25 | } 26 | ``` 27 | 28 | We'll be running a number of different benchmarks with an increasing number of concurrent clients to show off both the throughput and the scalability of fRPC when compared to Twirp. 29 | 30 | The following benchmarks were performed on a bare metal host running Debian 11, 2x AMD EPYC 7643 48-Core CPUs, and 256GB of DDR4 memory. The benchmarks were performed over a local network to avoid inconsistencies due to network latency. 31 | 32 | # Client Throughput 33 | 34 | For our first set of benchmarks we'll have a number of concurrently connected clients and each client will make RPCs to the fRPC or 35 | Twirp server using a randomly generated fixed-sized message, and then wait for a response before repeating. 36 | 37 | In each of our benchmark runs we're increasing the number of concurrently connected clients and we're measuring the average throughput of each client to see how well fRPC and Twirp scale. We're also 38 | running a number of separate benchmarks, each with an increasing message size. 39 | 40 | ## 32-Byte Messages 41 | 42 | ![32-byte messages](/images/twirp/32byte.png) 43 | 44 | Starting with **32-byte** messages, the results are very similar to the ones with gRPC. fRPC consistently and 45 | substantially outperforms Twirp - though the performance drop off for Twirp is significantly steeper than what we saw with gRPC. 46 | 47 | In the case of 8192 connected clients, Twirp's performance drops to only 4 RPCs/second per client while fRPC is able to handle 112 RPC/second. 48 | This means fRPC is 28x more performant than Twirp. 49 | 50 | Twirp seems to be relatively capable when there is a small number of connected clients, but quickly falls off as the number of clients increases. 51 | 52 | ## 512-Byte Messages 53 | 54 | ![512-byte messages](/images/twirp/512byte.png) 55 | 56 | When changing the message size to **512-bytes**, we can see an extremely sharp drop in Twirp's throughput, while fRPC seems to fare 57 | much better. In the case of 8192 connected clients, fRPC's throughput is still 98 RPCs/second while Twirp drops to only 4 - meaning 58 | fRPC performs almost 25x better than Twirp. 59 | 60 | ## 128-KB Messages 61 | 62 | ![128-KB messages](/images/twirp/128kb.png) 63 | 64 | With larger **128-KB** messages, we continue to see the same pattern as before - throughput for individual clients of both frameworks 65 | drops as more clients are added, but fRPC performs far better than Twirp - in this case between 2-7x better 66 | 67 | ## 1-MB Messages 68 | 69 | ![1-MB messages](/images/twirp/1mb.png) 70 | 71 | With the largest message size of the benchmark, **1MB**, the pattern from our previous runs continues. In this case, fRPC 72 | seems to perform between 3-6x better than Twirp and we're guessing that our bare metal host is starting the become the bottleneck as 73 | we increase the number of clients. 74 | 75 | # Server Throughput 76 | 77 | Now let's look at how fRPC servers scale compared to Twirp as we increase the number of connected clients. Twirp makes 78 | use of the standard `net/http` server so we're really comparing against that. 79 | 80 | For this benchmark, we're going to make it so that each client repeatedly sends 10 concurrent RPCs in order to 81 | saturate the underlying TCP connections and the accompanying RPC server. 82 | 83 | ![server throughput](/images/twirp/throughput.png) 84 | 85 | As before, we can see that fRPC consistently outperforms Twirp - but as we increase the number of clients beyond 1024, 86 | we actually saw the Twirp clients begin to fail. We couldn't get our benchmark to run for more than 1024 clients, which is 87 | why the benchmark reports a 0 for those runs. 88 | 89 | At 1024 clients, though, the fRPC is easily able to handle more than 60x more RPCs/second than Twirp is. 90 | 91 | These benchmarks show off just a small portion of fRPCs capabilities, and we encourage everyone to run 92 | these for themselves. We also have [benchmarks comparing fRPCs messaging format with protobuf and other serialization frameworks](https://github.com/loopholelabs/polyglot-go-benchmarks). 93 | -------------------------------------------------------------------------------- /docs/reference/client-methods.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client Methods 3 | --- 4 | 5 | # NewClient 6 | 7 | The `NewClient(tlsConfig *tls.Config, logger types.Logger) (*Client, error)` method is used to create a new fRPC client. 8 | 9 | It takes two arguments: 10 | 11 | - `tlsConfig`: a `*tls.Config` that will be used to configure TLS for the underlying connection. This can be left as `nil` if no TLS is required. 12 | - `logger`: a `types.Logger` that will be used to log all events. This can be left as `nil` if no logging is required. 13 | 14 | It returns an fRPC `*Client` on success and an error otherwise. 15 | 16 | # Connect 17 | 18 | The `Connect(addr string) error` method is used to initiate a connection to an fRPC server. 19 | If a `*tls.Config` was provided when the client was created (using `NewClient`) 20 | it will be used to configure TLS for the connection. 21 | 22 | An error will be returned if the connection fails. 23 | 24 | 25 | The Connect function should only be called once. If FromConn was already 26 | called on this client, Connect will return an error. 27 | 28 | 29 | # FromConn 30 | 31 | The `FromConn(conn net.Conn) error` method is used to create a new fRPC client from an existing net.Conn. This is useful if you want to reuse an existing connection, or 32 | if you have a custom transport that you want to use. If a `*tls.Config` was provided when the client was created (using `NewClient`), it will be ignored. 33 | 34 | An error will be returned if the connection fails. 35 | 36 | 37 | The FromConn function should only be called once. If Connect was already 38 | called on this client, FromConn will return an error." 39 | 40 | 41 | # Closed 42 | 43 | The `Closed() bool` method is used to check if the client is closed. This method will return `true` if the client is closed or has not yet been initialized, and `false` otherwise. 44 | 45 | # Error 46 | 47 | The `Error() error` method is used to check if the client has encountered an error. This method will return an `error` if the client has encountered an error, or `nil` otherwise. 48 | 49 | This method is meant to be used to check if the client encountered an error that caused it to close. 50 | 51 | # Close 52 | 53 | The `Close() error` method is used to close the client. It will return an `error` if the client encounters an error while closing (or if it is already closed), and will cancel any pending RPCs. 54 | 55 | # WritePacket 56 | 57 | The `WritePacket(p *packet.Packet) error` method is used to write a raw frisbee packet to the underlying connection. Normal fRPC operations should not use this method, however it is available 58 | when extending fRPC with custom protocols or messaging patterns directly for use with the underlying Frisbee library. 59 | 60 | # Flush 61 | 62 | The `Flush() error` method is used to flush the underlying connection. Normal fRPC operations should not use this method, however it is available 63 | when extending fRPC with custom protocols or messaging patterns directly for use with the underlying Frisbee library. 64 | 65 | # CloseChannel 66 | 67 | The `CloseChannel() <- chan struct{}` method is used to signal to a listener that the Client has been closed. 68 | The returned channel will be closed when the client is closed, and the `Error()` method can be used to check if the connection was closed due to an error. 69 | 70 | # Raw 71 | 72 | The `Raw() (net.Conn, error)` method is used to get the underlying `net.Conn` from the fRPC Client. This is useful if you want to extend fRPC with custom protocols or messaging patterns directly for use with the underlying Frisbee library. 73 | 74 | # Logger 75 | 76 | The `Logger() types.Logger` method is used to get the logger that was provided when the client was created. This is useful if you want to extend fRPC with custom protocols or messaging patterns directly for use with the underlying Frisbee library. 77 | 78 | # Generated Methods 79 | 80 | When generating the fRPC Client, each service in the `.proto` file also results in a generated `service` Client. 81 | Then, for each RPC in the service, a method is generated on the appropriate the `service` client. 82 | For example, if the `.proto` file contains the following service definition: 83 | 84 | ```proto 85 | service MyService { 86 | rpc MyMethod(MyRequest) returns (MyResponse) {} 87 | } 88 | ``` 89 | 90 | Then the generated `service` Client method is: 91 | 92 | ```go 93 | func (c *MyService) MyMethod(ctx context.Context, req *MyRequest) (res *MyResponse, err error) { 94 | ... 95 | } 96 | ``` 97 | 98 | And it's meant to be invoked using: 99 | 100 | ```go 101 | res, err := c.MyService.MyMethod(ctx, req) 102 | ``` 103 | 104 | Unlike gRPC, where each service requires creating a new RPC Client, fRPC creates a single client for all your services. 105 | -------------------------------------------------------------------------------- /docs/reference/overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | --- 4 | 5 | This is the reference documentation for fRPCs generated Client and Server. Since fRPC currently only supports the [Go](https://golang.org) programming language, this reference guide is currently only for [Go](https://golang.org). 6 | 7 | This guide describes the code generated with the `protoc-gen-go-frpc` plugin, when compiling `.proto` files with protoc. 8 | 9 | 10 | Client-side RPC invocations and server-side RPC handlers are meant to be 11 | thread-safe and will be run in concurrent goroutines. You must keep 12 | thread-safety in mind when implementing server-side RPC handlers for fRPC. 13 | 14 | -------------------------------------------------------------------------------- /docs/reference/server-methods.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server Methods 3 | --- 4 | 5 | # NewServer 6 | 7 | The `NewServer(... Services, *tls.Config, *ypes.Logger) (*Server, error)` method is used to create a new fRPC Server. It takes a list of structs that implement 8 | the RPC methods corresponding to the services defined in the `.proto` file. 9 | 10 | For example, if the `.proto` file contains the following service definition: 11 | 12 | ```proto 13 | service MyService { 14 | rpc MyMethod(MyRequest) returns (MyResponse) {} 15 | } 16 | 17 | service OtherService { 18 | rpc OtherMethod(MyRequest) returns (MyResponse) {} 19 | } 20 | ``` 21 | 22 | Then the generated `service implementation ` interfaces are expected: 23 | 24 | ```go 25 | type MyService interface { 26 | MyMethod(context.Context, *MyRequest) (*MyResponse, error) 27 | } 28 | 29 | type OtherService interface { 30 | OtherMethod(context.Context, *MyRequest) (*MyResponse, error) 31 | } 32 | ``` 33 | 34 | And the corresponding generated `NewServer` method would be: 35 | 36 | ```go 37 | func NewServer(myService MyService, otherService OtherService, tlsConfig *tls.Config, logger types.Logger) (*Server, error) { 38 | ... 39 | } 40 | ``` 41 | 42 | The generated `NewServer` method also takes the additional two arguments: 43 | 44 | - `tlsConfig`: a `*tls.Config` that will be used to configure TLS for fRPC server. This can be left as `nil` if no TLS is required. 45 | - `logger`: a `types.Logger` that will be used to log all events. This can be left as `nil` if no logging is required. 46 | 47 | It returns an fRPC `*Server` on success and an error otherwise. 48 | 49 | 50 | For long-running RPC handlers it's important to pay attention to the passed on 51 | context.Context - it will be cancelled when the server is shutdown and 52 | handlers are expected to return as soon as that happens. 53 | 54 | 55 | # SetBaseContext 56 | 57 | The `SetBaseContext(f func() context.Context) error` method is used to set the base context for all incoming RPCs. This is useful if you want to set a common context for all incoming RPCs. 58 | 59 | # SetOnClosed 60 | 61 | The `SetOnClosed(f func(*frisbee.Async, error)) error` method is used to set the callback function that will be called when a connection to an fRPC client is closed. This is useful if you want to do any cleanup when a connection to a client is closed. 62 | 63 | # SetHandlerTable 64 | 65 | The `SetHandlerTable(handlerTable frisbee.HandlerTable) error` method is used to set the handler table for the fRPC server. This is useful if you want to set a custom handler table for the fRPC server, and is commonly used to extend the fRPC server with custom handlers for 66 | alternative messaging patterns. In order to avoid breaking the fRPC functionality, it's recommended to first use the `GetHandlerTable` method to retrieve the base handler table, modify it, and then use the `SetHandlerTable` method to set the modified handler table. 67 | 68 | # GetHandlerTable 69 | 70 | The `GetHandlerTable() frisbee.HandlerTable` method is used to retrieve the handler table for the fRPC server. This is useful if you want to retrieve and extend handler table for the fRPC server, and is commonly used with the `SetHandlerTable` method. 71 | 72 | # SetPreWrite 73 | 74 | The `SetPreWrite(f func()) error` method is used to set the pre-write callback function for the fRPC server. This is useful if you want to handle metrics or do some logging before a request is written to the client. 75 | 76 | # SetConcurrency 77 | 78 | The `SetConcurrency(concurrency uint64)` method is used to set the concurrency for the fRPC server. This is useful if you want to set a maximum number of concurrent goroutines that can be spawned by the fRPC server (across all clients). 79 | 80 | Setting this value to `0` will result in the fRPC server spawning an unlimited number of goroutines to handle incoming RPCs, and setting the value to `1` will result in the fRPC server spawning a single goroutine (per fRPC Client) to handle incoming RPCs. 81 | 82 | All other values will result in the fRPC server spawning (at maximum) the specified number of goroutines to handle incoming RPCs. 83 | 84 | # Start 85 | 86 | The `Start(addr string) error` method is used to start the fRPC server. It takes the address to listen on as an argument and will return an error if the server fails to start. 87 | 88 | # ServeConn 89 | 90 | The `ServeConn(conn net.Conn)` method is used to serve a given net.Conn. It takes the connection to serve as an argument and is a non-blocking method - it will return immediately after starting to serve the connection in a separate goroutine, and if it 91 | encounters an error, the `OnClosed` callback function will be called with the error. 92 | 93 | # Logger 94 | 95 | The `Logger() types.Logger` method is used to retrieve the logger for the fRPC server. This is useful if you want to retrieve and extend the fRPC server. 96 | 97 | # Shutdown 98 | 99 | The `Shutdown() error` method is used to shutdown the fRPC server. It will return an error if the server fails to shutdown, and it will clean up all goroutines spawned by the server before returning. Any active connections will be closed before the server is shutdown, and any active RPCs 100 | will be cancelled. The contexts given to the RPCs will be cancelled as well. 101 | 102 | # Generated Interfaces 103 | 104 | When generating the fRPC Server, each service in the `.proto` file requires a `service implementation` that 105 | fulfills the RPC methods defined for the service. 106 | 107 | For example, if the `.proto` file contains the following service definition: 108 | 109 | ```proto 110 | service MyService { 111 | rpc MyMethod(MyRequest) returns (MyResponse) {} 112 | } 113 | ``` 114 | 115 | then the generated `service implementation ` interface looks like this: 116 | 117 | ```go 118 | type MyService interface { 119 | MyMethod(context.Context, *MyRequest) (*MyResponse, error) 120 | } 121 | ``` 122 | 123 | This is a similar function signature to the one gRPC would generate, making it easy to reuse the service implementation from gRPC. 124 | -------------------------------------------------------------------------------- /examples/echo/echo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "/echo"; 4 | 5 | service EchoService { 6 | rpc Echo(Request) returns (Response); 7 | } 8 | 9 | service TestService { 10 | rpc Echo(Request) returns (Response); 11 | } 12 | 13 | message Array { 14 | repeated string value = 1; 15 | } 16 | 17 | message Request { 18 | map metadata = 1; 19 | string Message = 2; 20 | } 21 | 22 | message Response{ 23 | string Message = 1; 24 | } -------------------------------------------------------------------------------- /examples/norpc/norpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "/norpc"; 4 | 5 | service NoRPC {} 6 | 7 | message Request { 8 | string Message = 1; 9 | } 10 | 11 | message Response{ 12 | string Message = 1; 13 | } -------------------------------------------------------------------------------- /examples/noservice/noservice.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "/noservice"; 4 | 5 | message Request { 6 | string Message = 1; 7 | } 8 | 9 | message Response{ 10 | string Message = 1; 11 | } -------------------------------------------------------------------------------- /examples/pubsub/pubsub.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pubsub; 4 | 5 | option go_package = "/pubsub"; 6 | 7 | message empty {} 8 | 9 | service PubSub { 10 | rpc Pub(empty) returns (empty); 11 | rpc Sub(empty) returns (empty); 12 | } 13 | 14 | service ExampleService { 15 | rpc ExampleCall1(ExampleMessage1) returns(ReturnType) {} 16 | rpc ExampleCall2(ExampleMessage2) returns(ReturnType) {} 17 | } 18 | 19 | // ExampleMessage1 - Example Leading Comment for ExampleMessage1 20 | message ExampleMessage1 { 21 | string MyString = 1; 22 | } 23 | 24 | /* 25 | ExampleMessage2 - Example Leading Comment for ExampleMessage2 26 | */ 27 | message ExampleMessage2 { 28 | int32 MyInt = 1; 29 | // MyInt - Example trailing Comment 30 | message ExampleNested { 31 | bytes data = 1; 32 | } 33 | ExampleNested nested = 2; 34 | } 35 | 36 | /* 37 | ReturnType - Empty Structure Placeholder 38 | */ 39 | message ReturnType {} -------------------------------------------------------------------------------- /examples/simple/simple.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "/simple"; 4 | 5 | service SimpleService { 6 | rpc Echo(stream Request) returns (stream Response); 7 | } 8 | 9 | message Request { 10 | string Message = 1; 11 | } 12 | 13 | message Response{ 14 | string Message = 1; 15 | } -------------------------------------------------------------------------------- /examples/test/test.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "/test"; 4 | 5 | service EchoService { 6 | rpc Echo(Request) returns (Response); 7 | rpc EchoStream(stream Request) returns (stream Response); 8 | rpc Testy(SearchResponse) returns (StockPricesWrapper); 9 | rpc Search(SearchResponse) returns (stream Response); 10 | rpc Upload(stream Data) returns (Response); 11 | } 12 | 13 | message Request { 14 | string Message = 1; 15 | enum Corpus { 16 | UNIVERSAL = 0; 17 | WEB = 1; 18 | IMAGES = 2; 19 | LOCAL = 3; 20 | NEWS = 4; 21 | PRODUCTS = 5; 22 | VIDEO = 6; 23 | } 24 | Corpus corpus = 4; 25 | } 26 | 27 | message Response{ 28 | string Message = 1; 29 | Data Test = 2; 30 | } 31 | 32 | enum Test { 33 | Potato = 0; 34 | Monkey = 1; 35 | } 36 | 37 | message Data{ 38 | string Message = 1; 39 | Test Checker = 2; 40 | } 41 | 42 | message MyMessage1 { 43 | enum EnumAllowingAlias { 44 | option allow_alias = true; 45 | UNKNOWN = 0; 46 | STARTED = 1; 47 | RUNNING = 1; 48 | } 49 | } 50 | message MyMessage2 { 51 | enum EnumNotAllowingAlias { 52 | UNKNOWN = 0; 53 | STARTED = 1; 54 | } 55 | } 56 | 57 | message SearchResponse { 58 | message Result { 59 | string url = 1; 60 | string title = 2; 61 | repeated string snippets = 3; 62 | } 63 | repeated Result results = 1; 64 | repeated Result results2 = 2; 65 | repeated string snippets = 3; 66 | repeated string snippets2 = 4; 67 | } 68 | 69 | message Resulting { 70 | string url = 1; 71 | string title = 2; 72 | repeated string snippets = 3; 73 | } 74 | 75 | message SomeOtherMessage { 76 | SearchResponse.Result result = 1; 77 | } 78 | 79 | message Outer {// Level 0 80 | message MiddleAA {// Level 1 81 | message Inner {// Level 2 82 | int64 ival = 1; 83 | bool booly = 2; 84 | } 85 | Inner inner = 1; 86 | } 87 | message MiddleBB {// Level 1 88 | message Inner {// Level 2 89 | int32 ival = 1; 90 | bool booly = 2; 91 | } 92 | Inner inner = 1; 93 | } 94 | MiddleAA a = 1; 95 | MiddleBB b = 2; 96 | } 97 | 98 | message SampleMessage { 99 | oneof test_oneof { 100 | string name = 4; 101 | string potato = 9; 102 | } 103 | } 104 | 105 | message TestPotato { 106 | map prices = 1; 107 | } 108 | 109 | 110 | message StockPrices { 111 | map prices = 1; 112 | } 113 | 114 | message StockPricesWrapper { 115 | repeated StockPrices sPrices = 1; 116 | } 117 | 118 | message StockPricesSuperWrap { 119 | map prices = 1; 120 | } 121 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/loopholelabs/frpc-go 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/loopholelabs/frisbee-go v0.10.2 7 | github.com/loopholelabs/logging v0.3.1 8 | github.com/loopholelabs/polyglot/v2 v2.0.3 9 | github.com/loopholelabs/testing v0.2.3 10 | github.com/stretchr/testify v1.9.0 11 | google.golang.org/protobuf v1.35.1 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/loopholelabs/common v0.4.10 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 6 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 7 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 8 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 9 | github.com/loopholelabs/common v0.4.10 h1:BMJSMwH0PiVtdpOlXNPlW827B9WPJ/Gkb/q20NLeOjw= 10 | github.com/loopholelabs/common v0.4.10/go.mod h1:wc17hLpzZaDbndb7Fh3MXQDnhf4Cmf/JKC+LmXaD6II= 11 | github.com/loopholelabs/frisbee-go v0.10.2 h1:TvfONSpoCrrvG7/8QzBbHou2gPqOwN5iFL+4fp3bu4s= 12 | github.com/loopholelabs/frisbee-go v0.10.2/go.mod h1:JJhInJ5zjxpwOLAqEviOjTo1k8E3NNEF/fQTrC4lZ/4= 13 | github.com/loopholelabs/logging v0.3.1 h1:VA9DF3WrbmvJC1uQJ/XcWgz8KWXydWwe3BdDiMbN2FY= 14 | github.com/loopholelabs/logging v0.3.1/go.mod h1:uRDUydiqPqKbZkb0WoQ3dfyAcJ2iOMhxdEafZssLVv0= 15 | github.com/loopholelabs/polyglot/v2 v2.0.3 h1:CpH2az5shkOgOBASnzjc1XC5SVzQMbWyHt4R7ds/FFc= 16 | github.com/loopholelabs/polyglot/v2 v2.0.3/go.mod h1:yodgE9ile0RS/npD0WnHfFpMLvL5FlC9n3qZ1tTkB9g= 17 | github.com/loopholelabs/testing v0.2.3 h1:4nVuK5ctaE6ua5Z0dYk2l7xTFmcpCYLUeGjRBp8keOA= 18 | github.com/loopholelabs/testing v0.2.3/go.mod h1:gqtGY91soYD1fQoKQt/6kP14OYpS7gcbcIgq5mc9m8Q= 19 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 20 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 21 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 22 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 26 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 27 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 28 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 29 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 30 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 31 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 32 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 33 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 34 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 35 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 38 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 41 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /pkg/generator/generator.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package generator 4 | 5 | import ( 6 | "bytes" 7 | "strings" 8 | "text/template" 9 | 10 | "google.golang.org/protobuf/compiler/protogen" 11 | "google.golang.org/protobuf/proto" 12 | "google.golang.org/protobuf/reflect/protoreflect" 13 | "google.golang.org/protobuf/types/pluginpb" 14 | 15 | generator "github.com/loopholelabs/polyglot/v2/generator/golang" 16 | "github.com/loopholelabs/polyglot/v2/utils" 17 | 18 | "github.com/loopholelabs/frpc-go/templates" 19 | "github.com/loopholelabs/frpc-go/version" 20 | ) 21 | 22 | type Generator struct { 23 | options *protogen.Options 24 | } 25 | 26 | var templ *template.Template 27 | var streamMethods = make(map[protoreflect.FullName]bool) 28 | 29 | func init() { 30 | templ = template.Must(template.New("main").Funcs(template.FuncMap{ 31 | "CamelCase": utils.CamelCaseFullName, 32 | "CamelCaseName": utils.CamelCaseName, 33 | "MakeIterable": utils.MakeIterable, 34 | "Counter": utils.Counter, 35 | "FirstLowerCase": utils.FirstLowerCase, 36 | "FirstLowerCaseName": utils.FirstLowerCaseName, 37 | "GetServerFields": getServerFields, 38 | "UsedForStreaming": func(typeName protoreflect.FullName) bool { 39 | return streamMethods[typeName] 40 | }, 41 | }).ParseFS(templates.FS, "*")) 42 | } 43 | 44 | func New() *Generator { 45 | return &Generator{ 46 | options: &protogen.Options{ 47 | ParamFunc: func(name string, value string) error { return nil }, 48 | ImportRewriteFunc: func(path protogen.GoImportPath) protogen.GoImportPath { return path }, 49 | }, 50 | } 51 | } 52 | 53 | func (*Generator) UnmarshalRequest(buf []byte) (*pluginpb.CodeGeneratorRequest, error) { 54 | req := new(pluginpb.CodeGeneratorRequest) 55 | return req, proto.Unmarshal(buf, req) 56 | } 57 | 58 | func (*Generator) MarshalResponse(res *pluginpb.CodeGeneratorResponse) ([]byte, error) { 59 | return proto.Marshal(res) 60 | } 61 | 62 | func (g *Generator) Generate(req *pluginpb.CodeGeneratorRequest) (res *pluginpb.CodeGeneratorResponse, err error) { 63 | plugin, err := g.options.New(req) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | var tplBuffer bytes.Buffer 69 | if err := templ.ExecuteTemplate(&tplBuffer, "customEncode.templ", nil); err != nil { 70 | return nil, err 71 | } 72 | customEncode := tplBuffer.String() 73 | tplBuffer.Reset() 74 | 75 | if err := templ.ExecuteTemplate(&tplBuffer, "customDecode.templ", nil); err != nil { 76 | return nil, err 77 | } 78 | customDecode := tplBuffer.String() 79 | tplBuffer.Reset() 80 | 81 | if err := templ.ExecuteTemplate(&tplBuffer, "customFields.templ", nil); err != nil { 82 | return nil, err 83 | } 84 | customFields := tplBuffer.String() 85 | 86 | gen := generator.New() 87 | gen.CustomEncode = func() string { 88 | return customEncode 89 | } 90 | gen.CustomDecode = func() string { 91 | return customDecode 92 | } 93 | gen.CustomFields = func() string { 94 | return customFields 95 | } 96 | 97 | for _, f := range plugin.Files { 98 | if !f.Generate { 99 | continue 100 | } 101 | genFile := plugin.NewGeneratedFile(FileName(f.GeneratedFilenamePrefix), f.GoImportPath) 102 | 103 | packageName := string(f.Desc.Package().Name()) 104 | if packageName == "" { 105 | packageName = string(f.GoPackageName) 106 | } 107 | 108 | numServices := f.Desc.Services().Len() 109 | numStreamMethods, numMethods := extractNumberOfMethods(f) 110 | 111 | err = templ.ExecuteTemplate(genFile, "prebase.templ", map[string]interface{}{ 112 | "pluginVersion": strings.TrimSpace(version.Version), 113 | "sourcePath": f.Desc.Path(), 114 | "package": packageName, 115 | "requiredImports": requiredImports, 116 | "serviceImports": serviceImports, 117 | "streamMethodImports": streamMethodImports, 118 | "numServices": numServices, 119 | "numMethods": numMethods, 120 | "numStreamMethods": numStreamMethods, 121 | }) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | err = gen.ExecuteTemplate(genFile, f, packageName, false) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = templ.ExecuteTemplate(genFile, "base.templ", map[string]interface{}{ 132 | "enums": f.Desc.Enums(), 133 | "messages": f.Desc.Messages(), 134 | "services": f.Desc.Services(), 135 | "numServices": numServices, 136 | "numMethods": numMethods, 137 | "numStreamMethods": numStreamMethods, 138 | }) 139 | if err != nil { 140 | return nil, err 141 | } 142 | } 143 | 144 | return plugin.Response(), nil 145 | } 146 | 147 | func extractNumberOfMethods(f *protogen.File) (int, int) { 148 | numServices := f.Desc.Services().Len() 149 | numMethods := 0 150 | streamMethods = make(map[protoreflect.FullName]bool) 151 | for i := 0; i < numServices; i++ { 152 | nM := f.Desc.Services().Get(i).Methods().Len() 153 | numMethods += nM 154 | for m := 0; m < nM; m++ { 155 | method := f.Desc.Services().Get(i).Methods().Get(m) 156 | if method.IsStreamingClient() { 157 | streamMethods[method.Input().FullName()] = true 158 | } 159 | if method.IsStreamingServer() { 160 | streamMethods[method.Output().FullName()] = true 161 | } 162 | } 163 | } 164 | 165 | return len(streamMethods), numMethods 166 | } 167 | -------------------------------------------------------------------------------- /pkg/generator/headers.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package generator 4 | 5 | import "github.com/loopholelabs/polyglot/v2/utils" 6 | 7 | const extension = ".frpc.go" 8 | 9 | func FileName(name string) string { 10 | return utils.AppendString(name, extension) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/generator/imports.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package generator 4 | 5 | var ( 6 | requiredImports = []string{ 7 | "errors", 8 | "net", 9 | "sync", 10 | "context", 11 | "github.com/loopholelabs/polyglot/v2", 12 | } 13 | 14 | serviceImports = []string{ 15 | "crypto/tls", 16 | "github.com/loopholelabs/frisbee-go", 17 | "github.com/loopholelabs/frisbee-go/pkg/packet", 18 | "github.com/loopholelabs/logging/types", 19 | } 20 | 21 | streamMethodImports = []string{ 22 | "sync/atomic", 23 | "io", 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/generator/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package generator 4 | 5 | import ( 6 | "strings" 7 | 8 | "google.golang.org/protobuf/reflect/protoreflect" 9 | 10 | "github.com/loopholelabs/polyglot/v2/utils" 11 | ) 12 | 13 | func getServerFields(services protoreflect.ServiceDescriptors) string { 14 | builder := new(strings.Builder) 15 | for i := 0; i < services.Len(); i++ { 16 | service := services.Get(i) 17 | serviceName := utils.CamelCase(string(service.Name())) 18 | builder.WriteString(utils.FirstLowerCase(serviceName)) 19 | builder.WriteString(" ") 20 | builder.WriteString(serviceName) 21 | builder.WriteString(",") 22 | builder.WriteString(" ") 23 | } 24 | serverFields := builder.String() 25 | serverFields = serverFields[:len(serverFields)-2] 26 | return serverFields 27 | } 28 | -------------------------------------------------------------------------------- /pkg/generator/test/generator_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package test 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "testing" 13 | "time" 14 | 15 | "github.com/loopholelabs/polyglot/v2" 16 | "github.com/loopholelabs/testing/conn/pair" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestRPC(t *testing.T) { 22 | cConn, sConn, err := pair.New() 23 | assert.NoError(t, err, "Client server pair creation failed") 24 | 25 | client, err := NewClient(nil, nil) 26 | assert.NoError(t, err, "Client creation failed") 27 | 28 | err = client.FromConn(cConn) 29 | assert.NoError(t, err, "Client connection assignment failed") 30 | 31 | serverMethods := new(svc) 32 | serverMethods.t = t 33 | server, err := NewServer(serverMethods, nil, nil) 34 | assert.NoError(t, err, "Server creation failed") 35 | 36 | go server.ServeConn(sConn) 37 | 38 | t.Run("Synchronous Request", func(t *testing.T) { 39 | testSynchronous(client, t) 40 | }) 41 | 42 | t.Run("Bi-directional Stream", func(t *testing.T) { 43 | testBidirectional(client, t) 44 | }) 45 | 46 | t.Run("Server Stream", func(t *testing.T) { 47 | testServerStreaming(client, t) 48 | }) 49 | 50 | t.Run("Client Stream", func(t *testing.T) { 51 | testClientStreaming(client, t) 52 | }) 53 | } 54 | 55 | func testSynchronous(client *Client, t *testing.T) { 56 | ctx := context.Background() 57 | req := &Request{ 58 | Message: "Hello World", 59 | Corpus: RequestUNIVERSAL, 60 | } 61 | response, err := client.EchoService.Echo(ctx, req) 62 | assert.NoError(t, err, "Request error") 63 | assert.Equal(t, "Hello World", response.Message) 64 | assert.Equal(t, "Hello World", response.Test.Message) 65 | assert.Equal(t, Potato, response.Test.Checker) 66 | } 67 | 68 | func testBidirectional(client *Client, t *testing.T) { 69 | ctx := context.Background() 70 | req := &Request{ 71 | Message: "Hello World", 72 | Corpus: RequestUNIVERSAL, 73 | } 74 | 75 | stream, err := client.EchoService.EchoStream(ctx, req) 76 | assert.NoError(t, err) 77 | 78 | roundtrips := 0 79 | for { 80 | res, err := stream.Recv() 81 | roundtrips++ 82 | assert.NoError(t, err, "Request error") 83 | assert.Equal(t, "Hello World", res.Message) 84 | assert.Equal(t, "Hello World", res.Test.Message) 85 | assert.Equal(t, Potato, res.Test.Checker) 86 | if roundtrips == 100 { 87 | err = stream.CloseSend() 88 | assert.NoError(t, err) 89 | break 90 | } 91 | 92 | err = stream.Send(req) 93 | assert.NoError(t, err) 94 | } 95 | assert.Equal(t, 100, roundtrips) 96 | } 97 | 98 | func testServerStreaming(client *Client, t *testing.T) { 99 | ctx := context.Background() 100 | search := NewSearchResponse() 101 | search.Results = []*SearchResponseResult{ 102 | { 103 | Url: "https://google.com", 104 | Title: "Google", 105 | Snippets: []string{}, 106 | }, 107 | } 108 | stream, err := client.EchoService.Search(ctx, search) 109 | assert.NoError(t, err) 110 | 111 | received := 0 112 | for { 113 | res, err := stream.Recv() 114 | if err == io.EOF { 115 | assert.Equal(t, 10, received) 116 | break 117 | } 118 | received++ 119 | assert.NoError(t, err) 120 | assert.Equal(t, "Hello World", res.Message) 121 | assert.Equal(t, "Hello World", res.Test.Message) 122 | assert.Equal(t, Potato, res.Test.Checker) 123 | } 124 | } 125 | 126 | func testClientStreaming(client *Client, t *testing.T) { 127 | ctx := context.Background() 128 | data := &Data{Message: "Hello World", Checker: Potato} 129 | stream, err := client.EchoService.Upload(ctx, data) 130 | assert.NoError(t, err) 131 | 132 | for i := 0; i < 10; i++ { 133 | err := stream.Send(data) 134 | assert.NoError(t, err) 135 | } 136 | res, err := stream.CloseAndRecv() 137 | assert.NoError(t, err) 138 | assert.Equal(t, "Hello World", res.Message) 139 | } 140 | 141 | func TestRPCInvalidConnection(t *testing.T) { 142 | // Create non-Frisbee server the client can connect to but not exchange 143 | // messages, so the connection will be broken soon after connect. 144 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 | fmt.Fprintln(w, "test") 146 | })) 147 | t.Cleanup(ts.Close) 148 | 149 | // Create a client and connect to test server. 150 | client, err := NewClient(nil, nil) 151 | require.NoError(t, err) 152 | 153 | err = client.Connect(ts.Listener.Addr().String()) 154 | require.NoError(t, err) 155 | 156 | // Make RPC request with a 3s timeout. 157 | timeout := 3 * time.Second 158 | if os.Getenv("CI") != "" { 159 | timeout = 10 * time.Second 160 | } 161 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 162 | t.Cleanup(cancel) 163 | 164 | req := &Request{ 165 | Message: "Hello World", 166 | Corpus: RequestUNIVERSAL, 167 | } 168 | response, err := client.EchoService.Echo(ctx, req) 169 | 170 | // Verify request doesn't block forever. 171 | require.NoError(t, ctx.Err()) 172 | 173 | // Verify request fails. 174 | require.Error(t, err) 175 | require.Nil(t, response) 176 | } 177 | 178 | func TestEncodeDecodePreservesNilFields(t *testing.T) { 179 | r := &Response{Message: "test", Test: nil} 180 | b := polyglot.NewBuffer() 181 | r.Encode(b) 182 | 183 | got := &Response{} 184 | err := got.Decode(b.Bytes()) 185 | require.NoError(t, err) 186 | require.Equal(t, r, got) 187 | } 188 | -------------------------------------------------------------------------------- /pkg/generator/test/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package test 4 | 5 | import ( 6 | "context" 7 | "io" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type svc struct { 14 | t *testing.T 15 | } 16 | 17 | func (s svc) Echo(_ context.Context, request *Request) (*Response, error) { 18 | assert.Equal(s.t, "Hello World", request.Message) 19 | assert.Equal(s.t, RequestUNIVERSAL, request.Corpus) 20 | return &Response{Message: "Hello World", Test: &Data{ 21 | Message: "Hello World", 22 | Checker: Potato, 23 | }}, nil 24 | } 25 | 26 | func (s svc) EchoStream(_ context.Context, srv *EchoStreamServer) error { 27 | for { 28 | request, err := srv.Recv() 29 | if err == io.EOF { 30 | err = srv.CloseSend() 31 | assert.NoError(s.t, err) 32 | break 33 | } 34 | 35 | assert.NoError(s.t, err) 36 | 37 | assert.Equal(s.t, "Hello World", request.Message) 38 | assert.Equal(s.t, RequestUNIVERSAL, request.Corpus) 39 | 40 | response := &Response{Message: "Hello World", Test: &Data{ 41 | Message: "Hello World", 42 | Checker: Potato, 43 | }} 44 | 45 | err = srv.Send(response) 46 | assert.NoError(s.t, err) 47 | } 48 | return nil 49 | } 50 | 51 | func (s svc) Testy(_ context.Context, _ *SearchResponse) (*StockPricesWrapper, error) { 52 | panic("not implemented") 53 | } 54 | 55 | func (s svc) Search(_ context.Context, req *SearchResponse, srv *SearchServer) error { 56 | assert.Equal(s.t, 1, len(req.Results)) 57 | for i := 0; i < 10; i++ { 58 | err := srv.Send(&Response{Message: "Hello World", Test: &Data{ 59 | Message: "Hello World", 60 | Checker: Potato, 61 | }}) 62 | assert.NoError(s.t, err) 63 | } 64 | return srv.CloseSend() 65 | } 66 | 67 | func (s svc) Upload(_ context.Context, srv *UploadServer) error { 68 | received := 0 69 | for { 70 | res, err := srv.Recv() 71 | if err == io.EOF { 72 | assert.Equal(s.t, 11, received) 73 | return srv.CloseAndSend(&Response{Message: "Hello World", Test: &Data{}}) 74 | } 75 | received++ 76 | assert.NoError(s.t, err) 77 | assert.Equal(s.t, "Hello World", res.Message) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/generator/test/test.frpc.go: -------------------------------------------------------------------------------- 1 | // Code generated by fRPC Go v0.10.0, DO NOT EDIT. 2 | // source: examples/test/test.proto 3 | 4 | package test 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "sync" 10 | "context" 11 | "github.com/loopholelabs/polyglot/v2" 12 | 13 | "crypto/tls" 14 | "github.com/loopholelabs/frisbee-go" 15 | "github.com/loopholelabs/frisbee-go/pkg/packet" 16 | "github.com/loopholelabs/logging/types" 17 | 18 | "sync/atomic" 19 | "io" 20 | ) 21 | 22 | var ( 23 | ErrDecodeNil = errors.New("cannot decode into a nil root struct") 24 | ) 25 | 26 | type Test uint32 27 | 28 | const ( 29 | Potato = Test(0) 30 | Monkey = Test(1) 31 | ) 32 | 33 | type RequestCorpus uint32 34 | 35 | const ( 36 | RequestUNIVERSAL = RequestCorpus(0) 37 | RequestWEB = RequestCorpus(1) 38 | RequestIMAGES = RequestCorpus(2) 39 | RequestLOCAL = RequestCorpus(3) 40 | RequestNEWS = RequestCorpus(4) 41 | RequestPRODUCTS = RequestCorpus(5) 42 | RequestVIDEO = RequestCorpus(6) 43 | ) 44 | 45 | type Request struct { 46 | error error 47 | flags uint8 48 | 49 | Message string 50 | Corpus RequestCorpus 51 | } 52 | 53 | func NewRequest() *Request { 54 | return &Request{} 55 | } 56 | 57 | func (x *Request) Error(b *polyglot.Buffer, err error) { 58 | polyglot.Encoder(b).Error(err) 59 | } 60 | 61 | func (x *Request) Encode(b *polyglot.Buffer) { 62 | if x == nil { 63 | polyglot.Encoder(b).Nil() 64 | } else { 65 | if x.error != nil { 66 | polyglot.Encoder(b).Error(x.error) 67 | return 68 | } 69 | polyglot.Encoder(b).Uint8(x.flags) 70 | polyglot.Encoder(b).String(x.Message).Uint32(uint32(x.Corpus)) 71 | } 72 | } 73 | 74 | func (x *Request) Decode(b []byte) error { 75 | if x == nil { 76 | return ErrDecodeNil 77 | } 78 | return x.decode(polyglot.Decoder(b)) 79 | } 80 | 81 | func (x *Request) decode(d *polyglot.BufferDecoder) error { 82 | if d.Nil() { 83 | return nil 84 | } 85 | 86 | var err error 87 | x.error, err = d.Error() 88 | if err == nil { 89 | return nil 90 | } 91 | x.flags, err = d.Uint8() 92 | if err != nil { 93 | return err 94 | } 95 | x.Message, err = d.String() 96 | if err != nil { 97 | return err 98 | } 99 | var CorpusTemp uint32 100 | CorpusTemp, err = d.Uint32() 101 | x.Corpus = RequestCorpus(CorpusTemp) 102 | if err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | type Response struct { 109 | error error 110 | flags uint8 111 | 112 | Message string 113 | Test *Data 114 | } 115 | 116 | func NewResponse() *Response { 117 | return &Response{} 118 | } 119 | 120 | func (x *Response) Error(b *polyglot.Buffer, err error) { 121 | polyglot.Encoder(b).Error(err) 122 | } 123 | 124 | func (x *Response) Encode(b *polyglot.Buffer) { 125 | if x == nil { 126 | polyglot.Encoder(b).Nil() 127 | } else { 128 | if x.error != nil { 129 | polyglot.Encoder(b).Error(x.error) 130 | return 131 | } 132 | polyglot.Encoder(b).Uint8(x.flags) 133 | polyglot.Encoder(b).String(x.Message) 134 | x.Test.Encode(b) 135 | } 136 | } 137 | 138 | func (x *Response) Decode(b []byte) error { 139 | if x == nil { 140 | return ErrDecodeNil 141 | } 142 | return x.decode(polyglot.Decoder(b)) 143 | } 144 | 145 | func (x *Response) decode(d *polyglot.BufferDecoder) error { 146 | if d.Nil() { 147 | return nil 148 | } 149 | 150 | var err error 151 | x.error, err = d.Error() 152 | if err == nil { 153 | return nil 154 | } 155 | x.flags, err = d.Uint8() 156 | if err != nil { 157 | return err 158 | } 159 | x.Message, err = d.String() 160 | if err != nil { 161 | return err 162 | } 163 | if !d.Nil() { 164 | x.Test = NewData() 165 | err = x.Test.decode(d) 166 | if err != nil { 167 | return err 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | type Data struct { 174 | error error 175 | flags uint8 176 | 177 | Message string 178 | Checker Test 179 | } 180 | 181 | func NewData() *Data { 182 | return &Data{} 183 | } 184 | 185 | func (x *Data) Error(b *polyglot.Buffer, err error) { 186 | polyglot.Encoder(b).Error(err) 187 | } 188 | 189 | func (x *Data) Encode(b *polyglot.Buffer) { 190 | if x == nil { 191 | polyglot.Encoder(b).Nil() 192 | } else { 193 | if x.error != nil { 194 | polyglot.Encoder(b).Error(x.error) 195 | return 196 | } 197 | polyglot.Encoder(b).Uint8(x.flags) 198 | polyglot.Encoder(b).String(x.Message).Uint32(uint32(x.Checker)) 199 | } 200 | } 201 | 202 | func (x *Data) Decode(b []byte) error { 203 | if x == nil { 204 | return ErrDecodeNil 205 | } 206 | return x.decode(polyglot.Decoder(b)) 207 | } 208 | 209 | func (x *Data) decode(d *polyglot.BufferDecoder) error { 210 | if d.Nil() { 211 | return nil 212 | } 213 | 214 | var err error 215 | x.error, err = d.Error() 216 | if err == nil { 217 | return nil 218 | } 219 | x.flags, err = d.Uint8() 220 | if err != nil { 221 | return err 222 | } 223 | x.Message, err = d.String() 224 | if err != nil { 225 | return err 226 | } 227 | var CheckerTemp uint32 228 | CheckerTemp, err = d.Uint32() 229 | x.Checker = Test(CheckerTemp) 230 | if err != nil { 231 | return err 232 | } 233 | return nil 234 | } 235 | 236 | type MyMessage1EnumAllowingAlias uint32 237 | 238 | const ( 239 | MyMessage1UNKNOWN = MyMessage1EnumAllowingAlias(0) 240 | MyMessage1STARTED = MyMessage1EnumAllowingAlias(1) 241 | MyMessage1RUNNING = MyMessage1EnumAllowingAlias(2) 242 | ) 243 | 244 | type MyMessage1 struct { 245 | error error 246 | flags uint8 247 | } 248 | 249 | func NewMyMessage1() *MyMessage1 { 250 | return &MyMessage1{} 251 | } 252 | 253 | func (x *MyMessage1) Error(b *polyglot.Buffer, err error) { 254 | polyglot.Encoder(b).Error(err) 255 | } 256 | 257 | func (x *MyMessage1) Encode(b *polyglot.Buffer) { 258 | if x == nil { 259 | polyglot.Encoder(b).Nil() 260 | } else { 261 | if x.error != nil { 262 | polyglot.Encoder(b).Error(x.error) 263 | return 264 | } 265 | polyglot.Encoder(b).Uint8(x.flags) 266 | } 267 | } 268 | 269 | func (x *MyMessage1) Decode(b []byte) error { 270 | if x == nil { 271 | return ErrDecodeNil 272 | } 273 | return x.decode(polyglot.Decoder(b)) 274 | } 275 | 276 | func (x *MyMessage1) decode(d *polyglot.BufferDecoder) error { 277 | if d.Nil() { 278 | return nil 279 | } 280 | 281 | var err error 282 | x.error, err = d.Error() 283 | if err == nil { 284 | return nil 285 | } 286 | x.flags, err = d.Uint8() 287 | if err != nil { 288 | return err 289 | } 290 | return nil 291 | } 292 | 293 | type MyMessage2EnumNotAllowingAlias uint32 294 | 295 | const ( 296 | MyMessage2UNKNOWN = MyMessage2EnumNotAllowingAlias(0) 297 | MyMessage2STARTED = MyMessage2EnumNotAllowingAlias(1) 298 | ) 299 | 300 | type MyMessage2 struct { 301 | error error 302 | flags uint8 303 | } 304 | 305 | func NewMyMessage2() *MyMessage2 { 306 | return &MyMessage2{} 307 | } 308 | 309 | func (x *MyMessage2) Error(b *polyglot.Buffer, err error) { 310 | polyglot.Encoder(b).Error(err) 311 | } 312 | 313 | func (x *MyMessage2) Encode(b *polyglot.Buffer) { 314 | if x == nil { 315 | polyglot.Encoder(b).Nil() 316 | } else { 317 | if x.error != nil { 318 | polyglot.Encoder(b).Error(x.error) 319 | return 320 | } 321 | polyglot.Encoder(b).Uint8(x.flags) 322 | } 323 | } 324 | 325 | func (x *MyMessage2) Decode(b []byte) error { 326 | if x == nil { 327 | return ErrDecodeNil 328 | } 329 | return x.decode(polyglot.Decoder(b)) 330 | } 331 | 332 | func (x *MyMessage2) decode(d *polyglot.BufferDecoder) error { 333 | if d.Nil() { 334 | return nil 335 | } 336 | 337 | var err error 338 | x.error, err = d.Error() 339 | if err == nil { 340 | return nil 341 | } 342 | x.flags, err = d.Uint8() 343 | if err != nil { 344 | return err 345 | } 346 | return nil 347 | } 348 | 349 | type SearchResponseResult struct { 350 | error error 351 | flags uint8 352 | 353 | Url string 354 | Title string 355 | Snippets []string 356 | } 357 | 358 | func NewSearchResponseResult() *SearchResponseResult { 359 | return &SearchResponseResult{} 360 | } 361 | 362 | func (x *SearchResponseResult) Error(b *polyglot.Buffer, err error) { 363 | polyglot.Encoder(b).Error(err) 364 | } 365 | 366 | func (x *SearchResponseResult) Encode(b *polyglot.Buffer) { 367 | if x == nil { 368 | polyglot.Encoder(b).Nil() 369 | } else { 370 | if x.error != nil { 371 | polyglot.Encoder(b).Error(x.error) 372 | return 373 | } 374 | polyglot.Encoder(b).Uint8(x.flags) 375 | polyglot.Encoder(b).String(x.Url).String(x.Title) 376 | polyglot.Encoder(b).Slice(uint32(len(x.Snippets)), polyglot.StringKind) 377 | for _, v := range x.Snippets { 378 | polyglot.Encoder(b).String(v) 379 | } 380 | } 381 | } 382 | 383 | func (x *SearchResponseResult) Decode(b []byte) error { 384 | if x == nil { 385 | return ErrDecodeNil 386 | } 387 | return x.decode(polyglot.Decoder(b)) 388 | } 389 | 390 | func (x *SearchResponseResult) decode(d *polyglot.BufferDecoder) error { 391 | if d.Nil() { 392 | return nil 393 | } 394 | 395 | var err error 396 | x.error, err = d.Error() 397 | if err == nil { 398 | return nil 399 | } 400 | x.flags, err = d.Uint8() 401 | if err != nil { 402 | return err 403 | } 404 | x.Url, err = d.String() 405 | if err != nil { 406 | return err 407 | } 408 | x.Title, err = d.String() 409 | if err != nil { 410 | return err 411 | } 412 | var sliceSize uint32 413 | sliceSize, err = d.Slice(polyglot.StringKind) 414 | if err != nil { 415 | return err 416 | } 417 | if uint32(len(x.Snippets)) != sliceSize { 418 | x.Snippets = make([]string, sliceSize) 419 | } 420 | for i := uint32(0); i < sliceSize; i++ { 421 | x.Snippets[i], err = d.String() 422 | if err != nil { 423 | return err 424 | } 425 | } 426 | return nil 427 | } 428 | 429 | type SearchResponse struct { 430 | error error 431 | flags uint8 432 | 433 | Results []*SearchResponseResult 434 | Results2 []*SearchResponseResult 435 | Snippets []string 436 | Snippets2 []string 437 | } 438 | 439 | func NewSearchResponse() *SearchResponse { 440 | return &SearchResponse{} 441 | } 442 | 443 | func (x *SearchResponse) Error(b *polyglot.Buffer, err error) { 444 | polyglot.Encoder(b).Error(err) 445 | } 446 | 447 | func (x *SearchResponse) Encode(b *polyglot.Buffer) { 448 | if x == nil { 449 | polyglot.Encoder(b).Nil() 450 | } else { 451 | if x.error != nil { 452 | polyglot.Encoder(b).Error(x.error) 453 | return 454 | } 455 | polyglot.Encoder(b).Uint8(x.flags) 456 | 457 | polyglot.Encoder(b).Slice(uint32(len(x.Results)), polyglot.AnyKind) 458 | for _, v := range x.Results { 459 | v.Encode(b) 460 | } 461 | polyglot.Encoder(b).Slice(uint32(len(x.Results2)), polyglot.AnyKind) 462 | for _, v := range x.Results2 { 463 | v.Encode(b) 464 | } 465 | polyglot.Encoder(b).Slice(uint32(len(x.Snippets)), polyglot.StringKind) 466 | for _, v := range x.Snippets { 467 | polyglot.Encoder(b).String(v) 468 | } 469 | polyglot.Encoder(b).Slice(uint32(len(x.Snippets2)), polyglot.StringKind) 470 | for _, v := range x.Snippets2 { 471 | polyglot.Encoder(b).String(v) 472 | } 473 | } 474 | } 475 | 476 | func (x *SearchResponse) Decode(b []byte) error { 477 | if x == nil { 478 | return ErrDecodeNil 479 | } 480 | return x.decode(polyglot.Decoder(b)) 481 | } 482 | 483 | func (x *SearchResponse) decode(d *polyglot.BufferDecoder) error { 484 | if d.Nil() { 485 | return nil 486 | } 487 | 488 | var err error 489 | x.error, err = d.Error() 490 | if err == nil { 491 | return nil 492 | } 493 | x.flags, err = d.Uint8() 494 | if err != nil { 495 | return err 496 | } 497 | var sliceSize uint32 498 | sliceSize, err = d.Slice(polyglot.AnyKind) 499 | if err != nil { 500 | return err 501 | } 502 | if uint32(len(x.Results)) != sliceSize { 503 | x.Results = make([]*SearchResponseResult, sliceSize) 504 | } 505 | for i := uint32(0); i < sliceSize; i++ { 506 | if x.Results[i] == nil { 507 | x.Results[i] = NewSearchResponseResult() 508 | } 509 | err = x.Results[i].decode(d) 510 | if err != nil { 511 | return err 512 | } 513 | } 514 | sliceSize, err = d.Slice(polyglot.AnyKind) 515 | if err != nil { 516 | return err 517 | } 518 | if uint32(len(x.Results2)) != sliceSize { 519 | x.Results2 = make([]*SearchResponseResult, sliceSize) 520 | } 521 | for i := uint32(0); i < sliceSize; i++ { 522 | if x.Results2[i] == nil { 523 | x.Results2[i] = NewSearchResponseResult() 524 | } 525 | err = x.Results2[i].decode(d) 526 | if err != nil { 527 | return err 528 | } 529 | } 530 | sliceSize, err = d.Slice(polyglot.StringKind) 531 | if err != nil { 532 | return err 533 | } 534 | if uint32(len(x.Snippets)) != sliceSize { 535 | x.Snippets = make([]string, sliceSize) 536 | } 537 | for i := uint32(0); i < sliceSize; i++ { 538 | x.Snippets[i], err = d.String() 539 | if err != nil { 540 | return err 541 | } 542 | } 543 | sliceSize, err = d.Slice(polyglot.StringKind) 544 | if err != nil { 545 | return err 546 | } 547 | if uint32(len(x.Snippets2)) != sliceSize { 548 | x.Snippets2 = make([]string, sliceSize) 549 | } 550 | for i := uint32(0); i < sliceSize; i++ { 551 | x.Snippets2[i], err = d.String() 552 | if err != nil { 553 | return err 554 | } 555 | } 556 | return nil 557 | } 558 | 559 | type Resulting struct { 560 | error error 561 | flags uint8 562 | 563 | Url string 564 | Title string 565 | Snippets []string 566 | } 567 | 568 | func NewResulting() *Resulting { 569 | return &Resulting{} 570 | } 571 | 572 | func (x *Resulting) Error(b *polyglot.Buffer, err error) { 573 | polyglot.Encoder(b).Error(err) 574 | } 575 | 576 | func (x *Resulting) Encode(b *polyglot.Buffer) { 577 | if x == nil { 578 | polyglot.Encoder(b).Nil() 579 | } else { 580 | if x.error != nil { 581 | polyglot.Encoder(b).Error(x.error) 582 | return 583 | } 584 | polyglot.Encoder(b).Uint8(x.flags) 585 | polyglot.Encoder(b).String(x.Url).String(x.Title) 586 | polyglot.Encoder(b).Slice(uint32(len(x.Snippets)), polyglot.StringKind) 587 | for _, v := range x.Snippets { 588 | polyglot.Encoder(b).String(v) 589 | } 590 | } 591 | } 592 | 593 | func (x *Resulting) Decode(b []byte) error { 594 | if x == nil { 595 | return ErrDecodeNil 596 | } 597 | return x.decode(polyglot.Decoder(b)) 598 | } 599 | 600 | func (x *Resulting) decode(d *polyglot.BufferDecoder) error { 601 | if d.Nil() { 602 | return nil 603 | } 604 | 605 | var err error 606 | x.error, err = d.Error() 607 | if err == nil { 608 | return nil 609 | } 610 | x.flags, err = d.Uint8() 611 | if err != nil { 612 | return err 613 | } 614 | x.Url, err = d.String() 615 | if err != nil { 616 | return err 617 | } 618 | x.Title, err = d.String() 619 | if err != nil { 620 | return err 621 | } 622 | var sliceSize uint32 623 | sliceSize, err = d.Slice(polyglot.StringKind) 624 | if err != nil { 625 | return err 626 | } 627 | if uint32(len(x.Snippets)) != sliceSize { 628 | x.Snippets = make([]string, sliceSize) 629 | } 630 | for i := uint32(0); i < sliceSize; i++ { 631 | x.Snippets[i], err = d.String() 632 | if err != nil { 633 | return err 634 | } 635 | } 636 | return nil 637 | } 638 | 639 | type SomeOtherMessage struct { 640 | error error 641 | flags uint8 642 | 643 | Result *SearchResponseResult 644 | } 645 | 646 | func NewSomeOtherMessage() *SomeOtherMessage { 647 | return &SomeOtherMessage{} 648 | } 649 | 650 | func (x *SomeOtherMessage) Error(b *polyglot.Buffer, err error) { 651 | polyglot.Encoder(b).Error(err) 652 | } 653 | 654 | func (x *SomeOtherMessage) Encode(b *polyglot.Buffer) { 655 | if x == nil { 656 | polyglot.Encoder(b).Nil() 657 | } else { 658 | if x.error != nil { 659 | polyglot.Encoder(b).Error(x.error) 660 | return 661 | } 662 | polyglot.Encoder(b).Uint8(x.flags) 663 | 664 | x.Result.Encode(b) 665 | } 666 | } 667 | 668 | func (x *SomeOtherMessage) Decode(b []byte) error { 669 | if x == nil { 670 | return ErrDecodeNil 671 | } 672 | return x.decode(polyglot.Decoder(b)) 673 | } 674 | 675 | func (x *SomeOtherMessage) decode(d *polyglot.BufferDecoder) error { 676 | if d.Nil() { 677 | return nil 678 | } 679 | 680 | var err error 681 | x.error, err = d.Error() 682 | if err == nil { 683 | return nil 684 | } 685 | x.flags, err = d.Uint8() 686 | if err != nil { 687 | return err 688 | } 689 | if !d.Nil() { 690 | x.Result = NewSearchResponseResult() 691 | err = x.Result.decode(d) 692 | if err != nil { 693 | return err 694 | } 695 | } 696 | return nil 697 | } 698 | 699 | type OuterMiddleAAInner struct { 700 | error error 701 | flags uint8 702 | 703 | Ival int64 704 | Booly bool 705 | } 706 | 707 | func NewOuterMiddleAAInner() *OuterMiddleAAInner { 708 | return &OuterMiddleAAInner{} 709 | } 710 | 711 | func (x *OuterMiddleAAInner) Error(b *polyglot.Buffer, err error) { 712 | polyglot.Encoder(b).Error(err) 713 | } 714 | 715 | func (x *OuterMiddleAAInner) Encode(b *polyglot.Buffer) { 716 | if x == nil { 717 | polyglot.Encoder(b).Nil() 718 | } else { 719 | if x.error != nil { 720 | polyglot.Encoder(b).Error(x.error) 721 | return 722 | } 723 | polyglot.Encoder(b).Uint8(x.flags) 724 | polyglot.Encoder(b).Int64(x.Ival).Bool(x.Booly) 725 | } 726 | } 727 | 728 | func (x *OuterMiddleAAInner) Decode(b []byte) error { 729 | if x == nil { 730 | return ErrDecodeNil 731 | } 732 | return x.decode(polyglot.Decoder(b)) 733 | } 734 | 735 | func (x *OuterMiddleAAInner) decode(d *polyglot.BufferDecoder) error { 736 | if d.Nil() { 737 | return nil 738 | } 739 | 740 | var err error 741 | x.error, err = d.Error() 742 | if err == nil { 743 | return nil 744 | } 745 | x.flags, err = d.Uint8() 746 | if err != nil { 747 | return err 748 | } 749 | x.Ival, err = d.Int64() 750 | if err != nil { 751 | return err 752 | } 753 | x.Booly, err = d.Bool() 754 | if err != nil { 755 | return err 756 | } 757 | return nil 758 | } 759 | 760 | type OuterMiddleAA struct { 761 | error error 762 | flags uint8 763 | 764 | Inner *OuterMiddleAAInner 765 | } 766 | 767 | func NewOuterMiddleAA() *OuterMiddleAA { 768 | return &OuterMiddleAA{} 769 | } 770 | 771 | func (x *OuterMiddleAA) Error(b *polyglot.Buffer, err error) { 772 | polyglot.Encoder(b).Error(err) 773 | } 774 | 775 | func (x *OuterMiddleAA) Encode(b *polyglot.Buffer) { 776 | if x == nil { 777 | polyglot.Encoder(b).Nil() 778 | } else { 779 | if x.error != nil { 780 | polyglot.Encoder(b).Error(x.error) 781 | return 782 | } 783 | polyglot.Encoder(b).Uint8(x.flags) 784 | 785 | x.Inner.Encode(b) 786 | } 787 | } 788 | 789 | func (x *OuterMiddleAA) Decode(b []byte) error { 790 | if x == nil { 791 | return ErrDecodeNil 792 | } 793 | return x.decode(polyglot.Decoder(b)) 794 | } 795 | 796 | func (x *OuterMiddleAA) decode(d *polyglot.BufferDecoder) error { 797 | if d.Nil() { 798 | return nil 799 | } 800 | 801 | var err error 802 | x.error, err = d.Error() 803 | if err == nil { 804 | return nil 805 | } 806 | x.flags, err = d.Uint8() 807 | if err != nil { 808 | return err 809 | } 810 | if !d.Nil() { 811 | x.Inner = NewOuterMiddleAAInner() 812 | err = x.Inner.decode(d) 813 | if err != nil { 814 | return err 815 | } 816 | } 817 | return nil 818 | } 819 | 820 | type OuterMiddleBBInner struct { 821 | error error 822 | flags uint8 823 | 824 | Ival int32 825 | Booly bool 826 | } 827 | 828 | func NewOuterMiddleBBInner() *OuterMiddleBBInner { 829 | return &OuterMiddleBBInner{} 830 | } 831 | 832 | func (x *OuterMiddleBBInner) Error(b *polyglot.Buffer, err error) { 833 | polyglot.Encoder(b).Error(err) 834 | } 835 | 836 | func (x *OuterMiddleBBInner) Encode(b *polyglot.Buffer) { 837 | if x == nil { 838 | polyglot.Encoder(b).Nil() 839 | } else { 840 | if x.error != nil { 841 | polyglot.Encoder(b).Error(x.error) 842 | return 843 | } 844 | polyglot.Encoder(b).Uint8(x.flags) 845 | polyglot.Encoder(b).Int32(x.Ival).Bool(x.Booly) 846 | } 847 | } 848 | 849 | func (x *OuterMiddleBBInner) Decode(b []byte) error { 850 | if x == nil { 851 | return ErrDecodeNil 852 | } 853 | return x.decode(polyglot.Decoder(b)) 854 | } 855 | 856 | func (x *OuterMiddleBBInner) decode(d *polyglot.BufferDecoder) error { 857 | if d.Nil() { 858 | return nil 859 | } 860 | 861 | var err error 862 | x.error, err = d.Error() 863 | if err == nil { 864 | return nil 865 | } 866 | x.flags, err = d.Uint8() 867 | if err != nil { 868 | return err 869 | } 870 | x.Ival, err = d.Int32() 871 | if err != nil { 872 | return err 873 | } 874 | x.Booly, err = d.Bool() 875 | if err != nil { 876 | return err 877 | } 878 | return nil 879 | } 880 | 881 | type OuterMiddleBB struct { 882 | error error 883 | flags uint8 884 | 885 | Inner *OuterMiddleBBInner 886 | } 887 | 888 | func NewOuterMiddleBB() *OuterMiddleBB { 889 | return &OuterMiddleBB{} 890 | } 891 | 892 | func (x *OuterMiddleBB) Error(b *polyglot.Buffer, err error) { 893 | polyglot.Encoder(b).Error(err) 894 | } 895 | 896 | func (x *OuterMiddleBB) Encode(b *polyglot.Buffer) { 897 | if x == nil { 898 | polyglot.Encoder(b).Nil() 899 | } else { 900 | if x.error != nil { 901 | polyglot.Encoder(b).Error(x.error) 902 | return 903 | } 904 | polyglot.Encoder(b).Uint8(x.flags) 905 | 906 | x.Inner.Encode(b) 907 | } 908 | } 909 | 910 | func (x *OuterMiddleBB) Decode(b []byte) error { 911 | if x == nil { 912 | return ErrDecodeNil 913 | } 914 | return x.decode(polyglot.Decoder(b)) 915 | } 916 | 917 | func (x *OuterMiddleBB) decode(d *polyglot.BufferDecoder) error { 918 | if d.Nil() { 919 | return nil 920 | } 921 | 922 | var err error 923 | x.error, err = d.Error() 924 | if err == nil { 925 | return nil 926 | } 927 | x.flags, err = d.Uint8() 928 | if err != nil { 929 | return err 930 | } 931 | if !d.Nil() { 932 | x.Inner = NewOuterMiddleBBInner() 933 | err = x.Inner.decode(d) 934 | if err != nil { 935 | return err 936 | } 937 | } 938 | return nil 939 | } 940 | 941 | type Outer struct { 942 | error error 943 | flags uint8 944 | 945 | A *OuterMiddleAA 946 | B *OuterMiddleBB 947 | } 948 | 949 | func NewOuter() *Outer { 950 | return &Outer{} 951 | } 952 | 953 | func (x *Outer) Error(b *polyglot.Buffer, err error) { 954 | polyglot.Encoder(b).Error(err) 955 | } 956 | 957 | func (x *Outer) Encode(b *polyglot.Buffer) { 958 | if x == nil { 959 | polyglot.Encoder(b).Nil() 960 | } else { 961 | if x.error != nil { 962 | polyglot.Encoder(b).Error(x.error) 963 | return 964 | } 965 | polyglot.Encoder(b).Uint8(x.flags) 966 | 967 | x.A.Encode(b) 968 | x.B.Encode(b) 969 | } 970 | } 971 | 972 | func (x *Outer) Decode(b []byte) error { 973 | if x == nil { 974 | return ErrDecodeNil 975 | } 976 | return x.decode(polyglot.Decoder(b)) 977 | } 978 | 979 | func (x *Outer) decode(d *polyglot.BufferDecoder) error { 980 | if d.Nil() { 981 | return nil 982 | } 983 | 984 | var err error 985 | x.error, err = d.Error() 986 | if err == nil { 987 | return nil 988 | } 989 | x.flags, err = d.Uint8() 990 | if err != nil { 991 | return err 992 | } 993 | if !d.Nil() { 994 | x.A = NewOuterMiddleAA() 995 | err = x.A.decode(d) 996 | if err != nil { 997 | return err 998 | } 999 | } 1000 | if !d.Nil() { 1001 | x.B = NewOuterMiddleBB() 1002 | err = x.B.decode(d) 1003 | if err != nil { 1004 | return err 1005 | } 1006 | } 1007 | return nil 1008 | } 1009 | 1010 | type SampleMessage struct { 1011 | error error 1012 | flags uint8 1013 | 1014 | Name string 1015 | Potato string 1016 | } 1017 | 1018 | func NewSampleMessage() *SampleMessage { 1019 | return &SampleMessage{} 1020 | } 1021 | 1022 | func (x *SampleMessage) Error(b *polyglot.Buffer, err error) { 1023 | polyglot.Encoder(b).Error(err) 1024 | } 1025 | 1026 | func (x *SampleMessage) Encode(b *polyglot.Buffer) { 1027 | if x == nil { 1028 | polyglot.Encoder(b).Nil() 1029 | } else { 1030 | if x.error != nil { 1031 | polyglot.Encoder(b).Error(x.error) 1032 | return 1033 | } 1034 | polyglot.Encoder(b).Uint8(x.flags) 1035 | polyglot.Encoder(b).String(x.Name).String(x.Potato) 1036 | } 1037 | } 1038 | 1039 | func (x *SampleMessage) Decode(b []byte) error { 1040 | if x == nil { 1041 | return ErrDecodeNil 1042 | } 1043 | return x.decode(polyglot.Decoder(b)) 1044 | } 1045 | 1046 | func (x *SampleMessage) decode(d *polyglot.BufferDecoder) error { 1047 | if d.Nil() { 1048 | return nil 1049 | } 1050 | 1051 | var err error 1052 | x.error, err = d.Error() 1053 | if err == nil { 1054 | return nil 1055 | } 1056 | x.flags, err = d.Uint8() 1057 | if err != nil { 1058 | return err 1059 | } 1060 | x.Name, err = d.String() 1061 | if err != nil { 1062 | return err 1063 | } 1064 | x.Potato, err = d.String() 1065 | if err != nil { 1066 | return err 1067 | } 1068 | return nil 1069 | } 1070 | 1071 | type TestPotatoPricesMap map[string]Test 1072 | 1073 | func NewTestPotatoPricesMap(size uint32) map[string]Test { 1074 | return make(map[string]Test, size) 1075 | } 1076 | 1077 | func (x TestPotatoPricesMap) Encode(b *polyglot.Buffer) { 1078 | if x == nil { 1079 | polyglot.Encoder(b).Map(0, polyglot.StringKind, polyglot.Uint32Kind) 1080 | } else { 1081 | polyglot.Encoder(b).Map(uint32(len(x)), polyglot.StringKind, polyglot.Uint32Kind) 1082 | for k, v := range x { 1083 | polyglot.Encoder(b).String(k) 1084 | polyglot.Encoder(b).Uint32(uint32(v)) 1085 | } 1086 | } 1087 | } 1088 | 1089 | func (x TestPotatoPricesMap) decode(d *polyglot.BufferDecoder, size uint32) error { 1090 | if size == 0 { 1091 | return nil 1092 | } 1093 | var k string 1094 | var v Test 1095 | var ValueTemp uint32 1096 | var err error 1097 | for i := uint32(0); i < size; i++ { 1098 | k, err = d.String() 1099 | if err != nil { 1100 | return err 1101 | } 1102 | ValueTemp, err = d.Uint32() 1103 | v = Test(ValueTemp) 1104 | if err != nil { 1105 | return err 1106 | } 1107 | x[k] = v 1108 | } 1109 | return nil 1110 | } 1111 | 1112 | type TestPotato struct { 1113 | error error 1114 | flags uint8 1115 | 1116 | Prices TestPotatoPricesMap 1117 | } 1118 | 1119 | func NewTestPotato() *TestPotato { 1120 | return &TestPotato{} 1121 | } 1122 | 1123 | func (x *TestPotato) Error(b *polyglot.Buffer, err error) { 1124 | polyglot.Encoder(b).Error(err) 1125 | } 1126 | 1127 | func (x *TestPotato) Encode(b *polyglot.Buffer) { 1128 | if x == nil { 1129 | polyglot.Encoder(b).Nil() 1130 | } else { 1131 | if x.error != nil { 1132 | polyglot.Encoder(b).Error(x.error) 1133 | return 1134 | } 1135 | polyglot.Encoder(b).Uint8(x.flags) 1136 | 1137 | x.Prices.Encode(b) 1138 | } 1139 | } 1140 | 1141 | func (x *TestPotato) Decode(b []byte) error { 1142 | if x == nil { 1143 | return ErrDecodeNil 1144 | } 1145 | return x.decode(polyglot.Decoder(b)) 1146 | } 1147 | 1148 | func (x *TestPotato) decode(d *polyglot.BufferDecoder) error { 1149 | if d.Nil() { 1150 | return nil 1151 | } 1152 | 1153 | var err error 1154 | x.error, err = d.Error() 1155 | if err == nil { 1156 | return nil 1157 | } 1158 | x.flags, err = d.Uint8() 1159 | if err != nil { 1160 | return err 1161 | } 1162 | if !d.Nil() { 1163 | PricesSize, err := d.Map(polyglot.StringKind, polyglot.Uint32Kind) 1164 | if err != nil { 1165 | return err 1166 | } 1167 | x.Prices = NewTestPotatoPricesMap(PricesSize) 1168 | err = x.Prices.decode(d, PricesSize) 1169 | if err != nil { 1170 | return err 1171 | } 1172 | } 1173 | return nil 1174 | } 1175 | 1176 | type StockPricesPricesMap map[string]float64 1177 | 1178 | func NewStockPricesPricesMap(size uint32) map[string]float64 { 1179 | return make(map[string]float64, size) 1180 | } 1181 | 1182 | func (x StockPricesPricesMap) Encode(b *polyglot.Buffer) { 1183 | if x == nil { 1184 | polyglot.Encoder(b).Map(0, polyglot.StringKind, polyglot.Float64Kind) 1185 | } else { 1186 | polyglot.Encoder(b).Map(uint32(len(x)), polyglot.StringKind, polyglot.Float64Kind) 1187 | for k, v := range x { 1188 | polyglot.Encoder(b).String(k) 1189 | polyglot.Encoder(b).Float64(v) 1190 | } 1191 | } 1192 | } 1193 | 1194 | func (x StockPricesPricesMap) decode(d *polyglot.BufferDecoder, size uint32) error { 1195 | if size == 0 { 1196 | return nil 1197 | } 1198 | var k string 1199 | var v float64 1200 | var err error 1201 | for i := uint32(0); i < size; i++ { 1202 | k, err = d.String() 1203 | if err != nil { 1204 | return err 1205 | } 1206 | v, err = d.Float64() 1207 | if err != nil { 1208 | return err 1209 | } 1210 | x[k] = v 1211 | } 1212 | return nil 1213 | } 1214 | 1215 | type StockPrices struct { 1216 | error error 1217 | flags uint8 1218 | 1219 | Prices StockPricesPricesMap 1220 | } 1221 | 1222 | func NewStockPrices() *StockPrices { 1223 | return &StockPrices{} 1224 | } 1225 | 1226 | func (x *StockPrices) Error(b *polyglot.Buffer, err error) { 1227 | polyglot.Encoder(b).Error(err) 1228 | } 1229 | 1230 | func (x *StockPrices) Encode(b *polyglot.Buffer) { 1231 | if x == nil { 1232 | polyglot.Encoder(b).Nil() 1233 | } else { 1234 | if x.error != nil { 1235 | polyglot.Encoder(b).Error(x.error) 1236 | return 1237 | } 1238 | polyglot.Encoder(b).Uint8(x.flags) 1239 | 1240 | x.Prices.Encode(b) 1241 | } 1242 | } 1243 | 1244 | func (x *StockPrices) Decode(b []byte) error { 1245 | if x == nil { 1246 | return ErrDecodeNil 1247 | } 1248 | return x.decode(polyglot.Decoder(b)) 1249 | } 1250 | 1251 | func (x *StockPrices) decode(d *polyglot.BufferDecoder) error { 1252 | if d.Nil() { 1253 | return nil 1254 | } 1255 | 1256 | var err error 1257 | x.error, err = d.Error() 1258 | if err == nil { 1259 | return nil 1260 | } 1261 | x.flags, err = d.Uint8() 1262 | if err != nil { 1263 | return err 1264 | } 1265 | if !d.Nil() { 1266 | PricesSize, err := d.Map(polyglot.StringKind, polyglot.Float64Kind) 1267 | if err != nil { 1268 | return err 1269 | } 1270 | x.Prices = NewStockPricesPricesMap(PricesSize) 1271 | err = x.Prices.decode(d, PricesSize) 1272 | if err != nil { 1273 | return err 1274 | } 1275 | } 1276 | return nil 1277 | } 1278 | 1279 | type StockPricesWrapper struct { 1280 | error error 1281 | flags uint8 1282 | 1283 | SPrices []*StockPrices 1284 | } 1285 | 1286 | func NewStockPricesWrapper() *StockPricesWrapper { 1287 | return &StockPricesWrapper{} 1288 | } 1289 | 1290 | func (x *StockPricesWrapper) Error(b *polyglot.Buffer, err error) { 1291 | polyglot.Encoder(b).Error(err) 1292 | } 1293 | 1294 | func (x *StockPricesWrapper) Encode(b *polyglot.Buffer) { 1295 | if x == nil { 1296 | polyglot.Encoder(b).Nil() 1297 | } else { 1298 | if x.error != nil { 1299 | polyglot.Encoder(b).Error(x.error) 1300 | return 1301 | } 1302 | polyglot.Encoder(b).Uint8(x.flags) 1303 | 1304 | polyglot.Encoder(b).Slice(uint32(len(x.SPrices)), polyglot.AnyKind) 1305 | for _, v := range x.SPrices { 1306 | v.Encode(b) 1307 | } 1308 | } 1309 | } 1310 | 1311 | func (x *StockPricesWrapper) Decode(b []byte) error { 1312 | if x == nil { 1313 | return ErrDecodeNil 1314 | } 1315 | return x.decode(polyglot.Decoder(b)) 1316 | } 1317 | 1318 | func (x *StockPricesWrapper) decode(d *polyglot.BufferDecoder) error { 1319 | if d.Nil() { 1320 | return nil 1321 | } 1322 | 1323 | var err error 1324 | x.error, err = d.Error() 1325 | if err == nil { 1326 | return nil 1327 | } 1328 | x.flags, err = d.Uint8() 1329 | if err != nil { 1330 | return err 1331 | } 1332 | var sliceSize uint32 1333 | sliceSize, err = d.Slice(polyglot.AnyKind) 1334 | if err != nil { 1335 | return err 1336 | } 1337 | if uint32(len(x.SPrices)) != sliceSize { 1338 | x.SPrices = make([]*StockPrices, sliceSize) 1339 | } 1340 | for i := uint32(0); i < sliceSize; i++ { 1341 | if x.SPrices[i] == nil { 1342 | x.SPrices[i] = NewStockPrices() 1343 | } 1344 | err = x.SPrices[i].decode(d) 1345 | if err != nil { 1346 | return err 1347 | } 1348 | } 1349 | return nil 1350 | } 1351 | 1352 | type StockPricesSuperWrapPricesMap map[string]*StockPricesWrapper 1353 | 1354 | func NewStockPricesSuperWrapPricesMap(size uint32) map[string]*StockPricesWrapper { 1355 | return make(map[string]*StockPricesWrapper, size) 1356 | } 1357 | 1358 | func (x StockPricesSuperWrapPricesMap) Encode(b *polyglot.Buffer) { 1359 | if x == nil { 1360 | polyglot.Encoder(b).Map(0, polyglot.StringKind, polyglot.AnyKind) 1361 | } else { 1362 | polyglot.Encoder(b).Map(uint32(len(x)), polyglot.StringKind, polyglot.AnyKind) 1363 | for k, v := range x { 1364 | polyglot.Encoder(b).String(k) 1365 | v.Encode(b) 1366 | } 1367 | } 1368 | } 1369 | 1370 | func (x StockPricesSuperWrapPricesMap) decode(d *polyglot.BufferDecoder, size uint32) error { 1371 | if size == 0 { 1372 | return nil 1373 | } 1374 | var k string 1375 | var v *StockPricesWrapper 1376 | var err error 1377 | for i := uint32(0); i < size; i++ { 1378 | k, err = d.String() 1379 | if err != nil { 1380 | return err 1381 | } 1382 | v = NewStockPricesWrapper() 1383 | err = v.decode(d) 1384 | if err != nil { 1385 | return err 1386 | } 1387 | x[k] = v 1388 | } 1389 | return nil 1390 | } 1391 | 1392 | type StockPricesSuperWrap struct { 1393 | error error 1394 | flags uint8 1395 | 1396 | Prices StockPricesSuperWrapPricesMap 1397 | } 1398 | 1399 | func NewStockPricesSuperWrap() *StockPricesSuperWrap { 1400 | return &StockPricesSuperWrap{} 1401 | } 1402 | 1403 | func (x *StockPricesSuperWrap) Error(b *polyglot.Buffer, err error) { 1404 | polyglot.Encoder(b).Error(err) 1405 | } 1406 | 1407 | func (x *StockPricesSuperWrap) Encode(b *polyglot.Buffer) { 1408 | if x == nil { 1409 | polyglot.Encoder(b).Nil() 1410 | } else { 1411 | if x.error != nil { 1412 | polyglot.Encoder(b).Error(x.error) 1413 | return 1414 | } 1415 | polyglot.Encoder(b).Uint8(x.flags) 1416 | 1417 | x.Prices.Encode(b) 1418 | } 1419 | } 1420 | 1421 | func (x *StockPricesSuperWrap) Decode(b []byte) error { 1422 | if x == nil { 1423 | return ErrDecodeNil 1424 | } 1425 | return x.decode(polyglot.Decoder(b)) 1426 | } 1427 | 1428 | func (x *StockPricesSuperWrap) decode(d *polyglot.BufferDecoder) error { 1429 | if d.Nil() { 1430 | return nil 1431 | } 1432 | 1433 | var err error 1434 | x.error, err = d.Error() 1435 | if err == nil { 1436 | return nil 1437 | } 1438 | x.flags, err = d.Uint8() 1439 | if err != nil { 1440 | return err 1441 | } 1442 | if !d.Nil() { 1443 | PricesSize, err := d.Map(polyglot.StringKind, polyglot.AnyKind) 1444 | if err != nil { 1445 | return err 1446 | } 1447 | x.Prices = NewStockPricesSuperWrapPricesMap(PricesSize) 1448 | err = x.Prices.decode(d, PricesSize) 1449 | if err != nil { 1450 | return err 1451 | } 1452 | } 1453 | return nil 1454 | } 1455 | 1456 | type EchoService interface { 1457 | Echo(context.Context, *Request) (*Response, error) 1458 | 1459 | EchoStream(context.Context, *EchoStreamServer) error 1460 | Testy(context.Context, *SearchResponse) (*StockPricesWrapper, error) 1461 | 1462 | Search(context.Context, *SearchResponse, *SearchServer) error 1463 | 1464 | Upload(context.Context, *UploadServer) error 1465 | } 1466 | 1467 | const ConnectionContextKey int = 1000 1468 | const StreamContextKey int = 1001 1469 | 1470 | func SetErrorFlag(flags uint8, error bool) uint8 { 1471 | return flags | 0x2 1472 | } 1473 | func HasErrorFlag(flags uint8) bool { 1474 | return flags&(1<<1) == 1 1475 | } 1476 | 1477 | type RPCStreamOpen struct { 1478 | operation uint16 1479 | } 1480 | 1481 | func (x *RPCStreamOpen) Error(b *polyglot.Buffer, err error) { 1482 | polyglot.Encoder(b).Error(err) 1483 | } 1484 | 1485 | func (x *RPCStreamOpen) Encode(b *polyglot.Buffer) { 1486 | polyglot.Encoder(b).Uint16(x.operation) 1487 | } 1488 | 1489 | func (x *RPCStreamOpen) Decode(b []byte) error { 1490 | if x == nil { 1491 | return ErrDecodeNil 1492 | } 1493 | d := polyglot.Decoder(b) 1494 | return x.decode(d) 1495 | } 1496 | 1497 | func (x *RPCStreamOpen) decode(d *polyglot.BufferDecoder) error { 1498 | var err error 1499 | x.operation, err = d.Uint16() 1500 | return err 1501 | } 1502 | 1503 | type Server struct { 1504 | server *frisbee.Server 1505 | wg sync.WaitGroup 1506 | } 1507 | 1508 | func NewServer(echoService EchoService, tlsConfig *tls.Config, logger types.Logger) (*Server, error) { 1509 | s := new(Server) 1510 | table := make(frisbee.HandlerTable) 1511 | 1512 | table[10] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 1513 | req := NewRequest() 1514 | err := req.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 1515 | if err == nil { 1516 | var res *Response 1517 | outgoing = incoming 1518 | outgoing.Content.Reset() 1519 | res, err = echoService.Echo(ctx, req) 1520 | if err != nil { 1521 | if _, ok := err.(CloseError); ok { 1522 | action = frisbee.CLOSE 1523 | } 1524 | res.Error(outgoing.Content, err) 1525 | } else { 1526 | res.Encode(outgoing.Content) 1527 | } 1528 | outgoing.Metadata.ContentLength = uint32(outgoing.Content.Len()) 1529 | } 1530 | return 1531 | } 1532 | table[12] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 1533 | req := NewSearchResponse() 1534 | err := req.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 1535 | if err == nil { 1536 | var res *StockPricesWrapper 1537 | outgoing = incoming 1538 | outgoing.Content.Reset() 1539 | res, err = echoService.Testy(ctx, req) 1540 | if err != nil { 1541 | if _, ok := err.(CloseError); ok { 1542 | action = frisbee.CLOSE 1543 | } 1544 | res.Error(outgoing.Content, err) 1545 | } else { 1546 | res.Encode(outgoing.Content) 1547 | } 1548 | outgoing.Metadata.ContentLength = uint32(outgoing.Content.Len()) 1549 | } 1550 | return 1551 | } 1552 | var err error 1553 | if tlsConfig != nil { 1554 | s.server, err = frisbee.NewServer(table, context.Background(), frisbee.WithTLS(tlsConfig), frisbee.WithLogger(logger)) 1555 | if err != nil { 1556 | return nil, err 1557 | } 1558 | } else { 1559 | s.server, err = frisbee.NewServer(table, context.Background(), frisbee.WithLogger(logger)) 1560 | if err != nil { 1561 | return nil, err 1562 | } 1563 | } 1564 | 1565 | s.server.SetStreamHandler(func(ctx context.Context, stream *frisbee.Stream) { 1566 | streamCtx := context.WithValue(ctx, ConnectionContextKey, stream.Conn()) 1567 | streamCtx = context.WithValue(streamCtx, StreamContextKey, stream) 1568 | p, err := stream.ReadPacket() 1569 | if err != nil { 1570 | return 1571 | } 1572 | open := &RPCStreamOpen{} 1573 | err = open.Decode((*p.Content).Bytes()) 1574 | if err != nil { 1575 | stream.Close() 1576 | return 1577 | } 1578 | switch open.operation { 1579 | case 11: 1580 | s.createEchoStreamServer(streamCtx, echoService, stream) 1581 | case 13: 1582 | s.createSearchServer(streamCtx, echoService, stream) 1583 | case 14: 1584 | s.createUploadServer(streamCtx, echoService, stream) 1585 | } 1586 | }) 1587 | 1588 | s.server.ConnContext = func(ctx context.Context, conn *frisbee.Async) context.Context { 1589 | return context.WithValue(ctx, ConnectionContextKey, conn) 1590 | } 1591 | 1592 | return s, nil 1593 | } 1594 | 1595 | func (s *Server) SetOnClosed(f func(*frisbee.Async, error)) error { 1596 | return s.server.SetOnClosed(f) 1597 | } 1598 | 1599 | func (s *Server) SetPreWrite(f func()) error { 1600 | return s.server.SetPreWrite(f) 1601 | } 1602 | 1603 | func (s *Server) SetConcurrency(concurrency uint64) { 1604 | s.server.SetConcurrency(concurrency) 1605 | } 1606 | 1607 | func (s *Server) Start(addr string) error { 1608 | return s.server.Start(addr) 1609 | } 1610 | 1611 | func (s *Server) StartWithListener(listener net.Listener) error { 1612 | return s.server.StartWithListener(listener) 1613 | } 1614 | 1615 | func (s *Server) ServeConn(conn net.Conn) { 1616 | s.server.ServeConn(conn) 1617 | } 1618 | 1619 | func (s *Server) Shutdown() error { 1620 | err := s.server.Shutdown() 1621 | if err != nil { 1622 | return err 1623 | } 1624 | s.wg.Wait() 1625 | return nil 1626 | } 1627 | 1628 | type EchoStreamServer struct { 1629 | recv func() (*Request, error) 1630 | send func(*Response) error 1631 | 1632 | stream *frisbee.Stream 1633 | closed *atomic.Bool 1634 | } 1635 | 1636 | func (s *Server) createEchoStreamServer(ctx context.Context, echoService EchoService, stream *frisbee.Stream) { 1637 | srv := &EchoStreamServer{ 1638 | stream: stream, 1639 | } 1640 | 1641 | srv.recv = func() (*Request, error) { 1642 | p, err := srv.stream.ReadPacket() 1643 | if err != nil { 1644 | return nil, err 1645 | } 1646 | 1647 | res := NewRequest() 1648 | err = res.Decode((*p.Content).Bytes()) 1649 | if err != nil { 1650 | return nil, err 1651 | } 1652 | if errors.Is(res.error, io.EOF) { 1653 | return nil, io.EOF 1654 | } 1655 | 1656 | return res, nil 1657 | } 1658 | srv.send = func(m *Response) error { 1659 | p := packet.Get() 1660 | 1661 | m.Encode(p.Content) 1662 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 1663 | return srv.stream.WritePacket(p) 1664 | } 1665 | 1666 | s.wg.Add(1) 1667 | go func() { 1668 | err := echoService.EchoStream(ctx, srv) 1669 | if err != nil { 1670 | res := Response{error: err} 1671 | res.flags = SetErrorFlag(res.flags, true) 1672 | srv.CloseAndSend(&res) 1673 | } else { 1674 | srv.CloseSend() 1675 | } 1676 | s.wg.Done() 1677 | }() 1678 | } 1679 | 1680 | func (x *EchoStreamServer) Recv() (*Request, error) { 1681 | return x.recv() 1682 | } 1683 | 1684 | func (x *EchoStreamServer) close() { 1685 | x.stream.Close() 1686 | } 1687 | func (x *EchoStreamServer) Send(m *Response) error { 1688 | return x.send(m) 1689 | } 1690 | func (x *EchoStreamServer) CloseSend() error { 1691 | return x.send(&Response{error: io.EOF}) 1692 | } 1693 | 1694 | func (x *EchoStreamServer) CloseChannel() <-chan struct{} { 1695 | return x.stream.Conn().CloseChannel() 1696 | } 1697 | 1698 | func (x *EchoStreamServer) CloseAndSend(m *Response) error { 1699 | err := x.send(m) 1700 | if err != nil { 1701 | return err 1702 | } 1703 | return x.CloseSend() 1704 | } 1705 | 1706 | type SearchServer struct { 1707 | recv func() (*SearchResponse, error) 1708 | send func(*Response) error 1709 | 1710 | stream *frisbee.Stream 1711 | closed *atomic.Bool 1712 | } 1713 | 1714 | func (s *Server) createSearchServer(ctx context.Context, echoService EchoService, stream *frisbee.Stream) { 1715 | srv := &SearchServer{ 1716 | stream: stream, 1717 | } 1718 | 1719 | srv.send = func(m *Response) error { 1720 | p := packet.Get() 1721 | 1722 | m.Encode(p.Content) 1723 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 1724 | return srv.stream.WritePacket(p) 1725 | } 1726 | 1727 | incoming, err := stream.ReadPacket() 1728 | if err != nil { 1729 | return 1730 | } 1731 | req := NewSearchResponse() 1732 | err = req.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 1733 | s.wg.Add(1) 1734 | go func() { 1735 | 1736 | err := echoService.Search(ctx, req, srv) 1737 | if err != nil { 1738 | res := Response{error: err} 1739 | res.flags = SetErrorFlag(res.flags, true) 1740 | srv.CloseAndSend(&res) 1741 | } else { 1742 | srv.CloseSend() 1743 | } 1744 | s.wg.Done() 1745 | }() 1746 | } 1747 | 1748 | func (x *SearchServer) Send(m *Response) error { 1749 | return x.send(m) 1750 | } 1751 | func (x *SearchServer) CloseSend() error { 1752 | return x.send(&Response{error: io.EOF}) 1753 | } 1754 | 1755 | func (x *SearchServer) CloseChannel() <-chan struct{} { 1756 | return x.stream.Conn().CloseChannel() 1757 | } 1758 | 1759 | func (x *SearchServer) CloseAndSend(m *Response) error { 1760 | err := x.send(m) 1761 | if err != nil { 1762 | return err 1763 | } 1764 | return x.CloseSend() 1765 | } 1766 | 1767 | type UploadServer struct { 1768 | recv func() (*Data, error) 1769 | send func(*Response) error 1770 | 1771 | stream *frisbee.Stream 1772 | closed *atomic.Bool 1773 | } 1774 | 1775 | func (s *Server) createUploadServer(ctx context.Context, echoService EchoService, stream *frisbee.Stream) { 1776 | srv := &UploadServer{ 1777 | stream: stream, 1778 | } 1779 | 1780 | srv.recv = func() (*Data, error) { 1781 | p, err := srv.stream.ReadPacket() 1782 | if err != nil { 1783 | return nil, err 1784 | } 1785 | 1786 | res := NewData() 1787 | err = res.Decode((*p.Content).Bytes()) 1788 | if err != nil { 1789 | return nil, err 1790 | } 1791 | if errors.Is(res.error, io.EOF) { 1792 | return nil, io.EOF 1793 | } 1794 | 1795 | return res, nil 1796 | } 1797 | srv.send = func(m *Response) error { 1798 | p := packet.Get() 1799 | 1800 | m.Encode(p.Content) 1801 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 1802 | return srv.stream.WritePacket(p) 1803 | } 1804 | 1805 | s.wg.Add(1) 1806 | go func() { 1807 | err := echoService.Upload(ctx, srv) 1808 | if err != nil { 1809 | res := Response{error: err} 1810 | res.flags = SetErrorFlag(res.flags, true) 1811 | srv.CloseAndSend(&res) 1812 | } else { 1813 | srv.CloseSend() 1814 | } 1815 | s.wg.Done() 1816 | }() 1817 | } 1818 | 1819 | func (x *UploadServer) Recv() (*Data, error) { 1820 | return x.recv() 1821 | } 1822 | 1823 | func (x *UploadServer) close() { 1824 | x.stream.Close() 1825 | } 1826 | func (x *UploadServer) CloseSend() error { 1827 | return x.send(&Response{error: io.EOF}) 1828 | } 1829 | 1830 | func (x *UploadServer) CloseChannel() <-chan struct{} { 1831 | return x.stream.Conn().CloseChannel() 1832 | } 1833 | 1834 | func (x *UploadServer) CloseAndSend(m *Response) error { 1835 | err := x.send(m) 1836 | if err != nil { 1837 | return err 1838 | } 1839 | return x.CloseSend() 1840 | } 1841 | 1842 | type subEchoServiceClient struct { 1843 | client *frisbee.Client 1844 | nextEcho uint16 1845 | nextEchoMu sync.RWMutex 1846 | inflightEcho map[uint16]chan *Response 1847 | inflightEchoMu sync.RWMutex 1848 | nextTesty uint16 1849 | nextTestyMu sync.RWMutex 1850 | inflightTesty map[uint16]chan *StockPricesWrapper 1851 | inflightTestyMu sync.RWMutex 1852 | nextStreamingID uint16 1853 | nextStreamingIDMu sync.RWMutex 1854 | } 1855 | type Client struct { 1856 | *frisbee.Client 1857 | EchoService *subEchoServiceClient 1858 | } 1859 | 1860 | func NewClient(tlsConfig *tls.Config, logger types.Logger) (*Client, error) { 1861 | c := new(Client) 1862 | table := make(frisbee.HandlerTable) 1863 | 1864 | table[10] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 1865 | c.EchoService.inflightEchoMu.RLock() 1866 | if ch, ok := c.EchoService.inflightEcho[incoming.Metadata.Id]; ok { 1867 | c.EchoService.inflightEchoMu.RUnlock() 1868 | res := NewResponse() 1869 | res.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 1870 | ch <- res 1871 | } else { 1872 | c.EchoService.inflightEchoMu.RUnlock() 1873 | } 1874 | return 1875 | } 1876 | table[12] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 1877 | c.EchoService.inflightTestyMu.RLock() 1878 | if ch, ok := c.EchoService.inflightTesty[incoming.Metadata.Id]; ok { 1879 | c.EchoService.inflightTestyMu.RUnlock() 1880 | res := NewStockPricesWrapper() 1881 | res.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 1882 | ch <- res 1883 | } else { 1884 | c.EchoService.inflightTestyMu.RUnlock() 1885 | } 1886 | return 1887 | } 1888 | var err error 1889 | if tlsConfig != nil { 1890 | c.Client, err = frisbee.NewClient(table, context.Background(), frisbee.WithTLS(tlsConfig), frisbee.WithLogger(logger)) 1891 | if err != nil { 1892 | return nil, err 1893 | } 1894 | } else { 1895 | c.Client, err = frisbee.NewClient(table, context.Background(), frisbee.WithLogger(logger)) 1896 | if err != nil { 1897 | return nil, err 1898 | } 1899 | } 1900 | 1901 | c.EchoService = new(subEchoServiceClient) 1902 | c.EchoService.client = c.Client 1903 | c.EchoService.nextEchoMu.Lock() 1904 | c.EchoService.nextEcho = 0 1905 | c.EchoService.nextEchoMu.Unlock() 1906 | c.EchoService.inflightEcho = make(map[uint16]chan *Response) 1907 | c.EchoService.nextTestyMu.Lock() 1908 | c.EchoService.nextTesty = 0 1909 | c.EchoService.nextTestyMu.Unlock() 1910 | c.EchoService.inflightTesty = make(map[uint16]chan *StockPricesWrapper) 1911 | return c, nil 1912 | } 1913 | 1914 | func (c *Client) Connect(addr string, streamHandler ...frisbee.NewStreamHandler) error { 1915 | return c.Client.Connect(addr, func(stream *frisbee.Stream) {}) 1916 | } 1917 | 1918 | func (c *Client) FromConn(conn net.Conn, streamHandler ...frisbee.NewStreamHandler) error { 1919 | return c.Client.FromConn(conn, func(stream *frisbee.Stream) {}) 1920 | } 1921 | 1922 | func (c *subEchoServiceClient) Echo(ctx context.Context, req *Request) (res *Response, err error) { 1923 | ch := make(chan *Response, 1) 1924 | p := packet.Get() 1925 | p.Metadata.Operation = 10 1926 | 1927 | c.nextEchoMu.Lock() 1928 | c.nextEcho += 1 1929 | id := c.nextEcho 1930 | c.nextEchoMu.Unlock() 1931 | p.Metadata.Id = id 1932 | 1933 | req.Encode(p.Content) 1934 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 1935 | c.inflightEchoMu.Lock() 1936 | c.inflightEcho[id] = ch 1937 | c.inflightEchoMu.Unlock() 1938 | err = c.client.WritePacket(p) 1939 | if err != nil { 1940 | packet.Put(p) 1941 | return 1942 | } 1943 | select { 1944 | case <-c.client.CloseChannel(): 1945 | err = c.client.Error() 1946 | case res = <-ch: 1947 | err = res.error 1948 | case <-ctx.Done(): 1949 | err = ctx.Err() 1950 | } 1951 | c.inflightEchoMu.Lock() 1952 | delete(c.inflightEcho, id) 1953 | c.inflightEchoMu.Unlock() 1954 | packet.Put(p) 1955 | return 1956 | } 1957 | 1958 | func (c *subEchoServiceClient) EchoStream(ctx context.Context, req *Request) (*EchoStreamClient, error) { 1959 | p := packet.Get() 1960 | 1961 | c.nextStreamingIDMu.Lock() 1962 | c.nextStreamingID += 1 1963 | id := c.nextStreamingID 1964 | c.nextStreamingIDMu.Unlock() 1965 | 1966 | open := &RPCStreamOpen{operation: 11} 1967 | 1968 | open.Encode(p.Content) 1969 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 1970 | 1971 | fStream := c.client.Stream(id) 1972 | fStream.WritePacket(p) 1973 | 1974 | if req != nil { 1975 | p2 := packet.Get() 1976 | req.Encode(p2.Content) 1977 | p2.Metadata.ContentLength = uint32((*p2.Content).Len()) 1978 | fStream.WritePacket(p2) 1979 | } 1980 | 1981 | stream := EchoStreamClient{ 1982 | context: ctx, 1983 | stream: fStream, 1984 | } 1985 | 1986 | stream.recv = func() (*Response, error) { 1987 | p, err := stream.stream.ReadPacket() 1988 | if err != nil { 1989 | return nil, err 1990 | } 1991 | 1992 | res := NewResponse() 1993 | err = res.Decode((*p.Content).Bytes()) 1994 | if err != nil { 1995 | return nil, err 1996 | } 1997 | if errors.Is(res.error, io.EOF) { 1998 | return nil, io.EOF 1999 | } 2000 | 2001 | return res, nil 2002 | } 2003 | 2004 | stream.close = func() { 2005 | stream.stream.Close() 2006 | } 2007 | stream.send = func(m *Request) error { 2008 | p := packet.Get() 2009 | 2010 | m.Encode(p.Content) 2011 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2012 | return stream.stream.WritePacket(p) 2013 | } 2014 | return &stream, nil 2015 | } 2016 | 2017 | type EchoStreamClient struct { 2018 | context context.Context 2019 | recv func() (*Response, error) 2020 | close func() 2021 | closed *atomic.Bool 2022 | 2023 | stream *frisbee.Stream 2024 | send func(*Request) error 2025 | } 2026 | 2027 | func (x *EchoStreamClient) CloseChannel() <-chan struct{} { 2028 | return x.stream.Conn().CloseChannel() 2029 | } 2030 | 2031 | func (x *EchoStreamClient) Recv() (*Response, error) { 2032 | return x.recv() 2033 | } 2034 | func (x *EchoStreamClient) Send(m *Request) error { 2035 | return x.send(m) 2036 | } 2037 | 2038 | func (x *EchoStreamClient) CloseSend() error { 2039 | return x.send(&Request{error: io.EOF}) 2040 | } 2041 | 2042 | func (x *EchoStreamClient) CloseAndRecv() (*Response, error) { 2043 | err := x.send(&Request{error: io.EOF}) 2044 | if err != nil { 2045 | return nil, err 2046 | } 2047 | return x.recv() 2048 | } 2049 | 2050 | func (c *subEchoServiceClient) Testy(ctx context.Context, req *SearchResponse) (res *StockPricesWrapper, err error) { 2051 | ch := make(chan *StockPricesWrapper, 1) 2052 | p := packet.Get() 2053 | p.Metadata.Operation = 12 2054 | 2055 | c.nextTestyMu.Lock() 2056 | c.nextTesty += 1 2057 | id := c.nextTesty 2058 | c.nextTestyMu.Unlock() 2059 | p.Metadata.Id = id 2060 | 2061 | req.Encode(p.Content) 2062 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2063 | c.inflightTestyMu.Lock() 2064 | c.inflightTesty[id] = ch 2065 | c.inflightTestyMu.Unlock() 2066 | err = c.client.WritePacket(p) 2067 | if err != nil { 2068 | packet.Put(p) 2069 | return 2070 | } 2071 | select { 2072 | case <-c.client.CloseChannel(): 2073 | err = c.client.Error() 2074 | case res = <-ch: 2075 | err = res.error 2076 | case <-ctx.Done(): 2077 | err = ctx.Err() 2078 | } 2079 | c.inflightTestyMu.Lock() 2080 | delete(c.inflightTesty, id) 2081 | c.inflightTestyMu.Unlock() 2082 | packet.Put(p) 2083 | return 2084 | } 2085 | 2086 | func (c *subEchoServiceClient) Search(ctx context.Context, req *SearchResponse) (*SearchClient, error) { 2087 | p := packet.Get() 2088 | 2089 | c.nextStreamingIDMu.Lock() 2090 | c.nextStreamingID += 1 2091 | id := c.nextStreamingID 2092 | c.nextStreamingIDMu.Unlock() 2093 | 2094 | open := &RPCStreamOpen{operation: 13} 2095 | 2096 | open.Encode(p.Content) 2097 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2098 | 2099 | fStream := c.client.Stream(id) 2100 | fStream.WritePacket(p) 2101 | 2102 | if req != nil { 2103 | p2 := packet.Get() 2104 | req.Encode(p2.Content) 2105 | p2.Metadata.ContentLength = uint32((*p2.Content).Len()) 2106 | fStream.WritePacket(p2) 2107 | } 2108 | 2109 | stream := SearchClient{ 2110 | context: ctx, 2111 | stream: fStream, 2112 | } 2113 | 2114 | stream.recv = func() (*Response, error) { 2115 | p, err := stream.stream.ReadPacket() 2116 | if err != nil { 2117 | return nil, err 2118 | } 2119 | 2120 | res := NewResponse() 2121 | err = res.Decode((*p.Content).Bytes()) 2122 | if err != nil { 2123 | return nil, err 2124 | } 2125 | if errors.Is(res.error, io.EOF) { 2126 | return nil, io.EOF 2127 | } 2128 | 2129 | return res, nil 2130 | } 2131 | 2132 | stream.close = func() { 2133 | stream.stream.Close() 2134 | } 2135 | stream.send = func(m *SearchResponse) error { 2136 | p := packet.Get() 2137 | 2138 | m.Encode(p.Content) 2139 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2140 | return stream.stream.WritePacket(p) 2141 | } 2142 | return &stream, nil 2143 | } 2144 | 2145 | type SearchClient struct { 2146 | context context.Context 2147 | recv func() (*Response, error) 2148 | close func() 2149 | closed *atomic.Bool 2150 | 2151 | stream *frisbee.Stream 2152 | send func(*SearchResponse) error 2153 | } 2154 | 2155 | func (x *SearchClient) CloseChannel() <-chan struct{} { 2156 | return x.stream.Conn().CloseChannel() 2157 | } 2158 | 2159 | func (x *SearchClient) Recv() (*Response, error) { 2160 | return x.recv() 2161 | } 2162 | 2163 | func (c *subEchoServiceClient) Upload(ctx context.Context, req *Data) (*UploadClient, error) { 2164 | p := packet.Get() 2165 | 2166 | c.nextStreamingIDMu.Lock() 2167 | c.nextStreamingID += 1 2168 | id := c.nextStreamingID 2169 | c.nextStreamingIDMu.Unlock() 2170 | 2171 | open := &RPCStreamOpen{operation: 14} 2172 | 2173 | open.Encode(p.Content) 2174 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2175 | 2176 | fStream := c.client.Stream(id) 2177 | fStream.WritePacket(p) 2178 | 2179 | if req != nil { 2180 | p2 := packet.Get() 2181 | req.Encode(p2.Content) 2182 | p2.Metadata.ContentLength = uint32((*p2.Content).Len()) 2183 | fStream.WritePacket(p2) 2184 | } 2185 | 2186 | stream := UploadClient{ 2187 | context: ctx, 2188 | stream: fStream, 2189 | } 2190 | 2191 | stream.recv = func() (*Response, error) { 2192 | p, err := stream.stream.ReadPacket() 2193 | if err != nil { 2194 | return nil, err 2195 | } 2196 | 2197 | res := NewResponse() 2198 | err = res.Decode((*p.Content).Bytes()) 2199 | if err != nil { 2200 | return nil, err 2201 | } 2202 | if errors.Is(res.error, io.EOF) { 2203 | return nil, io.EOF 2204 | } 2205 | 2206 | return res, nil 2207 | } 2208 | 2209 | stream.close = func() { 2210 | stream.stream.Close() 2211 | } 2212 | stream.send = func(m *Data) error { 2213 | p := packet.Get() 2214 | 2215 | m.Encode(p.Content) 2216 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 2217 | return stream.stream.WritePacket(p) 2218 | } 2219 | return &stream, nil 2220 | } 2221 | 2222 | type UploadClient struct { 2223 | context context.Context 2224 | recv func() (*Response, error) 2225 | close func() 2226 | closed *atomic.Bool 2227 | 2228 | stream *frisbee.Stream 2229 | send func(*Data) error 2230 | } 2231 | 2232 | func (x *UploadClient) CloseChannel() <-chan struct{} { 2233 | return x.stream.Conn().CloseChannel() 2234 | } 2235 | 2236 | func (x *UploadClient) Send(m *Data) error { 2237 | return x.send(m) 2238 | } 2239 | 2240 | func (x *UploadClient) CloseSend() error { 2241 | return x.send(&Data{error: io.EOF}) 2242 | } 2243 | 2244 | func (x *UploadClient) CloseAndRecv() (*Response, error) { 2245 | err := x.send(&Data{error: io.EOF}) 2246 | if err != nil { 2247 | return nil, err 2248 | } 2249 | return x.recv() 2250 | } 2251 | 2252 | type CloseError struct { 2253 | err error 2254 | } 2255 | 2256 | func NewCloseError(err error) CloseError { 2257 | return CloseError{err: err} 2258 | } 2259 | 2260 | func (e CloseError) Error() string { 2261 | return e.err.Error() 2262 | } 2263 | -------------------------------------------------------------------------------- /protoc-gen-go-frpc/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | 9 | "github.com/loopholelabs/frpc-go/pkg/generator" 10 | ) 11 | 12 | func main() { 13 | gen := generator.New() 14 | 15 | data, err := io.ReadAll(os.Stdin) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | req, err := gen.UnmarshalRequest(data) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | res, err := gen.Generate(req) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | data, err = gen.MarshalResponse(res) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | _, err = os.Stdout.Write(data) 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /templates/base.templ: -------------------------------------------------------------------------------- 1 | {{template "interfaces" .}} 2 | {{template "constants" .}} 3 | 4 | {{ if .numServices }} 5 | {{template "server" .}} 6 | 7 | {{template "client" .}} 8 | {{ end }} -------------------------------------------------------------------------------- /templates/client.templ: -------------------------------------------------------------------------------- 1 | {{define "client"}} 2 | {{ range $i, $v := (MakeIterable .services.Len) -}} 3 | {{ $service := $.services.Get $i -}} 4 | type sub{{ CamelCaseName $service.Name }}Client struct { 5 | client *frisbee.Client 6 | {{ range $i, $v := (MakeIterable $service.Methods.Len) -}} 7 | {{ $method := $service.Methods.Get $i -}} 8 | {{ if not (or $method.IsStreamingServer $method.IsStreamingClient) -}} 9 | next{{ CamelCaseName $method.Name }} uint16 10 | next{{ CamelCaseName $method.Name }}Mu sync.RWMutex 11 | inflight{{ CamelCaseName $method.Name }} map[uint16]chan *{{ CamelCase $method.Output.FullName }} 12 | inflight{{ CamelCaseName $method.Name }}Mu sync.RWMutex 13 | {{ end -}} 14 | {{end -}} 15 | nextStreamingID uint16 16 | nextStreamingIDMu sync.RWMutex 17 | } 18 | {{end -}} 19 | 20 | 21 | type Client struct { 22 | *frisbee.Client 23 | {{ range $i, $v := (MakeIterable .services.Len) -}} 24 | {{ $service := $.services.Get $i -}} 25 | {{ CamelCaseName $service.Name }} *sub{{ CamelCaseName $service.Name }}Client 26 | {{end -}} 27 | } 28 | 29 | func NewClient (tlsConfig *tls.Config, logger types.Logger) (*Client, error) { 30 | c := new(Client) 31 | table := make(frisbee.HandlerTable) 32 | {{template "clienthandlers" .services -}} 33 | 34 | var err error 35 | if tlsConfig != nil { 36 | c.Client, err = frisbee.NewClient(table, context.Background(), frisbee.WithTLS(tlsConfig), frisbee.WithLogger(logger)) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } else { 41 | c.Client, err = frisbee.NewClient(table, context.Background(), frisbee.WithLogger(logger)) 42 | if err != nil { 43 | return nil, err 44 | } 45 | } 46 | 47 | {{ range $i, $v := (MakeIterable .services.Len) -}} 48 | {{ $service := $.services.Get $i -}} 49 | c.{{ CamelCaseName $service.Name }} = new(sub{{ CamelCaseName $service.Name }}Client) 50 | c.{{ CamelCaseName $service.Name }}.client = c.Client 51 | {{ range $i, $v := (MakeIterable $service.Methods.Len) -}} 52 | {{ $method := $service.Methods.Get $i -}} 53 | {{ if not (or $method.IsStreamingServer $method.IsStreamingClient) -}} 54 | c.{{ CamelCaseName $service.Name }}.next{{ CamelCaseName $method.Name }}Mu.Lock() 55 | c.{{ CamelCaseName $service.Name }}.next{{ CamelCaseName $method.Name }} = 0 56 | c.{{ CamelCaseName $service.Name }}.next{{ CamelCaseName $method.Name }}Mu.Unlock() 57 | c.{{ CamelCaseName $service.Name }}.inflight{{ CamelCaseName $method.Name }} = make(map[uint16]chan *{{ CamelCase $method.Output.FullName }}) 58 | {{ end -}} 59 | {{end -}} 60 | {{end -}} 61 | return c, nil 62 | } 63 | 64 | func (c *Client) Connect(addr string, streamHandler ...frisbee.NewStreamHandler) error { 65 | return c.Client.Connect(addr, func (stream *frisbee.Stream) { }) 66 | } 67 | 68 | func (c *Client) FromConn(conn net.Conn, streamHandler ...frisbee.NewStreamHandler) error { 69 | return c.Client.FromConn(conn, func (stream *frisbee.Stream) { }) 70 | } 71 | 72 | {{template "clientmethods" .services }} 73 | 74 | type CloseError struct { 75 | err error 76 | } 77 | 78 | func NewCloseError(err error) CloseError { 79 | return CloseError{err: err} 80 | } 81 | 82 | func (e CloseError) Error() string { 83 | return e.err.Error() 84 | } 85 | {{ end -}} 86 | 87 | {{define "clienthandlers"}} 88 | {{ $counter := Counter 9 -}} 89 | {{ range $i, $v := (MakeIterable .Len) }} 90 | {{ $service := $.Get $i -}} 91 | {{ range $i, $v := (MakeIterable $service.Methods.Len) -}} 92 | {{ $method := $service.Methods.Get $i -}} 93 | {{ $count := call $counter -}} 94 | 95 | {{ if not (or $method.IsStreamingServer $method.IsStreamingClient) -}} 96 | table[{{ $count }}] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 97 | c.{{ CamelCaseName $service.Name }}.inflight{{ CamelCaseName $method.Name }}Mu.RLock() 98 | if ch, ok := c.{{ CamelCaseName $service.Name }}.inflight{{ CamelCaseName $method.Name }}[incoming.Metadata.Id]; ok { 99 | c.{{ CamelCaseName $service.Name }}.inflight{{ CamelCaseName $method.Name }}Mu.RUnlock() 100 | res := New{{ CamelCase $method.Output.FullName }}() 101 | res.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 102 | ch <- res 103 | } else { 104 | c.{{ CamelCaseName $service.Name }}.inflight{{ CamelCaseName $method.Name }}Mu.RUnlock() 105 | } 106 | return 107 | } 108 | {{ end -}} 109 | {{end -}} 110 | {{end -}} 111 | {{ end -}} 112 | 113 | {{define "clientmethods"}} 114 | {{ $counter := Counter 9 -}} 115 | {{ range $i, $v := (MakeIterable .Len) -}} 116 | {{ $service := $.Get $i -}} 117 | {{ range $i, $v := (MakeIterable $service.Methods.Len) }} 118 | {{ $method := $service.Methods.Get $i -}} 119 | {{ $opIndex := call $counter -}} 120 | {{if or $method.IsStreamingClient $method.IsStreamingServer -}} 121 | func (c *sub{{ CamelCaseName $service.Name }}Client) {{ CamelCaseName $method.Name }}(ctx context.Context, req *{{ CamelCase $method.Input.FullName }}) (*{{ CamelCaseName $method.Name }}Client, error) { 122 | p := packet.Get() 123 | 124 | c.nextStreamingIDMu.Lock() 125 | c.nextStreamingID += 1 126 | id := c.nextStreamingID 127 | c.nextStreamingIDMu.Unlock() 128 | 129 | open := &RPCStreamOpen{operation: {{ $opIndex}}}; 130 | 131 | open.Encode(p.Content) 132 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 133 | 134 | fStream := c.client.Stream(id) 135 | fStream.WritePacket(p) 136 | 137 | if req != nil { 138 | p2 := packet.Get() 139 | req.Encode(p2.Content) 140 | p2.Metadata.ContentLength = uint32((*p2.Content).Len()) 141 | fStream.WritePacket(p2) 142 | } 143 | 144 | stream := {{ CamelCaseName $method.Name }}Client{ 145 | context: ctx, 146 | stream: fStream, 147 | } 148 | 149 | stream.recv = func () (*{{ CamelCase $method.Output.FullName }}, error) { 150 | p, err := stream.stream.ReadPacket() 151 | if err != nil { 152 | return nil, err 153 | } 154 | 155 | res := New{{ CamelCase $method.Output.FullName }}() 156 | err = res.Decode((*p.Content).Bytes()) 157 | if err != nil { 158 | return nil, err 159 | } 160 | if errors.Is(res.error, io.EOF) { 161 | return nil, io.EOF 162 | } 163 | 164 | return res, nil 165 | } 166 | 167 | stream.close = func () { 168 | stream.stream.Close() 169 | } 170 | stream.send = func (m *{{ CamelCase $method.Input.FullName }}) error { 171 | p := packet.Get() 172 | 173 | m.Encode(p.Content) 174 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 175 | return stream.stream.WritePacket(p) 176 | } 177 | return &stream, nil 178 | } 179 | 180 | type {{ CamelCaseName $method.Name }}Client struct { 181 | context context.Context 182 | recv func() (*{{ CamelCase $method.Output.FullName }}, error) 183 | close func() 184 | closed *atomic.Bool 185 | 186 | stream *frisbee.Stream 187 | send func (*{{ CamelCase $method.Input.FullName }}) error 188 | } 189 | 190 | func (x *{{ CamelCaseName $method.Name }}Client) CloseChannel() <-chan struct{} { 191 | return x.stream.Conn().CloseChannel() 192 | } 193 | 194 | {{ if $method.IsStreamingServer -}} 195 | func (x *{{ CamelCaseName $method.Name }}Client) Recv() (*{{ CamelCase $method.Output.FullName }}, error) { 196 | return x.recv() 197 | } 198 | {{ end -}} 199 | 200 | {{ if $method.IsStreamingClient -}} 201 | func (x *{{ CamelCaseName $method.Name }}Client) Send(m *{{ CamelCase $method.Input.FullName }}) error { 202 | return x.send(m) 203 | } 204 | 205 | func (x *{{ CamelCaseName $method.Name }}Client) CloseSend() error { 206 | return x.send(&{{ CamelCase $method.Input.FullName }}{error: io.EOF}) 207 | } 208 | 209 | func (x *{{ CamelCaseName $method.Name }}Client) CloseAndRecv() (*{{ CamelCase $method.Output.FullName }}, error) { 210 | err := x.send(&{{ CamelCase $method.Input.FullName }}{error: io.EOF}) 211 | if err != nil { 212 | return nil, err 213 | } 214 | return x.recv() 215 | } 216 | {{ end -}} 217 | {{else -}} 218 | func (c *sub{{ CamelCaseName $service.Name }}Client) {{ CamelCaseName $method.Name }}(ctx context.Context, req *{{ CamelCase $method.Input.FullName }}) (res *{{ CamelCase $method.Output.FullName }}, err error) { 219 | ch := make(chan *{{ CamelCase $method.Output.FullName }}, 1) 220 | p := packet.Get() 221 | p.Metadata.Operation = {{ $opIndex }} 222 | 223 | c.next{{ CamelCaseName $method.Name }}Mu.Lock() 224 | c.next{{ CamelCaseName $method.Name }} += 1 225 | id := c.next{{ CamelCaseName $method.Name }} 226 | c.next{{ CamelCaseName $method.Name }}Mu.Unlock() 227 | p.Metadata.Id = id 228 | 229 | req.Encode(p.Content) 230 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 231 | c.inflight{{ CamelCaseName $method.Name }}Mu.Lock() 232 | c.inflight{{ CamelCaseName $method.Name }}[id] = ch 233 | c.inflight{{ CamelCaseName $method.Name }}Mu.Unlock() 234 | err = c.client.WritePacket(p) 235 | if err != nil { 236 | packet.Put(p) 237 | return 238 | } 239 | select { 240 | case <-c.client.CloseChannel(): 241 | err = c.client.Error() 242 | case res = <- ch: 243 | err = res.error 244 | case <- ctx.Done(): 245 | err = ctx.Err() 246 | } 247 | c.inflight{{ CamelCaseName $method.Name }}Mu.Lock() 248 | delete(c.inflight{{ CamelCaseName $method.Name }}, id) 249 | c.inflight{{ CamelCaseName $method.Name }}Mu.Unlock() 250 | packet.Put(p) 251 | return 252 | } 253 | {{end -}} 254 | {{end -}} 255 | {{ end -}} 256 | {{end}} 257 | -------------------------------------------------------------------------------- /templates/constants.templ: -------------------------------------------------------------------------------- 1 | {{define "constants"}} 2 | const ConnectionContextKey int = 1000 3 | const StreamContextKey int = 1001 4 | 5 | func SetErrorFlag(flags uint8, error bool) uint8 { 6 | return flags | 0x2 7 | } 8 | func HasErrorFlag(flags uint8) bool { 9 | return flags & (1 << 1) == 1 10 | } 11 | {{end}} -------------------------------------------------------------------------------- /templates/customDecode.templ: -------------------------------------------------------------------------------- 1 | x.error, err = d.Error() 2 | if err == nil { 3 | return nil 4 | } 5 | x.flags, err = d.Uint8() 6 | if err != nil { 7 | return err 8 | } -------------------------------------------------------------------------------- /templates/customEncode.templ: -------------------------------------------------------------------------------- 1 | if x.error != nil { 2 | polyglot.Encoder(b).Error(x.error) 3 | return 4 | } 5 | polyglot.Encoder(b).Uint8(x.flags) -------------------------------------------------------------------------------- /templates/customFields.templ: -------------------------------------------------------------------------------- 1 | error error 2 | flags uint8 -------------------------------------------------------------------------------- /templates/headers.templ: -------------------------------------------------------------------------------- 1 | {{define "headers"}} 2 | // Code generated by fRPC Go {{ .pluginVersion }}, DO NOT EDIT. 3 | // source: {{ .sourcePath }} 4 | 5 | package {{ .package }} 6 | {{end}} -------------------------------------------------------------------------------- /templates/imports.templ: -------------------------------------------------------------------------------- 1 | {{define "imports"}} 2 | import ( 3 | {{range $im := .requiredImports -}} 4 | "{{$im}}" 5 | {{end -}} 6 | {{ if .numServices }} 7 | {{range $im := .serviceImports -}} 8 | "{{$im}}" 9 | {{end -}} 10 | {{end}} 11 | {{ if .numStreamMethods -}} 12 | {{range $im := .streamMethodImports -}} 13 | "{{$im}}" 14 | {{end -}} 15 | {{end -}} 16 | ) 17 | {{end}} -------------------------------------------------------------------------------- /templates/interfaces.templ: -------------------------------------------------------------------------------- 1 | {{define "interfaces"}} 2 | {{range $i, $v := (MakeIterable .services.Len) -}} 3 | {{ $service := ($.services.Get $i) -}} 4 | {{template "interface" $service}} 5 | {{end -}} 6 | {{end}} 7 | 8 | 9 | {{define "interface"}} 10 | type {{ CamelCaseName .Name }} interface { 11 | {{ range $i, $v := MakeIterable .Methods.Len -}} 12 | {{ $method := $.Methods.Get $i -}} 13 | {{ if $method.IsStreamingClient }} 14 | {{ CamelCaseName $method.Name }} (context.Context, *{{ CamelCaseName $method.Name }}Server) error 15 | {{ else if $method.IsStreamingServer }} 16 | {{ CamelCaseName $method.Name }} (context.Context, *{{ CamelCase $method.Input.FullName }}, *{{ CamelCaseName $method.Name }}Server) error 17 | {{else -}} 18 | {{ CamelCaseName $method.Name }} (context.Context, *{{ CamelCase $method.Input.FullName }}) (*{{ CamelCase $method.Output.FullName }}, error) 19 | {{end -}} 20 | {{ end -}} 21 | } 22 | {{end}} -------------------------------------------------------------------------------- /templates/prebase.templ: -------------------------------------------------------------------------------- 1 | {{template "headers" .}} 2 | 3 | {{template "imports" .}} -------------------------------------------------------------------------------- /templates/server.templ: -------------------------------------------------------------------------------- 1 | {{define "server"}} 2 | 3 | {{ if .numStreamMethods -}} 4 | type RPCStreamOpen struct { 5 | operation uint16 6 | } 7 | 8 | func (x *RPCStreamOpen) Error(b *polyglot.Buffer, err error) { 9 | polyglot.Encoder(b).Error(err) 10 | } 11 | 12 | func (x *RPCStreamOpen) Encode(b *polyglot.Buffer) { 13 | polyglot.Encoder(b).Uint16(x.operation) 14 | } 15 | 16 | func (x *RPCStreamOpen) Decode(b []byte) error { 17 | if x == nil { 18 | return ErrDecodeNil 19 | } 20 | d := polyglot.Decoder(b) 21 | return x.decode(d) 22 | } 23 | 24 | func (x *RPCStreamOpen) decode(d *polyglot.BufferDecoder) error { 25 | var err error 26 | x.operation, err = d.Uint16() 27 | return err 28 | } 29 | {{ end -}} 30 | 31 | type Server struct { 32 | server *frisbee.Server 33 | {{ if .numStreamMethods -}} 34 | wg sync.WaitGroup 35 | {{end -}} 36 | } 37 | 38 | func NewServer({{ GetServerFields .services }}, tlsConfig *tls.Config, logger types.Logger) (*Server, error) { 39 | s := new(Server) 40 | table := make(frisbee.HandlerTable) 41 | {{template "serverhandlers" .services -}} 42 | var err error 43 | if tlsConfig != nil { 44 | s.server, err = frisbee.NewServer(table, context.Background(), frisbee.WithTLS(tlsConfig), frisbee.WithLogger(logger)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | } else { 49 | s.server, err = frisbee.NewServer(table, context.Background(), frisbee.WithLogger(logger)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | 55 | {{ if .numStreamMethods -}} 56 | s.server.SetStreamHandler(func(ctx context.Context, stream *frisbee.Stream) { 57 | streamCtx := context.WithValue(ctx, ConnectionContextKey, stream.Conn()) 58 | streamCtx = context.WithValue(streamCtx, StreamContextKey, stream) 59 | p, err := stream.ReadPacket() 60 | if err != nil { 61 | return 62 | } 63 | open := &RPCStreamOpen{} 64 | err = open.Decode((*p.Content).Bytes()) 65 | if err != nil { 66 | stream.Close() 67 | return 68 | } 69 | switch open.operation { 70 | {{ $counter := Counter 9 -}} 71 | {{ range $i, $v := (MakeIterable .services.Len) -}} 72 | {{ $service := $.services.Get $i -}} 73 | {{ range $i, $v := (MakeIterable $service.Methods.Len) -}} 74 | {{ $method := $service.Methods.Get $i -}} 75 | {{ $opIndex := call $counter -}} 76 | {{ if or $method.IsStreamingClient $method.IsStreamingServer -}} 77 | case {{ $opIndex }}: 78 | s.create{{ CamelCaseName $method.Name }}Server(streamCtx, {{ FirstLowerCase (CamelCaseName $service.Name) }}, stream) 79 | {{end -}} 80 | {{end -}} 81 | {{end -}} 82 | } 83 | }) 84 | 85 | s.server.StreamContext = func(ctx context.Context, stream *frisbee.Stream) context.Context { 86 | return context.WithValue(ctx, ConnectionContextKey, stream.Conn()) 87 | } 88 | 89 | {{ end -}} 90 | 91 | s.server.ConnContext = func (ctx context.Context, conn *frisbee.Async) context.Context { 92 | return context.WithValue(ctx, ConnectionContextKey, conn) 93 | } 94 | 95 | return s, nil 96 | } 97 | 98 | func (s *Server) SetOnClosed(f func(*frisbee.Async, error)) error { 99 | return s.server.SetOnClosed(f) 100 | } 101 | 102 | func (s *Server) SetPreWrite(f func()) error { 103 | return s.server.SetPreWrite(f) 104 | } 105 | 106 | func (s *Server) SetConcurrency(concurrency uint64) { 107 | s.server.SetConcurrency(concurrency) 108 | } 109 | 110 | func (s *Server) Start(addr string) error { 111 | return s.server.Start(addr) 112 | } 113 | 114 | func (s *Server) StartWithListener(listener net.Listener) error { 115 | return s.server.StartWithListener(listener) 116 | } 117 | 118 | func (s *Server) ServeConn(conn net.Conn) { 119 | s.server.ServeConn(conn) 120 | } 121 | 122 | func (s *Server) Shutdown() error { 123 | err := s.server.Shutdown() 124 | if err != nil { 125 | return err 126 | } 127 | {{ if .numStreamMethods -}} 128 | s.wg.Wait() 129 | {{end -}} 130 | return nil 131 | } 132 | 133 | {{template "servermethods" .services }} 134 | {{ end -}} 135 | 136 | {{define "servermethods"}} 137 | {{ $counter := Counter 9 -}} 138 | {{ range $i, $v := (MakeIterable .Len) -}} 139 | {{ $service := $.Get $i -}} 140 | {{ range $i, $v := (MakeIterable $service.Methods.Len) }} 141 | {{ $method := $service.Methods.Get $i -}} 142 | {{ $opIndex := call $counter -}} 143 | {{if or $method.IsStreamingClient $method.IsStreamingServer -}} 144 | type {{ CamelCaseName $method.Name }}Server struct { 145 | recv func() (*{{ CamelCase $method.Input.FullName }}, error) 146 | send func (*{{ CamelCase $method.Output.FullName }}) error 147 | 148 | stream *frisbee.Stream 149 | closed *atomic.Bool 150 | } 151 | 152 | func (s *Server) create{{ CamelCaseName $method.Name}}Server (ctx context.Context, {{ FirstLowerCase (CamelCaseName $service.Name) }} {{ CamelCaseName $service.Name }}, stream *frisbee.Stream) { 153 | srv := &{{ CamelCaseName $method.Name }}Server{ 154 | stream: stream, 155 | } 156 | 157 | {{ if $method.IsStreamingClient -}} 158 | srv.recv = func() (*{{ CamelCase $method.Input.FullName }}, error) { 159 | p, err := srv.stream.ReadPacket() 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | res := New{{ CamelCase $method.Input.FullName }}() 165 | err = res.Decode((*p.Content).Bytes()) 166 | if err != nil { 167 | return nil, err 168 | } 169 | if errors.Is(res.error, io.EOF) { 170 | return nil, io.EOF 171 | } 172 | 173 | return res, nil 174 | } 175 | {{ end -}} 176 | 177 | srv.send = func (m *{{ CamelCase $method.Output.FullName }}) error { 178 | p := packet.Get() 179 | 180 | m.Encode(p.Content) 181 | p.Metadata.ContentLength = uint32((*p.Content).Len()) 182 | return srv.stream.WritePacket(p) 183 | } 184 | 185 | {{ if not $method.IsStreamingClient -}} 186 | incoming, err := stream.ReadPacket() 187 | if err != nil { 188 | return 189 | } 190 | req := New{{ CamelCase $method.Input.FullName }}() 191 | err = req.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 192 | {{ end -}} 193 | s.wg.Add(1) 194 | go func() { 195 | {{ if $method.IsStreamingClient -}} 196 | err := {{ FirstLowerCaseName $service.Name }}.{{ CamelCaseName $method.Name }}(ctx, srv) 197 | {{ else }} 198 | err := {{ FirstLowerCaseName $service.Name }}.{{ CamelCaseName $method.Name }}(ctx, req, srv) 199 | {{ end -}} 200 | if err != nil { 201 | res := {{ CamelCase $method.Output.FullName }}{error: err} 202 | res.flags = SetErrorFlag(res.flags, true) 203 | srv.CloseAndSend(&res) 204 | } else { 205 | srv.CloseSend() 206 | } 207 | s.wg.Done() 208 | }() 209 | } 210 | 211 | {{ if $method.IsStreamingClient -}} 212 | func (x *{{ CamelCaseName $method.Name }}Server) Recv() (*{{ CamelCase $method.Input.FullName }}, error) { 213 | return x.recv() 214 | } 215 | 216 | func (x *{{ CamelCaseName $method.Name }}Server) close() { 217 | x.stream.Close() 218 | } 219 | {{ end -}} 220 | 221 | {{ if $method.IsStreamingServer -}} 222 | func (x *{{ CamelCaseName $method.Name }}Server) Send(m *{{ CamelCase $method.Output.FullName }}) error { 223 | return x.send(m) 224 | } 225 | {{ end -}} 226 | 227 | func (x *{{ CamelCaseName $method.Name }}Server) CloseSend() error { 228 | return x.send(&{{ CamelCase $method.Output.FullName }}{error: io.EOF}) 229 | } 230 | 231 | func (x *{{ CamelCaseName $method.Name }}Server) CloseChannel() <-chan struct{} { 232 | return x.stream.Conn().CloseChannel() 233 | } 234 | 235 | {{ if or $method.IsStreamingClient $method.IsStreamingServer -}} 236 | func (x *{{ CamelCaseName $method.Name }}Server) CloseAndSend(m *{{ CamelCase $method.Output.FullName }}) error { 237 | err := x.send(m) 238 | if err != nil { 239 | return err 240 | } 241 | return x.CloseSend() 242 | } 243 | {{ end -}} 244 | {{end -}} 245 | {{end -}} 246 | {{end -}} 247 | {{end}} 248 | 249 | {{define "serverhandlers"}} 250 | {{ $counter := Counter 9 -}} 251 | {{ range $i, $v := (MakeIterable .Len) -}} 252 | {{ $service := $.Get $i -}} 253 | {{ range $i, $v := (MakeIterable $service.Methods.Len) -}} 254 | {{ $method := $service.Methods.Get $i -}} 255 | {{ $count := call $counter -}} 256 | {{ if not (or $method.IsStreamingServer $method.IsStreamingClient) -}} 257 | table[{{ $count }}] = func(ctx context.Context, incoming *packet.Packet) (outgoing *packet.Packet, action frisbee.Action) { 258 | req := New{{ CamelCase $method.Input.FullName }}() 259 | err := req.Decode((*incoming.Content).Bytes()[:incoming.Metadata.ContentLength]) 260 | if err == nil { 261 | var res *{{ CamelCase $method.Output.FullName }} 262 | outgoing = incoming 263 | outgoing.Content.Reset() 264 | res, err = {{ FirstLowerCase (CamelCaseName $service.Name) }}.{{ CamelCaseName $method.Name }}(ctx, req) 265 | if err != nil { 266 | if _, ok := err.(CloseError); ok { 267 | action = frisbee.CLOSE 268 | } 269 | res.Error(outgoing.Content, err) 270 | } else { 271 | res.Encode(outgoing.Content) 272 | } 273 | outgoing.Metadata.ContentLength = uint32(outgoing.Content.Len()) 274 | } 275 | return 276 | } 277 | {{end -}} 278 | {{end -}} 279 | {{end -}} 280 | {{end}} 281 | -------------------------------------------------------------------------------- /templates/templates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Loophole Labs 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package templates 18 | 19 | import "embed" 20 | 21 | //go:embed * 22 | var FS embed.FS 23 | -------------------------------------------------------------------------------- /version/current_version: -------------------------------------------------------------------------------- 1 | v0.10.0 2 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | package version 4 | 5 | import ( 6 | _ "embed" 7 | ) 8 | 9 | //go:embed current_version 10 | var Version string 11 | --------------------------------------------------------------------------------