use darling::FromAttributes;
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};
use syn::{ItemFn, Signature};
use crate::parsing::reject_php_attrs;
use crate::prelude::*;
#[derive(FromAttributes, Default, Debug)]
#[darling(default, attributes(php))]
pub(crate) struct PhpModuleAttribute {
startup: Option<Ident>,
}
pub fn parser(input: ItemFn) -> Result<TokenStream> {
let crate_name = std::env::var("CARGO_CRATE_NAME").ok();
let static_ext = std::env::var("EXT_PHP_RS_STATIC_EXT").is_ok_and(|v| v == "1");
parser_impl(input, crate_name.as_deref(), static_ext)
}
fn get_module_delegate(
input: &ItemFn,
crate_name: Option<&str>,
static_ext: bool,
) -> Result<TokenStream> {
Ok(match crate_name {
Some(name) => {
let ident = format_ident!("{}_get_module", name);
quote! {
#[doc(hidden)]
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
extern "C" fn #ident() -> *mut ::ext_php_rs::zend::ModuleEntry {
get_module()
}
}
}
None if static_ext => bail!(
input => "EXT_PHP_RS_STATIC_EXT=1 requires the CARGO_CRATE_NAME environment variable (set by cargo) to derive the exported symbol name"
),
None => quote! {},
})
}
fn parser_impl(input: ItemFn, crate_name: Option<&str>, static_ext: bool) -> Result<TokenStream> {
for arg in &input.sig.inputs {
if let syn::FnArg::Typed(arg) = arg {
reject_php_attrs(&arg.attrs, "`#[php_module]` parameters")?;
}
}
let delegate = get_module_delegate(&input, crate_name, static_ext)?;
let get_module_no_mangle = (!static_ext).then(|| quote! { #[unsafe(no_mangle)] });
let ItemFn { sig, block, .. } = input;
let Signature { output, inputs, .. } = sig;
let stmts = &block.stmts;
let attr = PhpModuleAttribute::from_attributes(&input.attrs)?;
let startup = if let Some(startup) = attr.startup {
quote! { #startup(ty, mod_num) }
} else {
quote! { 0i32 }
};
Ok(quote! {
#[doc(hidden)]
#get_module_no_mangle
extern "C" fn get_module() -> *mut ::ext_php_rs::zend::ModuleEntry {
static __EXT_PHP_RS_MODULE_ENTRY: ::ext_php_rs::zend::StaticModuleEntry =
::ext_php_rs::zend::StaticModuleEntry::new();
static __EXT_PHP_RS_MODULE_STARTUP: ::ext_php_rs::internal::ModuleStartupMutex =
::ext_php_rs::internal::MODULE_STARTUP_INIT;
static __EXT_PHP_RS_BUILDER_STARTUP: ::std::sync::OnceLock<
unsafe extern "C" fn(i32, i32) -> i32,
> = ::std::sync::OnceLock::new();
extern "C" fn ext_php_rs_startup(ty: i32, mod_num: i32) -> i32 {
let a = unsafe { #startup };
let user = __EXT_PHP_RS_BUILDER_STARTUP
.get()
.map_or(0, |startup| unsafe { startup(ty, mod_num) });
if a | user != 0 {
return a | user;
}
::ext_php_rs::internal::startup_guard(|| {
::ext_php_rs::internal::ext_php_rs_startup();
match __EXT_PHP_RS_MODULE_STARTUP.lock().as_ref() {
Some(startup) => startup.startup(ty, mod_num),
None => Ok(()),
}
})
}
static __EXT_PHP_RS_BUILD_ERROR: ::std::sync::OnceLock<::std::string::String> =
::std::sync::OnceLock::new();
extern "C" fn ext_php_rs_failed_startup(_ty: i32, _mod_num: i32) -> i32 {
::ext_php_rs::internal::failed_module_startup(
__EXT_PHP_RS_BUILD_ERROR.get().map_or("unknown error", ::std::string::String::as_str),
)
}
__EXT_PHP_RS_MODULE_ENTRY.get_or_init(|| {
#[inline]
fn internal(#inputs) #output {
#(#stmts)*
}
let builder = internal(::ext_php_rs::builders::ModuleBuilder::new(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
));
match builder.try_into() {
Ok((mut entry, startup, owned)) => {
if let Some(user_startup) =
entry.module_startup_func.replace(ext_php_rs_startup)
{
let _ = __EXT_PHP_RS_BUILDER_STARTUP.set(user_startup);
}
__EXT_PHP_RS_MODULE_STARTUP.lock().replace(startup);
(entry, owned)
},
Err(e) => {
let _ = __EXT_PHP_RS_BUILD_ERROR.set(e.to_string());
let (entry, _, owned) = ::ext_php_rs::builders::ModuleBuilder::new(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
)
.startup_function(ext_php_rs_failed_startup)
.try_into()
.unwrap_or_else(|_| ::std::unreachable!("a module with only env! strings always builds"));
(entry, owned)
}
}
})
}
#delegate
#[cfg(debug_assertions)]
#[unsafe(no_mangle)]
pub extern "C" fn ext_php_rs_describe_module_v2() -> *mut ::ext_php_rs::describe::Description {
use ::ext_php_rs::describe::*;
#[inline]
fn internal(#inputs) #output {
#(#stmts)*
}
let builder = internal(::ext_php_rs::builders::ModuleBuilder::new(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION")
));
::std::boxed::Box::into_raw(::std::boxed::Box::new(Description::new(builder.into())))
}
})
}
#[cfg(test)]
mod tests {
use super::parser_impl;
use syn::parse_quote;
fn expand(crate_name: Option<&str>, static_ext: bool) -> String {
let input: syn::ItemFn =
parse_quote! { fn module(module: ModuleBuilder) -> ModuleBuilder { module } };
parser_impl(input, crate_name, static_ext)
.unwrap()
.to_string()
.replace(' ', "")
}
#[test]
fn dynamic_build_exports_get_module_and_prefixed_delegate() {
let out = expand(Some("my_ext"), false);
assert!(out.contains(r#"#[unsafe(no_mangle)]extern"C"fnget_module("#));
assert!(out.contains(r#"#[unsafe(no_mangle)]extern"C"fnmy_ext_get_module("#));
}
#[test]
fn static_ext_suppresses_get_module_export_but_keeps_the_fn() {
let out = expand(Some("my_ext"), true);
assert!(!out.contains(r#"no_mangle)]extern"C"fnget_module("#));
assert!(out.contains(r#"extern"C"fnget_module("#));
assert!(out.contains(r#"#[unsafe(no_mangle)]extern"C"fnmy_ext_get_module("#));
}
#[test]
fn build_failure_yields_a_placeholder_entry_whose_minit_fails() {
let out = expand(Some("my_ext"), false);
assert!(!out.contains("panic!"));
assert!(out.contains(r#"extern"C"fnext_php_rs_failed_startup("#));
assert!(out.contains("::ext_php_rs::internal::failed_module_startup("));
assert!(out.contains(".startup_function(ext_php_rs_failed_startup)"));
}
#[test]
fn minit_runs_registration_under_startup_guard() {
let out = expand(Some("my_ext"), false);
assert!(out.contains("::ext_php_rs::internal::startup_guard(||"));
assert!(!out.contains("unwrap_or(1)"));
}
#[test]
fn missing_crate_name_skips_delegate_in_dynamic_build() {
assert!(!expand(None, false).contains("_get_module"));
}
#[test]
fn shutdown_function_is_left_to_the_user() {
let out = expand(Some("my_ext"), false);
assert!(!out.contains("module_shutdown_func"));
assert!(!out.contains("cleanup_module_allocations"));
}
#[test]
fn missing_crate_name_errors_in_static_build() {
let input: syn::ItemFn =
parse_quote! { fn module(module: ModuleBuilder) -> ModuleBuilder { module } };
assert!(parser_impl(input, None, true).is_err());
}
}