{"id":20768522,"url":"https://github.com/papierkorb/cannon","last_synced_at":"2025-04-30T11:27:48.998Z","repository":{"id":77922516,"uuid":"81879668","full_name":"Papierkorb/cannon","owner":"Papierkorb","description":"Lightning fast data serialization and RPC for Crystal","archived":false,"fork":false,"pushed_at":"2020-06-07T21:03:41.000Z","size":35,"stargazers_count":61,"open_issues_count":3,"forks_count":5,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-30T15:47:04.122Z","etag":null,"topics":["crystal","data-serialization","rpc"],"latest_commit_sha":null,"homepage":null,"language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Papierkorb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-02-13T22:42:41.000Z","updated_at":"2025-01-11T18:48:40.000Z","dependencies_parsed_at":null,"dependency_job_id":"bca1b386-96bc-4c02-a380-dbc515d6b615","html_url":"https://github.com/Papierkorb/cannon","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Fcannon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Fcannon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Fcannon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Fcannon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Papierkorb","download_url":"https://codeload.github.com/Papierkorb/cannon/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251690146,"owners_count":21628052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crystal","data-serialization","rpc"],"created_at":"2024-11-17T11:39:24.874Z","updated_at":"2025-04-30T11:27:48.990Z","avatar_url":"https://github.com/Papierkorb.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# The Cannon [![Build Status](https://travis-ci.org/Papierkorb/cannon.svg?branch=master)](https://travis-ci.org/Papierkorb/cannon)\n\nReally fast data de-/serialisation and remote procedure calling, for when your\nprocess has other things to do than data serialisation.\n\n## Benchmark\n\nThe main point about **Cannon** is speed.  This is achieved by cutting some\ncorners.\n\n```crystal\nio = IO::Memory.new\ndata = [ 5, 6, 7 ]\n\nBenchmark.ips do |x|\n  x.report(\"encode\") do\n    Cannon.encode(io, data)\n    io.rewind\n  end\n\n  x.report(\"decode\") do\n    Cannon.decode(io, typeof(data))\n    io.rewind\n  end\nend\n```\n\nOn my i5 6600K (Skylake) I get numbers like these:\n\n```\nencode  96.84M ( 10.33ns) (± 4.19%)       fastest\ndecode  27.07M ( 36.95ns) (± 3.79%)  3.58× slower\n```\n\n## Usage\n\n(**Tip**: You can find all of these in the [samples/](https://github.com/Papierkorb/cannon/tree/master/samples) directory)\n\nMany common data types have support built-in:\n\n```crystal\nrequire \"cannon\" # Require the shard\n\n# Data de-/serialization.  Cannon operates on IOs\nio = IO::Memory.new # Use an in-memory store for this\n\noriginal = [ 5, 6, 7 ] # Some data to serialize\nCannon.encode io, original # Write `data` into `io`\nio.rewind # Don't forget to rewind the stream\ncopy = Cannon.decode io, typeof(data) # And read it back\n\npp original, copy # original == copy\n```\n\nYour own data structures can also be serialized.  Either by implementing\n`#to_cannon_io(io)` and `.from_cannon_io(io)` yourself, or simply use\n`Cannon::Auto`.  \n\n```crystal\nrequire \"cannon\"\n\nclass Session\n  include Cannon::Auto # Magic include\n\n  property username : String\n  property email : String\n\n  def initialize(@username, @email)\n  end\nend\n\nio = IO::Memory.new # Like in the example above\noriginal = Session.new(\"alice\", \"alice@example.com\")\nCannon.encode io, original\nio.rewind\ndecoded = Cannon.decode io, Session\n\npp original, decoded\n```\n\nEven better, if your data structure is a `struct`, `@[Packed]` and only uses\nsimple types, use `Cannon::FastAuto`.\n\n```crystal\nrequire \"cannon\"\n\n@[Packed] # Go with packed if you want to go fast!\nstruct Addition\n  include Cannon::FastAuto # Faster magic include\n\n  property a : Int32\n  property b : Int32\n\n  def initialize(@a, @b)\n  end\nend\n\nio = IO::Memory.new # Like in the example above\noriginal = Addition.new(4, 5)\nCannon.encode io, original\nio.rewind\ncopy = Cannon.decode io, Addition\n\npp original, copy\n```\n\n### Cutting corners\n\nYou'd be surprised how much **Cannon** actually supports.  Here are some tricks\ndone to speed things up:\n\n* Primitives, like `Int32`, are written directly.\n* `Array`s containing simple types are basically `Slice`s.\n* `Slice` is easy to blast out.\n* `Slice`s not containing primitive types are not supported at the moment.\n* Custom `struct`s can be marked as being \"simple\", meaning it can be serialized\n  by type-casting it to a `Slice`.\n* `Tuple`s are treated automatically as `Slice`, if they only contain \"simple\"\n  data types.\n* `Nil` is represented as nothing.\n* The RPC mechanism transparently uses `UInt32` identifiers instead of method names.\n\nHowever, there are things to keep in mind:\n\n* The data format is binary and uses the host endianness\n* The data is effectively unstructured, if you're using the wrong format things\n  will blow up :)\n\n### Simple data structures\n\nSimple data structures can be read and written directly.  Their constructor,\nif any, will most likely not be invoked at all, they just exist.\n\nRequirements are as follows:\n* Must be a `struct` - Not a `class`!\n* Only contains other \"simple\" data, like *primitives*\n* Does not contain any variable-length data like `Slice`s or `Array`s\n\nDo not falter: If your data-structure does not fit these, you can still use it\njust fine!  It just means it will be fast, but not blazing fast :)\n\n## RPC\n\n**Cannon** also comes with a RPC module.  It does the heavy lifting for you, so\nthat you can focus on actually writing code.  And it's fast too!\n\nWant to see some real code? Look into [samples/rpc/](https://github.com/Papierkorb/cannon/tree/master/samples/rpc)!\n\n### Services\n\nThe RPC module works on a service methodology.  A server provides one, or more\nservices for a client to consume.  In **Cannon**, both ends can provide services\nfor the other to consume.\n\nFor this to work, you need three components per Service:\n1. The description module, which describes the interface through `abstract`\n   methods.\n2. The service class, which includes `Cannon::Rpc::Service`.  This object lives\n   on the server.  There can be one or more instances of each service.  Each\n   instance has its own unique *identifier*, or *id*, which is a `UInt32`.  A\n   service can be owned by a client, more on that below.\n3. The client class, which includes `Cannon::Rpc::RemoteService`.  This object\n   lives on the client.  It's bound to a `Cannon::Rpc::Connection` and the\n   remote services *id*.\n\nIn real usage, you'll probably have two kinds of services: First, those used by\nevery client, and second, those used exclusively by one (or few) client(s).\n\n#### Singleton services\n\nSingleton services have no owner (Its owner is `nil`), and are registered to\nwell-known a identifiers.  Usually, only one instance of this service exists on\nthe server.\n\nYou can make your life easy by including `Cannon::Rpc::SingletonService` into\nthe description module of the service.  This module is instantiated with an\nidentifier.  When you now derive your service and client classes from it,\nthey'll automatically bind to the singletons service identifier.\n\n#### Instance services\n\nThe second kind of services are instance services.  These are used exclusively\nby one client, or by few clients.  If there's only one client, you can give the\nclient ownership over that service instance.\n\nWhen a client owns a service, it may release it (remove it) later on.  This can\nbe done through the `#release_now!` method of a client class.  Or you just\nforget about the client, wait until it's garbage-collected, and it'll\nautomatically be released remotely for you.  The same happens when a connection\nis closed automatically, too.\n\n#### The client\n\nThe client is more or less auto-generated from the description module using\n`Cannon::Rpc::RemoteService`.  An actually complete example is this:\n\n```crystal\nclass MyServiceClient\n  include Cannon::Rpc::RemoteService(MyServiceDescription)\nend\n```\n\nThat's it!  The class will get a `#initialize`r which you pass the `Connection`\nfirst and the service id (optional if it's a singleton service).  The\nimplemented abstract methods from the description module will point at the\nremote service, and function like normal methods to you.\n\nIf you don't care about the methods results anyway, use the\n`_without_response` version, which is also generated for each method.\n\n```crystal\n  my_client.greet(\"Alice\") # Wait for response\n  my_client.greet_without_response(\"Alice\") # Don't wait\n```\n\n#### Getting the calling connection\n\nOne last thing: If you need to know which `Connection` exactly is making the\ncall in your service class, just add an argument of type `Connection` to the\nend of the argument list.  For the client, this argument will \"disappear\". The\nservice instance will have it \"injected\".\n\n### Gotchas and Troubleshooting\n\n#### Type your method arguments and result\n\nIt's really important to type your methods.  It's acceptable to not type the\nresult, in which case it's treated as `Nil`, and thus will **silently drop**\nanything returned from the method.\n\n```crystal\n# Won't work\nabstract def add(a, b)\nabstract def greet(user : String, email)\nabstract def return_something_important\n\n# Will work fine\nabstract def add(a : Int32 | Float32, b : Int32) : Float64\nabstract def greet(user : String, email : String?) : String\nabstract def return_something_important : Hash(String, Int32)\n```\n\n#### You can't just pass a Service instance around\n\nRight now, you can't pass a `Service` or `RemoteService` instance around.\nPass around its *service_id* instead, and rebuild the client on the other\nend.\n\n```crystal\n# Won't compile\ndef create_chat_room(name : String) : ChatRoomService\n  ChatRoomService.new(name)\nend\n\n# Will work fine\ndef create_chat_room_service(name : String) : UInt32\n  manager.add ChatRoomService.new(name)\nend\n```\n\nThen, add a wrapper method to your client doing the conversion for you:\n\n```crystal\nclass ChatClient\n  # ...\n\n  def create_chat_room(name : String) : ChatRoomClient\n    ChatRoomClient.new connection, create_chat_room_service(name)\n  end\nend\n```\n\n## When to use **The Cannon**\n\n1. **Speed** is your primary concern\n2. You don't care about **inter-operability**\n3. You're fine with **sacrificing structure**\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  cannon:\n    github: Papierkorb/cannon\n```\n\n## Contributing\n\n1. Fork it ( https://github.com/Papierkorb/cannon/fork )\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## License\n\nThis library is licensed under the Mozilla Public License 2.0 (\"MPL-2\").\n\nFor a copy of the full license text see the included `LICENSE` file.\n\nFor a legally non-binding explanation visit:\n[tl;drLegal](https://tldrlegal.com/license/mozilla-public-license-2.0-%28mpl-2%29)\n\n## Contributors\n\n- [Papierkorb](https://github.com/Papierkorb) Stefan Merettig - creator, maintainer\n\n## Still looking down here?\n\nHave nice day!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapierkorb%2Fcannon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpapierkorb%2Fcannon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapierkorb%2Fcannon/lists"}