Skip to content

Commit

Permalink
Implement proc macro support
Browse files Browse the repository at this point in the history
  • Loading branch information
nyurik committed Sep 13, 2024
1 parent b5df3da commit ad3c90d
Show file tree
Hide file tree
Showing 27 changed files with 1,860 additions and 118 deletions.
21 changes: 19 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde = ["dep:serde"]
[dependencies]
pkg-config.workspace = true
serde = { workspace = true, optional = true }
varnish-macros.workspace = true

[build-dependencies]
pkg-config.workspace = true
Expand All @@ -29,19 +30,35 @@ workspace = true
#

[workspace]
members = ["vmod_test", "examples/vmod_*"]
members = ["vmod_test", "examples/vmod_*", "varnish-macros"]

[workspace.package]
# These fields could be used by multiple crates
# These fields are used by multiple crates, so it's defined here.
# Version must also be updated in the `varnish-macros` dependency below.
version = "0.0.19"
repository = "https://github.com/gquintard/varnish-rs"
edition = "2021"
license = "BSD-3-Clause"

[workspace.dependencies]
# This version must match the one in the [workspace.package] section above
varnish-macros = { path = "./varnish-macros", version = "=0.0.19" }
# These dependencies are used by one or more crates, and easier to maintain in one place.
bindgen = "0.70.1"
insta = "1"
pkg-config = "0.3.30"
prettyplease = "0.2.22"
proc-macro2 = "1.0.86"
quote = "1.0.37"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10.8"
syn = "2.0.77"

[profile.dev.package]
# Optimize build speed -- https://docs.rs/insta/latest/insta/#optional-faster-runs
insta.opt-level = 3
similar.opt-level = 3

[workspace.lints.rust]
unused_qualifications = "warn"
Expand Down
3 changes: 0 additions & 3 deletions examples/vmod_error/build.rs

This file was deleted.

98 changes: 52 additions & 46 deletions examples/vmod_error/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
varnish::boilerplate!();

use std::fs::read_to_string;

use varnish::vcl::ctx::Ctx;

varnish::vtc!(test01);

// no error, just return 0 if anything goes wrong
pub fn cannot_fail(_: &Ctx, fp: &str) -> i64 {
// try to read the path at fp into a string, but return if there was an error
let Ok(content) = read_to_string(fp) else {
return 0;
};

// try to convert the string into an i64, if parsing fails, force 0
// no need to return as the last expression is automatically returned
content.parse::<i64>().unwrap_or(0)
}

// we call ctx.fail() ourselves, but we still need to return an i64 (which will
// be discarded), so we just convert the 0_u8 returned into an i64 (.into() is
// smart enough to infer the type)
pub fn manual_fail(ctx: &mut Ctx, fp: &str) -> i64 {
// try to read the path at fp into a string, but return if there was an error
let Ok(content) = read_to_string(fp) else {
return ctx
.fail("manual_fail: couldn't read file into string")
.into();
};

// try to convert the string into an i64
// no need to return as the last expression is automatically returned
content
.parse::<i64>()
.unwrap_or_else(|_| ctx.fail("manual_fail: conversion failed").into())
/// This is a simple example of how to handle errors in a Varnish VMOD.
#[varnish::vmod]
mod error {
use std::fs::read_to_string;

use varnish::vcl::ctx::Ctx;

/// This function never fails, returning 0 if anything goes wrong
pub fn cannot_fail(path: &str) -> i64 {
// try to read the path at fp into a string, but return if there was an error
let Ok(content) = read_to_string(path) else {
return 0;
};

// try to convert the string into an i64, if parsing fails, force 0
// no need to return as the last expression is automatically returned
content.parse::<i64>().unwrap_or(0)
}

// we call ctx.fail() ourselves, but we still need to return an i64 (which will
// be discarded), so we just convert the 0_u8 returned into an i64 (.into() is
// smart enough to infer the type)
pub fn manual_fail(ctx: &mut Ctx, fp: &str) -> i64 {
// try to read the path at fp into a string, but return if there was an error
let Ok(content) = read_to_string(fp) else {
return ctx
.fail("manual_fail: couldn't read file into string")
.into();
};

// try to convert the string into an i64
// no need to return as the last expression is automatically returned
content
.parse::<i64>()
.unwrap_or_else(|_| ctx.fail("manual_fail: conversion failed").into())
}

// more idiomatic, we return a Result, and the generated boilerplate will be in charge of
// calling `ctx.fail() and return a dummy value
pub fn result_fail(fp: &str) -> Result<i64, String> {
read_to_string(fp) // read the file
.map_err(|e| format!("result_fail: {e}"))? // convert the error (if any!), into a string
// the ? will automatically return in case
// of an error
.parse::<i64>() // convert
// map the type, and we are good to
// automatically return
.map_err(|e| format!("result_fail: {e}"))
}
}

// more idiomatic, we return a Result, and the generated boilerplate will be in charge of
// calling `ctx.fail() and return a dummy value
pub fn result_fail(_: &mut Ctx, fp: &str) -> Result<i64, String> {
read_to_string(fp) // read the file
.map_err(|e| format!("result_fail: {e}"))? // convert the error (if any!), into a string
// the ? will automatically return in case
// of an error
.parse::<i64>() // convert
.map_err(|e| format!("result_fail: {e}")) // map the type, and we are good to
// automatically return
#[cfg(test)]
mod tests {
varnish::vtc!(test01);
}
File renamed without changes.
4 changes: 0 additions & 4 deletions examples/vmod_example/build.rs

This file was deleted.

65 changes: 25 additions & 40 deletions examples/vmod_example/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,60 +1,45 @@
// import the generated boilerplate
varnish::boilerplate!();
#[varnish::vmod]
mod example {
// we now implement both functions from vmod.vcc, but with rust types.
// Don't forget to make the function public with "pub" in front of them

// even though we won't use it here, we still need to know what the context type is
use varnish::vcl::ctx::Ctx;

// we now implement both functions from vmod.vcc, but with rust types.
// Don't forget to make the function public with "pub" in front of them

pub fn is_even(_: &Ctx, n: i64) -> bool {
n % 2 == 0
}
pub fn is_even(n: i64) -> bool {
n % 2 == 0
}

// in vmod.vcc, n was an optional INT, so here it translates into an Option<i64>
pub fn captain_obvious(_: &Ctx, opt: Option<i64>) -> String {
// we need to first "match" to know if a number was provided, if not,
// return a default message, otherwise, build a custom one
match opt {
// no need to return, we are the last expression of the function!
None => String::from("I was called without an argument"),
// pattern matching FTW!
Some(n) => format!("I was given {n} as argument"),
// in vmod.vcc, n was an optional INT, so here it translates into an Option<i64>
pub fn captain_obvious(opt: Option<i64>) -> String {
// we need to first "match" to know if a number was provided, if not,
// return a default message, otherwise, build a custom one
match opt {
// no need to return, we are the last expression of the function!
None => String::from("I was called without an argument"),
// pattern matching FTW!
Some(n) => format!("I was given {n} as argument"),
}
}
}

#[cfg(test)]
mod tests {
use varnish::vcl::ctx::TestCtx;
use super::example::*;

use super::*;
// test with test/test01.vtc
varnish::vtc!(test01);

#[test]
fn obviousness() {
let mut test_ctx = TestCtx::new(100);
let ctx = test_ctx.ctx();

assert_eq!(
"I was called without an argument",
captain_obvious(&ctx, None)
);
assert_eq!("I was called without an argument", captain_obvious(None));
assert_eq!(
"I was given 975322 as argument",
captain_obvious(&ctx, Some(975_322))
captain_obvious(Some(975_322))
);
}

#[test]
fn even_test() {
// we don't use it, but we still need one
let mut test_ctx = TestCtx::new(100);
let ctx = test_ctx.ctx();

assert!(is_even(&ctx, 0));
assert!(is_even(&ctx, 1024));
assert!(!is_even(&ctx, 421_321));
assert!(is_even(0));
assert!(is_even(1024));
assert!(!is_even(421_321));
}

// we also want to run test/test01.vtc
varnish::vtc!(test01);
}
File renamed without changes.
3 changes: 0 additions & 3 deletions examples/vmod_infiniteloop/build.rs

This file was deleted.

42 changes: 24 additions & 18 deletions examples/vmod_infiniteloop/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
varnish::boilerplate!();
#[varnish::vmod]
mod infiniteloop {
use varnish::ffi::{BUSYOBJ_MAGIC, REQ_MAGIC};
use varnish::vcl::ctx::Ctx;

use varnish::ffi::{BUSYOBJ_MAGIC, REQ_MAGIC};
use varnish::vcl::ctx::Ctx;

varnish::vtc!(test01);

/// # Safety
/// this function is unsafe from the varnish point of view, doing away with
/// important safeguards, but it's also unsafe in the rust way: it dereferences
/// pointers which may lead nowhere
pub unsafe fn reset(ctx: &mut Ctx) {
if let Some(req) = ctx.raw.req.as_mut() {
assert_eq!(req.magic, REQ_MAGIC);
req.restarts = 0;
}
if let Some(bo) = ctx.raw.bo.as_mut() {
assert_eq!(bo.magic, BUSYOBJ_MAGIC);
bo.retries = 0;
/// # Safety
/// this function is unsafe from the varnish point of view, doing away with
/// important safeguards, but it's also unsafe in the rust way: it dereferences
/// pointers which may lead nowhere
pub fn reset(ctx: &mut Ctx) {
unsafe {
if let Some(req) = ctx.raw.req.as_mut() {
assert_eq!(req.magic, REQ_MAGIC);
req.restarts = 0;
}
if let Some(bo) = ctx.raw.bo.as_mut() {
assert_eq!(bo.magic, BUSYOBJ_MAGIC);
bo.retries = 0;
}
}
}
}

#[cfg(test)]
mod tests {
varnish::vtc!(test01);
}
File renamed without changes.
17 changes: 17 additions & 0 deletions examples/vmod_proc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
lints.workspace = true

[package]
name = "vmod_proc"
version = "0.0.1"
edition = "2021"

# if you copy this into a standalone repository,
# replace `{ path = "../.." }` with `"0.0.19"`
[build-dependencies]
varnish = { path = "../.." }

[dependencies]
varnish = { path = "../.." }

[lib]
crate-type = ["cdylib"]
32 changes: 32 additions & 0 deletions examples/vmod_proc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# vmod_example

Here you will find a starting point for your own vmods, and to learn coding vmods in `rust`. Ideally, you should be familiar at least with either vmod development, or with `rust` development, but if your starting fresh, this should get you going too.

# Compiling

You need only two things:
- a stable version of `cargo`/`rust`
- the `libvarnish` development files installed where `pkg-config` can find them
- `python31

From within this directory, run:

```
# build
cargo build
# you should now have a file name target/debug/libvmod_example.so
# test (you need to build first!)
cargo test
```

That's it!

# Files

Look around, everything should be decently documented:
- [vmod.vcc](vmod.vcc): your starting point, where you will describe your vmod API
- [src/vmod.rs](src/vmod.rs): the file containing the actual implementation and unit tests
- [tests/test01.vtc](tests/test01.vtc): a VTC (full stack) test, actually running Varnish against mock clients and servers
- [Cargo.toml](Cargo.toml): the file describing the name of the vmod, as well as its dependencies
- [build.rs](build.rs): a short program in charge of generating some boilerplate before the compiler starts
21 changes: 21 additions & 0 deletions examples/vmod_proc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#[varnish::vmod]
mod proc {
pub fn is_even(n: i64) -> bool {
n % 2 == 0
}
}

#[cfg(test)]
mod tests {
use super::proc::*;

#[test]
fn even_test() {
assert!(is_even(0));
assert!(is_even(1024));
assert!(!is_even(421_321));
}

// we also want to run test/test01.vtc
varnish::vtc!(test01);
}
Loading

0 comments on commit ad3c90d

Please sign in to comment.