From e4e57b42a630aeccfcb3deb8be8f5cfa95be01da Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 10 Sep 2024 14:21:09 -0400 Subject: [PATCH] Implement proc macro support --- Cargo.toml | 21 +- examples/vmod_error/build.rs | 3 - examples/vmod_error/src/lib.rs | 98 +++-- .../vmod_error/{vmod.vcc => vmod.vcc.unused} | 0 examples/vmod_example/build.rs | 4 - examples/vmod_example/src/lib.rs | 65 ++- .../{vmod.vcc => vmod.vcc.unused} | 0 examples/vmod_infiniteloop/build.rs | 3 - examples/vmod_infiniteloop/src/lib.rs | 42 +- .../{vmod.vcc => vmod.vcc.unused} | 0 examples/vmod_timestamp/build.rs | 3 - examples/vmod_timestamp/src/lib.rs | 49 ++- .../{vmod.vcc => vmod.vcc.unused} | 0 src/lib.rs | 2 + src/vcl/utils.rs | 63 ++- varnish-macros/Cargo.toml | 25 ++ varnish-macros/generated-example-reference.rs | 156 +++++++ varnish-macros/src/func_processor.rs | 361 +++++++++++++++ varnish-macros/src/generator.rs | 189 ++++++++ varnish-macros/src/lib.rs | 21 + varnish-macros/src/model.rs | 116 +++++ varnish-macros/src/parser.rs | 336 ++++++++++++++ varnish-macros/src/tests/empty.rs | 97 ++++ varnish-macros/src/tests/mod.rs | 50 +++ varnish-macros/src/tests/one.rs | 135 ++++++ varnish-macros/src/tests/options.rs | 274 ++++++++++++ varnish-macros/src/tests/priv_task.rs | 263 +++++++++++ varnish-macros/src/tests/simple.rs | 414 ++++++++++++++++++ 28 files changed, 2646 insertions(+), 144 deletions(-) delete mode 100644 examples/vmod_error/build.rs rename examples/vmod_error/{vmod.vcc => vmod.vcc.unused} (100%) delete mode 100644 examples/vmod_example/build.rs rename examples/vmod_example/{vmod.vcc => vmod.vcc.unused} (100%) delete mode 100644 examples/vmod_infiniteloop/build.rs rename examples/vmod_infiniteloop/{vmod.vcc => vmod.vcc.unused} (100%) delete mode 100644 examples/vmod_timestamp/build.rs rename examples/vmod_timestamp/{vmod.vcc => vmod.vcc.unused} (100%) create mode 100644 varnish-macros/Cargo.toml create mode 100644 varnish-macros/generated-example-reference.rs create mode 100644 varnish-macros/src/func_processor.rs create mode 100644 varnish-macros/src/generator.rs create mode 100644 varnish-macros/src/lib.rs create mode 100644 varnish-macros/src/model.rs create mode 100644 varnish-macros/src/parser.rs create mode 100644 varnish-macros/src/tests/empty.rs create mode 100644 varnish-macros/src/tests/mod.rs create mode 100644 varnish-macros/src/tests/one.rs create mode 100644 varnish-macros/src/tests/options.rs create mode 100644 varnish-macros/src/tests/priv_task.rs create mode 100644 varnish-macros/src/tests/simple.rs 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_timestamp/build.rs b/examples/vmod_timestamp/build.rs deleted file mode 100644 index 4f48ef0..0000000 --- a/examples/vmod_timestamp/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - varnish::generate_boilerplate().unwrap(); -} diff --git a/examples/vmod_timestamp/src/lib.rs b/examples/vmod_timestamp/src/lib.rs index b3f2cb5..5a7fa64 100644 --- a/examples/vmod_timestamp/src/lib.rs +++ b/examples/vmod_timestamp/src/lib.rs @@ -1,26 +1,31 @@ -varnish::boilerplate!(); +#[varnish::vmod] +mod timestamp { + use std::mem; + use std::time::{Duration, Instant}; -use std::time::{Duration, Instant}; + /// Returns the time interval since the last time this function was called in the current task's context. + /// There could be only one type of per-task shared context data type in a Varnish VMOD. + pub fn timestamp(#[shared_per_task] task_ctx: &mut Option>) -> Duration { + // we will need this either way + let now = Instant::now(); -use varnish::vcl::ctx::Ctx; -use varnish::vcl::vpriv::VPriv; - -varnish::vtc!(test01); - -// VPriv can wrap any (possibly custom) struct, here we only need an Instant from std::time. -// Storing and getting is up to the vmod writer but this removes the worry of NULL dereferencing -// and of the memory management -pub fn timestamp(_: &Ctx, vp: &mut VPriv) -> Duration { - // we will need this either way - let now = Instant::now(); + match task_ctx { + None => { + // This is the first time we're running this function in the task's context + *task_ctx = Some(Box::new(now)); + Duration::default() + } + Some(task_ctx) => { + // Update box content in-place to the new value, and get the old value + let old_now = mem::replace(&mut **task_ctx, now); + // Since Instant implements Copy, we can continue using it and subtract the old value + now.duration_since(old_now) + } + } + } +} - let interval = match vp.as_ref() { - // if `.get()` returns None, we just store `now` and interval is 0 - None => Duration::new(0, 0), - // if there was a value, compute the difference with now - Some(old_now) => now.duration_since(*old_now), - }; - // store the current time and return `interval` - vp.store(now); - interval +#[cfg(test)] +mod tests { + varnish::vtc!(test01); } diff --git a/examples/vmod_timestamp/vmod.vcc b/examples/vmod_timestamp/vmod.vcc.unused similarity index 100% rename from examples/vmod_timestamp/vmod.vcc rename to examples/vmod_timestamp/vmod.vcc.unused diff --git a/src/lib.rs b/src/lib.rs index a94cfbd..47854e5 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: diff --git a/src/vcl/utils.rs b/src/vcl/utils.rs index 46f1b88..ca9eb63 100644 --- a/src/vcl/utils.rs +++ b/src/vcl/utils.rs @@ -1,10 +1,13 @@ -use std::ffi::{c_char, CStr}; +use std::ffi::{c_char, c_void, CStr}; +use std::ptr; +use std::ptr::null; use std::slice::from_raw_parts; use std::str::from_utf8; use crate::ffi::{ - director, req, sess, txt, vcldir, vfp_ctx, vfp_entry, vrt_ctx, ws, DIRECTOR_MAGIC, REQ_MAGIC, - SESS_MAGIC, VCLDIR_MAGIC, VFP_CTX_MAGIC, VFP_ENTRY_MAGIC, VRT_CTX_MAGIC, WS_MAGIC, + director, req, sess, txt, vcldir, vfp_ctx, vfp_entry, vmod_priv, vmod_priv_methods, vrt_ctx, + ws, DIRECTOR_MAGIC, REQ_MAGIC, SESS_MAGIC, VCLDIR_MAGIC, VFP_CTX_MAGIC, VFP_ENTRY_MAGIC, + VRT_CTX_MAGIC, WS_MAGIC, }; use crate::vcl::backend::{Serve, Transfer, VCLBackendPtr}; @@ -121,3 +124,57 @@ impl director { unsafe { self.priv_.cast::().as_ref().unwrap() } } } + +/// SAFETY: ensured by Varnish itself +unsafe impl Sync for vmod_priv_methods {} + +/// Take ownership of the object of type `T` and return it as a `Box`. +/// The original pointer is set to null. +/// +/// SAFETY: `priv_` must reference a valid `T` object pointer or `NULL` +unsafe fn get_owned_bbox(priv_: &mut *mut c_void) -> Option> { + let obj = ptr::replace(priv_, ptr::null_mut()); + if obj.is_null() { + None + } else { + Some(Box::from_raw(obj.cast::())) + } +} + +impl vmod_priv { + /// Transfer ownership of the object to the caller, cleaning up the internal state. + /// + /// SAFETY: `priv_` must reference a valid `T` object pointer or `NULL` + pub unsafe fn take(&mut self) -> Option> { + self.methods = null(); + get_owned_bbox(&mut self.priv_) + } + + /// Set the object and methods for the `vmod_priv`, and the corresponding static methods. + /// + /// SAFETY: The type of `obj` must match the type of the function pointers in `methods`. + pub unsafe fn put(&mut self, obj: Box, methods: &'static vmod_priv_methods) { + self.priv_ = Box::into_raw(obj).cast(); + self.methods = methods; + } + + /// Use the object as a reference, without taking ownership. + /// + /// SAFETY: + /// * `priv_` must reference a valid `T` object pointer or `NULL` + /// * `take()` must not be called on the same `vmod_priv` object until the returned reference is dropped + /// * cleanup must not be done on the object until the returned reference is dropped + pub unsafe fn get_ref(&self) -> Option<&T> { + Some(self.priv_.cast::>().as_ref()?) + } + + /// A Varnish callback function to free a `vmod_priv` object. + /// Here we take the ownership and immediately drop the object of type `T`. + /// Note that here we get `*priv_` directly, not the `*vmod_priv` + /// + /// SAFETY: `priv_` must be a valid pointer to a `T` object or `NULL`. + pub unsafe extern "C" fn on_fini(_ctx: *const vrt_ctx, mut priv_: *mut c_void) { + // we get the ownership and don't do anything with it, so it gets dropped at the end of this fn + get_owned_bbox::(&mut priv_); + } +} 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..5627312 --- /dev/null +++ b/varnish-macros/src/func_processor.rs @@ -0,0 +1,361 @@ +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::{FuncInfo, ParamTy, ParamType, ReturnType, TaskShared}; + +#[derive(Debug, Default)] +pub struct FuncProcessor { + /// 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, + + /// (FUNC) Full assignment expression to create a temp var. `[ {let ctx = &mut ctx; }, {let __var1 = args.foo;} ]` + func_init_vars: Vec, + /// (FUNC) Argument as passed to the user function `[ {&ctx}, {__var1;} ]` + func_call_vars: Vec, + /// (FUNC) Argument post-call processing, i.e. `[ { }, { if ... { ... } } ]` + func_after_call: 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, + + /// `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 wrapper_function_body: TokenStream, + /// JSON blob for the function + pub json: Value, +} + +impl FuncProcessor { + pub fn from_info(root: &Generator, info: &FuncInfo) -> Self { + let ident = &info.ident; + + let name_args_ty = if info.has_optional_args { + format!("arg_{}_{}", root.mod_name, info.ident) + } else { + String::new() + }; + + let mut obj = Self { + 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 info.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 (idx, arg) in info.args.iter().enumerate() { + self.do_fn_param(info, idx, 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.wrapper_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 info.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 info.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, ");"); + self.json = self.json_func(info); + } + + fn do_fn_param(&mut self, info: &FuncInfo, arg_idx: usize, arg: &ParamType) { + let temp_var_ident = format_ident!("__var{arg_idx}"); + 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.func_call_vars.push(if *is_mut { + quote! { &mut #ident } + } else { + quote! { &#ident } + }); + } + ParamType::Value(arg) => { + // Convert C arg into Rust arg and pass it to the user's function + let c_arg = Self::get_c_param_accessor(info, &arg.name); + let mut r_arg = match arg.ty_info { + ParamTy::Bool | ParamTy::I64 | ParamTy::Duration => { + quote! { #c_arg.into_rust() } + } + ParamTy::Str => quote! { &*#c_arg.into_rust() }, + }; + if arg.is_optional { + let arg_valid = format_ident!("valid_{}", arg.name); + r_arg = quote! { (args.#arg_valid != 0).then(|| #r_arg) }; + } + + let arg_ty = format_ident!("{}", arg.ty_info.to_c_type()); + self.func_init_vars + .push(quote! { let #temp_var_ident = #r_arg; }); + self.func_call_vars.push(quote! { #temp_var_ident }); + self.proc_handle2( + info, + arg.name.clone(), + arg.is_optional, + arg.ty_info.to_c_type(), + quote! { #arg_ty }, + ); + let json = + Self::arg_to_json(arg.name.clone(), arg.is_optional, arg.ty_info.to_vcc_type()); + self.args_json.push(json.into()); + } + ParamType::SharedPerTask(TaskShared { name, .. }) => { + let c_arg = Self::get_c_param_accessor(info, name); + let r_arg = quote! { (* #c_arg).take() }; + + self.func_init_vars + .push(quote! { let mut #temp_var_ident = #r_arg; }); + self.func_call_vars.push(quote! { &mut #temp_var_ident }); + self.func_after_call.push(quote! { + // Release ownership back to Varnish + if let Some(obj) = #temp_var_ident { + (* #c_arg).put(obj, &PRIV_TASK_METHODS); + } + }); + self.proc_handle2( + info, + name.clone(), + false, + "struct vmod_priv *", + quote! { *mut vmod_priv }, + ); + let json = Self::arg_to_json(name.clone(), false, "PRIV_TASK"); + self.args_json.push(json.into()); + } + } + } + + fn proc_handle2( + &mut self, + info: &FuncInfo, + arg_name: String, + is_optional_arg: bool, + arg_hdr: &'static str, + arg_ty: TokenStream, + ) { + if !info.has_optional_args { + self.args_hdr.push(arg_hdr); + } + + // 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 info.has_optional_args { + &mut self.args_c_opt_decl + } else { + &mut self.args_c_fn_decl + }; + let arg_name_ident = format_ident!("{arg_name}"); + let arg_valid = format_ident!("valid_{arg_name}"); + if is_optional_arg { + args_list.push(quote! { #arg_valid: c_char }); + self.args_hdr_opt_decl.push(format!("char {arg_valid}")); + } + args_list.push(quote! { #arg_name_ident: #arg_ty }); + if info.has_optional_args { + self.args_hdr_opt_decl.push(format!("{arg_hdr} {arg_name}")); + } + } + + fn arg_to_json(arg_name: String, is_optional_arg: bool, vcc_type: &str) -> Vec { + // JSON data for each argument: + // [VCC_type, arg_name, default_value, spec(?), is_optional] + let mut json_arg: Vec = vec![ + vcc_type.into(), + arg_name.into(), + Value::Null, // default param is not used at this point + Value::Null, // spec param is not used at this point + ]; + if is_optional_arg { + 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(); + } + } + json_arg + } + + fn get_c_param_accessor(info: &FuncInfo, arg_name: &str) -> TokenStream { + let arg_name = format_ident!("{arg_name}"); + if info.has_optional_args { + quote! { args.#arg_name } + } else { + quote! { #arg_name } + } + } + + 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"; + } + ReturnType::Value(ty) | ReturnType::Result(ty, _) => { + let ident = format_ident!("{}", ty.to_c_type()); + let ret = quote!(#ident); + self.output_c_decl = quote! { -> #ret }; + self.output_hdr = ty.to_c_type(); + self.output_vcc = ty.to_vcc_type(); + } + } + } + + fn json_func(&self, info: &FuncInfo) -> Value { + let args_struct = if info.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 ] } + } + + /// 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 output_c_decl = &self.output_c_decl; + let var_args = &self.func_call_vars; + let func_after_call = &self.func_after_call; + + let mut let_var_assigns = Vec::new(); + let_var_assigns.push(quote! { let mut ctx = Ctx::from_ptr(ctx); }); + + let opt_arg = if info.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,)* + } + }; + let_var_assigns.push(quote! { let args = args.as_ref().unwrap(); }); + opt_arg + } else { + quote! {} + }; + + let_var_assigns.extend(self.func_init_vars.iter().cloned()); + + let default_expr = match info.returns { + ReturnType::Default => quote! { Default::default() }, + ReturnType::Value(ok) | ReturnType::Result(ok, _) => ok.to_default(), + }; + + let unwrap_result = if let ReturnType::Result(..) = info.returns { + quote! { + let __result = match __result { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + return #default_expr; + }, + }; + } + } else { + quote! {} + }; + + quote! { + #opt_arg + unsafe extern "C" fn #name_c_fn(#(#args_c_fn_decl),*) #output_c_decl { + #(#let_var_assigns)* + let __result = super::#name_r_fn(#(#var_args),*); + #(#func_after_call)* + #unwrap_result + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + #default_expr + } + } + } + } + } +} diff --git a/varnish-macros/src/generator.rs b/varnish-macros/src/generator.rs new file mode 100644 index 0000000..94be715 --- /dev/null +++ b/varnish-macros/src/generator.rs @@ -0,0 +1,189 @@ +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, +} + +/// See https://varnish-cache.org/docs/7.5/reference/vmod.html#optional-arguments +impl Generator { + pub fn render(vmod: &VmodInfo) -> TokenStream { + let mut obj = Self { + mod_name: vmod.name.clone(), + file_id: Self::calc_file_id(vmod), + ..Self::default() + }; + for fn_info in &vmod.funcs { + obj.functions.push(FuncProcessor::from_info(&obj, fn_info)); + } + obj.render_generated_mod(vmod) + } + + /// 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_priv_structs(info: &VmodInfo) -> Vec { + let mut res = Vec::new(); + if let Some(type_name) = &info.shared_per_task_ty { + let ident = format_ident!("{type_name}"); + let ty_name = CString::new(type_name.as_str()).unwrap(); + // Static methods to clean up the `vmod_priv` object's `T` + res.push(quote! { + static PRIV_TASK_METHODS: vmod_priv_methods = vmod_priv_methods { + magic: VMOD_PRIV_METHODS_MAGIC, + type_: #ty_name.as_ptr(), + fini: Some(vmod_priv::on_fini::<#ident>), + }; + }); + } + + res + } + + fn gen_json(&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.clone()); + } + + let json = serde_json::to_string_pretty(&json! {json}).unwrap(); + CString::new(format!("VMOD_JSON_SPEC\n{json}\n")).unwrap() + } + + fn render_generated_mod(&self, vmod: &VmodInfo) -> TokenStream { + let mod_name = &self.mod_name; + let gen_mod_name = format_ident!("varnish_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 priv_structs = Self::gen_priv_structs(vmod); + let functions = self.functions.iter().map(|f| &f.wrapper_function_body); + let json = &self.gen_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(); + + quote!( + #[allow(unused_imports)] + mod #gen_mod_name { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, + VCL_DURATION, + VCL_INT, + VCL_STRING, + VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, + vmod_priv, + vmod_priv_methods, + vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + + #( #priv_structs )* + #( #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; + } + ) + } +} diff --git a/varnish-macros/src/lib.rs b/varnish-macros/src/lib.rs new file mode 100644 index 0000000..ca34f88 --- /dev/null +++ b/varnish-macros/src/lib.rs @@ -0,0 +1,21 @@ +// #![allow(warnings)] + +use {proc_macro as pm, proc_macro2 as pm2}; + +use crate::parser::{render_model, tokens_to_model}; + +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); + let (item_mod, info) = tokens_to_model(args, input); + render_model(item_mod, info).into() +} diff --git a/varnish-macros/src/model.rs b/varnish-macros/src/model.rs new file mode 100644 index 0000000..ba418f6 --- /dev/null +++ b/varnish-macros/src/model.rs @@ -0,0 +1,116 @@ +use proc_macro2::TokenStream; +use quote::quote; + +#[derive(Debug, Default)] +pub struct VmodInfo { + pub name: String, + pub funcs: Vec, + pub shared_per_task_ty: Option, +} + +#[derive(Debug)] +pub struct FuncInfo { + pub ident: String, + pub has_optional_args: bool, + pub args: Vec, + pub returns: ReturnType, +} + +#[derive(Debug, Clone)] +pub enum ReturnType { + Default, + Value(ReturnTy), + // The error return type is not directly used yet + Result(ReturnTy, #[allow(dead_code)] ReturnTy), +} + +#[derive(Debug, Clone)] +pub enum ParamType { + /// An argument representing Varnish context (VRT_CTX) wrapper + Context { is_mut: bool }, + /// An argument representing a VCL type + Value(ParamInfo), + /// An argument representing any Rust name and type shared across tasks (i.e. `PRIV_TASK`) + SharedPerTask(TaskShared), +} + +#[derive(Debug, Clone)] +pub struct TaskShared { + pub name: String, + pub ty_info: String, +} + +#[derive(Debug, Clone)] +pub struct ParamInfo { + pub name: String, + pub is_optional: bool, + pub ty_info: ParamTy, +} + +#[derive(Debug, Clone, Copy)] +pub enum ParamTy { + I64, + Bool, + Duration, + Str, +} + +impl ParamTy { + pub fn to_vcc_type(self) -> &'static str { + match self { + Self::I64 => "INT", + Self::Bool => "BOOL", + Self::Str => "STRING", + Self::Duration => "DURATION", + } + } + + 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::I64 => "VCL_INT", + Self::Bool => "VCL_BOOL", + Self::Str => "VCL_STRING", + Self::Duration => "VCL_DURATION", + } + } + + pub fn to_default(self) -> TokenStream { + match self { + Self::I64 | Self::Bool | Self::Duration => quote! { Default::default() }, + Self::Str => quote! { std::ptr::null() }, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum ReturnTy { + ParamType(ParamTy), + String, +} + +impl ReturnTy { + pub fn to_vcc_type(self) -> &'static str { + match self { + Self::ParamType(ty) => ty.to_vcc_type(), + Self::String => "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::ParamType(ty) => ty.to_c_type(), + Self::String => "VCL_STRING", + } + } + + pub fn to_default(self) -> TokenStream { + match self { + Self::ParamType(ty) => ty.to_default(), + Self::String => quote! { std::ptr::null() }, + } + } +} diff --git a/varnish-macros/src/parser.rs b/varnish-macros/src/parser.rs new file mode 100644 index 0000000..94254f6 --- /dev/null +++ b/varnish-macros/src/parser.rs @@ -0,0 +1,336 @@ +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, Item, ItemFn, ItemMod, Pat, Type, TypePath, Visibility}; + +use crate::generator::Generator; +use crate::model::{ + FuncInfo, ParamInfo, ParamTy, ParamType, ReturnTy, ReturnType, TaskShared, VmodInfo, +}; + +pub fn tokens_to_model(_args: TokenStream, input: TokenStream) -> (ItemMod, VmodInfo) { + let mut item_mod = + parse2::(input).expect("`vmod` attribute must be applied to a module"); + let info = VmodInfo::parse(&mut item_mod); + (item_mod, info) +} + +pub fn render_model(mut item_mod: ItemMod, info: VmodInfo) -> TokenStream { + let output = Generator::render(&info); + item_mod + .content + .as_mut() + .unwrap() + .1 + .insert(0, Item::Verbatim(output)); + quote! { #item_mod } +} + +impl VmodInfo { + pub fn parse(item: &mut ItemMod) -> Self { + // process all functions in the module + let mut funcs = Vec::::new(); + if let Some((_, content)) = &mut item.content { + for item in content { + if let Fn(fn_item) = item { + if matches!(fn_item.vis, Visibility::Public(..)) { + funcs.push(FuncInfo::parse(fn_item)); + } + } + } + } + + // Figure out the type of the shared_per_task argument - must be the same for all functions + let mut shared_per_task_ty: Vec<_> = funcs + .iter() + .filter_map(|f| { + f.args.iter().find_map(|arg| match arg { + ParamType::SharedPerTask(ty) => Some(ty.ty_info.clone()), + _ => None, + }) + }) + .collect(); + shared_per_task_ty.sort_unstable(); + shared_per_task_ty.dedup(); + assert!(shared_per_task_ty.len() <= 1, + "Multiple shared_per_task types found, no more than one is allowed: {shared_per_task_ty:?}"); + + Self { + name: item.ident.to_string(), + funcs, + shared_per_task_ty: shared_per_task_ty.pop(), + } + } +} + +impl FuncInfo { + pub fn parse(item_fn: &mut 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<_> = item_fn + .sig + .inputs + .iter_mut() + .map(ParamType::parse) + .collect(); + + let count = args + .iter() + .filter(|v| matches!(v, ParamType::Context { .. })) + .count(); + assert!( + count <= 1, + "Context parameter must be used no more than once" + ); + + let count = args + .iter() + .filter(|v| matches!(v, ParamType::SharedPerTask { .. })) + .count(); + assert!( + count <= 1, + "SharedPerTask parameter must be used no more than once" + ); + + let has_optional_args = args + .iter() + .any(|arg| matches!(arg, ParamType::Value(v) if v.is_optional)); + + Self { + ident: item_fn.sig.ident.to_string(), + has_optional_args, + returns: ReturnType::parse(&item_fn.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((ok_ty, err_ty)) = parse_result(parse_gen_type(path).as_ref()) { + let Some(ok_ty) = ReturnTy::try_parse(ok_ty) else { + panic!( + "Result content type must be a simple type, found {}", + quote! {#ok_ty} + ); + }; + let Some(err_ty) = ReturnTy::try_parse(err_ty) else { + panic!( + "Result error type must be a simple type, found {}", + quote! {#err_ty} + ); + }; + return Self::Result(ok_ty, err_ty); + } + let Some(ty) = ReturnTy::try_parse(path) else { + panic!( + "Result content type must be a simple type, found {}", + quote! {#ty} + ); + }; + ReturnType::Value(ty) + } else { + panic!("unsupported return type"); + } + } + } + } +} + +impl ParamType { + pub fn parse(arg: &mut FnArg) -> Self { + let arg = match arg { + FnArg::Receiver(_) => panic!("receiver not allowed"), + FnArg::Typed(pat) => pat, + }; + + // compute arg name + let pat = arg.pat.as_ref(); + let Pat::Ident(pat) = pat else { + panic!("Unsupported argument pattern: {}", quote! {#pat}) + }; + let arg_name = pat.ident.to_string(); + let arg_ty = arg.ty.as_ref(); + + // find position of the shared_per_task attribute in a vector + let task_shared_idx = arg + .attrs + .iter() + .position(|attr| attr.path().is_ident("shared_per_task")); + if let Some(task_shared_idx) = task_shared_idx { + arg.attrs.swap_remove(task_shared_idx); + ParamType::SharedPerTask(TaskShared::parse(arg_name, arg_ty)) + } else { + // parse the argument type + match arg_ty { + // A reference type: `&'a T` or `&'a mut T`. + 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::Value(ParamInfo::new(arg_name, ParamTy::Str, false)) + } + _ => { + panic!("References are not supported: {}", quote! {#arg}); + } + }, + // A simple type: `foo::bar::Baz`. + Path(path) => Self::Value(ParamInfo::parse(arg_name, path)), + _ => { + panic!("unsupported arg type {}", quote! {#arg_ty}); + } + } + } + } +} + +impl TaskShared { + /// Must be declared as `&mut Option>` + fn parse(name: String, arg_ty: &Type) -> Self { + if let Reference(rf) = arg_ty { + if rf.mutability.is_some() { + if let Path(path) = rf.elem.as_ref() { + if let Some(path) = parse_option(parse_gen_type(path).as_ref()) { + if let Some(path) = parse_box(parse_gen_type(path).as_ref()) { + return Self { + name, + ty_info: quote! {#path}.to_string(), + }; + } + } + } + } + } + panic!( + "#[shared_per_task] params must be declared as `&mut Option>`, found {}", + quote! {#arg_ty} + ); + } +} + +impl ParamInfo { + fn new(name: String, ty_info: ParamTy, is_optional: bool) -> Self { + Self { + name, + is_optional, + ty_info, + } + } + + fn parse(name: String, path: &TypePath) -> Self { + if let Some(arg_type) = ParamTy::try_parse(path) { + return Self::new(name, arg_type, false); + } + if let Some(arg) = parse_option(parse_gen_type(path).as_ref()) { + let Some(arg_type) = ParamTy::try_parse(arg) else { + panic!("Option must have a simple type argument"); + }; + return Self::new(name, arg_type, true); + } + panic!("unsupported type: {}", quote! {#path}); + } +} + +impl ParamTy { + fn try_parse(path: &TypePath) -> Option { + let ident = path.path.get_ident()?; + Some(if ident == "i64" { + Self::I64 + } else if ident == "bool" { + Self::Bool + } else if ident == "str" { + Self::Str + } else if ident == "Duration" { + Self::Duration + } else { + return None; + }) + } +} + +impl ReturnTy { + fn try_parse(path: &TypePath) -> Option { + Some(if let Some(ty) = ParamTy::try_parse(path) { + Self::ParamType(ty) + } else { + let ident = path.path.get_ident()?; + if ident == "String" { + Self::String + } else { + return None; + } + }) + } +} + +/// Parse the result of the `parse_gen_type()`, returning ok/err type paths if it is an `Result` +fn parse_result(value: Option<&(Ident, Vec)>) -> Option<(&TypePath, &TypePath)> { + if let Some((ident, args)) = value { + if ident == "Result" && args.len() == 2 { + return Some((&args[0], &args[1])); + } + } + None +} + +/// Parse the result of the `parse_gen_type()`, returning type path if it is an `Option` +fn parse_option(value: Option<&(Ident, Vec)>) -> Option<&TypePath> { + parse_gen_1(value, "Option") +} + +/// Parse the result of the `parse_gen_type()`, returning type path if it is an `Box` +fn parse_box(value: Option<&(Ident, Vec)>) -> Option<&TypePath> { + parse_gen_1(value, "Box") +} + +/// Parse the result of the `parse_gen_type()`, returning type path if it matches a type name with a single `` +fn parse_gen_1<'a>( + value: Option<&'a (Ident, Vec)>, + type_name: &'static str, +) -> Option<&'a TypePath> { + if let Some((ident, args)) = value { + if ident == type_name && args.len() == 1 { + return Some(&args[0]); + } + } + 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..d010161 --- /dev/null +++ b/varnish-macros/src/tests/empty.rs @@ -0,0 +1,97 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn empty() { + let (model, data, json) = expand_macros(quote! { + #[varnish::vmod] + mod empty { + } + }); + + assert_snapshot!(model, @r#" + VmodInfo { + name: "empty", + funcs: [], + shared_per_task_ty: None, + } + "#); + + assert_snapshot!(data, @r##" + #[varnish::vmod] + mod empty { + #[allow(unused_imports)] + mod varnish_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, VCL_DURATION, VCL_INT, VCL_STRING, VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, vmod_priv, vmod_priv_methods, vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + #[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"2917f4137aeb0e8e162530fe76433e1977237d2aac44941cca18d523ae81ed44" + .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 \"2917f4137aeb0e8e162530fe76433e1977237d2aac44941cca18d523ae81ed44\",\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}"; + } + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "empty", + "Vmod_empty", + "2917f4137aeb0e8e162530fe76433e1977237d2aac44941cca18d523ae81ed44", + "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..b1a9ba4 --- /dev/null +++ b/varnish-macros/src/tests/mod.rs @@ -0,0 +1,50 @@ +#![allow(clippy::too_many_lines)] // Some insta-generated code gets long + +use proc_macro2::TokenStream; + +use crate::parser::{render_model, tokens_to_model}; + +mod empty; +mod one; +mod options; +mod priv_task; +mod simple; + +/// Return model, generated code, and JSON string +fn expand_macros(input: TokenStream) -> (String, String, String) { + let args = TokenStream::default(); + let (item_mod, info) = tokens_to_model(args, input); + let model_dump = format!("{info:#?}"); + let file = render_model(item_mod, info).to_string(); + let parsed = match syn::parse_file(&file) { + Ok(v) => v, + Err(e) => { + return ( + model_dump, + 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"); + + (model_dump, result, json) +} diff --git a/varnish-macros/src/tests/one.rs b/varnish-macros/src/tests/one.rs new file mode 100644 index 0000000..af65318 --- /dev/null +++ b/varnish-macros/src/tests/one.rs @@ -0,0 +1,135 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn one() { + let (model, data, json) = expand_macros(quote! { + #[varnish::vmod] + mod one { + pub fn void_to_void() {} + } + }); + + assert_snapshot!(model, @r#" + VmodInfo { + name: "one", + funcs: [ + FuncInfo { + ident: "void_to_void", + has_optional_args: false, + args: [], + returns: Default, + }, + ], + shared_per_task_ty: None, + } + "#); + + assert_snapshot!(data, @r##" + #[varnish::vmod] + mod one { + #[allow(unused_imports)] + mod varnish_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, VCL_DURATION, VCL_INT, VCL_STRING, VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, vmod_priv, vmod_priv_methods, vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + unsafe extern "C" fn vmod_c_void_to_void(ctx: *mut vrt_ctx) { + let mut ctx = Ctx::from_ptr(ctx); + let __result = super::void_to_void(); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::default() + } + } + } + #[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"dcfb742c302b2ff267e7c5aec1b54e8b3024cefe0be9a53d05af3dddf62bc79b" + .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 \"dcfb742c302b2ff267e7c5aec1b54e8b3024cefe0be9a53d05af3dddf62bc79b\",\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}"; + } + pub fn void_to_void() {} + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "one", + "Vmod_one", + "dcfb742c302b2ff267e7c5aec1b54e8b3024cefe0be9a53d05af3dddf62bc79b", + "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..73a406e --- /dev/null +++ b/varnish-macros/src/tests/options.rs @@ -0,0 +1,274 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn options() { + let (model, 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!(model, @r#" + VmodInfo { + name: "options", + funcs: [ + FuncInfo { + ident: "opt1", + has_optional_args: true, + args: [ + Value( + ParamInfo { + name: "a1", + is_optional: true, + ty_info: I64, + }, + ), + ], + returns: Value( + String, + ), + }, + FuncInfo { + ident: "opt2", + has_optional_args: true, + args: [ + Value( + ParamInfo { + name: "a1", + is_optional: false, + ty_info: I64, + }, + ), + Value( + ParamInfo { + name: "a2", + is_optional: true, + ty_info: I64, + }, + ), + Value( + ParamInfo { + name: "a3", + is_optional: false, + ty_info: I64, + }, + ), + ], + returns: Value( + String, + ), + }, + ], + shared_per_task_ty: None, + } + "#); + + assert_snapshot!(data, @r##" + #[varnish::vmod] + mod options { + #[allow(unused_imports)] + mod varnish_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, VCL_DURATION, VCL_INT, VCL_STRING, VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, vmod_priv, vmod_priv_methods, vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + #[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(); + let __var0 = (args.valid_a1 != 0).then(|| args.a1.into_rust()); + let __result = super::opt1(__var0); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + std::ptr::null() + } + } + } + #[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(); + let __var0 = args.a1.into_rust(); + let __var1 = (args.valid_a2 != 0).then(|| args.a2.into_rust()); + let __var2 = args.a3.into_rust(); + let __result = super::opt2(__var0, __var1, __var2); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + std::ptr::null() + } + } + } + #[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"c5364bd855fbb4f1dc4e4e3ec517831e825cd847bcb7a160405a5fa6d66c45e5" + .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 \"c5364bd855fbb4f1dc4e4e3ec517831e825cd847bcb7a160405a5fa6d66c45e5\",\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}"; + } + 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", + "c5364bd855fbb4f1dc4e4e3ec517831e825cd847bcb7a160405a5fa6d66c45e5", + "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/priv_task.rs b/varnish-macros/src/tests/priv_task.rs new file mode 100644 index 0000000..07f8f18 --- /dev/null +++ b/varnish-macros/src/tests/priv_task.rs @@ -0,0 +1,263 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn priv_task() { + let (model, data, json) = expand_macros(quote! { + #[varnish::vmod] + mod ptsk { + pub fn pt(#[shared_per_task] vp: &mut Option>) -> Duration { todo!() } + + pub fn pt_opt(#[shared_per_task] vp: &mut Option>, op: Option) -> Duration { todo!() } + } + }); + + assert_snapshot!(model, @r#" + VmodInfo { + name: "ptsk", + funcs: [ + FuncInfo { + ident: "pt", + has_optional_args: false, + args: [ + SharedPerTask( + TaskShared { + name: "vp", + ty_info: "Instant", + }, + ), + ], + returns: Value( + ParamType( + Duration, + ), + ), + }, + FuncInfo { + ident: "pt_opt", + has_optional_args: true, + args: [ + SharedPerTask( + TaskShared { + name: "vp", + ty_info: "Instant", + }, + ), + Value( + ParamInfo { + name: "op", + is_optional: true, + ty_info: I64, + }, + ), + ], + returns: Value( + ParamType( + Duration, + ), + ), + }, + ], + shared_per_task_ty: Some( + "Instant", + ), + } + "#); + + assert_snapshot!(data, @r##" + #[varnish::vmod] + mod ptsk { + #[allow(unused_imports)] + mod varnish_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, VCL_DURATION, VCL_INT, VCL_STRING, VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, vmod_priv, vmod_priv_methods, vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + static PRIV_TASK_METHODS: vmod_priv_methods = vmod_priv_methods { + magic: VMOD_PRIV_METHODS_MAGIC, + type_: c"Instant".as_ptr(), + fini: Some(vmod_priv::on_fini::), + }; + unsafe extern "C" fn vmod_c_pt( + ctx: *mut vrt_ctx, + vp: *mut vmod_priv, + ) -> VCL_DURATION { + let mut ctx = Ctx::from_ptr(ctx); + let mut __var0 = (*vp).take(); + let __result = super::pt(&mut __var0); + if let Some(obj) = __var0 { + (*vp).put(obj, &PRIV_TASK_METHODS); + } + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::default() + } + } + } + #[repr(C)] + struct arg_ptsk_pt_opt { + vp: *mut vmod_priv, + valid_op: c_char, + op: VCL_INT, + } + unsafe extern "C" fn vmod_c_pt_opt( + ctx: *mut vrt_ctx, + args: *const arg_ptsk_pt_opt, + ) -> VCL_DURATION { + let mut ctx = Ctx::from_ptr(ctx); + let args = args.as_ref().unwrap(); + let mut __var0 = (*args.vp).take(); + let __var1 = (args.valid_op != 0).then(|| args.op.into_rust()); + let __result = super::pt_opt(&mut __var0, __var1); + if let Some(obj) = __var0 { + (*args.vp).put(obj, &PRIV_TASK_METHODS); + } + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::default() + } + } + } + #[repr(C)] + pub struct VmodExports { + pt: Option< + unsafe extern "C" fn( + ctx: *mut vrt_ctx, + vp: *mut vmod_priv, + ) -> VCL_DURATION, + >, + pt_opt: Option< + unsafe extern "C" fn( + ctx: *mut vrt_ctx, + args: *const arg_ptsk_pt_opt, + ) -> VCL_DURATION, + >, + } + pub static VMOD_EXPORTS: VmodExports = VmodExports { + pt: Some(vmod_c_pt), + pt_opt: Some(vmod_c_pt_opt), + }; + #[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_ptsk_Data: VmodData = VmodData { + vrt_major: 0, + vrt_minor: 0, + file_id: c"d99143adcece433864ce507c4874764df5fd8cf7c3884790b53aa08853a327a7" + .as_ptr(), + name: c"ptsk".as_ptr(), + func_name: c"Vmod_ptsk".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 \"ptsk\",\n \"Vmod_ptsk\",\n \"d99143adcece433864ce507c4874764df5fd8cf7c3884790b53aa08853a327a7\",\n \"Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1\",\n \"0\",\n \"0\"\n ],\n [\n \"$CPROTO\",\n \"\\ntypedef VCL_DURATION td_ptsk_pt(VRT_CTX, struct vmod_priv *);\\n\\nstruct arg_ptsk_pt_opt {\\n struct vmod_priv * vp;\\n char valid_op;\\n VCL_INT op;\\n};\\ntypedef VCL_DURATION td_ptsk_pt_opt(VRT_CTX, struct arg_ptsk_pt_opt*);\\n\\nstruct Vmod_ptsk {\\n td_ptsk_pt *f_pt;\\n td_ptsk_pt_opt *f_pt_opt;\\n};\\n\\nstatic struct Vmod_ptsk Vmod_ptsk;\"\n ],\n [\n \"$FUNC\",\n \"pt\",\n [\n [\n \"DURATION\"\n ],\n \"Vmod_ptsk.f_pt\",\n \"\",\n [\n \"PRIV_TASK\",\n \"vp\"\n ]\n ]\n ],\n [\n \"$FUNC\",\n \"pt_opt\",\n [\n [\n \"DURATION\"\n ],\n \"Vmod_ptsk.f_pt_opt\",\n \"struct arg_ptsk_pt_opt\",\n [\n \"PRIV_TASK\",\n \"vp\"\n ],\n [\n \"INT\",\n \"op\",\n null,\n null,\n true\n ]\n ]\n ]\n]\n\u{3}"; + } + pub fn pt(vp: &mut Option>) -> Duration { + todo!() + } + pub fn pt_opt(vp: &mut Option>, op: Option) -> Duration { + todo!() + } + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "ptsk", + "Vmod_ptsk", + "d99143adcece433864ce507c4874764df5fd8cf7c3884790b53aa08853a327a7", + "Varnish 7.5.0 eef25264e5ca5f96a77129308edb83ccf84cb1b1", + "0", + "0" + ], + [ + "$CPROTO", + " + typedef VCL_DURATION td_ptsk_pt(VRT_CTX, struct vmod_priv *); + + struct arg_ptsk_pt_opt { + struct vmod_priv * vp; + char valid_op; + VCL_INT op; + }; + typedef VCL_DURATION td_ptsk_pt_opt(VRT_CTX, struct arg_ptsk_pt_opt*); + + struct Vmod_ptsk { + td_ptsk_pt *f_pt; + td_ptsk_pt_opt *f_pt_opt; + }; + + static struct Vmod_ptsk Vmod_ptsk;" + ], + [ + "$FUNC", + "pt", + [ + [ + "DURATION" + ], + "Vmod_ptsk.f_pt", + "", + [ + "PRIV_TASK", + "vp" + ] + ] + ], + [ + "$FUNC", + "pt_opt", + [ + [ + "DURATION" + ], + "Vmod_ptsk.f_pt_opt", + "struct arg_ptsk_pt_opt", + [ + "PRIV_TASK", + "vp" + ], + [ + "INT", + "op", + null, + null, + true + ] + ] + ] + ] +  + "#); +} diff --git a/varnish-macros/src/tests/simple.rs b/varnish-macros/src/tests/simple.rs new file mode 100644 index 0000000..e306b87 --- /dev/null +++ b/varnish-macros/src/tests/simple.rs @@ -0,0 +1,414 @@ +use insta::assert_snapshot; +use quote::quote; + +use crate::tests::expand_macros; + +#[test] +fn simple() { + let (model, 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()) } + pub fn ctx_i64_i64_i64_to_string(ctx: &Ctx, a1: i64, a2: i64, a3: i64) -> String { Ok(s.to_owned()) } + } + }); + + assert_snapshot!(model, @r#" + VmodInfo { + name: "simple", + funcs: [ + FuncInfo { + ident: "void_to_void", + has_optional_args: false, + args: [], + returns: Default, + }, + FuncInfo { + ident: "i64_to_bool", + has_optional_args: false, + args: [ + Value( + ParamInfo { + name: "n", + is_optional: false, + ty_info: I64, + }, + ), + ], + returns: Value( + ParamType( + Bool, + ), + ), + }, + FuncInfo { + ident: "str_to_i64", + has_optional_args: false, + args: [ + Value( + ParamInfo { + name: "s", + is_optional: false, + ty_info: Str, + }, + ), + ], + returns: Value( + ParamType( + I64, + ), + ), + }, + FuncInfo { + ident: "str_to_i64", + has_optional_args: false, + args: [ + Value( + ParamInfo { + name: "s", + is_optional: false, + ty_info: Str, + }, + ), + ], + returns: Result( + String, + String, + ), + }, + FuncInfo { + ident: "ctx_i64_i64_i64_to_string", + has_optional_args: false, + args: [ + Context { + is_mut: false, + }, + Value( + ParamInfo { + name: "a1", + is_optional: false, + ty_info: I64, + }, + ), + Value( + ParamInfo { + name: "a2", + is_optional: false, + ty_info: I64, + }, + ), + Value( + ParamInfo { + name: "a3", + is_optional: false, + ty_info: I64, + }, + ), + ], + returns: Value( + String, + ), + }, + ], + shared_per_task_ty: None, + } + "#); + + assert_snapshot!(data, @r##" + #[varnish::vmod] + mod simple { + #[allow(unused_imports)] + mod varnish_generated { + use std::ffi::{c_char, c_int, c_uint, c_void, CStr}; + use std::ptr::null; + use varnish::ffi::{ + VCL_BOOL, VCL_DURATION, VCL_INT, VCL_STRING, VMOD_ABI_Version, + VMOD_PRIV_METHODS_MAGIC, vmod_priv, vmod_priv_methods, vrt_ctx, + }; + use varnish::vcl::convert::{IntoResult, IntoRust, IntoVCL, VCLDefault}; + use varnish::vcl::ctx::Ctx; + use super::*; + unsafe extern "C" fn vmod_c_void_to_void(ctx: *mut vrt_ctx) { + let mut ctx = Ctx::from_ptr(ctx); + let __result = super::void_to_void(); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::default() + } + } + } + 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); + let __var0 = n.into_rust(); + let __result = super::i64_to_bool(__var0); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::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); + let __var0 = &*s.into_rust(); + let __result = super::str_to_i64(__var0); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + Default::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); + let __var0 = &*s.into_rust(); + let __result = super::str_to_i64(__var0); + let __result = match __result { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + return std::ptr::null(); + } + }; + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + std::ptr::null() + } + } + } + unsafe extern "C" fn vmod_c_ctx_i64_i64_i64_to_string( + ctx: *mut vrt_ctx, + a1: VCL_INT, + a2: VCL_INT, + a3: VCL_INT, + ) -> VCL_STRING { + let mut ctx = Ctx::from_ptr(ctx); + let __var1 = a1.into_rust(); + let __var2 = a2.into_rust(); + let __var3 = a3.into_rust(); + let __result = super::ctx_i64_i64_i64_to_string( + &ctx, + __var1, + __var2, + __var3, + ); + match __result.into_vcl(&mut ctx.ws) { + Ok(v) => v, + Err(e) => { + ctx.fail(e); + std::ptr::null() + } + } + } + #[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, + >, + ctx_i64_i64_i64_to_string: Option< + unsafe extern "C" fn( + ctx: *mut vrt_ctx, + a1: VCL_INT, + a2: VCL_INT, + a3: VCL_INT, + ) -> 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), + ctx_i64_i64_i64_to_string: Some(vmod_c_ctx_i64_i64_i64_to_string), + }; + #[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"b38e82978eb0763ebcab153b41dabe3f6f320daeb4c4a03ce438416ef109875c" + .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 \"b38e82978eb0763ebcab153b41dabe3f6f320daeb4c4a03ce438416ef109875c\",\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\\ntypedef VCL_STRING td_simple_ctx_i64_i64_i64_to_string(VRT_CTX, VCL_INT, VCL_INT, VCL_INT);\\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 td_simple_ctx_i64_i64_i64_to_string *f_ctx_i64_i64_i64_to_string;\\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 \"$FUNC\",\n \"ctx_i64_i64_i64_to_string\",\n [\n [\n \"STRING\"\n ],\n \"Vmod_simple.f_ctx_i64_i64_i64_to_string\",\n \"\",\n [\n \"INT\",\n \"a1\"\n ],\n [\n \"INT\",\n \"a2\"\n ],\n [\n \"INT\",\n \"a3\"\n ]\n ]\n ]\n]\n\u{3}"; + } + 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()) + } + pub fn ctx_i64_i64_i64_to_string(ctx: &Ctx, a1: i64, a2: i64, a3: i64) -> String { + Ok(s.to_owned()) + } + } + "##); + + assert_snapshot!(json, @r#" + VMOD_JSON_SPEC + [ + [ + "$VMOD", + "1.0", + "simple", + "Vmod_simple", + "b38e82978eb0763ebcab153b41dabe3f6f320daeb4c4a03ce438416ef109875c", + "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); + + typedef VCL_STRING td_simple_ctx_i64_i64_i64_to_string(VRT_CTX, VCL_INT, VCL_INT, VCL_INT); + + 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; + td_simple_ctx_i64_i64_i64_to_string *f_ctx_i64_i64_i64_to_string; + }; + + 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" + ] + ] + ], + [ + "$FUNC", + "ctx_i64_i64_i64_to_string", + [ + [ + "STRING" + ], + "Vmod_simple.f_ctx_i64_i64_i64_to_string", + "", + [ + "INT", + "a1" + ], + [ + "INT", + "a2" + ], + [ + "INT", + "a3" + ] + ] + ] + ] +  + "#); +}