• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 14649493721

24 Apr 2025 06:56PM CUT coverage: 14.14% (+0.01%) from 14.129%
14649493721

Pull #426

github

Xenira
refactor(macro): use `#[php]` attribute for startup function

This unifies the behaviour of the `#[php_module]` macro and the other `#[php_*]` macros.

Refs: #423
Pull Request #426: refactor(macro): use `#[php]` attribute for startup function

0 of 5 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

553 of 3911 relevant lines covered (14.14%)

1.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/macros/src/function.rs
1
use std::collections::HashMap;
2

3
use darling::{FromAttributes, ToTokens};
4
use proc_macro2::{Ident, Span, TokenStream};
5
use quote::{format_ident, quote};
6
use syn::spanned::Spanned as _;
7
use syn::PatType;
8
use syn::{FnArg, GenericArgument, ItemFn, Lit, PathArguments, Type, TypePath};
9

10
use crate::helpers::get_docs;
11
use crate::parsing::{PhpRename, Visibility};
12
use crate::prelude::*;
13
use crate::syn_ext::DropLifetimes;
14

15
pub fn wrap(input: &syn::Path) -> Result<TokenStream> {
×
16
    let Some(func_name) = input.get_ident() else {
×
17
        bail!(input => "Pass a PHP function name into `wrap_function!()`.");
×
18
    };
19
    let builder_func = format_ident!("_internal_{func_name}");
×
20

21
    Ok(quote! {{
×
22
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
×
23
    }})
24
}
25

26
#[derive(FromAttributes, Default, Debug)]
27
#[darling(default, attributes(php), forward_attrs(doc))]
28
struct PhpFunctionAttribute {
29
    #[darling(flatten)]
30
    rename: PhpRename,
31
    defaults: HashMap<Ident, Lit>,
32
    optional: Option<Ident>,
33
    vis: Option<Visibility>,
34
    attrs: Vec<syn::Attribute>,
35
}
36

37
pub fn parser(mut input: ItemFn) -> Result<TokenStream> {
×
38
    let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?;
×
39
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
×
40

41
    let args = Args::parse_from_fnargs(input.sig.inputs.iter(), php_attr.defaults)?;
×
42
    if let Some(ReceiverArg { span, .. }) = args.receiver {
×
43
        bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`.");
×
44
    }
45

46
    let docs = get_docs(&php_attr.attrs)?;
×
47

48
    let func = Function::new(
49
        &input.sig,
×
50
        php_attr.rename.rename(input.sig.ident.to_string()),
×
51
        args,
×
52
        php_attr.optional,
×
53
        docs,
×
54
    );
55
    let function_impl = func.php_function_impl();
×
56

57
    Ok(quote! {
×
58
        #input
×
59
        #function_impl
×
60
    })
61
}
62

63
#[derive(Debug)]
64
pub struct Function<'a> {
65
    /// Identifier of the Rust function associated with the function.
66
    pub ident: &'a Ident,
67
    /// Name of the function in PHP.
68
    pub name: String,
69
    /// Function arguments.
70
    pub args: Args<'a>,
71
    /// Function outputs.
72
    pub output: Option<&'a Type>,
73
    /// The first optional argument of the function.
74
    pub optional: Option<Ident>,
75
    /// Doc comments for the function.
76
    pub docs: Vec<String>,
77
}
78

79
#[derive(Debug)]
80
pub enum CallType<'a> {
81
    Function,
82
    Method {
83
        class: &'a syn::Path,
84
        receiver: MethodReceiver,
85
    },
86
}
87

88
/// Type of receiver on the method.
89
#[derive(Debug)]
90
pub enum MethodReceiver {
91
    /// Static method - has no receiver.
92
    Static,
93
    /// Class method, takes `&self` or `&mut self`.
94
    Class,
95
    /// Class method, takes `&mut ZendClassObject<Self>`.
96
    ZendClassObject,
97
}
98

99
impl<'a> Function<'a> {
100
    /// Parse a function.
101
    ///
102
    /// # Parameters
103
    ///
104
    /// * `sig` - Function signature.
105
    /// * `name` - Function name in PHP land.
106
    /// * `args` - Function arguments.
107
    /// * `optional` - The ident of the first optional argument.
108
    pub fn new(
×
109
        sig: &'a syn::Signature,
110
        name: String,
111
        args: Args<'a>,
112
        optional: Option<Ident>,
113
        docs: Vec<String>,
114
    ) -> Self {
115
        Self {
116
            ident: &sig.ident,
×
117
            name,
118
            args,
119
            output: match &sig.output {
×
120
                syn::ReturnType::Default => None,
121
                syn::ReturnType::Type(_, ty) => Some(&**ty),
122
            },
123
            optional,
124
            docs,
125
        }
126
    }
127

128
    /// Generates an internal identifier for the function.
129
    pub fn internal_ident(&self) -> Ident {
×
130
        format_ident!("_internal_{}", &self.ident)
×
131
    }
132

133
    /// Generates the function builder for the function.
134
    pub fn function_builder(&self, call_type: CallType) -> TokenStream {
×
135
        let name = &self.name;
×
136
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
137

138
        // `handler` impl
139
        let arg_declarations = self
×
140
            .args
×
141
            .typed
×
142
            .iter()
143
            .map(TypedArg::arg_declaration)
×
144
            .collect::<Vec<_>>();
145

146
        // `entry` impl
147
        let required_args = required
×
148
            .iter()
149
            .map(TypedArg::arg_builder)
×
150
            .collect::<Vec<_>>();
151
        let not_required_args = not_required
×
152
            .iter()
153
            .map(TypedArg::arg_builder)
×
154
            .collect::<Vec<_>>();
155

156
        let returns = self.build_returns();
×
157
        let result = self.build_result(call_type, required, not_required);
×
158
        let docs = if self.docs.is_empty() {
×
159
            quote! {}
×
160
        } else {
161
            let docs = &self.docs;
×
162
            quote! {
×
163
                .docs(&[#(#docs),*])
×
164
            }
165
        };
166

167
        quote! {
×
168
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
×
169
                ::ext_php_rs::zend_fastcall! {
×
170
                    extern fn handler(
×
171
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
×
172
                        retval: &mut ::ext_php_rs::types::Zval,
×
173
                    ) {
174
                        use ::ext_php_rs::convert::IntoZval;
×
175

176
                        #(#arg_declarations)*
×
177
                        let result = {
×
178
                            #result
×
179
                        };
180

181
                        if let Err(e) = result.set_zval(retval, false) {
×
182
                            let e: ::ext_php_rs::exception::PhpException = e.into();
×
183
                            e.throw().expect("Failed to throw PHP exception.");
×
184
                        }
185
                    }
186
                }
187
                handler
×
188
            })
189
            #(.arg(#required_args))*
×
190
            .not_required()
×
191
            #(.arg(#not_required_args))*
×
192
            #returns
×
193
            #docs
×
194
        }
195
    }
196

197
    fn build_returns(&self) -> Option<TokenStream> {
×
198
        self.output.as_ref().map(|output| {
×
199
            quote! {
×
200
                .returns(
×
201
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
202
                    false,
×
203
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
204
                )
205
            }
206
        })
207
    }
208

209
    fn build_result(
×
210
        &self,
211
        call_type: CallType,
212
        required: &[TypedArg<'_>],
213
        not_required: &[TypedArg<'_>],
214
    ) -> TokenStream {
215
        let ident = self.ident;
×
216
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
217
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
218

219
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
220
            arg.accessor(|e| {
×
221
                quote! {
×
222
                    #e.throw().expect("Failed to throw PHP exception.");
×
223
                    return;
×
224
                }
225
            })
226
        });
227

228
        match call_type {
×
229
            CallType::Function => quote! {
×
230
                let parse = ex.parser()
×
231
                    #(.arg(&mut #required_arg_names))*
×
232
                    .not_required()
×
233
                    #(.arg(&mut #not_required_arg_names))*
×
234
                    .parse();
×
235
                if parse.is_err() {
×
236
                    return;
×
237
                }
238

239
                #ident(#({#arg_accessors}),*)
×
240
            },
241
            CallType::Method { class, receiver } => {
×
242
                let this = match receiver {
×
243
                    MethodReceiver::Static => quote! {
×
244
                        let parse = ex.parser();
×
245
                    },
246
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
×
247
                        let (parse, this) = ex.parser_method::<#class>();
×
248
                        let this = match this {
×
249
                            Some(this) => this,
×
250
                            None => {
×
251
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
×
252
                                    .throw()
×
253
                                    .unwrap();
×
254
                                return;
×
255
                            }
256
                        };
257
                    },
258
                };
259
                let call = match receiver {
×
260
                    MethodReceiver::Static => {
×
261
                        quote! { #class::#ident(#({#arg_accessors}),*) }
×
262
                    }
263
                    MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) },
×
264
                    MethodReceiver::ZendClassObject => {
×
265
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
×
266
                    }
267
                };
268
                quote! {
×
269
                    #this
×
270
                    let parse_result = parse
×
271
                        #(.arg(&mut #required_arg_names))*
×
272
                        .not_required()
×
273
                        #(.arg(&mut #not_required_arg_names))*
×
274
                        .parse();
×
275
                    if parse_result.is_err() {
×
276
                        return;
×
277
                    }
278

279
                    #call
×
280
                }
281
            }
282
        }
283
    }
284

285
    /// Generates a struct and impl for the `PhpFunction` trait.
286
    pub fn php_function_impl(&self) -> TokenStream {
×
287
        let internal_ident = self.internal_ident();
×
288
        let builder = self.function_builder(CallType::Function);
×
289

290
        quote! {
×
291
            #[doc(hidden)]
×
292
            #[allow(non_camel_case_types)]
×
293
            struct #internal_ident;
×
294

295
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
296
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
297
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
298
                    {
299
                        #builder
×
300
                    }
301
                    entry
×
302
                };
303
            }
304
        }
305
    }
306

307
    /// Returns a constructor metadata object for this function. This doesn't
308
    /// check if the function is a constructor, however.
309
    pub fn constructor_meta(&self, class: &syn::Path) -> TokenStream {
×
310
        let ident = self.ident;
×
311
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
312
        let required_args = required
×
313
            .iter()
314
            .map(TypedArg::arg_builder)
×
315
            .collect::<Vec<_>>();
316
        let not_required_args = not_required
×
317
            .iter()
318
            .map(TypedArg::arg_builder)
×
319
            .collect::<Vec<_>>();
320

321
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
322
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
323
        let arg_declarations = self
×
324
            .args
×
325
            .typed
×
326
            .iter()
327
            .map(TypedArg::arg_declaration)
×
328
            .collect::<Vec<_>>();
329
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
330
            arg.accessor(
×
331
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
332
            )
333
        });
334
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
335
            quote! {
×
336
                .variadic()
×
337
            }
338
        });
339

340
        quote! {
×
341
            ::ext_php_rs::class::ConstructorMeta {
×
342
                constructor: {
×
343
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
344
                        #(#arg_declarations)*
×
345
                        let parse = ex.parser()
×
346
                            #(.arg(&mut #required_arg_names))*
×
347
                            .not_required()
×
348
                            #(.arg(&mut #not_required_arg_names))*
×
349
                            #variadic
×
350
                            .parse();
×
351
                        if parse.is_err() {
×
352
                            return ::ext_php_rs::class::ConstructorResult::ArgError;
×
353
                        }
354
                        #class::#ident(#({#arg_accessors}),*).into()
×
355
                    }
356
                    inner
×
357
                },
358
                build_fn: {
×
359
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
360
                        func
×
361
                            #(.arg(#required_args))*
×
362
                            .not_required()
×
363
                            #(.arg(#not_required_args))*
×
364
                            #variadic
×
365
                    }
366
                    inner
×
367
                }
368
            }
369
        }
370
    }
371
}
372

373
#[derive(Debug)]
374
pub struct ReceiverArg {
375
    pub _mutable: bool,
376
    pub span: Span,
377
}
378

379
#[derive(Debug)]
380
pub struct TypedArg<'a> {
381
    pub name: &'a Ident,
382
    pub ty: Type,
383
    pub nullable: bool,
384
    pub default: Option<Lit>,
385
    pub as_ref: bool,
386
    pub variadic: bool,
387
}
388

389
#[derive(Debug)]
390
pub struct Args<'a> {
391
    pub receiver: Option<ReceiverArg>,
392
    pub typed: Vec<TypedArg<'a>>,
393
}
394

395
impl<'a> Args<'a> {
396
    pub fn parse_from_fnargs(
×
397
        args: impl Iterator<Item = &'a FnArg>,
398
        mut defaults: HashMap<Ident, Lit>,
399
    ) -> Result<Self> {
400
        let mut result = Self {
401
            receiver: None,
402
            typed: vec![],
×
403
        };
404
        for arg in args {
×
405
            match arg {
×
406
                FnArg::Receiver(receiver) => {
×
407
                    if receiver.reference.is_none() {
×
408
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
409
                    } else if result.receiver.is_some() {
×
410
                        bail!(receiver => "Too many receivers specified.")
×
411
                    }
412
                    result.receiver.replace(ReceiverArg {
×
413
                        _mutable: receiver.mutability.is_some(),
×
414
                        span: receiver.span(),
×
415
                    });
416
                }
417
                FnArg::Typed(PatType { pat, ty, .. }) => {
×
418
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
×
419
                        bail!(pat => "Unsupported argument.");
×
420
                    };
421

422
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
423
                    let default = defaults.remove(ident);
×
424
                    let nullable = type_is_nullable(ty.as_ref(), default.is_some())?;
×
425
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
×
426
                    result.typed.push(TypedArg {
×
427
                        name: ident,
×
428
                        ty,
×
429
                        nullable,
×
430
                        default,
×
431
                        as_ref,
×
432
                        variadic,
×
433
                    });
434
                }
435
            }
436
        }
437
        Ok(result)
×
438
    }
439

440
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
×
441
        match ty {
×
442
            Type::Reference(ref_) => {
×
443
                let as_ref = ref_.mutability.is_some();
×
444
                match ref_.elem.as_ref() {
×
445
                    Type::Slice(slice) => (
×
446
                        // TODO: Allow specifying the variadic type.
447
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
448
                        as_ref,
×
449
                        ty.clone(),
×
450
                    ),
451
                    _ => (false, as_ref, ty.clone()),
×
452
                }
453
            }
454
            Type::Path(TypePath { path, .. }) => {
×
455
                let mut as_ref = false;
×
456

457
                // For for types that are `Option<&mut T>` to turn them into
458
                // `Option<&T>`, marking the Arg as as "passed by reference".
459
                let ty = path
×
460
                    .segments
×
461
                    .last()
462
                    .filter(|seg| seg.ident == "Option")
×
463
                    .and_then(|seg| {
×
464
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
465
                            args.args
×
466
                                .iter()
×
467
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
468
                                .and_then(|ga| match ga {
×
469
                                    GenericArgument::Type(ty) => Some(match ty {
×
470
                                        Type::Reference(r) => {
×
471
                                            let mut new_ref = r.clone();
×
472
                                            new_ref.mutability = None;
×
473
                                            as_ref = true;
×
474
                                            Type::Reference(new_ref)
×
475
                                        }
476
                                        _ => ty.clone(),
×
477
                                    }),
478
                                    _ => None,
×
479
                                })
480
                        } else {
481
                            None
×
482
                        }
483
                    })
484
                    .unwrap_or_else(|| ty.clone());
×
485
                (false, as_ref, ty.clone())
×
486
            }
487
            _ => (false, false, ty.clone()),
×
488
        }
489
    }
490

491
    /// Splits the typed arguments into two slices:
492
    ///
493
    /// 1. Required arguments.
494
    /// 2. Non-required arguments.
495
    ///
496
    /// # Parameters
497
    ///
498
    /// * `optional` - The first optional argument. If [`None`], the optional
499
    ///   arguments will be from the first nullable argument after the last
500
    ///   non-nullable argument to the end of the arguments.
501
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
×
502
        let mut mid = None;
×
503
        for (i, arg) in self.typed.iter().enumerate() {
×
504
            if let Some(optional) = optional {
×
505
                if optional == arg.name {
×
506
                    mid.replace(i);
×
507
                }
508
            } else if mid.is_none() && arg.nullable {
×
509
                mid.replace(i);
×
510
            } else if !arg.nullable {
×
511
                mid.take();
×
512
            }
513
        }
514
        match mid {
×
515
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
516
            None => (&self.typed[..], &self.typed[0..0]),
×
517
        }
518
    }
519
}
520

521
impl TypedArg<'_> {
522
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
523
    /// to be used outside of the original function context.
524
    fn clean_ty(&self) -> Type {
×
525
        let mut ty = self.ty.clone();
×
526
        ty.drop_lifetimes();
×
527

528
        // Variadic arguments are passed as slices, so we need to extract the
529
        // inner type.
530
        if self.variadic {
×
531
            let Type::Reference(reference) = &ty else {
×
532
                return ty;
×
533
            };
534

535
            if let Type::Slice(inner) = &*reference.elem {
×
536
                return *inner.elem.clone();
×
537
            }
538
        }
539

540
        ty
×
541
    }
542

543
    /// Returns a token stream containing an argument declaration, where the
544
    /// name of the variable holding the arg is the name of the argument.
545
    fn arg_declaration(&self) -> TokenStream {
×
546
        let name = self.name;
×
547
        let val = self.arg_builder();
×
548
        quote! {
×
549
            let mut #name = #val;
×
550
        }
551
    }
552

553
    /// Returns a token stream containing the `Arg` definition to be passed to
554
    /// `ext-php-rs`.
555
    fn arg_builder(&self) -> TokenStream {
×
556
        let name = self.name.to_string();
×
557
        let ty = self.clean_ty();
×
558
        let null = if self.nullable {
×
559
            Some(quote! { .allow_null() })
×
560
        } else {
561
            None
×
562
        };
563
        let default = self.default.as_ref().map(|val| {
×
564
            let val = val.to_token_stream().to_string();
×
565
            quote! {
×
566
                .default(#val)
×
567
            }
568
        });
569
        let as_ref = if self.as_ref {
×
570
            Some(quote! { .as_ref() })
×
571
        } else {
572
            None
×
573
        };
574
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
×
575
        quote! {
×
576
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
×
577
                #null
×
578
                #default
×
579
                #as_ref
×
580
                #variadic
×
581
        }
582
    }
583

584
    /// Get the accessor used to access the value of the argument.
585
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
586
        let name = self.name;
×
587
        if let Some(default) = &self.default {
×
588
            quote! {
×
589
                #name.val().unwrap_or(#default.into())
×
590
            }
591
        } else if self.variadic {
×
592
            quote! {
×
593
                &#name.variadic_vals()
×
594
            }
595
        } else if self.nullable {
×
596
            // Originally I thought we could just use the below case for `null` options, as
597
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
598
            // the argument isn't given, as the underlying zval is null.
599
            quote! {
×
600
                #name.val()
×
601
            }
602
        } else {
603
            let bail = bail_fn(quote! {
×
604
                ::ext_php_rs::exception::PhpException::default(
×
605
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
606
                )
607
            });
608
            quote! {
×
609
                match #name.val() {
×
610
                    Some(val) => val,
×
611
                    None => {
×
612
                        #bail;
×
613
                    }
614
                }
615
            }
616
        }
617
    }
618
}
619

620
/// Returns true of the given type is nullable in PHP.
621
// TODO(david): Eventually move to compile-time constants for this (similar to
622
// FromZval::NULLABLE).
623
pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result<bool> {
×
624
    Ok(match ty {
×
625
        syn::Type::Path(path) => {
×
626
            has_default
×
627
                || path
×
628
                    .path
×
629
                    .segments
×
630
                    .iter()
×
631
                    .next_back()
×
632
                    .is_some_and(|seg| seg.ident == "Option")
×
633
        }
634
        syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
635
        // wrapped in `Option` (in that case it'd be a Path).
636
        _ => bail!(ty => "Unsupported argument type."),
×
637
    })
638
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc