Skip to content

Commit e5c61bb

Browse files
committed
feat(interface): php_impl_interface macro #590
1 parent 5449502 commit e5c61bb

File tree

28 files changed

+1280
-76
lines changed

28 files changed

+1280
-76
lines changed

build.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,19 +231,16 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<S
231231
// Add macOS SDK path for system headers (stdlib.h, etc.)
232232
// Required for libclang 19+ with preserve_none calling convention support
233233
#[cfg(target_os = "macos")]
234+
if let Ok(sdk_path) = std::process::Command::new("xcrun")
235+
.args(["--show-sdk-path"])
236+
.output()
237+
&& sdk_path.status.success()
234238
{
235-
if let Ok(sdk_path) = std::process::Command::new("xcrun")
236-
.args(["--show-sdk-path"])
237-
.output()
238-
{
239-
if sdk_path.status.success() {
240-
let path = String::from_utf8_lossy(&sdk_path.stdout);
241-
let path = path.trim();
242-
bindgen = bindgen
243-
.clang_arg(format!("-isysroot{}", path))
244-
.clang_arg(format!("-I{}/usr/include", path));
245-
}
246-
}
239+
let path = String::from_utf8_lossy(&sdk_path.stdout);
240+
let path = path.trim();
241+
bindgen = bindgen
242+
.clang_arg(format!("-isysroot{path}"))
243+
.clang_arg(format!("-I{path}/usr/include"));
247244
}
248245

249246
bindgen = bindgen

crates/macros/src/class.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use darling::util::Flag;
22
use darling::{FromAttributes, FromMeta, ToTokens};
33
use proc_macro2::TokenStream;
4-
use quote::{TokenStreamExt, quote};
4+
use quote::{quote, TokenStreamExt};
55
use syn::{Attribute, Expr, Fields, ItemStruct};
66

77
use crate::helpers::get_docs;
@@ -273,6 +273,12 @@ fn generate_registered_class_impl(
273273
use ::ext_php_rs::internal::class::PhpClassImpl;
274274
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
275275
}
276+
277+
#[inline]
278+
fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> {
279+
// TODO: Implement cross-crate interface discovery
280+
::std::vec::Vec::new()
281+
}
276282
}
277283
}
278284
}

crates/macros/src/enum_.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::convert::TryFrom;
22

3-
use darling::{FromAttributes, util::Flag};
3+
use darling::{util::Flag, FromAttributes};
44
use itertools::Itertools;
55
use proc_macro2::TokenStream;
6-
use quote::{ToTokens, quote};
6+
use quote::{quote, ToTokens};
77
use syn::{Fields, Ident, ItemEnum, Lit};
88

99
use crate::{

crates/macros/src/extern_.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use proc_macro2::TokenStream;
22
use quote::quote;
33
use syn::{
4-
ForeignItemFn, ItemForeignMod, ReturnType, Signature, Token, punctuated::Punctuated,
5-
spanned::Spanned as _, token::Unsafe,
4+
punctuated::Punctuated, spanned::Spanned as _, token::Unsafe, ForeignItemFn, ItemForeignMod,
5+
ReturnType, Signature, Token,
66
};
77

88
use crate::prelude::*;

crates/macros/src/impl_.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use darling::FromAttributes;
21
use darling::util::Flag;
2+
use darling::FromAttributes;
33
use proc_macro2::TokenStream;
44
use quote::quote;
55
use std::collections::{HashMap, HashSet};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//! Implementation for the `#[php_impl_interface]` macro.
2+
//!
3+
//! This macro allows classes to implement PHP interfaces by implementing Rust
4+
//! traits that are marked with `#[php_interface]`.
5+
6+
use proc_macro2::TokenStream;
7+
use quote::{format_ident, quote};
8+
use syn::ItemImpl;
9+
10+
use crate::prelude::*;
11+
12+
const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";
13+
14+
/// Parses a trait impl block and generates the interface implementation
15+
/// registration.
16+
///
17+
/// # Arguments
18+
///
19+
/// * `input` - The trait impl block (e.g., `impl SomeTrait for SomeStruct { ...
20+
/// }`)
21+
///
22+
/// # Generated Code
23+
///
24+
/// The macro generates:
25+
/// 1. The original trait impl block (passed through unchanged)
26+
/// 2. An implementation of `PhpInterfaceImpl` that registers the interface
27+
pub fn parser(input: &ItemImpl) -> Result<TokenStream> {
28+
// Extract the trait being implemented
29+
let Some((_, trait_path, _)) = &input.trait_ else {
30+
bail!(input => "`#[php_impl_interface]` can only be used on trait implementations (e.g., `impl SomeTrait for SomeStruct`)");
31+
};
32+
33+
// Get the last segment of the trait path (the trait name)
34+
let trait_ident = match trait_path.segments.last() {
35+
Some(segment) => &segment.ident,
36+
None => {
37+
bail!(trait_path => "Invalid trait path");
38+
}
39+
};
40+
41+
// Get the struct type being implemented
42+
let struct_ty = &input.self_ty;
43+
44+
// Generate the internal interface struct name (e.g., PhpInterfaceSomeTrait)
45+
let interface_struct_name = format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_ident);
46+
47+
Ok(quote! {
48+
// Pass through the original trait implementation
49+
#input
50+
51+
// Implement PhpInterfaceImpl for the struct to register the interface
52+
impl ::ext_php_rs::internal::class::PhpInterfaceImpl<#struct_ty>
53+
for ::ext_php_rs::internal::class::PhpInterfaceImplCollector<#struct_ty>
54+
{
55+
fn get_interfaces(self) -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> {
56+
vec![
57+
(
58+
|| <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
59+
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
60+
)
61+
]
62+
}
63+
}
64+
})
65+
}

crates/macros/src/interface.rs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ use std::collections::{HashMap, HashSet};
33
use crate::class::ClassEntryAttribute;
44
use crate::constant::PhpConstAttribute;
55
use crate::function::{Args, Function};
6-
use crate::helpers::{CleanPhpAttr, get_docs};
7-
use darling::FromAttributes;
6+
use crate::helpers::{get_docs, CleanPhpAttr};
87
use darling::util::Flag;
8+
use darling::FromAttributes;
99
use proc_macro2::TokenStream;
10-
use quote::{ToTokens, format_ident, quote};
11-
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn};
10+
use quote::{format_ident, quote, ToTokens};
11+
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn, TypeParamBound};
1212

1313
use crate::impl_::{FnBuilder, MethodModifier};
1414
use crate::parsing::{PhpRename, RenameRule, Visibility};
@@ -45,11 +45,36 @@ trait Parse<'a, T> {
4545
fn parse(&'a mut self) -> Result<T>;
4646
}
4747

48+
/// Represents a supertrait that should be converted to an interface extension.
49+
/// These are automatically detected from Rust trait bounds (e.g., `trait Foo:
50+
/// Bar`).
51+
struct SupertraitInterface {
52+
/// The name of the supertrait's PHP interface struct (e.g.,
53+
/// `PhpInterfaceBar`)
54+
interface_struct_name: Ident,
55+
}
56+
57+
impl ToTokens for SupertraitInterface {
58+
fn to_tokens(&self, tokens: &mut TokenStream) {
59+
let interface_struct_name = &self.interface_struct_name;
60+
quote! {
61+
(
62+
|| <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
63+
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
64+
)
65+
}
66+
.to_tokens(tokens);
67+
}
68+
}
69+
4870
struct InterfaceData<'a> {
4971
ident: &'a Ident,
5072
name: String,
5173
path: Path,
74+
/// Extends from `#[php(extends(...))]` attributes
5275
extends: Vec<ClassEntryAttribute>,
76+
/// Extends from Rust trait bounds (supertraits)
77+
supertrait_extends: Vec<SupertraitInterface>,
5378
constructor: Option<Function<'a>>,
5479
methods: Vec<FnBuilder>,
5580
constants: Vec<Constant<'a>>,
@@ -62,6 +87,7 @@ impl ToTokens for InterfaceData<'_> {
6287
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident);
6388
let name = &self.name;
6489
let implements = &self.extends;
90+
let supertrait_implements = &self.supertrait_extends;
6591
let methods_sig = &self.methods;
6692
let constants = &self.constants;
6793
let docs = &self.docs;
@@ -86,8 +112,10 @@ impl ToTokens for InterfaceData<'_> {
86112

87113
const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface;
88114

115+
// Interface inheritance from both explicit #[php(extends(...))] and Rust trait bounds
89116
const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[
90117
#(#implements,)*
118+
#(#supertrait_implements,)*
91119
];
92120

93121
const DOC_COMMENTS: &'static [&'static str] = &[
@@ -202,11 +230,16 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
202230
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}");
203231
let ts = quote! { #interface_name };
204232
let path: Path = syn::parse2(ts)?;
233+
234+
// Parse supertraits to automatically generate interface inheritance
235+
let supertrait_extends = parse_supertraits(&self.supertraits);
236+
205237
let mut data = InterfaceData {
206238
ident,
207239
name,
208240
path,
209241
extends: attrs.extends,
242+
supertrait_extends,
210243
constructor: None,
211244
methods: Vec::default(),
212245
constants: Vec::default(),
@@ -234,6 +267,31 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
234267
}
235268
}
236269

270+
/// Parses the supertraits of a trait definition and converts them to interface
271+
/// extensions. For a trait like `trait Foo: Bar + Baz`, this will generate
272+
/// references to `PhpInterfaceBar` and `PhpInterfaceBaz`.
273+
fn parse_supertraits(
274+
supertraits: &syn::punctuated::Punctuated<TypeParamBound, syn::token::Plus>,
275+
) -> Vec<SupertraitInterface> {
276+
supertraits
277+
.iter()
278+
.filter_map(|bound| {
279+
if let TypeParamBound::Trait(trait_bound) = bound {
280+
// Get the last segment of the trait path (the trait name)
281+
let trait_name = trait_bound.path.segments.last()?;
282+
// Generate the PHP interface struct name
283+
let interface_struct_name =
284+
format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_name.ident);
285+
Some(SupertraitInterface {
286+
interface_struct_name,
287+
})
288+
} else {
289+
None
290+
}
291+
})
292+
.collect()
293+
}
294+
237295
#[derive(FromAttributes, Default, Debug)]
238296
#[darling(default, attributes(php), forward_attrs(doc))]
239297
pub struct PhpFunctionInterfaceAttribute {

0 commit comments

Comments
 (0)