diff --git a/Cargo.toml b/Cargo.toml index 532b45d..555843f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 @@ -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" diff --git a/examples/vmod_error/build.rs b/examples/vmod_error/build.rs deleted file mode 100644 index 4f48ef0..0000000 --- a/examples/vmod_error/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - varnish::generate_boilerplate().unwrap(); -} diff --git a/examples/vmod_error/src/lib.rs b/examples/vmod_error/src/lib.rs index f9c9e39..470dc48 100644 --- a/examples/vmod_error/src/lib.rs +++ b/examples/vmod_error/src/lib.rs @@ -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::().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::() - .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::().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::() + .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 { + 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::() // 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 { - 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::() // 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); } diff --git a/examples/vmod_error/vmod.vcc b/examples/vmod_error/vmod.vcc.unused similarity index 100% rename from examples/vmod_error/vmod.vcc rename to examples/vmod_error/vmod.vcc.unused diff --git a/examples/vmod_example/build.rs b/examples/vmod_example/build.rs deleted file mode 100644 index c9821f6..0000000 --- a/examples/vmod_example/build.rs +++ /dev/null @@ -1,4 +0,0 @@ -// before we actually compile our code, parse `vmod.vcc` to generate some boilerplate -fn main() { - varnish::generate_boilerplate().unwrap(); -} diff --git a/examples/vmod_example/src/lib.rs b/examples/vmod_example/src/lib.rs index 69acf74..8a88f07 100644 --- a/examples/vmod_example/src/lib.rs +++ b/examples/vmod_example/src/lib.rs @@ -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 -pub fn captain_obvious(_: &Ctx, opt: Option) -> 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 + pub fn captain_obvious(opt: Option) -> 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); } diff --git a/examples/vmod_example/vmod.vcc b/examples/vmod_example/vmod.vcc.unused similarity index 100% rename from examples/vmod_example/vmod.vcc rename to examples/vmod_example/vmod.vcc.unused diff --git a/examples/vmod_infiniteloop/build.rs b/examples/vmod_infiniteloop/build.rs deleted file mode 100644 index 4f48ef0..0000000 --- a/examples/vmod_infiniteloop/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - varnish::generate_boilerplate().unwrap(); -} diff --git a/examples/vmod_infiniteloop/src/lib.rs b/examples/vmod_infiniteloop/src/lib.rs index be384ea..0c5ce2f 100644 --- a/examples/vmod_infiniteloop/src/lib.rs +++ b/examples/vmod_infiniteloop/src/lib.rs @@ -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); +} diff --git a/examples/vmod_infiniteloop/vmod.vcc b/examples/vmod_infiniteloop/vmod.vcc.unused similarity index 100% rename from examples/vmod_infiniteloop/vmod.vcc rename to examples/vmod_infiniteloop/vmod.vcc.unused diff --git a/examples/vmod_proc/Cargo.toml b/examples/vmod_proc/Cargo.toml new file mode 100644 index 0000000..5dd6d1f --- /dev/null +++ b/examples/vmod_proc/Cargo.toml @@ -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"] diff --git a/examples/vmod_proc/README.md b/examples/vmod_proc/README.md new file mode 100644 index 0000000..0b1a041 --- /dev/null +++ b/examples/vmod_proc/README.md @@ -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 diff --git a/examples/vmod_proc/src/lib.rs b/examples/vmod_proc/src/lib.rs new file mode 100644 index 0000000..fa8edfa --- /dev/null +++ b/examples/vmod_proc/src/lib.rs @@ -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); +} diff --git a/examples/vmod_proc/tests/test01.vtc b/examples/vmod_proc/tests/test01.vtc new file mode 100644 index 0000000..15aa00a --- /dev/null +++ b/examples/vmod_proc/tests/test01.vtc @@ -0,0 +1,43 @@ +# first thing to do: give a title to the test +varnishtest "testing .is_even()" + +# describe what the server should receive and send back +# "man vtc" is your friend here +server s1 { + rxreq + expect req.url == "/first" + expect req.http.even == "true" + txresp + + rxreq + expect req.url == "/second" + expect req.http.even == "false" + txresp +} -start + +# set up the varnish server with our custom logic +varnish v1 -vcl+backend { + import proc from "${vmod}"; + + sub vcl_recv { + # modify the request here and the changes will be carried on + # to the backend + if (req.url == "/first") { + set req.http.even = proc.is_even(8); + } else { + set req.http.even = proc.is_even(3); + } + } +} -start + +# finally, run the client, if something goes wrong, the server +# will complain, and we won't receive a response back +client c1 { + txreq -url "/first" + rxresp + expect resp.status == 200 + + txreq -url "/second" + rxresp + expect resp.status == 200 +} -run diff --git a/src/lib.rs b/src/lib.rs index a94cfbd..d858bbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,6 +128,8 @@ pub mod vcl { mod utils; } +pub use varnish_macros::vmod; + /// Automate VTC testing /// /// Varnish provides a very handy tool for end-to-end testing: @@ -174,6 +176,9 @@ pub mod vcl { #[macro_export] macro_rules! vtc { ( $name:ident ) => { + $crate::vtc!($name, false); + }; + ( $name:ident, $always_output:expr ) => { #[cfg(test)] #[test] fn $name() { @@ -201,15 +206,19 @@ macro_rules! vtc { Some(p) => p.to_str().unwrap().to_owned(), }; let mut cmd = Command::new("varnishtest"); - cmd.arg("-D") + cmd.arg("-L") + .arg("-v") + .arg("-D") .arg(format!("vmod={}", vmod_path)) .arg(concat!("tests/", stringify!($name), ".vtc")) .env("VARNISHTEST_DURATION", test_duration); let output = cmd.output().unwrap(); - if !output.status.success() { + if $always_output || !output.status.success() { io::stdout().write_all(&output.stdout).unwrap(); io::stdout().write_all(&output.stderr).unwrap(); + } + if !output.status.success() { panic!( "{}", format!( diff --git a/varnish-macros/Cargo.toml b/varnish-macros/Cargo.toml new file mode 100644 index 0000000..2713750 --- /dev/null +++ b/varnish-macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "varnish-macros" +description = "Varnish VMOD support - macros" +authors = ["Yuri Astrakhan ", "Guillaume Quintard "] +# FIXME: need categories 1and keywords +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +proc-macro = true + +[dependencies] +prettyplease.workspace = true +proc-macro2.workspace = true +quote.workspace = true +serde_json.workspace = true +sha2.workspace = true +syn.workspace = true + +[dev-dependencies] +insta.workspace = true + +[lints] +workspace = true diff --git a/varnish-macros/generated-example-reference.rs b/varnish-macros/generated-example-reference.rs new file mode 100644 index 0000000..ca8245c --- /dev/null +++ b/varnish-macros/generated-example-reference.rs @@ -0,0 +1,156 @@ +// +// This file was generated from the vmod_example, and optimized for readability and other things. +// Now it is being used as a reference for the generated code - this is the style of code we want to generate. +// + +mod generated_vmod { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + + unsafe extern "C" fn vmod_c_is_even(vrt_ctx: *mut vrt_ctx, arg1: VCL_INT) -> VCL_BOOL { + let mut ctx = Ctx::from_ptr(vrt_ctx); + crate::example::is_even(&ctx, arg1.into_rust()) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + VCL_BOOL::vcl_default() + }) + } + + #[allow(non_camel_case_types)] + #[repr(C)] + struct arg_vmod_example_captain_obvious { + valid_n: c_char, + n: VCL_INT, + } + + unsafe extern "C" fn vmod_c_captain_obvious( + vrt_ctx: *mut vrt_ctx, + args: *const arg_vmod_example_captain_obvious, + ) -> VCL_STRING { + let mut ctx = Ctx::from_ptr(vrt_ctx); + let args = args.as_ref().unwrap(); + let arg1 = (args.valid_n != 0).then(|| args.n.into_rust()); + crate::example::captain_obvious(&ctx, arg1) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + VCL_STRING::vcl_default() + }) + } + + #[repr(C)] + pub struct VmodExports { + is_even: Option VCL_BOOL>, + captain_obvious: Option< + unsafe extern "C" fn( + vrt_ctx: *mut vrt_ctx, + args: *const arg_vmod_example_captain_obvious, + ) -> VCL_STRING, + >, + } + + // #[no_mangle] // FIXME: no_mangle does not seem to be needed + pub static VMOD_EXPORTS: VmodExports = VmodExports { + is_even: Some(vmod_c_is_even), + captain_obvious: Some(vmod_c_captain_obvious), + }; + + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + + unsafe impl Sync for VmodData {} + + /// The name must be in the format `Vmod_{name}_Data`. + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static Vmod_example_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"82357c81c3bc450a6c9b3bbc0f31bf532e17f984203fd3a9d8887a00d076638b".as_ptr(), + name: c"example".as_ptr(), + func_name: c"Vmod_example".as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + + const JSON: &CStr = cr#"\ +VMOD_JSON_SPEC +[ + [ + "$VMOD", + "1.0", + "example", + "Vmod_example", + "82357c81c3bc450a6c9b3bbc0f31bf532e17f984203fd3a9d8887a00d076638b", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + "typedef VCL_BOOL td_vmod_example_is_even(VRT_CTX, VCL_INT);\n\nstruct arg_vmod_example_captain_obvious {\n\tchar\t\t\tvalid_n;\n\tVCL_INT\t\t\tn;\n};\ntypedef VCL_STRING td_vmod_example_captain_obvious(VRT_CTX,\n struct arg_vmod_example_captain_obvious*);\n\nstruct Vmod_example {\n\ttd_vmod_example_is_even\t\t*f_is_even;\n\ttd_vmod_example_captain_obvious\t*f_captain_obvious;\n};\nstatic struct Vmod_example Vmod_example;" + ], + [ + "$FUNC", + "is_even", + [ + ["BOOL"], + "Vmod_example.f_is_even", + "", + ["INT"] + ] + ], + [ + "$FUNC", + "captain_obvious", + [ + ["STRING"], + "Vmod_example.f_captain_obvious", + "struct arg_vmod_example_captain_obvious", + ["INT", "n", null, null, true] + ] + ] +] +"#; + + // + // DO NOT GENERATE THIS CODE - used only to make the above single string readable + // + const ___DEF___: &str = r"\ +typedef VCL_BOOL td_vmod_example_is_even(VRT_CTX, VCL_INT); + +struct arg_vmod_example_captain_obvious { + char valid_n; + VCL_INT n; +}; + +typedef VCL_STRING td_vmod_example_captain_obvious(VRT_CTX, struct arg_vmod_example_captain_obvious*); + +struct Vmod_example { + td_vmod_example_is_even *f_is_even; + td_vmod_example_captain_obvious *f_captain_obvious; +}; + +static struct Vmod_example Vmod_example;"; +} diff --git a/varnish-macros/src/func_processor.rs b/varnish-macros/src/func_processor.rs new file mode 100644 index 0000000..011080f --- /dev/null +++ b/varnish-macros/src/func_processor.rs @@ -0,0 +1,291 @@ +use std::fmt::Write as _; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use serde_json::{json, Value}; + +use crate::generator::Generator; +use crate::model::{ArgType, FuncInfo, ParamType, ReturnType}; + +#[derive(Debug, Default)] +pub struct FuncProcessor { + /// Are there any optional arguments in the function? + has_optional_args: bool, + + /// Name of the VMOD + name_mod: String, + /// Name of the user's Rust function + name_r_fn: String, + /// Name of the export "C" function, i.e. `vmod_c_fn_name` + name_c_fn: String, + /// For fn with optional args, the name of the struct that holds all arguments, i.e. `arg_simple_void_to_void` + name_args_ty: String, + + /// Arguments to the Rust function with the needed conversion, i.e. `[ {ctx}, {arg0.into_rust()} ]` + args_r_expr: Vec, + /// C function list of arguments for funcs with no optional args, e.g. `["VCL_INT", "VCL_STRING"]` + args_hdr: Vec<&'static str>, + /// For optional arguments, params to go into the C header $CPROTO, i.e. `[ {c_char valid_arg0}, {VCL_INT arg1} ]` + args_hdr_opt_decl: Vec, + /// For optional arguments, params to go into `args` struct, i.e. `[ {valid_arg0: c_char}, {arg1: VCL_INT} ]` + args_c_opt_decl: Vec, + /// Args to the export "C" function. Similar to `args_c_opt_decl`, i.e. `[ {ctx: *mut vrt_ctx}, {arg0: VCL_INT} ]` + args_c_fn_decl: Vec, + /// List of arguments as published in the JSON, with up to five values each e.g. `[[INT, ...], [STRING]]` + /// Order: `[VCC_type, arg_name, default_value, spec(?), is_optional]` + /// Any arg after the first one is optional - `NULL`, and all trailing `NULLs` should be removed. + args_json: Vec, + + /// `-> c_output_type` or empty if the export "C" function returns nothing + output_c_decl: TokenStream, + /// VCL types as used in the .c and .h files, e.g. `VCL_INT`, `VCL_STRING`, `VCL_VOID`, ... + output_hdr: &'static str, + /// VCC types as used in the .vcc file, e.g. `INT`, `STRING`, `VOID`, ... + output_vcc: &'static str, + /// Expression to return a default value for the return type + output_default_expr: TokenStream, + + /// `typedef VCL_VOID td_simple_void_to_void(VRT_CTX, VCL_STRING, ...);` + pub typedef_decl: String, + /// `td_simple_void_to_void *f_void_to_void;` - part of the `struct Vmod_simple { ... }` C code + pub typedef_init: String, + /// `rust_fn_name: Option< unsafe extern "C" fn name_c_fn(c_args) -> c_output_type >` + pub export_decl: TokenStream, + /// `rust_fn_name: Some(name_c_fn)` + pub export_init: TokenStream, + /// Full body of the export "C" function + pub c_function_body: TokenStream, +} + +impl FuncProcessor { + pub fn from_info(root: &Generator, info: &FuncInfo) -> Self { + let ident = &info.ident; + + let has_optional_args = info + .args + .iter() + .any(|arg| matches!(arg, ParamType::Arg(v) if v.is_optional)); + + let name_args_ty = if has_optional_args { + format!("arg_{}_{}", root.mod_name, info.ident) + } else { + String::new() + }; + + let mut obj = Self { + has_optional_args, + name_mod: root.mod_name.clone(), + name_r_fn: info.ident.clone(), + name_c_fn: format!("vmod_c_{ident}"), + name_args_ty, + typedef_decl: "\n".to_string(), // Start with a newline to make debugging output cleaner + ..Default::default() + }; + obj.init(info); + obj + } + + fn init(&mut self, info: &FuncInfo) { + self.do_fn_return(&info.returns); + self.args_c_fn_decl.push(quote! { ctx: *mut vrt_ctx }); + if self.has_optional_args { + let name_args_ty = format_ident!("{}", self.name_args_ty); + self.args_c_fn_decl + .push(quote! { args: *const #name_args_ty }); + } + for arg in &info.args { + self.do_fn_param(arg); + } + + let ident = &info.ident; + let args_c_fn_decl = &self.args_c_fn_decl; + let output_c_decl = &self.output_c_decl; + let rust_fn_name = format_ident!("{ident}"); + let name_c_fn = format_ident!("{}", self.name_c_fn); + + self.export_decl = quote! { #rust_fn_name: Option< unsafe extern "C" fn(#(#args_c_fn_decl),*) #output_c_decl > }; + self.export_init = quote! { #rust_fn_name: Some(#name_c_fn) }; + self.c_function_body = self.gen_callback_fn(info); + + let name_mod = &self.name_mod; + self.typedef_init = format!(" td_{name_mod}_{ident} *f_{ident};\n"); + if self.has_optional_args { + // This corresponds to the Rust declaration created in `gen_callback_fn` + // struct arg_vmod_example_captain_obvious { + // char valid_n; + // VCL_INT n; + // }; + // typedef VCL_STRING td_vmod_example_captain_obvious(VRT_CTX, struct arg_vmod_example_captain_obvious*); + let _ = writeln!(self.typedef_decl, "struct {} {{", self.name_args_ty); + for arg in &self.args_hdr_opt_decl { + let _ = writeln!(self.typedef_decl, " {arg};"); + } + let _ = writeln!(self.typedef_decl, "}};"); + } + let _ = write!( + self.typedef_decl, + "typedef {} td_{name_mod}_{ident}(VRT_CTX", + self.output_hdr + ); + if self.has_optional_args { + let _ = write!(self.typedef_decl, ", struct {}*", self.name_args_ty); + } else { + for arg in &self.args_hdr { + let _ = write!(self.typedef_decl, ", {arg}"); + } + } + let _ = writeln!(self.typedef_decl, ");"); + } + + fn do_fn_param(&mut self, arg: &ParamType) { + match arg { + ParamType::Context { is_mut } => { + // c_args: C fn always has ctx as first arg, skipping it here + let ident = format_ident!("ctx"); + self.args_r_expr.push(if *is_mut { + quote! { &mut #ident } + } else { + quote! { &#ident } + }); + } + ParamType::Arg(arg) => { + if !self.has_optional_args { + self.args_hdr.push(arg.ty_info.to_c_type()); + } + + // JSON data for each argument: + // [VCC_type, arg_name, default_value, spec(?), is_optional] + let mut json_arg: Vec = vec![ + arg.ty_info.to_vcc_type().into(), + arg.name.clone().into(), + arg.default.clone().into(), + Value::Null, // spec param is not used at this point + ]; + if arg.is_optional { + json_arg.push(true.into()); + } else { + // trim all NULLs from the end of json_arg list + while let Some(Value::Null) = json_arg.last() { + json_arg.pop(); + } + } + self.args_json.push(json_arg.into()); + + // Figure out how Varnish will pass this argument to the wrapper function + // In case of optional args, add arguments to a separate struct + let args_list = if self.has_optional_args { + &mut self.args_c_opt_decl + } else { + &mut self.args_c_fn_decl + }; + let arg_name = format_ident!("{}", arg.name); + let arg_valid = format_ident!("valid_{arg_name}"); + if arg.is_optional { + args_list.push(quote! { #arg_valid: c_char }); + self.args_hdr_opt_decl.push(format!("char {arg_valid}")); + } + let arg_ty = format_ident!("{}", arg.ty_info.to_c_type()); + args_list.push(quote! { #arg_name: #arg_ty }); + if self.has_optional_args { + self.args_hdr_opt_decl.push(format!("{arg_ty} {arg_name}")); + } + + // Convert C arg into Rust arg and pass it to the user's function + let c_arg = if arg.is_optional { + quote! { args.#arg_name } + } else { + quote! { #arg_name } + }; + let mut r_arg = match arg.ty_info { + ArgType::RustBool | ArgType::RustI64 => quote! { #c_arg.into_rust() }, + ArgType::RustStr => quote! { &*#c_arg.into_rust() }, + ArgType::RustString => { + panic!("String params not supported. Use &str instead.") + } + }; + if arg.is_optional { + r_arg = quote! { (args.#arg_valid != 0).then(|| #r_arg) }; + } + self.args_r_expr.push(r_arg); + } + } + } + + fn do_fn_return(&mut self, ty: &ReturnType) { + match ty { + ReturnType::Default => { + self.output_c_decl = quote! {}; + self.output_hdr = "VCL_VOID"; + self.output_vcc = "VOID"; + self.output_default_expr = quote! {}; + } + ReturnType::Arg(ty) | ReturnType::Result(ty, _) => { + let ret: TokenStream = (*ty).into(); + self.output_c_decl = quote! { -> #ret }; + self.output_hdr = ty.to_c_type(); + self.output_vcc = ty.to_vcc_type(); + self.output_default_expr = quote! { <#ret>::vcl_default() }; + } + } + } + + /// Generate an extern "C" wrapper function that calls user's Rust function + fn gen_callback_fn(&self, info: &FuncInfo) -> TokenStream { + let name_c_fn = format_ident!("{}", self.name_c_fn); + let name_r_fn = format_ident!("{}", self.name_r_fn); + let args_c_fn_decl = &self.args_c_fn_decl; + let args_r_expr = &self.args_r_expr; + let output_c_decl = &self.output_c_decl; + let output_default_expr = &self.output_default_expr; + + let (opt_arg, opt_args_init) = if self.has_optional_args { + let name_args_ty = format_ident!("{}", self.name_args_ty); + let args_c_opt_decl = &self.args_c_opt_decl; + let opt_arg = quote! { + #[repr(C)] + struct #name_args_ty { + #(#args_c_opt_decl,)* + } + }; + (opt_arg, quote! { let args = args.as_ref().unwrap(); }) + } else { + (quote! {}, quote! {}) + }; + + let into_result = if matches!(info.returns, ReturnType::Result(_, _)) { + quote! {} + } else { + quote! { .into_result() } + }; + + quote! { + #opt_arg + unsafe extern "C" fn #name_c_fn(#(#args_c_fn_decl),*) #output_c_decl { + let mut ctx = Ctx::from_ptr(ctx); + #opt_args_init + #name_r_fn(#(#args_r_expr),*) + #into_result + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + #output_default_expr + }) + } + } + } + + pub fn json_func(&self) -> Value { + let args_struct = if self.has_optional_args { + format!("struct {}", self.name_args_ty) + } else { + String::new() + }; + let mut json_func: Vec = vec![ + vec![self.output_vcc].into(), + format!("Vmod_{}.f_{}", self.name_mod, self.name_r_fn).into(), + args_struct.into(), + ]; + json_func.extend(self.args_json.iter().cloned()); + json! { [ "$FUNC", self.name_r_fn, json_func ] } + } +} diff --git a/varnish-macros/src/generator.rs b/varnish-macros/src/generator.rs new file mode 100644 index 0000000..43ee48a --- /dev/null +++ b/varnish-macros/src/generator.rs @@ -0,0 +1,168 @@ +use std::ffi::CString; +use std::fmt::Write as _; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use serde_json::{json, Value}; +use sha2::{Digest as _, Sha256}; + +use crate::func_processor::FuncProcessor; +use crate::model::VmodInfo; + +#[derive(Debug, Default)] +pub struct Generator { + pub mod_name: String, + pub file_id: CString, + pub functions: Vec, + pub json: CString, +} + +/// See https://varnish-cache.org/docs/7.5/reference/vmod.html#optional-arguments +impl Generator { + pub fn render(info: &VmodInfo) -> TokenStream { + let mut obj = Self { + mod_name: info.name.clone(), + file_id: Self::calc_file_id(info), + ..Self::default() + }; + for fn_info in &info.funcs { + obj.functions.push(FuncProcessor::from_info(&obj, fn_info)); + } + obj.json = obj.gen_json(); + obj.render_int() + } + + /// Use the entire data model parsed from sources to generate a hash. + /// Should be somewhat consistent and unique for each set of functions. + fn calc_file_id(info: &VmodInfo) -> CString { + CString::new( + Sha256::digest(format!("{info:?}").as_bytes()) + .into_iter() + .fold(String::new(), |mut output, b| { + let _ = write!(output, "{b:02x}"); + output + }), + ) + .unwrap() + } + + fn gen_json(&mut self) -> CString { + let mod_name = &self.mod_name; + let c_func_name = CString::new(format!("Vmod_{mod_name}")).unwrap(); + let mut json = Vec::::new(); + + json.push(json! {[ + "$VMOD", + "1.0", + mod_name, + c_func_name.to_str().unwrap(), + self.file_id.to_str().unwrap(), + // FIXME! Oh the irony - this string is from VMOD_ABI_Version + // but it has to be hardcoded in a string constant, + // so varnish-macros must be able to read it... + // In other words, we may have to bring back varnish-sys just for this :( + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ]}); + + let mut cproto = String::new(); + for func in &self.functions { + cproto.push_str(&func.typedef_decl); + } + let _ = write!(cproto, "\nstruct Vmod_{mod_name} {{\n"); + for func in &self.functions { + cproto.push_str(&func.typedef_init); + } + let _ = write!( + cproto, + "}};\n\nstatic struct Vmod_{mod_name} Vmod_{mod_name};" + ); + json.push(json! {[ "$CPROTO", cproto ]}); + + for func in &self.functions { + json.push(func.json_func()); + } + + let json = serde_json::to_string_pretty(&json! {json}).unwrap(); + CString::new(format!("VMOD_JSON_SPEC\n{json}\n")).unwrap() + } + + fn render_int(&mut self) -> TokenStream { + let mod_name = &self.mod_name; + let mod_name_ident = format_ident!("{mod_name}"); + let gen_mod_name = format_ident!("{mod_name}_generated"); + let vmod_name_data = format_ident!("Vmod_{mod_name}_Data"); + let c_name = CString::new(mod_name.as_bytes()).unwrap(); + let c_func_name = CString::new(format!("Vmod_{mod_name}")).unwrap(); + let file_id = &self.file_id; + let functions = self.functions.iter().map(|f| &f.c_function_body); + let json = &self.json; + + let export_decls: Vec<_> = self.functions.iter().map(|f| &f.export_decl).collect(); + let export_inits: Vec<_> = self.functions.iter().map(|f| &f.export_init).collect(); + + let content = quote! { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + + use super::#mod_name_ident::*; + + #( #functions )* + + #[repr(C)] + pub struct VmodExports { + #(#export_decls,)* + } + + // #[no_mangle] // FIXME: no_mangle does not seem to be needed + pub static VMOD_EXPORTS: VmodExports = VmodExports { + #(#export_inits,)* + }; + + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + + unsafe impl Sync for VmodData {} + + // This name must be in the format `Vmod_{name}_Data`. + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static #vmod_name_data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: #file_id.as_ptr(), + name: #c_name.as_ptr(), + func_name: #c_func_name.as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + + const JSON: &CStr = #json; + }; + + quote!( + #[allow(unused_imports)] + mod #gen_mod_name { + #content + } + ) + } +} diff --git a/varnish-macros/src/lib.rs b/varnish-macros/src/lib.rs new file mode 100644 index 0000000..b9fb4c4 --- /dev/null +++ b/varnish-macros/src/lib.rs @@ -0,0 +1,18 @@ +// #![allow(warnings)] + +use {proc_macro as pm, proc_macro2 as pm2}; + +mod func_processor; +mod generator; +mod model; +mod parser; + +#[cfg(test)] +mod tests; + +#[proc_macro_attribute] +pub fn vmod(args: pm::TokenStream, input: pm::TokenStream) -> pm::TokenStream { + let args = pm2::TokenStream::from(args); + let input = pm2::TokenStream::from(input); + parser::vmod(args, input).into() +} diff --git a/varnish-macros/src/model.rs b/varnish-macros/src/model.rs new file mode 100644 index 0000000..db51910 --- /dev/null +++ b/varnish-macros/src/model.rs @@ -0,0 +1,72 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +#[derive(Debug, Default)] +pub struct VmodInfo { + pub name: String, + pub funcs: Vec, +} + +#[derive(Debug)] +pub struct FuncInfo { + pub ident: String, + pub args: Vec, + pub returns: ReturnType, +} + +#[derive(Debug, Clone)] +pub enum ReturnType { + Default, + Arg(ArgType), + // The error return type is not directly used yet + Result(ArgType, #[allow(dead_code)] ArgType), +} + +#[derive(Debug, Clone)] +pub enum ParamType { + Context { is_mut: bool }, + Arg(ParamInfo), +} + +#[derive(Debug, Clone)] +pub struct ParamInfo { + pub name: String, + pub is_optional: bool, + pub ty_info: ArgType, + pub default: Option, +} + +#[derive(Debug, Clone, Copy)] +pub enum ArgType { + RustI64, + RustBool, + RustStr, + RustString, +} + +impl From for TokenStream { + fn from(value: ArgType) -> Self { + let ident = format_ident!("{}", value.to_c_type()); + quote!(#ident) + } +} + +impl ArgType { + pub fn to_vcc_type(self) -> &'static str { + match self { + Self::RustI64 => "INT", + Self::RustBool => "BOOL", + Self::RustStr | Self::RustString => "STRING", + } + } + + pub fn to_c_type(self) -> &'static str { + // ATTENTION: Each VCL_* type here must also be listed in the `use varnish::...` + // statement in the `varnish-macros/src/generator.rs` file. + match self { + Self::RustI64 => "VCL_INT", + Self::RustBool => "VCL_BOOL", + Self::RustStr | Self::RustString => "VCL_STRING", + } + } +} diff --git a/varnish-macros/src/parser.rs b/varnish-macros/src/parser.rs new file mode 100644 index 0000000..369d2df --- /dev/null +++ b/varnish-macros/src/parser.rs @@ -0,0 +1,203 @@ +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::Item::Fn; +use syn::PathArguments::AngleBracketed; +use syn::Type::{Path, Reference}; +use syn::{parse2, FnArg, GenericArgument, ItemFn, ItemMod, Pat, Signature, TypePath, Visibility}; + +use crate::generator::Generator; +use crate::model::{ArgType, FuncInfo, ParamInfo, ParamType, ReturnType, VmodInfo}; + +pub fn vmod(_args: TokenStream, input: TokenStream) -> TokenStream { + let original = input.clone(); + let item = parse2::(input).unwrap(); + let info = VmodInfo::parse(item); + let mut output = Generator::render(&info); + output.extend(original); + + output +} + +impl VmodInfo { + pub fn parse(item: ItemMod) -> Self { + // process all functions in the module + let mut funcs = Vec::::new(); + if let Some((_, content)) = item.content { + for item in content { + let Fn(fn_item) = item else { + continue; + }; + if matches!(fn_item.vis, Visibility::Public(..)) { + funcs.push(FuncInfo::parse(fn_item)); + } + } + } + Self { + name: item.ident.to_string(), + funcs, + } + } +} + +impl FuncInfo { + pub fn parse(item_fn: ItemFn) -> Self { + let sig = item_fn.sig; + assert!( + sig.asyncness.is_none() && sig.unsafety.is_none(), + "async and unsafe functions are not supported: {}", + quote! {#sig} + ); + + let args: Vec<_> = sig + .inputs + .iter() + .map(|v| ParamType::parse(v, &sig)) + .collect(); + + let ctx_arg_count = args + .iter() + .filter(|v| matches!(v, ParamType::Context { .. })) + .count(); + if ctx_arg_count > 1 { + panic!("Context must be used no more than once"); + } + + Self { + ident: sig.ident.to_string(), + returns: ReturnType::parse(sig.output), + args, + } + } +} + +impl ReturnType { + fn parse(return_type: syn::ReturnType) -> Self { + match &return_type { + syn::ReturnType::Default => ReturnType::Default, + syn::ReturnType::Type(_, ty) => { + if let Path(path) = ty.as_ref() { + if let Some((name, params)) = parse_gen_type(path) { + if name == "Result" && params.len() == 2 { + let Some(ok_ty) = ArgType::try_parse(¶ms[0]) else { + panic!("Result content type must be a simple type"); + }; + let Some(err_ty) = ArgType::try_parse(¶ms[1]) else { + panic!("Result error type must be a simple type"); + }; + return Self::Result(ok_ty, err_ty); + } + } + let Some(ty) = ArgType::try_parse(path) else { + panic!("Result content type must be a simple type"); + }; + ReturnType::Arg(ty) + } else { + panic!("unsupported return type"); + } + } + } + } +} + +impl ParamType { + pub fn parse(arg: &FnArg, sig: &Signature) -> Self { + let arg = match arg { + FnArg::Receiver(_) => panic!("receiver not allowed"), + FnArg::Typed(pat) => pat, + }; + let pat = arg.pat.as_ref(); + let Pat::Ident(pat) = pat else { + panic!("Unsupported argument pattern: {}", quote! {#pat}) + }; + let name = pat.ident.to_string(); + let arg_ty = arg.ty.as_ref(); + match arg_ty { + Reference(rf) => match &rf.elem.as_ref() { + Path(path) if path.path.is_ident("Ctx") => { + let is_mut = rf.mutability.is_some(); + Self::Context { is_mut } + } + Path(path) if path.path.is_ident("str") => { + assert!(rf.mutability.is_none(), "&str params must be immutable"); + Self::Arg(ParamInfo::new(name, ArgType::RustStr, false)) + } + _ => { + panic!("References are not supported: {}", quote! {#sig}); + } + }, + Path(path) => Self::Arg(ParamInfo::parse(name, path)), + _ => { + let a = arg_ty; + panic!("unsupported arg type {} in {}", quote! {#a}, quote! {#sig}); + } + } + } +} + +impl ParamInfo { + fn new(name: String, ty_info: ArgType, is_optional: bool) -> Self { + Self { + name, + is_optional, + ty_info, + default: None, + } + } + + fn parse(name: String, path: &TypePath) -> Self { + if let Some(arg_type) = ArgType::try_parse(path) { + return Self::new(name, arg_type, false); + } + if let Some((ident, args)) = parse_gen_type(path) { + if ident == "Option" && args.len() == 1 { + let Some(arg_type) = ArgType::try_parse(&args[0]) else { + panic!("Option must have a simple type argument"); + }; + return Self::new(name, arg_type, true); + } + } + panic!("unsupported type: {}", quote! {#path}); + } +} + +impl ArgType { + fn try_parse(path: &TypePath) -> Option { + let ident = path.path.get_ident()?; + Some(if ident == "i64" { + Self::RustI64 + } else if ident == "bool" { + Self::RustBool + } else if ident == "String" { + Self::RustString + } else if ident == "str" { + Self::RustStr + } else { + return None; + }) + } +} + +/// Parse a generic type, e.g. `Option` or `Result`. +/// Returns the type name and a list of generic arguments, or `None` if the type is not generic. +fn parse_gen_type(path: &TypePath) -> Option<(Ident, Vec)> { + if path.qself.is_some() || path.path.leading_colon.is_some() || path.path.segments.len() != 1 { + return None; + } + let seg = &path.path.segments[0]; + if let AngleBracketed(args) = &seg.arguments { + let args: Option> = args + .args + .iter() + .map(|arg| { + if let GenericArgument::Type(Path(path)) = arg { + Some(path.clone()) + } else { + None + } + }) + .collect(); + Some((seg.ident.clone(), args?)) + } else { + None + } +} diff --git a/varnish-macros/src/tests/empty.rs b/varnish-macros/src/tests/empty.rs new file mode 100644 index 0000000..9089f35 --- /dev/null +++ b/varnish-macros/src/tests/empty.rs @@ -0,0 +1,85 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn empty() { + let (data, json) = expand_macros(quote! { + #[varnish::vmod] + mod empty { + } + }); + + assert_snapshot!(data, @r##" + #[allow(unused_imports)] + mod empty_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::empty::*; + #[repr(C)] + pub struct VmodExports {} + pub static VMOD_EXPORTS: VmodExports = VmodExports {}; + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + unsafe impl Sync for VmodData {} + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static Vmod_empty_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"14a1ee47dc2c51f3ffb91b6dfe8b6814649d8ad3f89a6c1b44eb6f27a00fca95" + .as_ptr(), + name: c"empty".as_ptr(), + func_name: c"Vmod_empty".as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + const JSON: &CStr = c"VMOD_JSON_SPEC\u{2}\n[\n [\n \"$VMOD\",\n \"1.0\",\n \"empty\",\n \"Vmod_empty\",\n \"14a1ee47dc2c51f3ffb91b6dfe8b6814649d8ad3f89a6c1b44eb6f27a00fca95\",\n \"Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1\",\n \"0\",\n \"0\"\n ],\n [\n \"$CPROTO\",\n \"\\nstruct Vmod_empty {\\n};\\n\\nstatic struct Vmod_empty Vmod_empty;\"\n ]\n]\n\u{3}"; + } + #[varnish::vmod] + mod empty {} + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "empty", + "Vmod_empty", + "14a1ee47dc2c51f3ffb91b6dfe8b6814649d8ad3f89a6c1b44eb6f27a00fca95", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + " + struct Vmod_empty { + }; + + static struct Vmod_empty Vmod_empty;" + ] + ] +  + "#); +} diff --git a/varnish-macros/src/tests/mod.rs b/varnish-macros/src/tests/mod.rs new file mode 100644 index 0000000..cd6b532 --- /dev/null +++ b/varnish-macros/src/tests/mod.rs @@ -0,0 +1,44 @@ +#![allow(clippy::too_many_lines)] // Some insta-generated code gets long + +use proc_macro2::TokenStream; + +use crate::parser::vmod; + +mod empty; +mod one; +mod options; +mod simple; + +fn expand_macros(input: TokenStream) -> (String, String) { + let file = vmod(TokenStream::default(), input).to_string(); + let parsed = match syn::parse_file(&file) { + Ok(v) => v, + Err(e) => { + return ( + format!("Failed to parse generated code: {e}\n\n{file}"), + String::new(), + ) + } + }; + let result = prettyplease::unparse(&parsed); + + // Extract JSON string + let pat = "const JSON: &CStr = c\""; + let json = if let Some(pos) = result.find(pat) { + let json = &result[pos + pat.len()..]; + json.split("\";\n").next() + } else { + None + }; + + let json = json + .unwrap_or("") + .replace("\\\"", "\"") + .replace("\\u{2}", "\u{2}") + .replace("\\u{3}", "\u{3}") + .replace("\\\\", "\\") + // this is a bit of a hack because the double-escaping gets somewhat incorrectly parsed + .replace("\\n", "\n"); + + (result, json) +} diff --git a/varnish-macros/src/tests/one.rs b/varnish-macros/src/tests/one.rs new file mode 100644 index 0000000..6cdd520 --- /dev/null +++ b/varnish-macros/src/tests/one.rs @@ -0,0 +1,115 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn one() { + let (data, json) = expand_macros(quote! { + #[varnish::vmod] + mod one { + pub fn void_to_void() {} + } + }); + + assert_snapshot!(data, @r##" + #[allow(unused_imports)] + mod one_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::one::*; + unsafe extern "C" fn vmod_c_void_to_void(ctx: *mut vrt_ctx) { + let mut ctx = Ctx::from_ptr(ctx); + void_to_void() + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + }) + } + #[repr(C)] + pub struct VmodExports { + void_to_void: Option, + } + pub static VMOD_EXPORTS: VmodExports = VmodExports { + void_to_void: Some(vmod_c_void_to_void), + }; + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + unsafe impl Sync for VmodData {} + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static Vmod_one_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"64ce15ccb7e662447a93a9dcce69c04007c4c8d363d11d0c9e74ab91df994f8b" + .as_ptr(), + name: c"one".as_ptr(), + func_name: c"Vmod_one".as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + const JSON: &CStr = c"VMOD_JSON_SPEC\u{2}\n[\n [\n \"$VMOD\",\n \"1.0\",\n \"one\",\n \"Vmod_one\",\n \"64ce15ccb7e662447a93a9dcce69c04007c4c8d363d11d0c9e74ab91df994f8b\",\n \"Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1\",\n \"0\",\n \"0\"\n ],\n [\n \"$CPROTO\",\n \"\\ntypedef VCL_VOID td_one_void_to_void(VRT_CTX);\\n\\nstruct Vmod_one {\\n td_one_void_to_void *f_void_to_void;\\n};\\n\\nstatic struct Vmod_one Vmod_one;\"\n ],\n [\n \"$FUNC\",\n \"void_to_void\",\n [\n [\n \"VOID\"\n ],\n \"Vmod_one.f_void_to_void\",\n \"\"\n ]\n ]\n]\n\u{3}"; + } + #[varnish::vmod] + mod one { + pub fn void_to_void() {} + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "one", + "Vmod_one", + "64ce15ccb7e662447a93a9dcce69c04007c4c8d363d11d0c9e74ab91df994f8b", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + " + typedef VCL_VOID td_one_void_to_void(VRT_CTX); + + struct Vmod_one { + td_one_void_to_void *f_void_to_void; + }; + + static struct Vmod_one Vmod_one;" + ], + [ + "$FUNC", + "void_to_void", + [ + [ + "VOID" + ], + "Vmod_one.f_void_to_void", + "" + ] + ] + ] +  + "#); +} diff --git a/varnish-macros/src/tests/options.rs b/varnish-macros/src/tests/options.rs new file mode 100644 index 0000000..76047fc --- /dev/null +++ b/varnish-macros/src/tests/options.rs @@ -0,0 +1,214 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn options() { + let (data, json) = expand_macros(quote! { + #[varnish::vmod] + mod options { + pub fn opt1(a1: Option) -> String { todo!() } + pub fn opt2(a1: i64, a2: Option, a3: i64) -> String { todo!() } + } + }); + + assert_snapshot!(data, @r##" + #[allow(unused_imports)] + mod options_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::options::*; + #[repr(C)] + struct arg_options_opt1 { + valid_a1: c_char, + a1: VCL_INT, + } + unsafe extern "C" fn vmod_c_opt1( + ctx: *mut vrt_ctx, + args: *const arg_options_opt1, + ) -> VCL_STRING { + let mut ctx = Ctx::from_ptr(ctx); + let args = args.as_ref().unwrap(); + opt1((args.valid_a1 != 0).then(|| args.a1.into_rust())) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + ::vcl_default() + }) + } + #[repr(C)] + struct arg_options_opt2 { + a1: VCL_INT, + valid_a2: c_char, + a2: VCL_INT, + a3: VCL_INT, + } + unsafe extern "C" fn vmod_c_opt2( + ctx: *mut vrt_ctx, + args: *const arg_options_opt2, + ) -> VCL_STRING { + let mut ctx = Ctx::from_ptr(ctx); + let args = args.as_ref().unwrap(); + opt2( + a1.into_rust(), + (args.valid_a2 != 0).then(|| args.a2.into_rust()), + a3.into_rust(), + ) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + ::vcl_default() + }) + } + #[repr(C)] + pub struct VmodExports { + opt1: Option< + unsafe extern "C" fn( + ctx: *mut vrt_ctx, + args: *const arg_options_opt1, + ) -> VCL_STRING, + >, + opt2: Option< + unsafe extern "C" fn( + ctx: *mut vrt_ctx, + args: *const arg_options_opt2, + ) -> VCL_STRING, + >, + } + pub static VMOD_EXPORTS: VmodExports = VmodExports { + opt1: Some(vmod_c_opt1), + opt2: Some(vmod_c_opt2), + }; + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + unsafe impl Sync for VmodData {} + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static Vmod_options_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"8adbbd139d01b40518d02c864c7774bbeccd986b824c3310621dac36b70f962f" + .as_ptr(), + name: c"options".as_ptr(), + func_name: c"Vmod_options".as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + const JSON: &CStr = c"VMOD_JSON_SPEC\u{2}\n[\n [\n \"$VMOD\",\n \"1.0\",\n \"options\",\n \"Vmod_options\",\n \"8adbbd139d01b40518d02c864c7774bbeccd986b824c3310621dac36b70f962f\",\n \"Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1\",\n \"0\",\n \"0\"\n ],\n [\n \"$CPROTO\",\n \"\\nstruct arg_options_opt1 {\\n char valid_a1;\\n VCL_INT a1;\\n};\\ntypedef VCL_STRING td_options_opt1(VRT_CTX, struct arg_options_opt1*);\\n\\nstruct arg_options_opt2 {\\n VCL_INT a1;\\n char valid_a2;\\n VCL_INT a2;\\n VCL_INT a3;\\n};\\ntypedef VCL_STRING td_options_opt2(VRT_CTX, struct arg_options_opt2*);\\n\\nstruct Vmod_options {\\n td_options_opt1 *f_opt1;\\n td_options_opt2 *f_opt2;\\n};\\n\\nstatic struct Vmod_options Vmod_options;\"\n ],\n [\n \"$FUNC\",\n \"opt1\",\n [\n [\n \"STRING\"\n ],\n \"Vmod_options.f_opt1\",\n \"struct arg_options_opt1\",\n [\n \"INT\",\n \"a1\",\n null,\n null,\n true\n ]\n ]\n ],\n [\n \"$FUNC\",\n \"opt2\",\n [\n [\n \"STRING\"\n ],\n \"Vmod_options.f_opt2\",\n \"struct arg_options_opt2\",\n [\n \"INT\",\n \"a1\"\n ],\n [\n \"INT\",\n \"a2\",\n null,\n null,\n true\n ],\n [\n \"INT\",\n \"a3\"\n ]\n ]\n ]\n]\n\u{3}"; + } + #[varnish::vmod] + mod options { + pub fn opt1(a1: Option) -> String { + todo!() + } + pub fn opt2(a1: i64, a2: Option, a3: i64) -> String { + todo!() + } + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "options", + "Vmod_options", + "8adbbd139d01b40518d02c864c7774bbeccd986b824c3310621dac36b70f962f", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + " + struct arg_options_opt1 { + char valid_a1; + VCL_INT a1; + }; + typedef VCL_STRING td_options_opt1(VRT_CTX, struct arg_options_opt1*); + + struct arg_options_opt2 { + VCL_INT a1; + char valid_a2; + VCL_INT a2; + VCL_INT a3; + }; + typedef VCL_STRING td_options_opt2(VRT_CTX, struct arg_options_opt2*); + + struct Vmod_options { + td_options_opt1 *f_opt1; + td_options_opt2 *f_opt2; + }; + + static struct Vmod_options Vmod_options;" + ], + [ + "$FUNC", + "opt1", + [ + [ + "STRING" + ], + "Vmod_options.f_opt1", + "struct arg_options_opt1", + [ + "INT", + "a1", + null, + null, + true + ] + ] + ], + [ + "$FUNC", + "opt2", + [ + [ + "STRING" + ], + "Vmod_options.f_opt2", + "struct arg_options_opt2", + [ + "INT", + "a1" + ], + [ + "INT", + "a2", + null, + null, + true + ], + [ + "INT", + "a3" + ] + ] + ] + ] +  + "#); +} diff --git a/varnish-macros/src/tests/simple.rs b/varnish-macros/src/tests/simple.rs new file mode 100644 index 0000000..d47fe86 --- /dev/null +++ b/varnish-macros/src/tests/simple.rs @@ -0,0 +1,225 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn simple() { + let (data, json) = expand_macros(quote! { + #[varnish::vmod] + mod simple { + pub fn void_to_void() {} + pub fn i64_to_bool(n: i64) -> bool { true } + pub fn str_to_i64(s: &str) -> i64 { 0 } + pub fn str_to_i64(s: &str) -> Result { Ok(s.to_owned()) } + } + }); + + assert_snapshot!(data, @r##" + #[allow(unused_imports)] + mod simple_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{vrt_ctx, VMOD_ABI_Version, VCL_BOOL, VCL_INT, VCL_STRING}; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::simple::*; + unsafe extern "C" fn vmod_c_void_to_void(ctx: *mut vrt_ctx) { + let mut ctx = Ctx::from_ptr(ctx); + void_to_void() + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + }) + } + unsafe extern "C" fn vmod_c_i64_to_bool(ctx: *mut vrt_ctx, n: VCL_INT) -> VCL_BOOL { + let mut ctx = Ctx::from_ptr(ctx); + i64_to_bool(n.into_rust()) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + ::vcl_default() + }) + } + unsafe extern "C" fn vmod_c_str_to_i64(ctx: *mut vrt_ctx, s: VCL_STRING) -> VCL_INT { + let mut ctx = Ctx::from_ptr(ctx); + str_to_i64(&*s.into_rust()) + .into_result() + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + ::vcl_default() + }) + } + unsafe extern "C" fn vmod_c_str_to_i64( + ctx: *mut vrt_ctx, + s: VCL_STRING, + ) -> VCL_STRING { + let mut ctx = Ctx::from_ptr(ctx); + str_to_i64(&*s.into_rust()) + .and_then(|v| v.into_vcl(&mut ctx.ws)) + .unwrap_or_else(|e| { + ctx.fail(&e); + ::vcl_default() + }) + } + #[repr(C)] + pub struct VmodExports { + void_to_void: Option, + i64_to_bool: Option< + unsafe extern "C" fn(ctx: *mut vrt_ctx, n: VCL_INT) -> VCL_BOOL, + >, + str_to_i64: Option< + unsafe extern "C" fn(ctx: *mut vrt_ctx, s: VCL_STRING) -> VCL_INT, + >, + str_to_i64: Option< + unsafe extern "C" fn(ctx: *mut vrt_ctx, s: VCL_STRING) -> VCL_STRING, + >, + } + pub static VMOD_EXPORTS: VmodExports = VmodExports { + void_to_void: Some(vmod_c_void_to_void), + i64_to_bool: Some(vmod_c_i64_to_bool), + str_to_i64: Some(vmod_c_str_to_i64), + str_to_i64: Some(vmod_c_str_to_i64), + }; + #[repr(C)] + pub struct VmodData { + vrt_major: c_uint, + vrt_minor: c_uint, + file_id: *const c_char, + name: *const c_char, + func_name: *const c_char, + func: *const c_void, + func_len: c_int, + proto: *const c_char, + json: *const c_char, + abi: *const c_char, + } + unsafe impl Sync for VmodData {} + #[allow(non_upper_case_globals)] + #[no_mangle] + pub static Vmod_simple_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"6f0c6c8b004b2cac3d572930e8dcb377721852b3d51b6721cb3802752d4eecfc" + .as_ptr(), + name: c"simple".as_ptr(), + func_name: c"Vmod_simple".as_ptr(), + func_len: size_of::() as c_int, + func: &VMOD_EXPORTS as *const _ as *const c_void, + abi: VMOD_ABI_Version.as_ptr(), + json: JSON.as_ptr(), + proto: null(), + }; + const JSON: &CStr = c"VMOD_JSON_SPEC\u{2}\n[\n [\n \"$VMOD\",\n \"1.0\",\n \"simple\",\n \"Vmod_simple\",\n \"6f0c6c8b004b2cac3d572930e8dcb377721852b3d51b6721cb3802752d4eecfc\",\n \"Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1\",\n \"0\",\n \"0\"\n ],\n [\n \"$CPROTO\",\n \"\\ntypedef VCL_VOID td_simple_void_to_void(VRT_CTX);\\n\\ntypedef VCL_BOOL td_simple_i64_to_bool(VRT_CTX, VCL_INT);\\n\\ntypedef VCL_INT td_simple_str_to_i64(VRT_CTX, VCL_STRING);\\n\\ntypedef VCL_STRING td_simple_str_to_i64(VRT_CTX, VCL_STRING);\\n\\nstruct Vmod_simple {\\n td_simple_void_to_void *f_void_to_void;\\n td_simple_i64_to_bool *f_i64_to_bool;\\n td_simple_str_to_i64 *f_str_to_i64;\\n td_simple_str_to_i64 *f_str_to_i64;\\n};\\n\\nstatic struct Vmod_simple Vmod_simple;\"\n ],\n [\n \"$FUNC\",\n \"void_to_void\",\n [\n [\n \"VOID\"\n ],\n \"Vmod_simple.f_void_to_void\",\n \"\"\n ]\n ],\n [\n \"$FUNC\",\n \"i64_to_bool\",\n [\n [\n \"BOOL\"\n ],\n \"Vmod_simple.f_i64_to_bool\",\n \"\",\n [\n \"INT\",\n \"n\"\n ]\n ]\n ],\n [\n \"$FUNC\",\n \"str_to_i64\",\n [\n [\n \"INT\"\n ],\n \"Vmod_simple.f_str_to_i64\",\n \"\",\n [\n \"STRING\",\n \"s\"\n ]\n ]\n ],\n [\n \"$FUNC\",\n \"str_to_i64\",\n [\n [\n \"STRING\"\n ],\n \"Vmod_simple.f_str_to_i64\",\n \"\",\n [\n \"STRING\",\n \"s\"\n ]\n ]\n ]\n]\n\u{3}"; + } + #[varnish::vmod] + mod simple { + pub fn void_to_void() {} + pub fn i64_to_bool(n: i64) -> bool { + true + } + pub fn str_to_i64(s: &str) -> i64 { + 0 + } + pub fn str_to_i64(s: &str) -> Result { + Ok(s.to_owned()) + } + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "simple", + "Vmod_simple", + "6f0c6c8b004b2cac3d572930e8dcb377721852b3d51b6721cb3802752d4eecfc", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + " + typedef VCL_VOID td_simple_void_to_void(VRT_CTX); + + typedef VCL_BOOL td_simple_i64_to_bool(VRT_CTX, VCL_INT); + + typedef VCL_INT td_simple_str_to_i64(VRT_CTX, VCL_STRING); + + typedef VCL_STRING td_simple_str_to_i64(VRT_CTX, VCL_STRING); + + struct Vmod_simple { + td_simple_void_to_void *f_void_to_void; + td_simple_i64_to_bool *f_i64_to_bool; + td_simple_str_to_i64 *f_str_to_i64; + td_simple_str_to_i64 *f_str_to_i64; + }; + + static struct Vmod_simple Vmod_simple;" + ], + [ + "$FUNC", + "void_to_void", + [ + [ + "VOID" + ], + "Vmod_simple.f_void_to_void", + "" + ] + ], + [ + "$FUNC", + "i64_to_bool", + [ + [ + "BOOL" + ], + "Vmod_simple.f_i64_to_bool", + "", + [ + "INT", + "n" + ] + ] + ], + [ + "$FUNC", + "str_to_i64", + [ + [ + "INT" + ], + "Vmod_simple.f_str_to_i64", + "", + [ + "STRING", + "s" + ] + ] + ], + [ + "$FUNC", + "str_to_i64", + [ + [ + "STRING" + ], + "Vmod_simple.f_str_to_i64", + "", + [ + "STRING", + "s" + ] + ] + ] + ] +  + "#); +}