Skip to content

Commit

Permalink
Add proc-macro for importing protobuf and defining filter IDs. (#252)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark Mandel <[email protected]>
  • Loading branch information
XAMPPRocky and markmandel authored Jun 15, 2021
1 parent 582ab80 commit fd77e76
Show file tree
Hide file tree
Showing 30 changed files with 419 additions and 205 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# limitations under the License.
#

[workspace]
members = [".", "./macros"]

[package]
name = "quilkin"
version = "0.1.0-dev"
Expand All @@ -28,6 +31,10 @@ categories = ["game-development", "network-programming"]
edition = "2018"

[dependencies]
# Local
quilkin-macros = { version = "0.1.0-dev", path = "./macros" }

# Crates.io
backoff = "0.3"
base64 = "0.13"
base64-serde = "0.6"
Expand Down
24 changes: 16 additions & 8 deletions docs/extensions/filters/writing_custom_filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ To extend Quilkin's code with our own custom filter, we need to do the following
// src/main.rs
use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse};

// This creates adds an associated const named `FILTER_NAME` that points
// to `"greet.v1"`.
#[quilkin::filter("greet.v1")]
struct Greet;

impl Filter for Greet {
fn read(&self, mut ctx: ReadContext) -> Option<ReadResponse> {
ctx.contents.splice(0..0, String::from("Hello ").into_bytes());
Expand All @@ -84,15 +88,17 @@ To extend Quilkin's code with our own custom filter, we need to do the following

```rust
// src/main.rs
# #[quilkin::filter("greet.v1")]
# struct Greet;
# impl Filter for Greet {}
# use quilkin::extensions::Filter;
use quilkin::extensions::{CreateFilterArgs, Error, FilterFactory};

struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> String {
"greet.v1".into()
fn name(&self) -> &'static str {
// We provide the name of filter that we defined with `#[quilkin::filter]`
Greet::FILTER_NAME
}
fn create_filter(&self, _: CreateFilterArgs) -> Result<Box<dyn Filter>, Error> {
Ok(Box::new(Greet))
Expand Down Expand Up @@ -120,8 +126,8 @@ To extend Quilkin's code with our own custom filter, we need to do the following
# struct GreetFilterFactory;
# impl FilterFactory for GreetFilterFactory {
# fn name(&self) -> String {
# "greet.v1".into()
# fn name(&self) -> &'static str {
# "greet.v1"
# }
# fn create_filter(&self, _: CreateFilterArgs) -> Result<Box<dyn Filter>, Error> {
# unimplemented!()
Expand Down Expand Up @@ -202,7 +208,9 @@ The [Serde] crate is used to describe static YAML configuration in code while [P

# use quilkin::extensions::{Filter, ReadContext, ReadResponse, WriteContext, WriteResponse};

#[quilkin::filter("greet.v1")]
struct Greet(String);

impl Filter for Greet {
fn read(&self, mut ctx: ReadContext) -> Option<ReadResponse> {
ctx.contents
Expand Down Expand Up @@ -236,8 +244,8 @@ The [Serde] crate is used to describe static YAML configuration in code while [P

struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> String {
"greet.v1".into()
fn name(&self) -> &'static str {
"greet.v1"
}
fn create_filter(&self, args: CreateFilterArgs) -> Result<Box<dyn Filter>, Error> {
let greeting = match args.config.unwrap() {
Expand Down Expand Up @@ -366,8 +374,8 @@ However, it usually contains a Protobuf equivalent of the filter's static config

struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> String {
"greet.v1".into()
fn name(&self) -> &'static str {
"greet.v1"
}
fn create_filter(&self, args: CreateFilterArgs) -> Result<Box<dyn Filter>, Error> {
let greeting = match args.config.unwrap() {
Expand Down
6 changes: 4 additions & 2 deletions examples/quilkin-filter-example/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ mod greet {
include!(concat!(env!("OUT_DIR"), "/greet.rs"));
}

#[quilkin::filter("greet.v1")]
struct Greet(String);

impl Filter for Greet {
fn read(&self, mut ctx: ReadContext) -> Option<ReadResponse> {
ctx.contents
Expand All @@ -46,8 +48,8 @@ impl Filter for Greet {

struct GreetFilterFactory;
impl FilterFactory for GreetFilterFactory {
fn name(&self) -> String {
"greet.v1".into()
fn name(&self) -> &'static str {
Greet::FILTER_NAME
}
fn create_filter(&self, args: CreateFilterArgs) -> Result<Box<dyn Filter>, Error> {
let greeting = match args.config.unwrap() {
Expand Down
38 changes: 38 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#
# Copyright 2021 Google LLC All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

[package]
name = "quilkin-macros"
version = "0.1.0-dev"
authors = ["Erin Power <[email protected]>"]
license-file = "../LICENSE"
description = "Quilkin is a non-transparent UDP proxy specifically designed for use with large scale multiplayer dedicated game server deployments, to ensure security, access control, telemetry data, metrics and more."
homepage = "https://github.com/googleforgames/quilkin"
repository = "https://github.com/googleforgames/quilkin"
readme = "README.md"
keywords = ["proxy", "game-server", "game-development", "networking", "multiplayer"]
categories = ["game-development", "network-programming"]
edition = "2018"

[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
proc-macro2 = "1.0.26"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full", "derive"] }
115 changes: 115 additions & 0 deletions macros/src/filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2021 Google LLC All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
Ident, NestedMeta, Token,
};

/// The data representation of `#[quilkin::filter]`.
pub(crate) struct FilterAttribute {
/// The protobuf ID for the filter.
id: String,
/// The visibility of the `PROTOBUF_ID` constant.
vis: syn::Visibility,
}

fn parse_str_literal(input: &syn::Lit, span: Span) -> syn::Result<String> {
match input {
syn::Lit::Str(s) => {
let s = s.value();

if s.is_empty() {
Err(syn::Error::new(span, "Str literal must not be empty."))
} else {
Ok(s)
}
}
_ => Err(syn::Error::new(span, "Expected str literal.")),
}
}

fn parse_meta_lit_str(input: &NestedMeta, span: Span) -> syn::Result<String> {
match input {
NestedMeta::Lit(lit) => parse_str_literal(lit, lit.span()),
_ => Err(syn::Error::new(span, "Expected literal.")),
}
}

fn parse_meta_name_value(input: &NestedMeta, span: Span) -> syn::Result<syn::MetaNameValue> {
match input {
NestedMeta::Meta(syn::Meta::NameValue(value)) => Ok(value.clone()),
_ => Err(syn::Error::new(span, "Expected `<name>=<value>`.")),
}
}

impl Parse for FilterAttribute {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args = syn::punctuated::Punctuated::<NestedMeta, Token![,]>::parse_terminated(input)?;

let mut args = args.iter();

let id = {
let arg = args
.next()
.ok_or_else(|| syn::Error::new(input.span(), "Expected a protobuf identifier."))?;
parse_meta_lit_str(arg, arg.span())?
};

let mut vis = None;

for arg in args {
let name_value = parse_meta_name_value(arg, arg.span())?;

if name_value.path.is_ident("vis") {
if vis.is_some() {
return Err(syn::Error::new(
name_value.span(),
"`vis` defined more than once.",
));
}

let input = parse_str_literal(&name_value.lit, name_value.lit.span())?;
vis = Some(syn::parse_str(&input)?);
}
}

Ok(Self {
id,
vis: vis.unwrap_or_else(|| syn::parse_quote!(pub (crate))),
})
}
}

impl ToTokens for FilterAttribute {
fn to_tokens(&self, tokens: &mut TokenStream) {
let id = &self.id;
let vis = &self.vis;
let mut protobuf_path =
syn::punctuated::Punctuated::<syn::PathSegment, syn::token::Colon2>::new();

let split = self.id.split('.');
protobuf_path
.extend(split.map(|s| syn::PathSegment::from(Ident::new(s, Span::mixed_site()))));

tokens.append_all(quote! {
#vis const FILTER_NAME: &'static str = #id;
})
}
}
61 changes: 61 additions & 0 deletions macros/src/include.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2021 Google LLC All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, TokenStreamExt};
use syn::parse::{Parse, ParseStream};

pub(crate) struct IncludeProto {
id: String,
}

impl Parse for IncludeProto {
fn parse(input: ParseStream) -> syn::Result<Self> {
let lit = input.parse::<syn::LitStr>()?;
let id = lit.value();

if id.is_empty() {
Err(syn::Error::new(
lit.span(),
"Expected package name to not be empty.",
))
} else {
Ok(Self { id })
}
}
}

impl ToTokens for IncludeProto {
fn to_tokens(&self, tokens: &mut TokenStream) {
let id = &self.id;

let doc_hidden: syn::Attribute = syn::parse_quote!(#![doc(hidden)]);
let tonic_include_proto: syn::Stmt = syn::parse_quote!(tonic::include_proto!(#id););
let items: Vec<syn::Item> = vec![
syn::Item::Verbatim(doc_hidden.to_token_stream()),
syn::Item::Verbatim(tonic_include_proto.to_token_stream()),
];

let module = id.split('.').rev().fold::<Vec<_>, _>(items, |acc, module| {
let module = syn::Ident::new(module, Span::mixed_site());
let result: syn::ItemMod = syn::parse_quote!(pub(crate) mod #module { #(#acc)* });

vec![syn::Item::Mod(result)]
});

tokens.append_all(module);
}
}
Loading

0 comments on commit fd77e76

Please sign in to comment.