From ace0fea5bfb01038047115a7e1306991c7521712 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 17:14:00 +1300 Subject: [PATCH 01/34] Preliminary Windows support --- .cargo/config | 2 - Cargo.toml | 7 +- allowed_bindings.rs | 16 +- build.rs | 392 +++++++++++++++++++++++++++++++++- crates/macros/Cargo.toml | 2 +- crates/macros/src/fastcall.rs | 16 ++ crates/macros/src/function.rs | 20 +- crates/macros/src/lib.rs | 12 ++ src/builders/class.rs | 61 +++--- src/builders/function.rs | 8 + src/builders/module.rs | 7 +- src/describe/stub.rs | 8 +- src/ffi.rs | 23 ++ src/lib.rs | 39 ++++ src/wrapper.h | 14 ++ src/zend/function.rs | 13 +- 16 files changed, 584 insertions(+), 56 deletions(-) delete mode 100644 .cargo/config create mode 100644 crates/macros/src/fastcall.rs diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 0ba009090e..0000000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 0c80d8bb8e..0f888a4b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,14 @@ anyhow = { version = "1", optional = true } ext-php-rs-derive = { version = "=0.7.3", path = "./crates/macros" } [build-dependencies] -bindgen = { version = "0.59" } +anyhow = "1" +# bindgen = { version = "0.59" } +# bindgen = { git = "https://github.com/davidcole1340/rust-bindgen", branch = "abi_vectorcall" } +bindgen = { path = "../rust-bindgen" } regex = "1" cc = "1.0" +reqwest = { version = "*", features = ["blocking"] } +zip = "0.5" [features] closure = [] diff --git a/allowed_bindings.rs b/allowed_bindings.rs index ef834d90e1..5ff76b6a47 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -23,12 +23,12 @@ bind! { _zend_new_array, _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, - ext_php_rs_executor_globals, - ext_php_rs_php_build_id, - ext_php_rs_zend_object_alloc, - ext_php_rs_zend_object_release, - ext_php_rs_zend_string_init, - ext_php_rs_zend_string_release, + // ext_php_rs_executor_globals, + // ext_php_rs_php_build_id, + // ext_php_rs_zend_object_alloc, + // ext_php_rs_zend_object_release, + // ext_php_rs_zend_string_init, + // ext_php_rs_zend_string_release, object_properties_init, php_info_print_table_end, php_info_print_table_header, @@ -165,8 +165,8 @@ bind! { ZEND_DEBUG, ZEND_HAS_STATIC_IN_METHODS, ZEND_ISEMPTY, - ZEND_MM_ALIGNMENT, - ZEND_MM_ALIGNMENT_MASK, + // ZEND_MM_ALIGNMENT, + // ZEND_MM_ALIGNMENT_MASK, ZEND_MODULE_API_NO, ZEND_PROPERTY_EXISTS, ZEND_PROPERTY_ISSET, diff --git a/build.rs b/build.rs index 69f6fc0b4b..b336ad14a2 100644 --- a/build.rs +++ b/build.rs @@ -1,15 +1,354 @@ use std::{ + convert::TryInto, env, + fs::File, + io::{BufRead, BufReader, BufWriter, Cursor, Write}, path::{Path, PathBuf}, process::Command, - str, + str::FromStr, }; +use anyhow::{anyhow, bail, Context, Result}; +use bindgen::RustTarget; use regex::Regex; const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20210902; +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); +const DEFINES: &[&str] = &["ZEND_WIN32", "WINDOWS", "PHP_WIN32", "WIN32"]; + +/// Attempt to find a `vswhere` binary in the common locations. +pub fn find_vswhere() -> Option { + let candidates = [format!( + r"{}\Microsoft Visual Studio\Installer\vswhere.exe", + std::env::var("ProgramFiles(x86)").ok()?, + )]; + for candidate in candidates { + let candidate = PathBuf::from(candidate); + if candidate.exists() { + return Some(candidate); + } + } + None +} + +#[derive(Debug)] +struct LinkerVersion { + major: u32, + minor: u32, +} + +/// Retrieve the version of a MSVC linker. +fn get_linker_version(linker: &Path) -> Result { + let cmd = Command::new(linker) + .output() + .context("Failed to call linker")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + let linker = stdout + .split("\r\n") + .next() + .context("Linker output was empty")?; + let version = linker + .split(' ') + .last() + .context("Linker version string was empty")?; + let components = version + .split('.') + .take(2) + .map(|v| v.parse()) + .collect::, _>>() + .context("Linker version component was empty")?; + Ok(LinkerVersion { + major: components[0], + minor: components[1], + }) +} + +fn get_linkers(vswhere: &Path) -> Result> { + let cmd = Command::new(vswhere) + .arg("-all") + .arg("-prerelease") + .arg("-format") + .arg("value") + .arg("-utf8") + .arg("-find") + .arg(r"VC\**\link.exe") + .output() + .context("Failed to call vswhere")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + let linkers: Vec<_> = stdout + .split("\r\n") + .map(PathBuf::from) + .filter(|linker| linker.exists()) + .collect(); + Ok(linkers) +} + +#[cfg(windows)] +const WHICH: &str = "where"; +#[cfg(not(windows))] +const WHICH: &str = "which"; + +fn find_executable(name: &str) -> Option { + let cmd = Command::new(WHICH).arg(name).output().ok()?; + if cmd.status.success() { + let stdout = String::from_utf8_lossy(&cmd.stdout); + Some(stdout.trim().into()) + } else { + None + } +} + +fn find_php() -> Result { + // If PHP path is given via env, it takes priority. + let env = std::env::var("PHP"); + if let Ok(env) = env { + return Ok(env.into()); + } + + find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") +} + +// https://windows.php.net/downloads/releases/php-devel-pack-8.1.3-nts-Win32-vs16-x64.zip +// https://windows.php.net/downloads/releases/php-devel-pack-8.1.3-Win32-vs16-x64.zip + +struct DevelPack(PathBuf); + +impl DevelPack { + pub fn includes(&self) -> PathBuf { + self.0.join("include") + } + + pub fn php_lib(&self) -> PathBuf { + self.0.join("lib").join("php8.lib") + } + + pub fn include_paths(&self) -> Vec { + let includes = self.includes(); + ["", "main", "Zend", "TSRM", "ext"] + .iter() + .map(|p| includes.join(p)) + .collect() + } + + pub fn linker_version(&self) -> Result { + let config_path = self.includes().join("main").join("config.w32.h"); + let config = File::open(&config_path).context("Failed to open PHP config header")?; + let reader = BufReader::new(config); + let mut major = None; + let mut minor = None; + for line in reader.lines() { + let line = line.context("Failed to read line from PHP config header")?; + if major.is_none() { + let components: Vec<_> = line.split("#define PHP_LINKER_MAJOR ").collect(); + if components.len() > 1 { + major.replace( + u32::from_str(components[1]) + .context("Failed to convert major linker version to integer")?, + ); + continue; + } + } + if minor.is_none() { + let components: Vec<_> = line.split("#define PHP_LINKER_MINOR ").collect(); + if components.len() > 1 { + minor.replace( + u32::from_str(components[1]) + .context("Failed to convert minor linker version to integer")?, + ); + continue; + } + } + } + Ok(LinkerVersion { + major: major.context("Failed to read major linker version from config header")?, + minor: minor.context("Failed to read minor linker version from config header")?, + }) + } +} + +fn download_devel_pack(version: &str, is_zts: bool, arch: &str) -> Result { + let zip_name = format!( + "php-devel-pack-{}{}-Win32-{}-{}.zip", + version, + if is_zts { "" } else { "-nts" }, + "vs16", /* TODO(david): At the moment all PHPs supported by ext-php-rs use VS16 so this + * is constant. */ + arch + ); + + fn download(zip_name: &str, archive: bool) -> Result { + let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let url = format!( + "https://windows.php.net/downloads/releases{}/{}", + if archive { "/archives" } else { "" }, + zip_name + ); + let request = reqwest::blocking::ClientBuilder::new() + .user_agent(USER_AGENT) + .build() + .context("Failed to create HTTP client")? + .get(url) + .send() + .context("Failed to download development pack")?; + request + .error_for_status_ref() + .context("Failed to download development pack")?; + let bytes = request + .bytes() + .context("Failed to read content from PHP website")?; + let mut content = Cursor::new(bytes); + let mut zip_content = + zip::read::ZipArchive::new(&mut content).context("Failed to unzip development pack")?; + let inner_name = zip_content + .file_names() + .next() + .and_then(|f| f.split('/').next()) + .context("Failed to get development pack name")?; + let devpack_path = out_dir.join(inner_name); + let _ = std::fs::remove_dir_all(&devpack_path); + zip_content + .extract(&out_dir) + .context("Failed to extract devpack to directory")?; + Ok(devpack_path) + } + + download(&zip_name, false) + .or_else(|_| download(&zip_name, true)) + .map(DevelPack) +} + +fn get_defines(is_debug: bool) -> Vec<(&'static str, &'static str)> { + vec![ + ("ZEND_WIN32", "1"), + ("PHP_WIN32", "1"), + ("WINDOWS", "1"), + ("WIN32", "1"), + ("ZEND_DEBUG", if is_debug { "1" } else { "0" }), + ] +} + +fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { + let mut build = cc::Build::new(); + for (var, val) in defines { + build.define(*var, *val); + } + build + .file("src/wrapper.c") + .includes(includes) + .try_compile("wrapper") + .context("Failed to compile ext-php-rs C interface")?; + Ok(()) +} + +fn generate_bindings( + defines: &[(&str, &str)], + includes: &[PathBuf], + php_lib_name: &str, +) -> Result<()> { + let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?; + let out_path = PathBuf::from(out_dir).join("bindings.rs"); + let mut bindgen = bindgen::Builder::default() + .header("src/wrapper.h") + .clang_args( + includes + .iter() + .map(|inc| format!("-I{}", inc.to_string_lossy())), + ) + .clang_args( + defines + .iter() + .map(|(var, val)| format!("-D{}={}", var, val)), + ) + // .clang_args(&["-DMSC_VER=1800", "-DZEND_FASTCALL=__vectorcall"]) + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .rustfmt_bindings(true) + .no_copy("_zval_struct") + .no_copy("_zend_string") + .no_copy("_zend_array") + .no_debug("_zend_function_entry") // On Windows when the handler uses vectorcall, Debug cannot be derived so we do it in code. + .layout_tests(env::var("EXT_PHP_RS_TEST").is_ok()) + .rust_target(RustTarget::Nightly); + + for binding in ALLOWED_BINDINGS.iter() { + bindgen = bindgen + .allowlist_function(binding) + .allowlist_type(binding) + .allowlist_var(binding); + } + + let bindings = bindgen + .generate() + .map_err(|_| anyhow!("Unable to generate bindings for PHP"))? + .to_string(); + + // For some reason some symbols don't link without a `#[link(name = "php8")]` + // attribute on each extern block. Bindgen doesn't give us the option to add + // this so we need to add it manually. + // + // Also, some functions need to be vectorcall. Primarily array functions. + let out_file = File::create(&out_path).context("Failed to open output bindings file")?; + let mut writer = BufWriter::new(out_file); + for line in bindings.lines() { + match &*line { + "extern \"C\" {" | "extern \"fastcall\" {" => { + writeln!(&mut writer, "#[link(name = \"{}\")]", php_lib_name)?; + } + _ => {} + } + writeln!(&mut writer, "{}", line)?; + } + + Ok(()) +} + +#[cfg(windows)] +fn main() -> Result<()> { + let manifest: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into(); + for path in [ + manifest.join("src").join("wrapper.h"), + manifest.join("src").join("wrapper.c"), + manifest.join("allowed_bindings.rs"), + ] { + println!("cargo:rerun-if-changed={}", path.to_string_lossy()); + } + + let php = find_php()?; + let info = PHPInfo::get(&php)?; + let arch = info.architecture()?; + let is_zts = info.thread_safety()?; + let version = info.version()?; + let debug = info.debug()?; + let devkit = download_devel_pack(version, is_zts, arch)?; + let includes = devkit.include_paths(); + let defines = get_defines(debug); + let php_lib = devkit.php_lib(); + let php_lib_name = php_lib + .file_stem() + .context("Failed to get PHP library name")? + .to_string_lossy(); + + build_wrapper(&defines, &includes)?; + generate_bindings(&defines, &includes, &*php_lib_name)?; + + println!("cargo:rustc-cfg=php81"); + if debug { + println!("cargo:rustc-cfg=php_debug"); + } + println!("cargo:rustc-link-lib=dylib={}", php_lib_name); + println!( + "cargo:rustc-link-search={}", + php_lib + .parent() + .context("Failed to get PHP library parent directory")? + .to_string_lossy() + ); + + Ok(()) +} + +#[cfg(not(windows))] fn main() { // rerun if wrapper header is changed println!("cargo:rerun-if-changed=src/wrapper.h"); @@ -131,6 +470,57 @@ fn main() { } } +struct PHPInfo(String); + +impl PHPInfo { + pub fn get(php: &Path) -> Result { + let cmd = Command::new(php) + .arg("-i") + .output() + .context("Failed to call `php -i`")?; + if !cmd.status.success() { + bail!("Failed to call `php -i` status code {}", cmd.status); + } + let stdout = String::from_utf8_lossy(&cmd.stdout); + Ok(Self(stdout.to_string())) + } + + pub fn architecture(&self) -> Result<&str> { + self.get_key("Architecture") + .context("Could not find architecture of PHP") + } + + pub fn thread_safety(&self) -> Result { + Ok(self + .get_key("Thread Safety") + .context("Could not find thread safety of PHP")? + == "enabled") + } + + pub fn debug(&self) -> Result { + Ok(self + .get_key("Debug Build") + .context("Could not find debug build of PHP")? + == "yes") + } + + pub fn version(&self) -> Result<&str> { + self.get_key("PHP Version") + .context("Failed to get PHP version") + } + + fn get_key(&self, key: &str) -> Option<&str> { + let split = format!("{} => ", key); + for line in self.0.lines() { + let components: Vec<_> = line.split(&split).collect(); + if components.len() > 1 { + return Some(components[1]); + } + } + None + } +} + struct Configure(String); impl Configure { diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 0549486556..e4784c71fc 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -12,7 +12,7 @@ edition = "2018" proc-macro = true [dependencies] -syn = { version = "1.0.68", features = ["full", "extra-traits"] } +syn = { version = "1.0.68", features = ["full", "extra-traits", "printing"] } darling = "0.12" ident_case = "1.0.1" quote = "1.0.9" diff --git a/crates/macros/src/fastcall.rs b/crates/macros/src/fastcall.rs new file mode 100644 index 0000000000..5d8e4a2bd6 --- /dev/null +++ b/crates/macros/src/fastcall.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; +use syn::{ItemFn, LitStr}; + +#[cfg(windows)] +const ABI: &str = "vectorcall"; +#[cfg(not(windows))] +const ABI: &str = "C"; + +pub fn parser(mut input: ItemFn) -> Result { + if let Some(abi) = &mut input.sig.abi { + abi.name = Some(LitStr::new(ABI, Span::call_site())); + } + Ok(input.to_token_stream()) +} diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index bdd7deae5c..4c9520c09f 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -68,18 +68,20 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi let func = quote! { #input - #[doc(hidden)] - pub extern "C" fn #internal_ident(ex: &mut ::ext_php_rs::zend::ExecuteData, retval: &mut ::ext_php_rs::types::Zval) { - use ::ext_php_rs::convert::IntoZval; + ::ext_php_rs::zend_fastcall! { + #[doc(hidden)] + pub extern fn #internal_ident(ex: &mut ::ext_php_rs::zend::ExecuteData, retval: &mut ::ext_php_rs::types::Zval) { + use ::ext_php_rs::convert::IntoZval; - #(#arg_definitions)* - #arg_parser + #(#arg_definitions)* + #arg_parser - let result = #ident(#(#arg_accessors, )*); + let result = #ident(#(#arg_accessors, )*); - if let Err(e) = result.set_zval(retval, false) { - let e: ::ext_php_rs::exception::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); + if let Err(e) = result.set_zval(retval, false) { + let e: ::ext_php_rs::exception::PhpException = e.into(); + e.throw().expect("Failed to throw exception"); + } } } }; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 2ac9814318..e744703641 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,6 +1,7 @@ mod class; mod constant; mod extern_; +mod fastcall; mod function; mod helpers; mod impl_; @@ -140,3 +141,14 @@ pub fn zval_convert_derive(input: TokenStream) -> TokenStream { } .into() } + +#[proc_macro] +pub fn zend_fastcall(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemFn); + + match fastcall::parser(input) { + Ok(parsed) => parsed, + Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), + } + .into() +} diff --git a/src/builders/class.rs b/src/builders/class.rs index 9fe57b5f55..f09a76a60e 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -13,6 +13,7 @@ use crate::{ flags::{ClassFlags, MethodFlags, PropertyFlags}, types::{ZendClassObject, ZendObject, ZendStr, Zval}, zend::{ClassEntry, ExecuteData, FunctionEntry}, + zend_fastcall, }; /// Builder for registering a class in PHP. @@ -167,36 +168,38 @@ impl ClassBuilder { obj.into_raw().get_mut_zend_obj() } - extern "C" fn constructor(ex: &mut ExecuteData, _: &mut Zval) { - let ConstructorMeta { constructor, .. } = match T::CONSTRUCTOR { - Some(c) => c, - None => { - PhpException::default("You cannot instantiate this class from PHP.".into()) - .throw() - .expect("Failed to throw exception when constructing class"); - return; - } - }; + zend_fastcall! { + extern fn constructor(ex: &mut ExecuteData, _: &mut Zval) { + let ConstructorMeta { constructor, .. } = match T::CONSTRUCTOR { + Some(c) => c, + None => { + PhpException::default("You cannot instantiate this class from PHP.".into()) + .throw() + .expect("Failed to throw exception when constructing class"); + return; + } + }; - let this = match constructor(ex) { - ConstructorResult::Ok(this) => this, - ConstructorResult::Exception(e) => { - e.throw() - .expect("Failed to throw exception while constructing class"); - return; - } - ConstructorResult::ArgError => return, - }; - let this_obj = match ex.get_object::() { - Some(obj) => obj, - None => { - PhpException::default("Failed to retrieve reference to `this` object.".into()) - .throw() - .expect("Failed to throw exception while constructing class"); - return; - } - }; - this_obj.initialize(this); + let this = match constructor(ex) { + ConstructorResult::Ok(this) => this, + ConstructorResult::Exception(e) => { + e.throw() + .expect("Failed to throw exception while constructing class"); + return; + } + ConstructorResult::ArgError => return, + }; + let this_obj = match ex.get_object::() { + Some(obj) => obj, + None => { + PhpException::default("Failed to retrieve reference to `this` object.".into()) + .throw() + .expect("Failed to throw exception while constructing class"); + return; + } + }; + this_obj.initialize(this); + } } debug_assert_eq!( diff --git a/src/builders/function.rs b/src/builders/function.rs index 64fc7a08cb..1fd3bbf2dc 100644 --- a/src/builders/function.rs +++ b/src/builders/function.rs @@ -8,10 +8,18 @@ use crate::{ use std::{ffi::CString, mem, ptr}; /// Function representation in Rust. +#[cfg(not(windows))] pub type FunctionHandler = extern "C" fn(execute_data: &mut ExecuteData, retval: &mut Zval); +#[cfg(windows)] +pub type FunctionHandler = + extern "vectorcall" fn(execute_data: &mut ExecuteData, retval: &mut Zval); /// Function representation in Rust using pointers. +#[cfg(not(windows))] type FunctionPointerHandler = extern "C" fn(execute_data: *mut ExecuteData, retval: *mut Zval); +#[cfg(windows)] +type FunctionPointerHandler = + extern "vectorcall" fn(execute_data: *mut ExecuteData, retval: *mut Zval); /// Builder for registering a function in PHP. #[derive(Debug)] diff --git a/src/builders/module.rs b/src/builders/module.rs index f4106db846..a8aeccd635 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -1,7 +1,8 @@ use crate::{ error::Result, - ffi::{ext_php_rs_php_build_id, USING_ZTS, ZEND_DEBUG, ZEND_MODULE_API_NO}, + ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO}, zend::{FunctionEntry, ModuleEntry}, + PHP_DEBUG, PHP_ZTS, }; use std::{ffi::CString, mem, ptr}; @@ -55,8 +56,8 @@ impl ModuleBuilder { module: ModuleEntry { size: mem::size_of::() as u16, zend_api: ZEND_MODULE_API_NO, - zend_debug: ZEND_DEBUG as u8, - zts: USING_ZTS as u8, + zend_debug: if PHP_DEBUG { 1 } else { 0 }, + zts: if PHP_ZTS { 1 } else { 0 }, ini_entry: ptr::null(), deps: ptr::null(), name: ptr::null(), diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 6673a7cbcd..43fb338fdf 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -366,6 +366,8 @@ fn indent(s: &str, depth: usize) -> String { #[cfg(test)] mod test { + use crate::describe::stub::NEW_LINE_SEPARATOR; + use super::{indent, split_namespace}; #[test] @@ -376,8 +378,12 @@ mod test { } #[test] + #[cfg(not(windows))] pub fn test_indent() { assert_eq!(indent("hello", 4), " hello"); - assert_eq!(indent("hello\nworld\n", 4), " hello\n world\n"); + assert_eq!( + indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4), + format!(" hello{nl} world{nl}", nl = NEW_LINE_SEPARATOR) + ); } } diff --git a/src/ffi.rs b/src/ffi.rs index 46f9e7431a..e750f472c4 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -2,4 +2,27 @@ #![allow(clippy::all)] #![allow(warnings)] + +use std::{ffi::c_void, os::raw::c_char}; + +pub const ZEND_MM_ALIGNMENT: u32 = 8; +pub const ZEND_MM_ALIGNMENT_MASK: i32 = -8; + +// These are not generated by Bindgen as everything in `bindings.rs` will have +// the `#[link(name = "php")]` attribute appended. This will cause the wrapper +// functions to fail to link. +#[link(name = "wrapper")] +extern "C" { + pub fn ext_php_rs_zend_string_init( + str_: *const c_char, + len: usize, + persistent: bool, + ) -> *mut zend_string; + pub fn ext_php_rs_zend_string_release(zs: *mut zend_string); + pub fn ext_php_rs_php_build_id() -> *const c_char; + pub fn ext_php_rs_zend_object_alloc(obj_size: usize, ce: *mut zend_class_entry) -> *mut c_void; + pub fn ext_php_rs_zend_object_release(obj: *mut zend_object); + pub fn ext_php_rs_executor_globals() -> *mut zend_executor_globals; +} + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/src/lib.rs b/src/lib.rs index 212cc45542..ac09e87c4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #![allow(non_camel_case_types)] #![allow(non_snake_case)] #![cfg_attr(docs, feature(doc_cfg))] +#![cfg_attr(windows, feature(abi_vectorcall))] pub mod alloc; pub mod args; @@ -54,6 +55,12 @@ pub mod prelude { /// `ext-php-rs` version. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Whether the extension is compiled for PHP debug mode. +pub const PHP_DEBUG: bool = cfg!(php_debug); + +/// Whether the extension is compiled for PHP thread-safe mode. +pub const PHP_ZTS: bool = cfg!(php_zts); + /// Attribute used to annotate constants to be exported to PHP. /// /// The declared constant is left intact (apart from the addition of the @@ -650,3 +657,35 @@ pub use ext_php_rs_derive::php_startup; /// [`Zval`]: crate::php::types::zval::Zval /// [`Zval::string`]: crate::php::types::zval::Zval::string pub use ext_php_rs_derive::ZvalConvert; + +/// Defines an `extern` function with the Zend fastcall convention based on +/// operating system. +/// +/// On Windows, Zend fastcall functions use the vector calling convention, while +/// on all other operating systems no fastcall convention is used (just the +/// regular C calling convention). +/// +/// This macro wraps a function and applies the correct calling convention. +/// +/// ## Examples +/// +/// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// use ext_php_rs::zend_fastcall; +/// +/// zend_fastcall! { +/// pub extern fn test_hello_world(a: i32, b: i32) -> i32 { +/// a + b +/// } +/// } +/// ``` +/// +/// On Windows, this function will have the signature `pub extern "vectorcall" +/// fn(i32, i32) -> i32`, while on macOS/Linux the function will have the +/// signature `pub extern "C" fn(i32, i32) -> i32`. +/// +/// ## Support +/// +/// The `vectorcall` ABI is currently only supported on Windows with nightly +/// Rust and the `abi_vectorcall` feature enabled. +pub use ext_php_rs_derive::zend_fastcall; diff --git a/src/wrapper.h b/src/wrapper.h index 2793fe213e..016dc4a0c5 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -1,3 +1,17 @@ +// PHP for Windows uses the `vectorcall` calling convention on some functions. This is guarded by +// the `ZEND_FASTCALL` macro, which is set to `__vectorcall` on Windows and nothing on other systems. +// +// However, `ZEND_FASTCALL` is only set when compiling with MSVC and the PHP source code checks for +// the __clang__ macro and will not define `__vectorcall` if it is set (even on Windows). This is a +// problem as Bindgen uses libclang to generate bindings. To work around this, we include the header +// file containing the `ZEND_FASTCALL` macro but not before undefining `__clang__` to pretend we are +// compiling on MSVC. +#if defined(_MSC_VER) && defined(__clang__) +# undef __clang__ +# include "zend_portability.h" +# define __clang__ +#endif + #include "php.h" #include "ext/standard/info.h" #include "zend_exceptions.h" diff --git a/src/zend/function.rs b/src/zend/function.rs index 11c7fcd0c6..ba889d775f 100644 --- a/src/zend/function.rs +++ b/src/zend/function.rs @@ -1,12 +1,23 @@ //! Builder for creating functions and methods in PHP. -use std::{os::raw::c_char, ptr}; +use std::{fmt::Debug, os::raw::c_char, ptr}; use crate::ffi::zend_function_entry; /// A Zend function entry. pub type FunctionEntry = zend_function_entry; +impl Debug for FunctionEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("_zend_function_entry") + .field("fname", &self.fname) + .field("arg_info", &self.arg_info) + .field("num_args", &self.num_args) + .field("flags", &self.flags) + .finish() + } +} + impl FunctionEntry { /// Returns an empty function entry, signifing the end of a function list. pub fn end() -> Self { From 3990ad34ffd70a488caa64097cbd6ffe2dfed1b6 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 19:30:07 +1300 Subject: [PATCH 02/34] Start work on cross-platform build script --- build.rs | 413 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 267 insertions(+), 146 deletions(-) diff --git a/build.rs b/build.rs index b336ad14a2..845d493439 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, convert::TryInto, env, fs::File, @@ -10,7 +11,6 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use bindgen::RustTarget; -use regex::Regex; const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20210902; @@ -18,6 +18,129 @@ const MAX_PHP_API_VER: u32 = 20210902; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); const DEFINES: &[&str] = &["ZEND_WIN32", "WINDOWS", "PHP_WIN32", "WIN32"]; +trait PHPProvider<'a>: Sized { + fn new(info: &'a PHPInfo) -> Result; + fn get_includes(&self) -> Result>; + fn get_defines(&self) -> Result>; + fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { + for line in bindings.lines() { + writeln!(writer, "{}", line)?; + } + Ok(()) + } + fn print_extra_link_args(&self) -> Result<()> { + Ok(()) + } +} + +struct Windows<'a> { + info: &'a PHPInfo, + devel: DevelPack, +} + +impl<'a> Windows<'a> { + fn get_php_lib_name(&self) -> Result { + Ok(self + .devel + .php_lib() + .file_stem() + .context("Failed to get PHP library name")? + .to_string_lossy() + .to_string()) + } +} + +impl<'a> PHPProvider<'a> for Windows<'a> { + fn new(info: &'a PHPInfo) -> Result { + let version = info.version()?; + let is_zts = info.thread_safety()?; + let arch = info.architecture()?; + let devel = DevelPack::new(version, is_zts, arch)?; + Ok(Self { info, devel }) + } + + fn get_includes(&self) -> Result> { + Ok(self.devel.include_paths()) + } + + fn get_defines(&self) -> Result> { + Ok(vec![ + ("ZEND_WIN32", "1"), + ("PHP_WIN32", "1"), + ("WINDOWS", "1"), + ("WIN32", "1"), + ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), + ]) + } + + fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { + // For some reason some symbols don't link without a `#[link(name = "php8")]` + // attribute on each extern block. Bindgen doesn't give us the option to add + // this so we need to add it manually. + let php_lib_name = self.get_php_lib_name()?; + for line in bindings.lines() { + match &*line { + "extern \"C\" {" | "extern \"fastcall\" {" => { + writeln!(writer, "#[link(name = \"{}\")]", php_lib_name)?; + } + _ => {} + } + writeln!(writer, "{}", line)?; + } + Ok(()) + } + + fn print_extra_link_args(&self) -> Result<()> { + let php_lib_name = self.get_php_lib_name()?; + let php_lib_search = self + .devel + .php_lib() + .parent() + .context("Failed to get PHP library parent folder")? + .to_string_lossy() + .to_string(); + println!("cargo:rustc-link-lib=dylib={}", php_lib_name); + println!("cargo:rustc-link-search={}", php_lib_search); + Ok(()) + } +} + +struct Unix {} + +impl Unix { + fn php_config(&self, arg: &str) -> Result { + let cmd = Command::new("php-config") + .arg(arg) + .output() + .context("Failed to run `php-config`")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + if !cmd.status.success() { + let stderr = String::from_utf8_lossy(&cmd.stderr); + bail!("Failed to run `php-config`: {} {}", stdout, stderr); + } + Ok(stdout.to_string()) + } +} + +impl<'a> PHPProvider<'a> for Unix { + fn new(_: &'a PHPInfo) -> Result { + Ok(Self {}) + } + + fn get_includes(&self) -> Result> { + Ok(self + .php_config("--includes")? + .split(' ') + .map(|s| s.trim_start_matches("-I")) + .map(PathBuf::from) + .collect()) + } + + fn get_defines(&self) -> Result> { + Ok(vec![]) + } +} + /// Attempt to find a `vswhere` binary in the common locations. pub fn find_vswhere() -> Option { let candidates = [format!( @@ -85,12 +208,8 @@ fn get_linkers(vswhere: &Path) -> Result> { Ok(linkers) } -#[cfg(windows)] -const WHICH: &str = "where"; -#[cfg(not(windows))] -const WHICH: &str = "which"; - fn find_executable(name: &str) -> Option { + const WHICH: &str = if cfg!(windows) { "where" } else { "which" }; let cmd = Command::new(WHICH).arg(name).output().ok()?; if cmd.status.success() { let stdout = String::from_utf8_lossy(&cmd.stdout); @@ -110,12 +229,60 @@ fn find_php() -> Result { find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") } -// https://windows.php.net/downloads/releases/php-devel-pack-8.1.3-nts-Win32-vs16-x64.zip -// https://windows.php.net/downloads/releases/php-devel-pack-8.1.3-Win32-vs16-x64.zip - struct DevelPack(PathBuf); impl DevelPack { + fn new(version: &str, is_zts: bool, arch: &str) -> Result { + let zip_name = format!( + "php-devel-pack-{}{}-Win32-{}-{}.zip", + version, + if is_zts { "" } else { "-nts" }, + "vs16", /* TODO(david): At the moment all PHPs supported by ext-php-rs use VS16 so + * this is constant. */ + arch + ); + + fn download(zip_name: &str, archive: bool) -> Result { + let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let url = format!( + "https://windows.php.net/downloads/releases{}/{}", + if archive { "/archives" } else { "" }, + zip_name + ); + let request = reqwest::blocking::ClientBuilder::new() + .user_agent(USER_AGENT) + .build() + .context("Failed to create HTTP client")? + .get(url) + .send() + .context("Failed to download development pack")?; + request + .error_for_status_ref() + .context("Failed to download development pack")?; + let bytes = request + .bytes() + .context("Failed to read content from PHP website")?; + let mut content = Cursor::new(bytes); + let mut zip_content = zip::read::ZipArchive::new(&mut content) + .context("Failed to unzip development pack")?; + let inner_name = zip_content + .file_names() + .next() + .and_then(|f| f.split('/').next()) + .context("Failed to get development pack name")?; + let devpack_path = out_dir.join(inner_name); + let _ = std::fs::remove_dir_all(&devpack_path); + zip_content + .extract(&out_dir) + .context("Failed to extract devpack to directory")?; + Ok(devpack_path) + } + + download(&zip_name, false) + .or_else(|_| download(&zip_name, true)) + .map(DevelPack) + } + pub fn includes(&self) -> PathBuf { self.0.join("include") } @@ -168,67 +335,6 @@ impl DevelPack { } } -fn download_devel_pack(version: &str, is_zts: bool, arch: &str) -> Result { - let zip_name = format!( - "php-devel-pack-{}{}-Win32-{}-{}.zip", - version, - if is_zts { "" } else { "-nts" }, - "vs16", /* TODO(david): At the moment all PHPs supported by ext-php-rs use VS16 so this - * is constant. */ - arch - ); - - fn download(zip_name: &str, archive: bool) -> Result { - let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); - let url = format!( - "https://windows.php.net/downloads/releases{}/{}", - if archive { "/archives" } else { "" }, - zip_name - ); - let request = reqwest::blocking::ClientBuilder::new() - .user_agent(USER_AGENT) - .build() - .context("Failed to create HTTP client")? - .get(url) - .send() - .context("Failed to download development pack")?; - request - .error_for_status_ref() - .context("Failed to download development pack")?; - let bytes = request - .bytes() - .context("Failed to read content from PHP website")?; - let mut content = Cursor::new(bytes); - let mut zip_content = - zip::read::ZipArchive::new(&mut content).context("Failed to unzip development pack")?; - let inner_name = zip_content - .file_names() - .next() - .and_then(|f| f.split('/').next()) - .context("Failed to get development pack name")?; - let devpack_path = out_dir.join(inner_name); - let _ = std::fs::remove_dir_all(&devpack_path); - zip_content - .extract(&out_dir) - .context("Failed to extract devpack to directory")?; - Ok(devpack_path) - } - - download(&zip_name, false) - .or_else(|_| download(&zip_name, true)) - .map(DevelPack) -} - -fn get_defines(is_debug: bool) -> Vec<(&'static str, &'static str)> { - vec![ - ("ZEND_WIN32", "1"), - ("PHP_WIN32", "1"), - ("WINDOWS", "1"), - ("WIN32", "1"), - ("ZEND_DEBUG", if is_debug { "1" } else { "0" }), - ] -} - fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { let mut build = cc::Build::new(); for (var, val) in defines { @@ -242,13 +348,7 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { Ok(()) } -fn generate_bindings( - defines: &[(&str, &str)], - includes: &[PathBuf], - php_lib_name: &str, -) -> Result<()> { - let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?; - let out_path = PathBuf::from(out_dir).join("bindings.rs"); +fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result { let mut bindgen = bindgen::Builder::default() .header("src/wrapper.h") .clang_args( @@ -283,27 +383,34 @@ fn generate_bindings( .map_err(|_| anyhow!("Unable to generate bindings for PHP"))? .to_string(); - // For some reason some symbols don't link without a `#[link(name = "php8")]` - // attribute on each extern block. Bindgen doesn't give us the option to add - // this so we need to add it manually. + Ok(bindings) +} + +fn check_php_version(info: &PHPInfo) -> Result<()> { + let version = info.zend_version()?; + + if !(MIN_PHP_API_VER..=MAX_PHP_API_VER).contains(&version) { + bail!("The current version of PHP is not supported. Current PHP API version: {}, requires a version between {} and {}", version, MIN_PHP_API_VER, MAX_PHP_API_VER); + } + + // Infra cfg flags - use these for things that change in the Zend API that don't + // rely on a feature and the crate user won't care about (e.g. struct field + // changes). Use a feature flag for an actual feature (e.g. enums being + // introduced in PHP 8.1). // - // Also, some functions need to be vectorcall. Primarily array functions. - let out_file = File::create(&out_path).context("Failed to open output bindings file")?; - let mut writer = BufWriter::new(out_file); - for line in bindings.lines() { - match &*line { - "extern \"C\" {" | "extern \"fastcall\" {" => { - writeln!(&mut writer, "#[link(name = \"{}\")]", php_lib_name)?; - } - _ => {} - } - writeln!(&mut writer, "{}", line)?; + // PHP 8.0 is the baseline - no feature flags will be introduced here. + // + // The PHP version cfg flags should also stack - if you compile on PHP 8.2 you + // should get both the `php81` and `php82` flags. + const PHP_81_API_VER: u32 = 20210902; + + if version >= PHP_81_API_VER { + println!("cargo:rustc-cfg=php81"); } Ok(()) } -#[cfg(windows)] fn main() -> Result<()> { let manifest: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into(); for path in [ @@ -316,38 +423,75 @@ fn main() -> Result<()> { let php = find_php()?; let info = PHPInfo::get(&php)?; - let arch = info.architecture()?; - let is_zts = info.thread_safety()?; - let version = info.version()?; - let debug = info.debug()?; - let devkit = download_devel_pack(version, is_zts, arch)?; - let includes = devkit.include_paths(); - let defines = get_defines(debug); - let php_lib = devkit.php_lib(); - let php_lib_name = php_lib - .file_stem() - .context("Failed to get PHP library name")? - .to_string_lossy(); + #[cfg(windows)] + let provider = Windows::new(&info)?; + #[cfg(not(windows))] + let provider = Unix::new(&info)?; + + let includes = provider.get_includes()?; + let defines = provider.get_defines()?; + + check_php_version(&info)?; build_wrapper(&defines, &includes)?; - generate_bindings(&defines, &includes, &*php_lib_name)?; + let bindings = generate_bindings(&defines, &includes)?; - println!("cargo:rustc-cfg=php81"); - if debug { + let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?; + let out_path = PathBuf::from(out_dir).join("bindings.rs"); + let out_file = + File::create(&out_path).context("Failed to open output bindings file for writing")?; + let mut out_writer = BufWriter::new(out_file); + provider.write_bindings(bindings, &mut out_writer)?; + + if info.debug()? { println!("cargo:rustc-cfg=php_debug"); } - println!("cargo:rustc-link-lib=dylib={}", php_lib_name); - println!( - "cargo:rustc-link-search={}", - php_lib - .parent() - .context("Failed to get PHP library parent directory")? - .to_string_lossy() - ); + if info.thread_safety()? { + println!("cargo:rustc-cfg=php_zts"); + } + provider.print_extra_link_args()?; Ok(()) } +// #[cfg(windows)] +// fn main() -> Result<()> { +// return Ok(()); + +// let php = find_php()?; +// let info = PHPInfo::get(&php)?; +// let arch = info.architecture()?; +// let is_zts = info.thread_safety()?; +// let version = info.version()?; +// let debug = info.debug()?; +// let devkit = download_devel_pack(version, is_zts, arch)?; +// let includes = devkit.include_paths(); +// let defines = get_defines(debug); +// let php_lib = devkit.php_lib(); +// let php_lib_name = php_lib +// .file_stem() +// .context("Failed to get PHP library name")? +// .to_string_lossy(); + +// build_wrapper(&defines, &includes)?; +// generate_bindings(&defines, &includes, &*php_lib_name)?; + +// println!("cargo:rustc-cfg=php81"); +// if debug { +// println!("cargo:rustc-cfg=php_debug"); +// } +// println!("cargo:rustc-link-lib=dylib={}", php_lib_name); +// println!( +// "cargo:rustc-link-search={}", +// php_lib +// .parent() +// .context("Failed to get PHP library parent directory")? +// .to_string_lossy() +// ); + +// Ok(()) +// } + #[cfg(not(windows))] fn main() { // rerun if wrapper header is changed @@ -459,7 +603,7 @@ fn main() { .write_to_file(out_path) .expect("Unable to write bindings file."); - let configure = Configure::get(); + let configure = PHPConfig::get(); if configure.has_zts() { println!("cargo:rustc-cfg=php_zts"); @@ -485,6 +629,7 @@ impl PHPInfo { Ok(Self(stdout.to_string())) } + #[cfg(windows)] pub fn architecture(&self) -> Result<&str> { self.get_key("Architecture") .context("Could not find architecture of PHP") @@ -509,6 +654,12 @@ impl PHPInfo { .context("Failed to get PHP version") } + pub fn zend_version(&self) -> Result { + self.get_key("PHP API") + .context("Failed to get Zend version") + .and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer")) + } + fn get_key(&self, key: &str) -> Option<&str> { let split = format!("{} => ", key); for line in self.0.lines() { @@ -521,36 +672,6 @@ impl PHPInfo { } } -struct Configure(String); - -impl Configure { - pub fn get() -> Self { - let cmd = Command::new("php-config") - .arg("--configure-options") - .output() - .expect("Unable to run `php-config --configure-options`. Please ensure it is visible in your PATH."); - - if !cmd.status.success() { - let stderr = String::from_utf8(cmd.stderr) - .unwrap_or_else(|_| String::from("Unable to read stderr")); - panic!("Error running `php -i`: {}", stderr); - } - - // check for the ZTS feature flag in configure - let stdout = - String::from_utf8(cmd.stdout).expect("Unable to read stdout from `php-config`."); - Self(stdout) - } - - pub fn has_zts(&self) -> bool { - self.0.contains("--enable-zts") - } - - pub fn debug(&self) -> bool { - self.0.contains("--enable-debug") - } -} - // Mock macro for the `allowed_bindings.rs` script. macro_rules! bind { ($($s: ident),*) => { From 91cee21a86b24e5f9625734aecb57631b7815398 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 19:33:27 +1300 Subject: [PATCH 03/34] Fix compilation on macOS --- build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.rs b/build.rs index 845d493439..287fce3ab1 100644 --- a/build.rs +++ b/build.rs @@ -492,7 +492,7 @@ fn main() -> Result<()> { // Ok(()) // } -#[cfg(not(windows))] +#[cfg(never)] fn main() { // rerun if wrapper header is changed println!("cargo:rerun-if-changed=src/wrapper.h"); @@ -629,7 +629,7 @@ impl PHPInfo { Ok(Self(stdout.to_string())) } - #[cfg(windows)] + /// N.B. does not work on non-Windows. pub fn architecture(&self) -> Result<&str> { self.get_key("Architecture") .context("Could not find architecture of PHP") From 9c22179d59aa8febfa6ab058708d1219a0bd516f Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 21:16:03 +1300 Subject: [PATCH 04/34] Updated README, tidied up build script --- README.md | 20 +++- build.rs | 279 ++++++++++++------------------------------------------ 2 files changed, 75 insertions(+), 224 deletions(-) diff --git a/README.md b/README.md index 369e2b9062..bfd7a253a8 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ best resource at the moment. This can be viewed at [docs.rs]. - PHP 8.0 or later - No support is planned for lower versions. -- Linux or Darwin-based OS +- Linux, macOS or Windows-based operating system - Rust - no idea which version - Clang 3.9 or greater @@ -115,6 +115,20 @@ See the following links for the dependency crate requirements: - [`cc`](https://github.com/alexcrichton/cc-rs#compile-time-requirements) - [`bindgen`](https://rust-lang.github.io/rust-bindgen/requirements.html) +### Windows Support + +Windows has some extra requirements: + +- Extensions can only be compiled for PHP installations sourced from + [windows.php.net]. +- Only PHP installations compiled with MSVC are supported (no support for + `x86_64-pc-windows-gnu`). +- Microsoft Visual C++ must be installed. The compiler version must match or be + older than the compiler that was used to compile your PHP installation (at the + time of writing Visual Studio 2019 is supported). +- Extensions can only be compiled with nightly Rust, and the `abi_vectorcall` + feature must be enabled in your crates's Cargo Features + ## Cargo Features All features are disabled by default. @@ -134,8 +148,8 @@ Check out one of the example projects: - [anonaddy-sequoia](https://gitlab.com/willbrowning/anonaddy-sequoia) - Sequoia encryption PHP extension. -- [opus-php](https://github.com/davidcole1340/opus-php) - - Audio encoder for the Opus codec in PHP. +- [opus-php](https://github.com/davidcole1340/opus-php) - Audio encoder for the + Opus codec in PHP. ## Contributions diff --git a/build.rs b/build.rs index 287fce3ab1..d96d5aa9a7 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,4 @@ use std::{ - borrow::Cow, - convert::TryInto, env, fs::File, io::{BufRead, BufReader, BufWriter, Cursor, Write}, @@ -16,7 +14,6 @@ const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20210902; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); -const DEFINES: &[&str] = &["ZEND_WIN32", "WINDOWS", "PHP_WIN32", "WIN32"]; trait PHPProvider<'a>: Sized { fn new(info: &'a PHPInfo) -> Result; @@ -335,6 +332,64 @@ impl DevelPack { } } +struct PHPInfo(String); + +impl PHPInfo { + pub fn get(php: &Path) -> Result { + let cmd = Command::new(php) + .arg("-i") + .output() + .context("Failed to call `php -i`")?; + if !cmd.status.success() { + bail!("Failed to call `php -i` status code {}", cmd.status); + } + let stdout = String::from_utf8_lossy(&cmd.stdout); + Ok(Self(stdout.to_string())) + } + + /// N.B. does not work on non-Windows. + pub fn architecture(&self) -> Result<&str> { + self.get_key("Architecture") + .context("Could not find architecture of PHP") + } + + pub fn thread_safety(&self) -> Result { + Ok(self + .get_key("Thread Safety") + .context("Could not find thread safety of PHP")? + == "enabled") + } + + pub fn debug(&self) -> Result { + Ok(self + .get_key("Debug Build") + .context("Could not find debug build of PHP")? + == "yes") + } + + pub fn version(&self) -> Result<&str> { + self.get_key("PHP Version") + .context("Failed to get PHP version") + } + + pub fn zend_version(&self) -> Result { + self.get_key("PHP API") + .context("Failed to get Zend version") + .and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer")) + } + + fn get_key(&self, key: &str) -> Option<&str> { + let split = format!("{} => ", key); + for line in self.0.lines() { + let components: Vec<_> = line.split(&split).collect(); + if components.len() > 1 { + return Some(components[1]); + } + } + None + } +} + fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { let mut build = cc::Build::new(); for (var, val) in defines { @@ -454,224 +509,6 @@ fn main() -> Result<()> { Ok(()) } -// #[cfg(windows)] -// fn main() -> Result<()> { -// return Ok(()); - -// let php = find_php()?; -// let info = PHPInfo::get(&php)?; -// let arch = info.architecture()?; -// let is_zts = info.thread_safety()?; -// let version = info.version()?; -// let debug = info.debug()?; -// let devkit = download_devel_pack(version, is_zts, arch)?; -// let includes = devkit.include_paths(); -// let defines = get_defines(debug); -// let php_lib = devkit.php_lib(); -// let php_lib_name = php_lib -// .file_stem() -// .context("Failed to get PHP library name")? -// .to_string_lossy(); - -// build_wrapper(&defines, &includes)?; -// generate_bindings(&defines, &includes, &*php_lib_name)?; - -// println!("cargo:rustc-cfg=php81"); -// if debug { -// println!("cargo:rustc-cfg=php_debug"); -// } -// println!("cargo:rustc-link-lib=dylib={}", php_lib_name); -// println!( -// "cargo:rustc-link-search={}", -// php_lib -// .parent() -// .context("Failed to get PHP library parent directory")? -// .to_string_lossy() -// ); - -// Ok(()) -// } - -#[cfg(never)] -fn main() { - // rerun if wrapper header is changed - println!("cargo:rerun-if-changed=src/wrapper.h"); - println!("cargo:rerun-if-changed=src/wrapper.c"); - println!("cargo:rerun-if-changed=allowed_bindings.rs"); - - let out_dir = env::var_os("OUT_DIR").expect("Failed to get OUT_DIR"); - let out_path = PathBuf::from(out_dir).join("bindings.rs"); - - // check for docs.rs and use stub bindings if required - if env::var("DOCS_RS").is_ok() { - println!("cargo:warning=docs.rs detected - using stub bindings"); - println!("cargo:rustc-cfg=php_debug"); - println!("cargo:rustc-cfg=php81"); - - std::fs::copy("docsrs_bindings.rs", out_path) - .expect("Unable to copy docs.rs stub bindings to output directory."); - return; - } - - // use php-config to fetch includes - let includes_cmd = Command::new("php-config") - .arg("--includes") - .output() - .expect("Unable to run `php-config`. Please ensure it is visible in your PATH."); - - if !includes_cmd.status.success() { - let stderr = String::from_utf8(includes_cmd.stderr) - .unwrap_or_else(|_| String::from("Unable to read stderr")); - panic!("Error running `php-config`: {}", stderr); - } - - // Ensure the PHP API version is supported. - // We could easily use grep and sed here but eventually we want to support - // Windows, so it's easier to just use regex. - let php_i_cmd = Command::new("php") - .arg("-i") - .output() - .expect("Unable to run `php -i`. Please ensure it is visible in your PATH."); - - if !php_i_cmd.status.success() { - let stderr = str::from_utf8(&includes_cmd.stderr).unwrap_or("Unable to read stderr"); - panic!("Error running `php -i`: {}", stderr); - } - - let api_ver = Regex::new(r"PHP API => ([0-9]+)") - .unwrap() - .captures_iter( - str::from_utf8(&php_i_cmd.stdout).expect("Unable to parse `php -i` stdout as UTF-8"), - ) - .next() - .and_then(|ver| ver.get(1)) - .and_then(|ver| ver.as_str().parse::().ok()) - .expect("Unable to retrieve PHP API version from `php -i`."); - - if !(MIN_PHP_API_VER..=MAX_PHP_API_VER).contains(&api_ver) { - panic!("The current version of PHP is not supported. Current PHP API version: {}, requires a version between {} and {}", api_ver, MIN_PHP_API_VER, MAX_PHP_API_VER); - } - - // Infra cfg flags - use these for things that change in the Zend API that don't - // rely on a feature and the crate user won't care about (e.g. struct field - // changes). Use a feature flag for an actual feature (e.g. enums being - // introduced in PHP 8.1). - // - // PHP 8.0 is the baseline - no feature flags will be introduced here. - // - // The PHP version cfg flags should also stack - if you compile on PHP 8.2 you - // should get both the `php81` and `php82` flags. - const PHP_81_API_VER: u32 = 20210902; - - if api_ver >= PHP_81_API_VER { - println!("cargo:rustc-cfg=php81"); - } - - let includes = - String::from_utf8(includes_cmd.stdout).expect("unable to parse `php-config` stdout"); - - // Build `wrapper.c` and link to Rust. - cc::Build::new() - .file("src/wrapper.c") - .includes( - str::replace(includes.as_ref(), "-I", "") - .split(' ') - .map(Path::new), - ) - .compile("wrapper"); - - let mut bindgen = bindgen::Builder::default() - .header("src/wrapper.h") - .clang_args(includes.split(' ')) - .parse_callbacks(Box::new(bindgen::CargoCallbacks)) - .rustfmt_bindings(true) - .no_copy("_zval_struct") - .no_copy("_zend_string") - .no_copy("_zend_array") - .layout_tests(env::var("EXT_PHP_RS_TEST").is_ok()); - - for binding in ALLOWED_BINDINGS.iter() { - bindgen = bindgen - .allowlist_function(binding) - .allowlist_type(binding) - .allowlist_var(binding); - } - - bindgen - .generate() - .expect("Unable to generate bindings for PHP") - .write_to_file(out_path) - .expect("Unable to write bindings file."); - - let configure = PHPConfig::get(); - - if configure.has_zts() { - println!("cargo:rustc-cfg=php_zts"); - } - - if configure.debug() { - println!("cargo:rustc-cfg=php_debug"); - } -} - -struct PHPInfo(String); - -impl PHPInfo { - pub fn get(php: &Path) -> Result { - let cmd = Command::new(php) - .arg("-i") - .output() - .context("Failed to call `php -i`")?; - if !cmd.status.success() { - bail!("Failed to call `php -i` status code {}", cmd.status); - } - let stdout = String::from_utf8_lossy(&cmd.stdout); - Ok(Self(stdout.to_string())) - } - - /// N.B. does not work on non-Windows. - pub fn architecture(&self) -> Result<&str> { - self.get_key("Architecture") - .context("Could not find architecture of PHP") - } - - pub fn thread_safety(&self) -> Result { - Ok(self - .get_key("Thread Safety") - .context("Could not find thread safety of PHP")? - == "enabled") - } - - pub fn debug(&self) -> Result { - Ok(self - .get_key("Debug Build") - .context("Could not find debug build of PHP")? - == "yes") - } - - pub fn version(&self) -> Result<&str> { - self.get_key("PHP Version") - .context("Failed to get PHP version") - } - - pub fn zend_version(&self) -> Result { - self.get_key("PHP API") - .context("Failed to get Zend version") - .and_then(|s| u32::from_str(s).context("Failed to convert Zend version to integer")) - } - - fn get_key(&self, key: &str) -> Option<&str> { - let split = format!("{} => ", key); - for line in self.0.lines() { - let components: Vec<_> = line.split(&split).collect(); - if components.len() > 1 { - return Some(components[1]); - } - } - None - } -} - // Mock macro for the `allowed_bindings.rs` script. macro_rules! bind { ($($s: ident),*) => { From 6b5bc6c4f7cbb75cd7318b5411e26201510dd66a Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 22:31:16 +1300 Subject: [PATCH 05/34] Check linker version before starting compilation It doesn't seem like it's possible to change the linker from within the build script, however, we can retrieve the linker in use and give the user a suggestion if the linker will not work. --- build.rs | 304 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 277 insertions(+), 27 deletions(-) diff --git a/build.rs b/build.rs index d96d5aa9a7..36b348e6bb 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,7 @@ use std::{ + convert::{TryFrom, TryInto}, env, + fmt::Display, fs::File, io::{BufRead, BufReader, BufWriter, Cursor, Write}, path::{Path, PathBuf}, @@ -16,15 +18,24 @@ const MAX_PHP_API_VER: u32 = 20210902; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); trait PHPProvider<'a>: Sized { + /// Create a new PHP provider. fn new(info: &'a PHPInfo) -> Result; + + /// Retrieve a list of absolute include paths. fn get_includes(&self) -> Result>; + + /// Retrieve a list of macro definitions to pass to the compiler. fn get_defines(&self) -> Result>; + + /// Writes the bindings to a file. fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { for line in bindings.lines() { writeln!(writer, "{}", line)?; } Ok(()) } + + /// Prints any extra link arguments. fn print_extra_link_args(&self) -> Result<()> { Ok(()) } @@ -36,6 +47,7 @@ struct Windows<'a> { } impl<'a> Windows<'a> { + /// Retrieves the PHP library name (filename without extension). fn get_php_lib_name(&self) -> Result { Ok(self .devel @@ -45,6 +57,55 @@ impl<'a> Windows<'a> { .to_string_lossy() .to_string()) } + + /// Checks whether the rustc linker is compatible with the linker used in + /// the PHP development kit which was downloaded. + /// + /// If not compatible, attempts to find a compatible linker and notifies the + /// user if one is found. + fn check_linker_compatibility(&self) -> Result<()> { + let rustc_linker = get_rustc_linker()?; + let rustc_linker_version = LinkerVersion::from_linker_path(&rustc_linker)?; + let php_linker_version = self.devel.linker_version()?; + let compatible = php_linker_version.is_forwards_compatible(&rustc_linker_version); + if compatible { + Ok(()) + } else { + let mut error = format!("Incompatible linker versions. PHP was linked with MSVC {}, while Rust is using MSVC {}.", php_linker_version, rustc_linker_version); + if let Some(potential_linker) = find_potential_linker(&php_linker_version)? { + let path = potential_linker.path.to_string_lossy(); + let target_triple = std::env::var("TARGET").expect("Failed to get target triple"); + error.push_str(&format!( + " +A potentially compatible linker was found (MSVC version {}) located at `{}`. + +Use this linker by creating a `.cargo/config.toml` file in your extension's +manifest directory with the following content: +``` +[target.{}] +linker = \"{}\" +``` +", + potential_linker.version, + path, + target_triple, + path.escape_default() + )) + } else { + error.push_str(&format!( + " +You need a linker with a version earlier or equal to MSVC {}. +Download MSVC from https://visualstudio.microsoft.com/vs/features/cplusplus/. +Make sure to select C++ Development Tools in the installer. +You can correspond MSVC version with Visual Studio version +here: https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +", + php_linker_version + )); + } + bail!(error); + } + } } impl<'a> PHPProvider<'a> for Windows<'a> { @@ -105,6 +166,7 @@ impl<'a> PHPProvider<'a> for Windows<'a> { struct Unix {} impl Unix { + /// Runs `php-config` with one argument, returning the stdout. fn php_config(&self, arg: &str) -> Result { let cmd = Command::new("php-config") .arg(arg) @@ -159,32 +221,57 @@ struct LinkerVersion { minor: u32, } -/// Retrieve the version of a MSVC linker. -fn get_linker_version(linker: &Path) -> Result { - let cmd = Command::new(linker) - .output() - .context("Failed to call linker")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - let linker = stdout - .split("\r\n") - .next() - .context("Linker output was empty")?; - let version = linker - .split(' ') - .last() - .context("Linker version string was empty")?; - let components = version - .split('.') - .take(2) - .map(|v| v.parse()) - .collect::, _>>() - .context("Linker version component was empty")?; - Ok(LinkerVersion { - major: components[0], - minor: components[1], - }) +impl LinkerVersion { + /// Retrieve the version of a MSVC linker. + fn from_linker_path(linker: &Path) -> Result { + let cmd = Command::new(linker) + .output() + .context("Failed to call linker")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + let linker = stdout + .split("\r\n") + .next() + .context("Linker output was empty")?; + let version = linker + .split(' ') + .last() + .context("Linker version string was empty")?; + let components = version + .split('.') + .take(2) + .map(|v| v.parse()) + .collect::, _>>() + .context("Linker version component was empty")?; + Ok(Self { + major: components[0], + minor: components[1], + }) + } + + /// Checks if this linker is forwards-compatible with another linker. + fn is_forwards_compatible(&self, other: &LinkerVersion) -> bool { + // To be forwards compatible, the other linker must have the same major + // version and the minor version must greater or equal to this linker. + self.major == other.major && self.minor >= other.minor + } +} + +/// Returns the path to rustc's linker. +fn get_rustc_linker() -> Result { + // `RUSTC_LINKER` is set if the linker has been overriden anywhere. + if let Ok(link) = std::env::var("RUSTC_LINKER") { + return Ok(link.into()); + } + + let link = cc::windows_registry::find_tool( + &std::env::var("TARGET").context("`TARGET` environment variable not set")?, + "link.exe", + ) + .context("Failed to retrieve linker tool")?; + Ok(link.path().to_owned()) } +/// Uses vswhere to find all the linkers installed on a system. fn get_linkers(vswhere: &Path) -> Result> { let cmd = Command::new(vswhere) .arg("-all") @@ -205,6 +292,148 @@ fn get_linkers(vswhere: &Path) -> Result> { Ok(linkers) } +/// Attempts to find a potential linker that is compatible with PHP. +/// +/// It must fit the following criteria: +/// +/// 1. It must be forwards compatible with the PHP linker. +/// 2. The linker target architecture must match the target triple architecture. +/// 3. Optionally, the linker host architecture should match the host triple +/// architecture. On x86_64 systems, if a x64 host compiler is not found it will +/// fallback to x86. +/// +/// Returns an error if there is an error. Returns None if no linker could be +/// found. +fn find_potential_linker(php_linker: &LinkerVersion) -> Result> { + let vswhere = find_vswhere().context("Could not find `vswhere`")?; + let linkers = get_linkers(&vswhere)?; + let host_arch = msvc_host_arch()?; + let target_arch = msvc_target_arch()?; + let mut prelim_linker = None; + + for linker in &linkers { + let linker = Linker::from_linker_path(linker)?; + if php_linker.is_forwards_compatible(&linker.version) && linker.target_arch == target_arch { + if linker.host_arch == host_arch { + return Ok(Some(linker)); + } else if prelim_linker.is_none() + && host_arch == Arch::X64 + && linker.host_arch == Arch::X86 + { + // This linker will work - the host architectures do not match but that's OK for + // x86_64. + prelim_linker.replace(linker); + } + } + } + Ok(prelim_linker) +} + +#[derive(Debug)] +struct Linker { + host_arch: Arch, + target_arch: Arch, + version: LinkerVersion, + path: PathBuf, +} + +#[derive(Debug, PartialEq, Eq)] +enum Arch { + X86, + X64, + AArch64, +} + +impl TryFrom<&str> for Arch { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + "x86" => Self::X86, + "x64" => Self::X64, + "arm64" => Self::AArch64, + a => bail!("Unknown architecture {}", a), + }) + } +} + +impl Display for Arch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Arch::X86 => "x86", + Arch::X64 => "x64", + Arch::AArch64 => "arm64", + } + ) + } +} + +impl Linker { + /// Retrieves information about the linker based on its path. + fn from_linker_path(linker: &Path) -> Result { + let version = LinkerVersion::from_linker_path(linker)?; + let target_arch_folder = linker + .parent() + .context("Could not get linker parent folder")?; + let target_arch = Arch::try_from( + &*target_arch_folder + .file_stem() + .context("Could not get linker target architecture")? + .to_string_lossy() + .to_lowercase(), + )?; + let host_arch = Arch::try_from( + &*target_arch_folder + .parent() + .context("Could not get linker parent folder")? + .file_stem() + .context("Could not get linker host architecture")? + .to_string_lossy() + .replace("Host", "") + .to_lowercase(), + )?; + Ok(Linker { + host_arch, + target_arch, + version, + path: linker.to_owned(), + }) + } +} + +/// Returns the architecture of a triple. +fn triple_arch(triple: &str) -> Result { + let arch = triple.split('-').next().context("Triple was invalid")?; + Ok(match arch { + "x86_64" => Arch::X64, + "i686" => Arch::X86, + "aarch64" => Arch::AArch64, + a => bail!("Unknown architecture {}", a), + }) +} + +/// Returns the architecture of the target the compilation is running on. +/// +/// If running on an AArch64 host, X86 is returned as there are no MSVC tools +/// for AArch64 hosts. +fn msvc_host_arch() -> Result { + let host_triple = std::env::var("HOST").context("Failed to get host triple")?; + Ok(match triple_arch(&host_triple)? { + Arch::AArch64 => Arch::X86, // AArch64 does not have host tools + a => a, + }) +} + +/// Returns the architecture of the target being compiled for. +fn msvc_target_arch() -> Result { + let host_triple = std::env::var("TARGET").context("Failed to get host triple")?; + triple_arch(&host_triple) +} + +/// Finds the location of an executable `name`. fn find_executable(name: &str) -> Option { const WHICH: &str = if cfg!(windows) { "where" } else { "which" }; let cmd = Command::new(WHICH).arg(name).output().ok()?; @@ -216,6 +445,7 @@ fn find_executable(name: &str) -> Option { } } +/// Finds the location of the PHP executable. fn find_php() -> Result { // If PHP path is given via env, it takes priority. let env = std::env::var("PHP"); @@ -229,7 +459,9 @@ fn find_php() -> Result { struct DevelPack(PathBuf); impl DevelPack { - fn new(version: &str, is_zts: bool, arch: &str) -> Result { + /// Downloads a new PHP development pack, unzips it in the build script + /// temporary directory. + fn new(version: &str, is_zts: bool, arch: Arch) -> Result { let zip_name = format!( "php-devel-pack-{}{}-Win32-{}-{}.zip", version, @@ -280,14 +512,17 @@ impl DevelPack { .map(DevelPack) } + /// Returns the path to the include folder. pub fn includes(&self) -> PathBuf { self.0.join("include") } + /// Returns the path of the PHP library containing symbols for linking. pub fn php_lib(&self) -> PathBuf { self.0.join("lib").join("php8.lib") } + /// Returns a list of include paths to pass to the compiler. pub fn include_paths(&self) -> Vec { let includes = self.includes(); ["", "main", "Zend", "TSRM", "ext"] @@ -296,6 +531,7 @@ impl DevelPack { .collect() } + /// Retrieves the version of MSVC PHP was linked with. pub fn linker_version(&self) -> Result { let config_path = self.includes().join("main").join("config.w32.h"); let config = File::open(&config_path).context("Failed to open PHP config header")?; @@ -332,6 +568,12 @@ impl DevelPack { } } +impl Display for LinkerVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + struct PHPInfo(String); impl PHPInfo { @@ -348,9 +590,10 @@ impl PHPInfo { } /// N.B. does not work on non-Windows. - pub fn architecture(&self) -> Result<&str> { + pub fn architecture(&self) -> Result { self.get_key("Architecture") - .context("Could not find architecture of PHP") + .context("Could not find architecture of PHP")? + .try_into() } pub fn thread_safety(&self) -> Result { @@ -390,6 +633,7 @@ impl PHPInfo { } } +/// Builds the wrapper library. fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { let mut build = cc::Build::new(); for (var, val) in defines { @@ -403,6 +647,7 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { Ok(()) } +/// Generates bindings to the Zend API. fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result { let mut bindgen = bindgen::Builder::default() .header("src/wrapper.h") @@ -441,6 +686,8 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Result<()> { let version = info.zend_version()?; @@ -484,6 +731,9 @@ fn main() -> Result<()> { #[cfg(not(windows))] let provider = Unix::new(&info)?; + #[cfg(windows)] + provider.check_linker_compatibility()?; + let includes = provider.get_includes()?; let defines = provider.get_defines()?; From 69372c7f5a2b391de86e887edf44c420b5a2efaf Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 22:35:24 +1300 Subject: [PATCH 06/34] Switch to using Github repository for bindgen --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0f888a4b3f..3cf66d8f8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,8 +22,7 @@ ext-php-rs-derive = { version = "=0.7.3", path = "./crates/macros" } [build-dependencies] anyhow = "1" # bindgen = { version = "0.59" } -# bindgen = { git = "https://github.com/davidcole1340/rust-bindgen", branch = "abi_vectorcall" } -bindgen = { path = "../rust-bindgen" } +bindgen = { git = "https://github.com/davidcole1340/rust-bindgen", branch = "abi_vectorcall" } regex = "1" cc = "1.0" reqwest = { version = "*", features = ["blocking"] } From c91d7ae417c27c3ce8af4a18bab42069cd74709c Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 23:07:54 +1300 Subject: [PATCH 07/34] Split Windows and Unix implementations into two files --- build.rs | 533 +---------------------------------------------- unix_build.rs | 42 ++++ windows_build.rs | 488 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 541 insertions(+), 522 deletions(-) create mode 100644 unix_build.rs create mode 100644 windows_build.rs diff --git a/build.rs b/build.rs index 36b348e6bb..e497366cbd 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,11 @@ +#[cfg_attr(windows, path = "windows_build.rs")] +#[cfg_attr(not(windows), path = "unix_build.rs")] +mod impl_; + use std::{ - convert::{TryFrom, TryInto}, env, - fmt::Display, fs::File, - io::{BufRead, BufReader, BufWriter, Cursor, Write}, + io::{BufWriter, Write}, path::{Path, PathBuf}, process::Command, str::FromStr, @@ -11,13 +13,12 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use bindgen::RustTarget; +use impl_::Provider; const MIN_PHP_API_VER: u32 = 20200930; const MAX_PHP_API_VER: u32 = 20210902; -const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); - -trait PHPProvider<'a>: Sized { +pub trait PHPProvider<'a>: Sized { /// Create a new PHP provider. fn new(info: &'a PHPInfo) -> Result; @@ -41,398 +42,6 @@ trait PHPProvider<'a>: Sized { } } -struct Windows<'a> { - info: &'a PHPInfo, - devel: DevelPack, -} - -impl<'a> Windows<'a> { - /// Retrieves the PHP library name (filename without extension). - fn get_php_lib_name(&self) -> Result { - Ok(self - .devel - .php_lib() - .file_stem() - .context("Failed to get PHP library name")? - .to_string_lossy() - .to_string()) - } - - /// Checks whether the rustc linker is compatible with the linker used in - /// the PHP development kit which was downloaded. - /// - /// If not compatible, attempts to find a compatible linker and notifies the - /// user if one is found. - fn check_linker_compatibility(&self) -> Result<()> { - let rustc_linker = get_rustc_linker()?; - let rustc_linker_version = LinkerVersion::from_linker_path(&rustc_linker)?; - let php_linker_version = self.devel.linker_version()?; - let compatible = php_linker_version.is_forwards_compatible(&rustc_linker_version); - if compatible { - Ok(()) - } else { - let mut error = format!("Incompatible linker versions. PHP was linked with MSVC {}, while Rust is using MSVC {}.", php_linker_version, rustc_linker_version); - if let Some(potential_linker) = find_potential_linker(&php_linker_version)? { - let path = potential_linker.path.to_string_lossy(); - let target_triple = std::env::var("TARGET").expect("Failed to get target triple"); - error.push_str(&format!( - " -A potentially compatible linker was found (MSVC version {}) located at `{}`. - -Use this linker by creating a `.cargo/config.toml` file in your extension's -manifest directory with the following content: -``` -[target.{}] -linker = \"{}\" -``` -", - potential_linker.version, - path, - target_triple, - path.escape_default() - )) - } else { - error.push_str(&format!( - " -You need a linker with a version earlier or equal to MSVC {}. -Download MSVC from https://visualstudio.microsoft.com/vs/features/cplusplus/. -Make sure to select C++ Development Tools in the installer. -You can correspond MSVC version with Visual Studio version -here: https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering -", - php_linker_version - )); - } - bail!(error); - } - } -} - -impl<'a> PHPProvider<'a> for Windows<'a> { - fn new(info: &'a PHPInfo) -> Result { - let version = info.version()?; - let is_zts = info.thread_safety()?; - let arch = info.architecture()?; - let devel = DevelPack::new(version, is_zts, arch)?; - Ok(Self { info, devel }) - } - - fn get_includes(&self) -> Result> { - Ok(self.devel.include_paths()) - } - - fn get_defines(&self) -> Result> { - Ok(vec![ - ("ZEND_WIN32", "1"), - ("PHP_WIN32", "1"), - ("WINDOWS", "1"), - ("WIN32", "1"), - ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), - ]) - } - - fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { - // For some reason some symbols don't link without a `#[link(name = "php8")]` - // attribute on each extern block. Bindgen doesn't give us the option to add - // this so we need to add it manually. - let php_lib_name = self.get_php_lib_name()?; - for line in bindings.lines() { - match &*line { - "extern \"C\" {" | "extern \"fastcall\" {" => { - writeln!(writer, "#[link(name = \"{}\")]", php_lib_name)?; - } - _ => {} - } - writeln!(writer, "{}", line)?; - } - Ok(()) - } - - fn print_extra_link_args(&self) -> Result<()> { - let php_lib_name = self.get_php_lib_name()?; - let php_lib_search = self - .devel - .php_lib() - .parent() - .context("Failed to get PHP library parent folder")? - .to_string_lossy() - .to_string(); - println!("cargo:rustc-link-lib=dylib={}", php_lib_name); - println!("cargo:rustc-link-search={}", php_lib_search); - Ok(()) - } -} - -struct Unix {} - -impl Unix { - /// Runs `php-config` with one argument, returning the stdout. - fn php_config(&self, arg: &str) -> Result { - let cmd = Command::new("php-config") - .arg(arg) - .output() - .context("Failed to run `php-config`")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - if !cmd.status.success() { - let stderr = String::from_utf8_lossy(&cmd.stderr); - bail!("Failed to run `php-config`: {} {}", stdout, stderr); - } - Ok(stdout.to_string()) - } -} - -impl<'a> PHPProvider<'a> for Unix { - fn new(_: &'a PHPInfo) -> Result { - Ok(Self {}) - } - - fn get_includes(&self) -> Result> { - Ok(self - .php_config("--includes")? - .split(' ') - .map(|s| s.trim_start_matches("-I")) - .map(PathBuf::from) - .collect()) - } - - fn get_defines(&self) -> Result> { - Ok(vec![]) - } -} - -/// Attempt to find a `vswhere` binary in the common locations. -pub fn find_vswhere() -> Option { - let candidates = [format!( - r"{}\Microsoft Visual Studio\Installer\vswhere.exe", - std::env::var("ProgramFiles(x86)").ok()?, - )]; - for candidate in candidates { - let candidate = PathBuf::from(candidate); - if candidate.exists() { - return Some(candidate); - } - } - None -} - -#[derive(Debug)] -struct LinkerVersion { - major: u32, - minor: u32, -} - -impl LinkerVersion { - /// Retrieve the version of a MSVC linker. - fn from_linker_path(linker: &Path) -> Result { - let cmd = Command::new(linker) - .output() - .context("Failed to call linker")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - let linker = stdout - .split("\r\n") - .next() - .context("Linker output was empty")?; - let version = linker - .split(' ') - .last() - .context("Linker version string was empty")?; - let components = version - .split('.') - .take(2) - .map(|v| v.parse()) - .collect::, _>>() - .context("Linker version component was empty")?; - Ok(Self { - major: components[0], - minor: components[1], - }) - } - - /// Checks if this linker is forwards-compatible with another linker. - fn is_forwards_compatible(&self, other: &LinkerVersion) -> bool { - // To be forwards compatible, the other linker must have the same major - // version and the minor version must greater or equal to this linker. - self.major == other.major && self.minor >= other.minor - } -} - -/// Returns the path to rustc's linker. -fn get_rustc_linker() -> Result { - // `RUSTC_LINKER` is set if the linker has been overriden anywhere. - if let Ok(link) = std::env::var("RUSTC_LINKER") { - return Ok(link.into()); - } - - let link = cc::windows_registry::find_tool( - &std::env::var("TARGET").context("`TARGET` environment variable not set")?, - "link.exe", - ) - .context("Failed to retrieve linker tool")?; - Ok(link.path().to_owned()) -} - -/// Uses vswhere to find all the linkers installed on a system. -fn get_linkers(vswhere: &Path) -> Result> { - let cmd = Command::new(vswhere) - .arg("-all") - .arg("-prerelease") - .arg("-format") - .arg("value") - .arg("-utf8") - .arg("-find") - .arg(r"VC\**\link.exe") - .output() - .context("Failed to call vswhere")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - let linkers: Vec<_> = stdout - .split("\r\n") - .map(PathBuf::from) - .filter(|linker| linker.exists()) - .collect(); - Ok(linkers) -} - -/// Attempts to find a potential linker that is compatible with PHP. -/// -/// It must fit the following criteria: -/// -/// 1. It must be forwards compatible with the PHP linker. -/// 2. The linker target architecture must match the target triple architecture. -/// 3. Optionally, the linker host architecture should match the host triple -/// architecture. On x86_64 systems, if a x64 host compiler is not found it will -/// fallback to x86. -/// -/// Returns an error if there is an error. Returns None if no linker could be -/// found. -fn find_potential_linker(php_linker: &LinkerVersion) -> Result> { - let vswhere = find_vswhere().context("Could not find `vswhere`")?; - let linkers = get_linkers(&vswhere)?; - let host_arch = msvc_host_arch()?; - let target_arch = msvc_target_arch()?; - let mut prelim_linker = None; - - for linker in &linkers { - let linker = Linker::from_linker_path(linker)?; - if php_linker.is_forwards_compatible(&linker.version) && linker.target_arch == target_arch { - if linker.host_arch == host_arch { - return Ok(Some(linker)); - } else if prelim_linker.is_none() - && host_arch == Arch::X64 - && linker.host_arch == Arch::X86 - { - // This linker will work - the host architectures do not match but that's OK for - // x86_64. - prelim_linker.replace(linker); - } - } - } - Ok(prelim_linker) -} - -#[derive(Debug)] -struct Linker { - host_arch: Arch, - target_arch: Arch, - version: LinkerVersion, - path: PathBuf, -} - -#[derive(Debug, PartialEq, Eq)] -enum Arch { - X86, - X64, - AArch64, -} - -impl TryFrom<&str> for Arch { - type Error = anyhow::Error; - - fn try_from(value: &str) -> Result { - Ok(match value { - "x86" => Self::X86, - "x64" => Self::X64, - "arm64" => Self::AArch64, - a => bail!("Unknown architecture {}", a), - }) - } -} - -impl Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Arch::X86 => "x86", - Arch::X64 => "x64", - Arch::AArch64 => "arm64", - } - ) - } -} - -impl Linker { - /// Retrieves information about the linker based on its path. - fn from_linker_path(linker: &Path) -> Result { - let version = LinkerVersion::from_linker_path(linker)?; - let target_arch_folder = linker - .parent() - .context("Could not get linker parent folder")?; - let target_arch = Arch::try_from( - &*target_arch_folder - .file_stem() - .context("Could not get linker target architecture")? - .to_string_lossy() - .to_lowercase(), - )?; - let host_arch = Arch::try_from( - &*target_arch_folder - .parent() - .context("Could not get linker parent folder")? - .file_stem() - .context("Could not get linker host architecture")? - .to_string_lossy() - .replace("Host", "") - .to_lowercase(), - )?; - Ok(Linker { - host_arch, - target_arch, - version, - path: linker.to_owned(), - }) - } -} - -/// Returns the architecture of a triple. -fn triple_arch(triple: &str) -> Result { - let arch = triple.split('-').next().context("Triple was invalid")?; - Ok(match arch { - "x86_64" => Arch::X64, - "i686" => Arch::X86, - "aarch64" => Arch::AArch64, - a => bail!("Unknown architecture {}", a), - }) -} - -/// Returns the architecture of the target the compilation is running on. -/// -/// If running on an AArch64 host, X86 is returned as there are no MSVC tools -/// for AArch64 hosts. -fn msvc_host_arch() -> Result { - let host_triple = std::env::var("HOST").context("Failed to get host triple")?; - Ok(match triple_arch(&host_triple)? { - Arch::AArch64 => Arch::X86, // AArch64 does not have host tools - a => a, - }) -} - -/// Returns the architecture of the target being compiled for. -fn msvc_target_arch() -> Result { - let host_triple = std::env::var("TARGET").context("Failed to get host triple")?; - triple_arch(&host_triple) -} - /// Finds the location of an executable `name`. fn find_executable(name: &str) -> Option { const WHICH: &str = if cfg!(windows) { "where" } else { "which" }; @@ -456,125 +65,7 @@ fn find_php() -> Result { find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") } -struct DevelPack(PathBuf); - -impl DevelPack { - /// Downloads a new PHP development pack, unzips it in the build script - /// temporary directory. - fn new(version: &str, is_zts: bool, arch: Arch) -> Result { - let zip_name = format!( - "php-devel-pack-{}{}-Win32-{}-{}.zip", - version, - if is_zts { "" } else { "-nts" }, - "vs16", /* TODO(david): At the moment all PHPs supported by ext-php-rs use VS16 so - * this is constant. */ - arch - ); - - fn download(zip_name: &str, archive: bool) -> Result { - let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); - let url = format!( - "https://windows.php.net/downloads/releases{}/{}", - if archive { "/archives" } else { "" }, - zip_name - ); - let request = reqwest::blocking::ClientBuilder::new() - .user_agent(USER_AGENT) - .build() - .context("Failed to create HTTP client")? - .get(url) - .send() - .context("Failed to download development pack")?; - request - .error_for_status_ref() - .context("Failed to download development pack")?; - let bytes = request - .bytes() - .context("Failed to read content from PHP website")?; - let mut content = Cursor::new(bytes); - let mut zip_content = zip::read::ZipArchive::new(&mut content) - .context("Failed to unzip development pack")?; - let inner_name = zip_content - .file_names() - .next() - .and_then(|f| f.split('/').next()) - .context("Failed to get development pack name")?; - let devpack_path = out_dir.join(inner_name); - let _ = std::fs::remove_dir_all(&devpack_path); - zip_content - .extract(&out_dir) - .context("Failed to extract devpack to directory")?; - Ok(devpack_path) - } - - download(&zip_name, false) - .or_else(|_| download(&zip_name, true)) - .map(DevelPack) - } - - /// Returns the path to the include folder. - pub fn includes(&self) -> PathBuf { - self.0.join("include") - } - - /// Returns the path of the PHP library containing symbols for linking. - pub fn php_lib(&self) -> PathBuf { - self.0.join("lib").join("php8.lib") - } - - /// Returns a list of include paths to pass to the compiler. - pub fn include_paths(&self) -> Vec { - let includes = self.includes(); - ["", "main", "Zend", "TSRM", "ext"] - .iter() - .map(|p| includes.join(p)) - .collect() - } - - /// Retrieves the version of MSVC PHP was linked with. - pub fn linker_version(&self) -> Result { - let config_path = self.includes().join("main").join("config.w32.h"); - let config = File::open(&config_path).context("Failed to open PHP config header")?; - let reader = BufReader::new(config); - let mut major = None; - let mut minor = None; - for line in reader.lines() { - let line = line.context("Failed to read line from PHP config header")?; - if major.is_none() { - let components: Vec<_> = line.split("#define PHP_LINKER_MAJOR ").collect(); - if components.len() > 1 { - major.replace( - u32::from_str(components[1]) - .context("Failed to convert major linker version to integer")?, - ); - continue; - } - } - if minor.is_none() { - let components: Vec<_> = line.split("#define PHP_LINKER_MINOR ").collect(); - if components.len() > 1 { - minor.replace( - u32::from_str(components[1]) - .context("Failed to convert minor linker version to integer")?, - ); - continue; - } - } - } - Ok(LinkerVersion { - major: major.context("Failed to read major linker version from config header")?, - minor: minor.context("Failed to read minor linker version from config header")?, - }) - } -} - -impl Display for LinkerVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}", self.major, self.minor) - } -} - -struct PHPInfo(String); +pub struct PHPInfo(String); impl PHPInfo { pub fn get(php: &Path) -> Result { @@ -589,7 +80,8 @@ impl PHPInfo { Ok(Self(stdout.to_string())) } - /// N.B. does not work on non-Windows. + // Only present on Windows. + #[cfg(windows)] pub fn architecture(&self) -> Result { self.get_key("Architecture") .context("Could not find architecture of PHP")? @@ -726,10 +218,7 @@ fn main() -> Result<()> { let php = find_php()?; let info = PHPInfo::get(&php)?; - #[cfg(windows)] - let provider = Windows::new(&info)?; - #[cfg(not(windows))] - let provider = Unix::new(&info)?; + let provider = Provider::new(&info)?; #[cfg(windows)] provider.check_linker_compatibility()?; diff --git a/unix_build.rs b/unix_build.rs new file mode 100644 index 0000000000..623d3561ba --- /dev/null +++ b/unix_build.rs @@ -0,0 +1,42 @@ +use std::{path::PathBuf, process::Command}; + +use anyhow::{bail, Context, Result}; + +use crate::{PHPInfo, PHPProvider}; + +pub struct Provider {} + +impl Provider { + /// Runs `php-config` with one argument, returning the stdout. + fn php_config(&self, arg: &str) -> Result { + let cmd = Command::new("php-config") + .arg(arg) + .output() + .context("Failed to run `php-config`")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + if !cmd.status.success() { + let stderr = String::from_utf8_lossy(&cmd.stderr); + bail!("Failed to run `php-config`: {} {}", stdout, stderr); + } + Ok(stdout.to_string()) + } +} + +impl<'a> PHPProvider<'a> for Provider { + fn new(_: &'a PHPInfo) -> Result { + Ok(Self {}) + } + + fn get_includes(&self) -> Result> { + Ok(self + .php_config("--includes")? + .split(' ') + .map(|s| s.trim_start_matches("-I")) + .map(PathBuf::from) + .collect()) + } + + fn get_defines(&self) -> Result> { + Ok(vec![]) + } +} diff --git a/windows_build.rs b/windows_build.rs new file mode 100644 index 0000000000..26bc29ccb4 --- /dev/null +++ b/windows_build.rs @@ -0,0 +1,488 @@ +use std::{ + convert::TryFrom, + fmt::Display, + fs::File, + io::{BufRead, BufReader, Cursor, Write}, + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; + +use anyhow::{bail, Context, Result}; + +use crate::{PHPInfo, PHPProvider}; + +const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +pub struct Provider<'a> { + info: &'a PHPInfo, + devel: DevelPack, +} + +impl<'a> Provider<'a> { + /// Retrieves the PHP library name (filename without extension). + fn get_php_lib_name(&self) -> Result { + Ok(self + .devel + .php_lib() + .file_stem() + .context("Failed to get PHP library name")? + .to_string_lossy() + .to_string()) + } + + /// Checks whether the rustc linker is compatible with the linker used in + /// the PHP development kit which was downloaded. + /// + /// If not compatible, attempts to find a compatible linker and notifies the + /// user if one is found. + fn check_linker_compatibility(&self) -> Result<()> { + let rustc_linker = get_rustc_linker()?; + let rustc_linker_version = LinkerVersion::from_linker_path(&rustc_linker)?; + let php_linker_version = self.devel.linker_version()?; + let compatible = php_linker_version.is_forwards_compatible(&rustc_linker_version); + if compatible { + Ok(()) + } else { + let mut error = format!("Incompatible linker versions. PHP was linked with MSVC {}, while Rust is using MSVC {}.", php_linker_version, rustc_linker_version); + if let Some(potential_linker) = find_potential_linker(&php_linker_version)? { + let path = potential_linker.path.to_string_lossy(); + let target_triple = std::env::var("TARGET").expect("Failed to get target triple"); + error.push_str(&format!( + " +A potentially compatible linker was found (MSVC version {}) located at `{}`. + +Use this linker by creating a `.cargo/config.toml` file in your extension's +manifest directory with the following content: +``` +[target.{}] +linker = \"{}\" +``` +", + potential_linker.version, + path, + target_triple, + path.escape_default() + )) + } else { + error.push_str(&format!( + " +You need a linker with a version earlier or equal to MSVC {}. +Download MSVC from https://visualstudio.microsoft.com/vs/features/cplusplus/. +Make sure to select C++ Development Tools in the installer. +You can correspond MSVC version with Visual Studio version +here: https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +", + php_linker_version + )); + } + bail!(error); + } + } +} + +impl<'a> PHPProvider<'a> for Provider<'a> { + fn new(info: &'a PHPInfo) -> Result { + let version = info.version()?; + let is_zts = info.thread_safety()?; + let arch = info.architecture()?; + let devel = DevelPack::new(version, is_zts, arch)?; + Ok(Self { info, devel }) + } + + fn get_includes(&self) -> Result> { + Ok(self.devel.include_paths()) + } + + fn get_defines(&self) -> Result> { + Ok(vec![ + ("ZEND_WIN32", "1"), + ("PHP_WIN32", "1"), + ("WINDOWS", "1"), + ("WIN32", "1"), + ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), + ]) + } + + fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { + // For some reason some symbols don't link without a `#[link(name = "php8")]` + // attribute on each extern block. Bindgen doesn't give us the option to add + // this so we need to add it manually. + let php_lib_name = self.get_php_lib_name()?; + for line in bindings.lines() { + match &*line { + "extern \"C\" {" | "extern \"fastcall\" {" => { + writeln!(writer, "#[link(name = \"{}\")]", php_lib_name)?; + } + _ => {} + } + writeln!(writer, "{}", line)?; + } + Ok(()) + } + + fn print_extra_link_args(&self) -> Result<()> { + let php_lib_name = self.get_php_lib_name()?; + let php_lib_search = self + .devel + .php_lib() + .parent() + .context("Failed to get PHP library parent folder")? + .to_string_lossy() + .to_string(); + println!("cargo:rustc-link-lib=dylib={}", php_lib_name); + println!("cargo:rustc-link-search={}", php_lib_search); + Ok(()) + } +} + +#[derive(Debug)] +struct LinkerVersion { + major: u32, + minor: u32, +} + +impl LinkerVersion { + /// Retrieve the version of a MSVC linker. + fn from_linker_path(linker: &Path) -> Result { + let cmd = Command::new(linker) + .output() + .context("Failed to call linker")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + let linker = stdout + .split("\r\n") + .next() + .context("Linker output was empty")?; + let version = linker + .split(' ') + .last() + .context("Linker version string was empty")?; + let components = version + .split('.') + .take(2) + .map(|v| v.parse()) + .collect::, _>>() + .context("Linker version component was empty")?; + Ok(Self { + major: components[0], + minor: components[1], + }) + } + + /// Checks if this linker is forwards-compatible with another linker. + fn is_forwards_compatible(&self, other: &LinkerVersion) -> bool { + // To be forwards compatible, the other linker must have the same major + // version and the minor version must greater or equal to this linker. + self.major == other.major && self.minor >= other.minor + } +} + +impl Display for LinkerVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +/// Returns the path to rustc's linker. +fn get_rustc_linker() -> Result { + // `RUSTC_LINKER` is set if the linker has been overriden anywhere. + if let Ok(link) = std::env::var("RUSTC_LINKER") { + return Ok(link.into()); + } + + let link = cc::windows_registry::find_tool( + &std::env::var("TARGET").context("`TARGET` environment variable not set")?, + "link.exe", + ) + .context("Failed to retrieve linker tool")?; + Ok(link.path().to_owned()) +} + +/// Uses vswhere to find all the linkers installed on a system. +fn get_linkers(vswhere: &Path) -> Result> { + let cmd = Command::new(vswhere) + .arg("-all") + .arg("-prerelease") + .arg("-format") + .arg("value") + .arg("-utf8") + .arg("-find") + .arg(r"VC\**\link.exe") + .output() + .context("Failed to call vswhere")?; + let stdout = String::from_utf8_lossy(&cmd.stdout); + let linkers: Vec<_> = stdout + .split("\r\n") + .map(PathBuf::from) + .filter(|linker| linker.exists()) + .collect(); + Ok(linkers) +} + +/// Attempts to find a potential linker that is compatible with PHP. +/// +/// It must fit the following criteria: +/// +/// 1. It must be forwards compatible with the PHP linker. +/// 2. The linker target architecture must match the target triple architecture. +/// 3. Optionally, the linker host architecture should match the host triple +/// architecture. On x86_64 systems, if a x64 host compiler is not found it will +/// fallback to x86. +/// +/// Returns an error if there is an error. Returns None if no linker could be +/// found. +fn find_potential_linker(php_linker: &LinkerVersion) -> Result> { + let vswhere = find_vswhere().context("Could not find `vswhere`")?; + let linkers = get_linkers(&vswhere)?; + let host_arch = msvc_host_arch()?; + let target_arch = msvc_target_arch()?; + let mut prelim_linker = None; + + for linker in &linkers { + let linker = Linker::from_linker_path(linker)?; + if php_linker.is_forwards_compatible(&linker.version) && linker.target_arch == target_arch { + if linker.host_arch == host_arch { + return Ok(Some(linker)); + } else if prelim_linker.is_none() + && host_arch == Arch::X64 + && linker.host_arch == Arch::X86 + { + // This linker will work - the host architectures do not match but that's OK for + // x86_64. + prelim_linker.replace(linker); + } + } + } + Ok(prelim_linker) +} + +#[derive(Debug)] +struct Linker { + host_arch: Arch, + target_arch: Arch, + version: LinkerVersion, + path: PathBuf, +} + +#[derive(Debug, PartialEq, Eq)] +enum Arch { + X86, + X64, + AArch64, +} + +impl TryFrom<&str> for Arch { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + Ok(match value { + "x86" => Self::X86, + "x64" => Self::X64, + "arm64" => Self::AArch64, + a => bail!("Unknown architecture {}", a), + }) + } +} + +impl Display for Arch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Arch::X86 => "x86", + Arch::X64 => "x64", + Arch::AArch64 => "arm64", + } + ) + } +} + +impl Linker { + /// Retrieves information about the linker based on its path. + fn from_linker_path(linker: &Path) -> Result { + let version = LinkerVersion::from_linker_path(linker)?; + let target_arch_folder = linker + .parent() + .context("Could not get linker parent folder")?; + let target_arch = Arch::try_from( + &*target_arch_folder + .file_stem() + .context("Could not get linker target architecture")? + .to_string_lossy() + .to_lowercase(), + )?; + let host_arch = Arch::try_from( + &*target_arch_folder + .parent() + .context("Could not get linker parent folder")? + .file_stem() + .context("Could not get linker host architecture")? + .to_string_lossy() + .replace("Host", "") + .to_lowercase(), + )?; + Ok(Linker { + host_arch, + target_arch, + version, + path: linker.to_owned(), + }) + } +} + +/// Returns the architecture of a triple. +fn triple_arch(triple: &str) -> Result { + let arch = triple.split('-').next().context("Triple was invalid")?; + Ok(match arch { + "x86_64" => Arch::X64, + "i686" => Arch::X86, + "aarch64" => Arch::AArch64, + a => bail!("Unknown architecture {}", a), + }) +} + +/// Returns the architecture of the target the compilation is running on. +/// +/// If running on an AArch64 host, X86 is returned as there are no MSVC tools +/// for AArch64 hosts. +fn msvc_host_arch() -> Result { + let host_triple = std::env::var("HOST").context("Failed to get host triple")?; + Ok(match triple_arch(&host_triple)? { + Arch::AArch64 => Arch::X86, // AArch64 does not have host tools + a => a, + }) +} + +/// Returns the architecture of the target being compiled for. +fn msvc_target_arch() -> Result { + let host_triple = std::env::var("TARGET").context("Failed to get host triple")?; + triple_arch(&host_triple) +} + +struct DevelPack(PathBuf); + +impl DevelPack { + /// Downloads a new PHP development pack, unzips it in the build script + /// temporary directory. + fn new(version: &str, is_zts: bool, arch: Arch) -> Result { + let zip_name = format!( + "php-devel-pack-{}{}-Win32-{}-{}.zip", + version, + if is_zts { "" } else { "-nts" }, + "vs16", /* TODO(david): At the moment all PHPs supported by ext-php-rs use VS16 so + * this is constant. */ + arch + ); + + fn download(zip_name: &str, archive: bool) -> Result { + let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let url = format!( + "https://windows.php.net/downloads/releases{}/{}", + if archive { "/archives" } else { "" }, + zip_name + ); + let request = reqwest::blocking::ClientBuilder::new() + .user_agent(USER_AGENT) + .build() + .context("Failed to create HTTP client")? + .get(url) + .send() + .context("Failed to download development pack")?; + request + .error_for_status_ref() + .context("Failed to download development pack")?; + let bytes = request + .bytes() + .context("Failed to read content from PHP website")?; + let mut content = Cursor::new(bytes); + let mut zip_content = zip::read::ZipArchive::new(&mut content) + .context("Failed to unzip development pack")?; + let inner_name = zip_content + .file_names() + .next() + .and_then(|f| f.split('/').next()) + .context("Failed to get development pack name")?; + let devpack_path = out_dir.join(inner_name); + let _ = std::fs::remove_dir_all(&devpack_path); + zip_content + .extract(&out_dir) + .context("Failed to extract devpack to directory")?; + Ok(devpack_path) + } + + download(&zip_name, false) + .or_else(|_| download(&zip_name, true)) + .map(DevelPack) + } + + /// Returns the path to the include folder. + pub fn includes(&self) -> PathBuf { + self.0.join("include") + } + + /// Returns the path of the PHP library containing symbols for linking. + pub fn php_lib(&self) -> PathBuf { + self.0.join("lib").join("php8.lib") + } + + /// Returns a list of include paths to pass to the compiler. + pub fn include_paths(&self) -> Vec { + let includes = self.includes(); + ["", "main", "Zend", "TSRM", "ext"] + .iter() + .map(|p| includes.join(p)) + .collect() + } + + /// Retrieves the version of MSVC PHP was linked with. + pub fn linker_version(&self) -> Result { + let config_path = self.includes().join("main").join("config.w32.h"); + let config = File::open(&config_path).context("Failed to open PHP config header")?; + let reader = BufReader::new(config); + let mut major = None; + let mut minor = None; + for line in reader.lines() { + let line = line.context("Failed to read line from PHP config header")?; + if major.is_none() { + let components: Vec<_> = line.split("#define PHP_LINKER_MAJOR ").collect(); + if components.len() > 1 { + major.replace( + u32::from_str(components[1]) + .context("Failed to convert major linker version to integer")?, + ); + continue; + } + } + if minor.is_none() { + let components: Vec<_> = line.split("#define PHP_LINKER_MINOR ").collect(); + if components.len() > 1 { + minor.replace( + u32::from_str(components[1]) + .context("Failed to convert minor linker version to integer")?, + ); + continue; + } + } + } + Ok(LinkerVersion { + major: major.context("Failed to read major linker version from config header")?, + minor: minor.context("Failed to read minor linker version from config header")?, + }) + } +} + +/// Attempt to find a `vswhere` binary in the common locations. +pub fn find_vswhere() -> Option { + let candidates = [format!( + r"{}\Microsoft Visual Studio\Installer\vswhere.exe", + std::env::var("ProgramFiles(x86)").ok()?, + )]; + for candidate in candidates { + let candidate = PathBuf::from(candidate); + if candidate.exists() { + return Some(candidate); + } + } + None +} From 5303b1c9b647c1e191a219c732170c49f445e0be Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 23:10:29 +1300 Subject: [PATCH 08/34] Fix building on Windows --- build.rs | 5 +++-- windows_build.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index e497366cbd..7449769c20 100644 --- a/build.rs +++ b/build.rs @@ -82,7 +82,9 @@ impl PHPInfo { // Only present on Windows. #[cfg(windows)] - pub fn architecture(&self) -> Result { + pub fn architecture(&self) -> Result { + use std::convert::TryInto; + self.get_key("Architecture") .context("Could not find architecture of PHP")? .try_into() @@ -153,7 +155,6 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Provider<'a> { /// /// If not compatible, attempts to find a compatible linker and notifies the /// user if one is found. - fn check_linker_compatibility(&self) -> Result<()> { + pub fn check_linker_compatibility(&self) -> Result<()> { let rustc_linker = get_rustc_linker()?; let rustc_linker_version = LinkerVersion::from_linker_path(&rustc_linker)?; let php_linker_version = self.devel.linker_version()?; @@ -265,7 +265,7 @@ struct Linker { } #[derive(Debug, PartialEq, Eq)] -enum Arch { +pub enum Arch { X86, X64, AArch64, From 6343bc479bd5cecebb074867d5023315c49f7595 Mon Sep 17 00:00:00 2001 From: David Cole Date: Sun, 13 Mar 2022 23:16:28 +1300 Subject: [PATCH 09/34] Remove `reqwest` and `zip` as dependencies on Unix --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3cf66d8f8e..49d151f583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,9 @@ ext-php-rs-derive = { version = "=0.7.3", path = "./crates/macros" } anyhow = "1" # bindgen = { version = "0.59" } bindgen = { git = "https://github.com/davidcole1340/rust-bindgen", branch = "abi_vectorcall" } -regex = "1" cc = "1.0" + +[target.'cfg(windows)'.build-dependencies] reqwest = { version = "*", features = ["blocking"] } zip = "0.5" From a70f443d442f6116d4c34116ac7ce1f8dea172c6 Mon Sep 17 00:00:00 2001 From: David Cole Date: Mon, 14 Mar 2022 18:19:42 +1300 Subject: [PATCH 10/34] Fix guide tests on Windows --- README.md | 2 ++ crates/macros/src/method.rs | 32 ++++++++++++++++-------------- guide/src/examples/hello_world.md | 1 + guide/src/exceptions.md | 1 + guide/src/macros/classes.md | 2 ++ guide/src/macros/constant.md | 1 + guide/src/macros/function.md | 4 ++++ guide/src/macros/impl.md | 1 + guide/src/macros/module.md | 1 + guide/src/macros/module_startup.md | 1 + guide/src/macros/zval_convert.md | 3 +++ guide/src/types/binary.md | 1 + guide/src/types/bool.md | 1 + guide/src/types/class_object.md | 2 ++ guide/src/types/closure.md | 3 +++ guide/src/types/hashmap.md | 1 + guide/src/types/numbers.md | 1 + guide/src/types/object.md | 2 ++ guide/src/types/option.md | 1 + guide/src/types/str.md | 1 + guide/src/types/string.md | 1 + guide/src/types/vec.md | 1 + src/closure.rs | 13 +++++++----- src/lib.rs | 14 +++++++++++++ 24 files changed, 71 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index bfd7a253a8..f28a084148 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Rust. Export a simple function `function hello_world(string $name): string` to PHP: ```rust +#![cfg_attr(windows, feature(abi_vectorcall))] + use ext_php_rs::prelude::*; /// Gives you a nice greeting! diff --git a/crates/macros/src/method.rs b/crates/macros/src/method.rs index c0380887b7..d6bbaabc7c 100644 --- a/crates/macros/src/method.rs +++ b/crates/macros/src/method.rs @@ -180,21 +180,23 @@ pub fn parser( quote! { #input - #[doc(hidden)] - pub extern "C" fn #internal_ident( - ex: &mut ::ext_php_rs::zend::ExecuteData, - retval: &mut ::ext_php_rs::types::Zval - ) { - use ::ext_php_rs::convert::IntoZval; - - #(#arg_definitions)* - #arg_parser - - let result = #this #ident(#(#arg_accessors,)*); - - if let Err(e) = result.set_zval(retval, false) { - let e: ::ext_php_rs::exception::PhpException = e.into(); - e.throw().expect("Failed to throw exception"); + ::ext_php_rs::zend_fastcall! { + #[doc(hidden)] + pub extern fn #internal_ident( + ex: &mut ::ext_php_rs::zend::ExecuteData, + retval: &mut ::ext_php_rs::types::Zval + ) { + use ::ext_php_rs::convert::IntoZval; + + #(#arg_definitions)* + #arg_parser + + let result = #this #ident(#(#arg_accessors,)*); + + if let Err(e) = result.set_zval(retval, false) { + let e: ::ext_php_rs::exception::PhpException = e.into(); + e.throw().expect("Failed to throw exception"); + } } } } diff --git a/guide/src/examples/hello_world.md b/guide/src/examples/hello_world.md index ea982ed995..7d7ae11534 100644 --- a/guide/src/examples/hello_world.md +++ b/guide/src/examples/hello_world.md @@ -50,6 +50,7 @@ we were given. ### `src/lib.rs` ```rust,ignore +# #![cfg_attr(windows, feature(abi_vectorcall))] use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/exceptions.md b/guide/src/exceptions.md index d842d3342b..f1d90d2f1f 100644 --- a/guide/src/exceptions.md +++ b/guide/src/exceptions.md @@ -27,6 +27,7 @@ the `#[php_function]` attribute. ### Examples ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; use std::convert::TryInto; diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index e44246ae8e..50010058c2 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -37,6 +37,7 @@ You can rename the property with options: This example creates a PHP class `Human`, adding a PHP property `address`. ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_class] @@ -56,6 +57,7 @@ Create a custom exception `RedisException`, which extends `Exception`, and put it in the `Redis\Exception` namespace: ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; use ext_php_rs::{exception::PhpException, zend::ce}; diff --git a/guide/src/macros/constant.md b/guide/src/macros/constant.md index 77e1cc6318..bbc4822ba6 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -6,6 +6,7 @@ that implements `IntoConst`. ## Examples ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_const] diff --git a/guide/src/macros/function.md b/guide/src/macros/function.md index 9e7c549cbb..681ca27c90 100644 --- a/guide/src/macros/function.md +++ b/guide/src/macros/function.md @@ -14,6 +14,7 @@ using the last consecutive arguments that are a variant of `Option` or have a default value. ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] @@ -33,6 +34,7 @@ through the `defaults` attribute option. When an optional parameter has a default, it does not need to be a variant of `Option`: ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function(defaults(offset = 0))] @@ -47,6 +49,7 @@ variant of `Option`, the `Option` argument will be deemed a nullable argument rather than an optional argument. ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; /// `age` will be deemed required and nullable rather than optional. @@ -68,6 +71,7 @@ arguments before optional arguments. This is done through an attribute parameter: ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; /// `age` will be deemed required and nullable rather than optional, diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index 95d9423f32..a66deb0bd0 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -96,6 +96,7 @@ constructor, as well as getters for the properties. We will also define a constant for the maximum age of a `Human`. ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::{prelude::*, types::ZendClassObject}; # #[php_class] diff --git a/guide/src/macros/module.md b/guide/src/macros/module.md index 750134d011..273fd43872 100644 --- a/guide/src/macros/module.md +++ b/guide/src/macros/module.md @@ -33,6 +33,7 @@ registered inside the extension startup function. ## Usage ```rust,ignore +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; # use ext_php_rs::{info_table_start, info_table_row, info_table_end}; diff --git a/guide/src/macros/module_startup.md b/guide/src/macros/module_startup.md index 610555e018..f8b031479e 100644 --- a/guide/src/macros/module_startup.md +++ b/guide/src/macros/module_startup.md @@ -17,6 +17,7 @@ Read more about what the module startup function is used for ## Example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_startup] diff --git a/guide/src/macros/zval_convert.md b/guide/src/macros/zval_convert.md index 9ed476141d..e0a26095af 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -14,6 +14,7 @@ all generics types. ### Examples ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -56,6 +57,7 @@ var_dump(give_object()); Another example involving generics: ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -93,6 +95,7 @@ to a string and passed as the string variant. Basic example showing the importance of variant ordering and default field: ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; diff --git a/guide/src/types/binary.md b/guide/src/types/binary.md index ea6d72548f..b8cf3229fe 100644 --- a/guide/src/types/binary.md +++ b/guide/src/types/binary.md @@ -22,6 +22,7 @@ f32, f64). ## Rust Usage ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; use ext_php_rs::binary::Binary; diff --git a/guide/src/types/bool.md b/guide/src/types/bool.md index 11a8c797a9..d2e1ceeceb 100644 --- a/guide/src/types/bool.md +++ b/guide/src/types/bool.md @@ -23,6 +23,7 @@ enum Zval { ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/types/class_object.md b/guide/src/types/class_object.md index 309d46d92c..f48269a773 100644 --- a/guide/src/types/class_object.md +++ b/guide/src/types/class_object.md @@ -13,6 +13,7 @@ object as a superset of an object, as a class object contains a Zend object. ### Returning a reference to `self` ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendClassObject}; @@ -40,6 +41,7 @@ impl Example { ### Creating a new class instance ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; diff --git a/guide/src/types/closure.md b/guide/src/types/closure.md index 9a41a4b842..c4f6e342ba 100644 --- a/guide/src/types/closure.md +++ b/guide/src/types/closure.md @@ -43,6 +43,7 @@ fact that it can modify variables in its scope. ### Example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -82,6 +83,7 @@ will be thrown. ### Example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -108,6 +110,7 @@ function by its name, or as a parameter. They can be called through the ### Callable parameter ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; diff --git a/guide/src/types/hashmap.md b/guide/src/types/hashmap.md index ddc06b0147..69f39256f6 100644 --- a/guide/src/types/hashmap.md +++ b/guide/src/types/hashmap.md @@ -17,6 +17,7 @@ Converting from a `HashMap` to a zval is valid when the key implements ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; # use std::collections::HashMap; diff --git a/guide/src/types/numbers.md b/guide/src/types/numbers.md index b50636ad6c..111a3186a2 100644 --- a/guide/src/types/numbers.md +++ b/guide/src/types/numbers.md @@ -22,6 +22,7 @@ fallible. ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/types/object.md b/guide/src/types/object.md index de7fe36a31..f74715c283 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -18,6 +18,7 @@ object. ### Taking an object reference ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendObject}; @@ -36,6 +37,7 @@ pub fn take_obj(obj: &mut ZendObject) -> &mut ZendObject { ### Creating a new object ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox}; diff --git a/guide/src/types/option.md b/guide/src/types/option.md index e2391d121e..c4f265ecb7 100644 --- a/guide/src/types/option.md +++ b/guide/src/types/option.md @@ -19,6 +19,7 @@ null to PHP. ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/types/str.md b/guide/src/types/str.md index 08759291ba..b4c4e39978 100644 --- a/guide/src/types/str.md +++ b/guide/src/types/str.md @@ -18,6 +18,7 @@ PHP strings. ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/types/string.md b/guide/src/types/string.md index fa7fd137b7..e9a9a5b252 100644 --- a/guide/src/types/string.md +++ b/guide/src/types/string.md @@ -17,6 +17,7 @@ be thrown if one is encountered while converting a `String` to a zval. ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/guide/src/types/vec.md b/guide/src/types/vec.md index 03b5b81173..0aface47f1 100644 --- a/guide/src/types/vec.md +++ b/guide/src/types/vec.md @@ -19,6 +19,7 @@ fail. ## Rust example ```rust +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] diff --git a/src/closure.rs b/src/closure.rs index 9767d9d3cf..03e02653c7 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -12,6 +12,7 @@ use crate::{ props::Property, types::Zval, zend::ExecuteData, + zend_fastcall, }; /// Class entry and handlers for Rust closures. @@ -137,12 +138,14 @@ impl Closure { CLOSURE_META.set_ce(ce); } - /// External function used by the Zend interpreter to call the closure. - extern "C" fn invoke(ex: &mut ExecuteData, ret: &mut Zval) { - let (parser, this) = ex.parser_method::(); - let this = this.expect("Internal closure function called on non-closure class"); + zend_fastcall! { + /// External function used by the Zend interpreter to call the closure. + extern "C" fn invoke(ex: &mut ExecuteData, ret: &mut Zval) { + let (parser, this) = ex.parser_method::(); + let this = this.expect("Internal closure function called on non-closure class"); - this.0.invoke(parser, ret) + this.0.invoke(parser, ret) + } } } diff --git a/src/lib.rs b/src/lib.rs index ac09e87c4b..79d2333032 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,7 @@ pub const PHP_ZTS: bool = cfg!(php_zts); /// # Example /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_const] /// const TEST_CONSTANT: i32 = 100; @@ -118,6 +119,7 @@ pub use ext_php_rs_derive::php_const; /// as the return type is an integer-boolean union. /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// # use ext_php_rs::types::Zval; /// #[php_extern] @@ -183,6 +185,7 @@ pub use ext_php_rs_derive::php_extern; /// function which looks like so: /// /// ```no_run +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::{prelude::*, exception::PhpException, zend::ExecuteData, convert::{FromZvalMut, IntoZval}, types::Zval, args::{Arg, ArgParser}}; /// pub fn hello(name: String) -> String { /// format!("Hello, {}!", name) @@ -227,6 +230,7 @@ pub use ext_php_rs_derive::php_extern; /// must be declared in the PHP module to be able to call. /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_function] /// pub fn hello(name: String) -> String { @@ -243,6 +247,7 @@ pub use ext_php_rs_derive::php_extern; /// two optional parameters (`description` and `age`). /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_function(optional = "description")] /// pub fn hello(name: String, description: Option, age: Option) -> String { @@ -269,6 +274,7 @@ pub use ext_php_rs_derive::php_extern; /// the attribute to the following: /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_function(optional = "description", defaults(description = "David", age = 10))] /// pub fn hello(name: String, description: String, age: i32) -> String { @@ -339,6 +345,7 @@ pub use ext_php_rs_derive::php_function; /// # Example /// /// ```no_run +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_class] /// #[derive(Debug)] @@ -408,6 +415,7 @@ pub use ext_php_rs_derive::php_impl; /// automatically be registered when the module attribute is called. /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_function] /// pub fn hello(name: String) -> String { @@ -455,6 +463,7 @@ pub use ext_php_rs_derive::php_module; /// Export a simple class called `Example`, with 3 Rust fields. /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_class] /// pub struct Example { @@ -473,6 +482,7 @@ pub use ext_php_rs_derive::php_module; /// `Redis\Exception`: /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// use ext_php_rs::exception::PhpException; /// use ext_php_rs::zend::ce; @@ -510,6 +520,7 @@ pub use ext_php_rs_derive::php_class; /// # Example /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_startup] /// pub fn startup_function() { @@ -544,6 +555,7 @@ pub use ext_php_rs_derive::php_startup; /// Basic example with some primitive PHP type. /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[derive(Debug, ZvalConvert)] /// pub struct ExampleStruct<'a> { @@ -582,6 +594,7 @@ pub use ext_php_rs_derive::php_startup; /// Another example involving generics: /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[derive(Debug, ZvalConvert)] /// pub struct CompareVals> { @@ -620,6 +633,7 @@ pub use ext_php_rs_derive::php_startup; /// Basic example showing the importance of variant ordering and default field: /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[derive(Debug, ZvalConvert)] /// pub enum UnionExample<'a> { From ed423a8bbbc4406a0c6331dd599ffcb2ebfd497b Mon Sep 17 00:00:00 2001 From: David Cole Date: Mon, 14 Mar 2022 18:26:46 +1300 Subject: [PATCH 11/34] Started work on Windows CI --- .github/workflows/build.yml | 129 +++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 37 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8eff9d004a..c7b421b787 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,22 +6,14 @@ on: pull_request: jobs: - build: - name: Build and Test - runs-on: ${{ matrix.os }} + build-win32: + name: Build and Test (Windows) + runs-on: windows-latest strategy: matrix: - os: - - ubuntu-latest - - macos-latest - rust-toolchain: - - stable - - nightly php: - - '8.0' + # - '8.0' - '8.1' - llvm: - - '11.0' steps: - name: Checkout code uses: actions/checkout@v2 @@ -32,7 +24,7 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust-toolchain }} + toolchain: nightly override: true components: rustfmt, clippy - name: Setup LLVM & Clang @@ -41,17 +33,9 @@ jobs: with: version: ${{ matrix.llvm }} directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - - name: Configure Clang - run: | - echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV - echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV - - name: Configure Clang (macOS only) - if: "contains(matrix.os, 'macos')" - run: echo "SDKROOT=$(xcrun --show-sdk-path)" >> $GITHUB_ENV - name: Install mdbook - uses: peaceiris/actions-mdbook@v1 - with: - mdbook-version: latest + runs: | + cargo install mdbook - name: Build env: EXT_PHP_RS_TEST: @@ -77,17 +61,88 @@ jobs: with: command: clippy args: --all -- -D warnings - - name: Build with docs stub - if: "contains(matrix.os, 'ubuntu') && ${{ matrix.php }} == '8.1'" - env: - DOCS_RS: - run: - cargo clean && cargo build - build-zts: - name: Build with ZTS - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Build - uses: ./.github/actions/zts + # build: + # name: Build and Test + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # os: + # - ubuntu-latest + # - macos-latest + # rust-toolchain: + # - stable + # - nightly + # php: + # - '8.0' + # - '8.1' + # llvm: + # - '11.0' + # steps: + # - name: Checkout code + # uses: actions/checkout@v2 + # - name: Setup PHP + # uses: shivammathur/setup-php@v2 + # with: + # php-version: ${{ matrix.php }} + # - name: Setup Rust + # uses: actions-rs/toolchain@v1 + # with: + # toolchain: ${{ matrix.rust-toolchain }} + # override: true + # components: rustfmt, clippy + # - name: Setup LLVM & Clang + # id: clang + # uses: KyleMayes/install-llvm-action@v1 + # with: + # version: ${{ matrix.llvm }} + # directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} + # - name: Configure Clang + # run: | + # echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV + # echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV + # - name: Configure Clang (macOS only) + # if: "contains(matrix.os, 'macos')" + # run: echo "SDKROOT=$(xcrun --show-sdk-path)" >> $GITHUB_ENV + # - name: Install mdbook + # uses: peaceiris/actions-mdbook@v1 + # with: + # mdbook-version: latest + # - name: Build + # env: + # EXT_PHP_RS_TEST: + # run: cargo build --release --all-features --all + # - name: Test guide examples + # env: + # CARGO_PKG_NAME: mdbook-tests + # CARGO_PKG_VERSION: 0.1.0 + # run: | + # mdbook test guide -L target/release/deps + # - name: Test inline examples + # uses: actions-rs/cargo@v1 + # with: + # command: test + # args: --release --all + # - name: Run rustfmt + # uses: actions-rs/cargo@v1 + # with: + # command: fmt + # args: --all -- --check + # - name: Run clippy + # uses: actions-rs/cargo@v1 + # with: + # command: clippy + # args: --all -- -D warnings + # - name: Build with docs stub + # if: "contains(matrix.os, 'ubuntu') && ${{ matrix.php }} == '8.1'" + # env: + # DOCS_RS: + # run: + # cargo clean && cargo build + # build-zts: + # name: Build with ZTS + # runs-on: ubuntu-latest + # steps: + # - name: Checkout code + # uses: actions/checkout@v2 + # - name: Build + # uses: ./.github/actions/zts From 54e10f4f49a1a025e7cf0738567ca6f88f4f6a6d Mon Sep 17 00:00:00 2001 From: David Cole Date: Mon, 14 Mar 2022 18:27:50 +1300 Subject: [PATCH 12/34] runs -> run --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7b421b787..d682c0f0a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: version: ${{ matrix.llvm }} directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - name: Install mdbook - runs: | + run: | cargo install mdbook - name: Build env: From 7e52c1a1c9a8e8c04c69a2eeef523728a23a55e8 Mon Sep 17 00:00:00 2001 From: David Cole Date: Mon, 14 Mar 2022 18:29:33 +1300 Subject: [PATCH 13/34] Use preinstalled LLVM on Windows --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d682c0f0a4..627c9fdb08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,12 +27,12 @@ jobs: toolchain: nightly override: true components: rustfmt, clippy - - name: Setup LLVM & Clang - id: clang - uses: KyleMayes/install-llvm-action@v1 - with: - version: ${{ matrix.llvm }} - directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} + # - name: Setup LLVM & Clang + # id: clang + # uses: KyleMayes/install-llvm-action@v1 + # with: + # version: ${{ matrix.llvm }} + # directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - name: Install mdbook run: | cargo install mdbook From 84bb60076dfb768287f57697f51d9f2e97e88be8 Mon Sep 17 00:00:00 2001 From: David Cole Date: Mon, 14 Mar 2022 21:31:52 +1300 Subject: [PATCH 14/34] Debugging for Windows CI --- .github/workflows/build.yml | 6 +++--- windows_build.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 627c9fdb08..1ddc0cfd97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,13 +33,13 @@ jobs: # with: # version: ${{ matrix.llvm }} # directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - - name: Install mdbook - run: | - cargo install mdbook - name: Build env: EXT_PHP_RS_TEST: run: cargo build --release --all-features --all + - name: Install mdbook + run: | + cargo install mdbook - name: Test guide examples env: CARGO_PKG_NAME: mdbook-tests diff --git a/windows_build.rs b/windows_build.rs index f29e1d4de4..1f86d57bb4 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -152,17 +152,17 @@ impl LinkerVersion { let linker = stdout .split("\r\n") .next() - .context("Linker output was empty")?; + .with_context(|| format!("Linker output was empty: {:?}", cmd))?; let version = linker .split(' ') .last() - .context("Linker version string was empty")?; + .with_context(|| format!("Linker version string was empty: {:?}", cmd))?; let components = version .split('.') .take(2) .map(|v| v.parse()) .collect::, _>>() - .context("Linker version component was empty")?; + .with_context(|| format!("Linker version component was empty: {:?}", cmd))?; Ok(Self { major: components[0], minor: components[1], From 562990930db71832ea12bfa4bea55c5c0e6759ae Mon Sep 17 00:00:00 2001 From: David Cole Date: Tue, 15 Mar 2022 16:59:24 +1300 Subject: [PATCH 15/34] Switch to upstream `rust-bindgen` master branch --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 49d151f583..8f4f66a121 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ ext-php-rs-derive = { version = "=0.7.3", path = "./crates/macros" } [build-dependencies] anyhow = "1" # bindgen = { version = "0.59" } -bindgen = { git = "https://github.com/davidcole1340/rust-bindgen", branch = "abi_vectorcall" } +bindgen = { git = "https://github.com/rust-lang/rust-bindgen", branch = "master" } cc = "1.0" [target.'cfg(windows)'.build-dependencies] From 6cdc28a9a9d86e3e102e1a3d66572ddf3828fb38 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 00:15:51 +1300 Subject: [PATCH 16/34] Switch to `rust-lld` for Windows linking --- .cargo/config.toml | 5 + build.rs | 7 +- windows_build.rs | 289 +++------------------------------------------ 3 files changed, 22 insertions(+), 279 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..470934bbb2 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.'cfg(target_os = "macos")'] +rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] + +[target.x86_64-pc-windows-msvc] +linker = "rust-lld" diff --git a/build.rs b/build.rs index 7449769c20..22de0cdd7a 100644 --- a/build.rs +++ b/build.rs @@ -155,7 +155,6 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result Result<()> { manifest.join("src").join("wrapper.h"), manifest.join("src").join("wrapper.c"), manifest.join("allowed_bindings.rs"), + manifest.join("windows_build.rs"), + manifest.join("unix_build.rs"), ] { println!("cargo:rerun-if-changed={}", path.to_string_lossy()); } let php = find_php()?; let info = PHPInfo::get(&php)?; - let provider = Provider::new(&info)?; - #[cfg(windows)] - provider.check_linker_compatibility()?; - let includes = provider.get_includes()?; let defines = provider.get_defines()?; diff --git a/windows_build.rs b/windows_build.rs index 1f86d57bb4..1987f3f172 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -1,11 +1,9 @@ use std::{ convert::TryFrom, fmt::Display, - fs::File, - io::{BufRead, BufReader, Cursor, Write}, + io::{Cursor, Write}, path::{Path, PathBuf}, process::Command, - str::FromStr, }; use anyhow::{bail, Context, Result}; @@ -30,55 +28,6 @@ impl<'a> Provider<'a> { .to_string_lossy() .to_string()) } - - /// Checks whether the rustc linker is compatible with the linker used in - /// the PHP development kit which was downloaded. - /// - /// If not compatible, attempts to find a compatible linker and notifies the - /// user if one is found. - pub fn check_linker_compatibility(&self) -> Result<()> { - let rustc_linker = get_rustc_linker()?; - let rustc_linker_version = LinkerVersion::from_linker_path(&rustc_linker)?; - let php_linker_version = self.devel.linker_version()?; - let compatible = php_linker_version.is_forwards_compatible(&rustc_linker_version); - if compatible { - Ok(()) - } else { - let mut error = format!("Incompatible linker versions. PHP was linked with MSVC {}, while Rust is using MSVC {}.", php_linker_version, rustc_linker_version); - if let Some(potential_linker) = find_potential_linker(&php_linker_version)? { - let path = potential_linker.path.to_string_lossy(); - let target_triple = std::env::var("TARGET").expect("Failed to get target triple"); - error.push_str(&format!( - " -A potentially compatible linker was found (MSVC version {}) located at `{}`. - -Use this linker by creating a `.cargo/config.toml` file in your extension's -manifest directory with the following content: -``` -[target.{}] -linker = \"{}\" -``` -", - potential_linker.version, - path, - target_triple, - path.escape_default() - )) - } else { - error.push_str(&format!( - " -You need a linker with a version earlier or equal to MSVC {}. -Download MSVC from https://visualstudio.microsoft.com/vs/features/cplusplus/. -Make sure to select C++ Development Tools in the installer. -You can correspond MSVC version with Visual Studio version -here: https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering -", - php_linker_version - )); - } - bail!(error); - } - } } impl<'a> PHPProvider<'a> for Provider<'a> { @@ -87,6 +36,12 @@ impl<'a> PHPProvider<'a> for Provider<'a> { let is_zts = info.thread_safety()?; let arch = info.architecture()?; let devel = DevelPack::new(version, is_zts, arch)?; + if let Ok(linker) = get_rustc_linker() { + if looks_like_msvc_linker(&linker) { + println!("cargo:warning=It looks like you are using a MSVC linker. You may encounter issues when attempting to load your compiled extension into PHP if your MSVC linker version is not compatible with the linker used to compile your PHP. It is recommended to use `rust-lld` as your linker."); + } + } + Ok(Self { info, devel }) } @@ -136,53 +91,6 @@ impl<'a> PHPProvider<'a> for Provider<'a> { } } -#[derive(Debug)] -struct LinkerVersion { - major: u32, - minor: u32, -} - -impl LinkerVersion { - /// Retrieve the version of a MSVC linker. - fn from_linker_path(linker: &Path) -> Result { - let cmd = Command::new(linker) - .output() - .context("Failed to call linker")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - let linker = stdout - .split("\r\n") - .next() - .with_context(|| format!("Linker output was empty: {:?}", cmd))?; - let version = linker - .split(' ') - .last() - .with_context(|| format!("Linker version string was empty: {:?}", cmd))?; - let components = version - .split('.') - .take(2) - .map(|v| v.parse()) - .collect::, _>>() - .with_context(|| format!("Linker version component was empty: {:?}", cmd))?; - Ok(Self { - major: components[0], - minor: components[1], - }) - } - - /// Checks if this linker is forwards-compatible with another linker. - fn is_forwards_compatible(&self, other: &LinkerVersion) -> bool { - // To be forwards compatible, the other linker must have the same major - // version and the minor version must greater or equal to this linker. - self.major == other.major && self.minor >= other.minor - } -} - -impl Display for LinkerVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.{}", self.major, self.minor) - } -} - /// Returns the path to rustc's linker. fn get_rustc_linker() -> Result { // `RUSTC_LINKER` is set if the linker has been overriden anywhere. @@ -198,70 +106,16 @@ fn get_rustc_linker() -> Result { Ok(link.path().to_owned()) } -/// Uses vswhere to find all the linkers installed on a system. -fn get_linkers(vswhere: &Path) -> Result> { - let cmd = Command::new(vswhere) - .arg("-all") - .arg("-prerelease") - .arg("-format") - .arg("value") - .arg("-utf8") - .arg("-find") - .arg(r"VC\**\link.exe") - .output() - .context("Failed to call vswhere")?; - let stdout = String::from_utf8_lossy(&cmd.stdout); - let linkers: Vec<_> = stdout - .split("\r\n") - .map(PathBuf::from) - .filter(|linker| linker.exists()) - .collect(); - Ok(linkers) -} - -/// Attempts to find a potential linker that is compatible with PHP. -/// -/// It must fit the following criteria: -/// -/// 1. It must be forwards compatible with the PHP linker. -/// 2. The linker target architecture must match the target triple architecture. -/// 3. Optionally, the linker host architecture should match the host triple -/// architecture. On x86_64 systems, if a x64 host compiler is not found it will -/// fallback to x86. -/// -/// Returns an error if there is an error. Returns None if no linker could be -/// found. -fn find_potential_linker(php_linker: &LinkerVersion) -> Result> { - let vswhere = find_vswhere().context("Could not find `vswhere`")?; - let linkers = get_linkers(&vswhere)?; - let host_arch = msvc_host_arch()?; - let target_arch = msvc_target_arch()?; - let mut prelim_linker = None; - - for linker in &linkers { - let linker = Linker::from_linker_path(linker)?; - if php_linker.is_forwards_compatible(&linker.version) && linker.target_arch == target_arch { - if linker.host_arch == host_arch { - return Ok(Some(linker)); - } else if prelim_linker.is_none() - && host_arch == Arch::X64 - && linker.host_arch == Arch::X86 - { - // This linker will work - the host architectures do not match but that's OK for - // x86_64. - prelim_linker.replace(linker); - } +/// Checks if a linker looks like the MSVC link.exe linker. +fn looks_like_msvc_linker(linker: &Path) -> bool { + let command = Command::new(linker).output(); + if let Ok(command) = command { + let stdout = String::from_utf8_lossy(&command.stdout); + if stdout.contains("Microsoft (R) Incremental Linker") { + return true; } } - Ok(prelim_linker) -} - -#[derive(Debug)] -struct Linker { - host_arch: Arch, - target_arch: Arch, - version: LinkerVersion, - path: PathBuf, + false } #[derive(Debug, PartialEq, Eq)] @@ -298,68 +152,6 @@ impl Display for Arch { } } -impl Linker { - /// Retrieves information about the linker based on its path. - fn from_linker_path(linker: &Path) -> Result { - let version = LinkerVersion::from_linker_path(linker)?; - let target_arch_folder = linker - .parent() - .context("Could not get linker parent folder")?; - let target_arch = Arch::try_from( - &*target_arch_folder - .file_stem() - .context("Could not get linker target architecture")? - .to_string_lossy() - .to_lowercase(), - )?; - let host_arch = Arch::try_from( - &*target_arch_folder - .parent() - .context("Could not get linker parent folder")? - .file_stem() - .context("Could not get linker host architecture")? - .to_string_lossy() - .replace("Host", "") - .to_lowercase(), - )?; - Ok(Linker { - host_arch, - target_arch, - version, - path: linker.to_owned(), - }) - } -} - -/// Returns the architecture of a triple. -fn triple_arch(triple: &str) -> Result { - let arch = triple.split('-').next().context("Triple was invalid")?; - Ok(match arch { - "x86_64" => Arch::X64, - "i686" => Arch::X86, - "aarch64" => Arch::AArch64, - a => bail!("Unknown architecture {}", a), - }) -} - -/// Returns the architecture of the target the compilation is running on. -/// -/// If running on an AArch64 host, X86 is returned as there are no MSVC tools -/// for AArch64 hosts. -fn msvc_host_arch() -> Result { - let host_triple = std::env::var("HOST").context("Failed to get host triple")?; - Ok(match triple_arch(&host_triple)? { - Arch::AArch64 => Arch::X86, // AArch64 does not have host tools - a => a, - }) -} - -/// Returns the architecture of the target being compiled for. -fn msvc_target_arch() -> Result { - let host_triple = std::env::var("TARGET").context("Failed to get host triple")?; - triple_arch(&host_triple) -} - struct DevelPack(PathBuf); impl DevelPack { @@ -434,55 +226,4 @@ impl DevelPack { .map(|p| includes.join(p)) .collect() } - - /// Retrieves the version of MSVC PHP was linked with. - pub fn linker_version(&self) -> Result { - let config_path = self.includes().join("main").join("config.w32.h"); - let config = File::open(&config_path).context("Failed to open PHP config header")?; - let reader = BufReader::new(config); - let mut major = None; - let mut minor = None; - for line in reader.lines() { - let line = line.context("Failed to read line from PHP config header")?; - if major.is_none() { - let components: Vec<_> = line.split("#define PHP_LINKER_MAJOR ").collect(); - if components.len() > 1 { - major.replace( - u32::from_str(components[1]) - .context("Failed to convert major linker version to integer")?, - ); - continue; - } - } - if minor.is_none() { - let components: Vec<_> = line.split("#define PHP_LINKER_MINOR ").collect(); - if components.len() > 1 { - minor.replace( - u32::from_str(components[1]) - .context("Failed to convert minor linker version to integer")?, - ); - continue; - } - } - } - Ok(LinkerVersion { - major: major.context("Failed to read major linker version from config header")?, - minor: minor.context("Failed to read minor linker version from config header")?, - }) - } -} - -/// Attempt to find a `vswhere` binary in the common locations. -pub fn find_vswhere() -> Option { - let candidates = [format!( - r"{}\Microsoft Visual Studio\Installer\vswhere.exe", - std::env::var("ProgramFiles(x86)").ok()?, - )]; - for candidate in candidates { - let candidate = PathBuf::from(candidate); - if candidate.exists() { - return Some(candidate); - } - } - None } From 75fb9a77c3709e110a0a4f60ec061ac0cd6088c6 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 00:30:02 +1300 Subject: [PATCH 17/34] Don't compile `cargo-php` on Windows --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ddc0cfd97..65b4fc4a45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - name: Build env: EXT_PHP_RS_TEST: - run: cargo build --release --all-features --all + run: cargo build --release --all-features - name: Install mdbook run: | cargo install mdbook From e38a7116b1a6f36ddf4b98a1f494dda2eb552c36 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 11:52:12 +1300 Subject: [PATCH 18/34] Switch to using skeptic for tests --- .github/workflows/build.yml | 17 ++++------------- Cargo.toml | 4 ++++ build.rs | 10 ++++++++++ guide/src/exceptions.md | 3 ++- guide/src/macros/classes.md | 6 ++++-- guide/src/macros/constant.md | 5 ++++- guide/src/macros/function.md | 12 ++++++++---- guide/src/macros/impl.md | 3 ++- guide/src/macros/module_startup.md | 3 ++- guide/src/macros/zval_convert.md | 12 +++++++++--- guide/src/types/binary.md | 3 ++- guide/src/types/bool.md | 3 ++- guide/src/types/class_object.md | 6 ++++-- guide/src/types/closure.md | 9 ++++++--- guide/src/types/hashmap.md | 3 ++- guide/src/types/numbers.md | 3 ++- guide/src/types/object.md | 6 ++++-- guide/src/types/option.md | 3 ++- guide/src/types/str.md | 3 ++- guide/src/types/string.md | 3 ++- guide/src/types/vec.md | 3 ++- tests/guide.rs | 1 + 22 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 tests/guide.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65b4fc4a45..0fc7931819 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,30 +37,21 @@ jobs: env: EXT_PHP_RS_TEST: run: cargo build --release --all-features - - name: Install mdbook - run: | - cargo install mdbook - - name: Test guide examples - env: - CARGO_PKG_NAME: mdbook-tests - CARGO_PKG_VERSION: 0.1.0 - run: | - mdbook test guide -L target/release/deps - - name: Test inline examples + - name: Run tests uses: actions-rs/cargo@v1 with: command: test - args: --release --all + args: --release --all-features - name: Run rustfmt uses: actions-rs/cargo@v1 with: command: fmt - args: --all -- --check + args: -- --check - name: Run clippy uses: actions-rs/cargo@v1 with: command: clippy - args: --all -- -D warnings + args: -- -D warnings # build: # name: Build and Test # runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index 8f4f66a121..247788466e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,15 @@ once_cell = "1.8.0" anyhow = { version = "1", optional = true } ext-php-rs-derive = { version = "=0.7.3", path = "./crates/macros" } +[dev-dependencies] +skeptic = "0.13" + [build-dependencies] anyhow = "1" # bindgen = { version = "0.59" } bindgen = { git = "https://github.com/rust-lang/rust-bindgen", branch = "master" } cc = "1.0" +skeptic = "0.13" [target.'cfg(windows)'.build-dependencies] reqwest = { version = "*", features = ["blocking"] } diff --git a/build.rs b/build.rs index 22de0cdd7a..dd40d81001 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,7 @@ mod impl_; use std::{ env, + ffi::OsStr, fs::File, io::{BufWriter, Write}, path::{Path, PathBuf}, @@ -243,6 +244,15 @@ fn main() -> Result<()> { } provider.print_extra_link_args()?; + // Generate guide tests + let test_md = skeptic::markdown_files_of_directory("guide"); + #[cfg(not(feature = "closure"))] + let test_md: Vec<_> = test_md + .into_iter() + .filter(|p| p.file_stem() != Some(OsStr::new("closure"))) + .collect(); + skeptic::generate_doc_tests(&test_md); + Ok(()) } diff --git a/guide/src/exceptions.md b/guide/src/exceptions.md index f1d90d2f1f..4e5abc74c8 100644 --- a/guide/src/exceptions.md +++ b/guide/src/exceptions.md @@ -26,7 +26,7 @@ the `#[php_function]` attribute. ### Examples -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -44,6 +44,7 @@ pub fn something_fallible(n: u64) -> PhpResult { pub fn module(module: ModuleBuilder) -> ModuleBuilder { module } +# fn main() {} ``` [`PhpException`]: https://docs.rs/ext-php-rs/0.5.0/ext_php_rs/php/exceptions/struct.PhpException.html diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 50010058c2..9e6a2b011e 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -36,7 +36,7 @@ You can rename the property with options: This example creates a PHP class `Human`, adding a PHP property `address`. -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -51,12 +51,13 @@ pub struct Human { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` Create a custom exception `RedisException`, which extends `Exception`, and put it in the `Redis\Exception` namespace: -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -76,4 +77,5 @@ pub fn throw_exception() -> PhpResult { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` diff --git a/guide/src/macros/constant.md b/guide/src/macros/constant.md index bbc4822ba6..72f83446ad 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -5,7 +5,7 @@ that implements `IntoConst`. ## Examples -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -14,6 +14,9 @@ const TEST_CONSTANT: i32 = 100; #[php_const] const ANOTHER_STRING_CONST: &'static str = "Hello world!"; +# #[php_module] +# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } +# fn main() {} ``` ## PHP usage diff --git a/guide/src/macros/function.md b/guide/src/macros/function.md index 681ca27c90..30fdb84d48 100644 --- a/guide/src/macros/function.md +++ b/guide/src/macros/function.md @@ -13,7 +13,7 @@ of `Option`. The macro will then figure out which parameters are optional by using the last consecutive arguments that are a variant of `Option` or have a default value. -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -27,13 +27,14 @@ pub fn greet(name: String, age: Option) -> String { greeting } +# fn main() {} ``` Default parameter values can also be set for optional parameters. This is done through the `defaults` attribute option. When an optional parameter has a default, it does not need to be a variant of `Option`: -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -42,13 +43,14 @@ pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option let haystack: String = haystack.chars().skip(offset as usize).collect(); haystack.find(needle) } +# fn main() {} ``` Note that if there is a non-optional argument after an argument that is a variant of `Option`, the `Option` argument will be deemed a nullable argument rather than an optional argument. -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -64,13 +66,14 @@ pub fn greet(name: String, age: Option, description: String) -> String { greeting += &format!(" {}.", description); greeting } +# fn main() {} ``` You can also specify the optional arguments if you want to have nullable arguments before optional arguments. This is done through an attribute parameter: -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -90,6 +93,7 @@ pub fn greet(name: String, age: Option, description: Option) -> Str greeting } +# fn main() {} ``` ## Returning `Result` diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index a66deb0bd0..e342dfc676 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -95,7 +95,7 @@ Continuing on from our `Human` example in the structs section, we will define a constructor, as well as getters for the properties. We will also define a constant for the maximum age of a `Human`. -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::{prelude::*, types::ZendClassObject}; @@ -147,6 +147,7 @@ impl Human { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` Using our newly created class in PHP: diff --git a/guide/src/macros/module_startup.md b/guide/src/macros/module_startup.md index f8b031479e..cd950171d9 100644 --- a/guide/src/macros/module_startup.md +++ b/guide/src/macros/module_startup.md @@ -16,7 +16,7 @@ Read more about what the module startup function is used for ## Example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -24,4 +24,5 @@ Read more about what the module startup function is used for pub fn startup_function() { } +# fn main() {} ``` diff --git a/guide/src/macros/zval_convert.md b/guide/src/macros/zval_convert.md index e0a26095af..876a4b1757 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -13,7 +13,7 @@ all generics types. ### Examples -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -38,6 +38,8 @@ pub fn give_object() -> ExampleClass<'static> { c: "Borrowed", } } +# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } +# fn main() {} ``` Calling from PHP: @@ -56,7 +58,7 @@ var_dump(give_object()); Another example involving generics: -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -72,6 +74,8 @@ pub struct CompareVals> { pub fn take_object(obj: CompareVals) { dbg!(obj); } +# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } +# fn main() {} ``` ## Enums @@ -94,7 +98,7 @@ to a string and passed as the string variant. Basic example showing the importance of variant ordering and default field: -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -116,6 +120,8 @@ pub fn test_union(val: UnionExample) { pub fn give_union() -> UnionExample<'static> { UnionExample::Long(5) } +# #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } +# fn main() {} ``` Use in PHP: diff --git a/guide/src/types/binary.md b/guide/src/types/binary.md index b8cf3229fe..aa296b16cd 100644 --- a/guide/src/types/binary.md +++ b/guide/src/types/binary.md @@ -21,7 +21,7 @@ f32, f64). ## Rust Usage -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -37,6 +37,7 @@ pub fn test_binary(input: Binary) -> Binary { .into_iter() .collect::>() } +# fn main() {} ``` ## PHP Usage diff --git a/guide/src/types/bool.md b/guide/src/types/bool.md index d2e1ceeceb..1974ff305a 100644 --- a/guide/src/types/bool.md +++ b/guide/src/types/bool.md @@ -22,7 +22,7 @@ enum Zval { ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -34,6 +34,7 @@ pub fn test_bool(input: bool) -> String { "No!".into() } } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/class_object.md b/guide/src/types/class_object.md index f48269a773..ed6125cf4a 100644 --- a/guide/src/types/class_object.md +++ b/guide/src/types/class_object.md @@ -12,7 +12,7 @@ object as a superset of an object, as a class object contains a Zend object. ### Returning a reference to `self` -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendClassObject}; @@ -36,11 +36,12 @@ impl Example { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` ### Creating a new class instance -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -61,4 +62,5 @@ impl Example { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` diff --git a/guide/src/types/closure.md b/guide/src/types/closure.md index c4f6e342ba..d1586caa52 100644 --- a/guide/src/types/closure.md +++ b/guide/src/types/closure.md @@ -42,7 +42,7 @@ fact that it can modify variables in its scope. ### Example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -66,6 +66,7 @@ pub fn closure_count() -> Closure { count }) as Box i32>) } +# fn main() {} ``` ## `FnOnce` @@ -82,7 +83,7 @@ will be thrown. ### Example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -96,6 +97,7 @@ pub fn closure_return_string() -> Closure { example }) as Box String>) } +# fn main() {} ``` Closures must be boxed as PHP classes cannot support generics, therefore trait @@ -109,7 +111,7 @@ function by its name, or as a parameter. They can be called through the ### Callable parameter -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -119,4 +121,5 @@ pub fn callable_parameter(call: ZendCallable) { let val = call.try_call(vec![&0, &1, &"Hello"]).expect("Failed to call function"); dbg!(val); } +# fn main() {} ``` diff --git a/guide/src/types/hashmap.md b/guide/src/types/hashmap.md index 69f39256f6..d0003e2b2f 100644 --- a/guide/src/types/hashmap.md +++ b/guide/src/types/hashmap.md @@ -16,7 +16,7 @@ Converting from a `HashMap` to a zval is valid when the key implements ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -31,6 +31,7 @@ pub fn test_hashmap(hm: HashMap) -> Vec { .map(|(_, v)| v) .collect::>() } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/numbers.md b/guide/src/types/numbers.md index 111a3186a2..68755a1dbf 100644 --- a/guide/src/types/numbers.md +++ b/guide/src/types/numbers.md @@ -21,7 +21,7 @@ fallible. ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -30,6 +30,7 @@ pub fn test_numbers(a: i32, b: u32, c: f32) -> u8 { println!("a {} b {} c {}", a, b, c); 0 } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/object.md b/guide/src/types/object.md index f74715c283..c953d99d36 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -17,7 +17,7 @@ object. ### Taking an object reference -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendObject}; @@ -32,11 +32,12 @@ pub fn take_obj(obj: &mut ZendObject) -> &mut ZendObject { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` ### Creating a new object -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::{prelude::*, types::ZendObject, boxed::ZBox}; @@ -52,6 +53,7 @@ pub fn make_object() -> ZBox { # pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { # module # } +# fn main() {} ``` [class object]: ./class_object.md diff --git a/guide/src/types/option.md b/guide/src/types/option.md index c4f265ecb7..8acb48765f 100644 --- a/guide/src/types/option.md +++ b/guide/src/types/option.md @@ -18,7 +18,7 @@ null to PHP. ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -26,6 +26,7 @@ null to PHP. pub fn test_option_null(input: Option) -> Option { input.map(|input| format!("Hello {}", input).into()) } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/str.md b/guide/src/types/str.md index b4c4e39978..26688553c5 100644 --- a/guide/src/types/str.md +++ b/guide/src/types/str.md @@ -17,7 +17,7 @@ PHP strings. ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -30,6 +30,7 @@ pub fn str_example(input: &str) -> String { pub fn str_return_example() -> &'static str { "Hello from Rust" } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/string.md b/guide/src/types/string.md index e9a9a5b252..317bcf994f 100644 --- a/guide/src/types/string.md +++ b/guide/src/types/string.md @@ -16,7 +16,7 @@ be thrown if one is encountered while converting a `String` to a zval. ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -24,6 +24,7 @@ be thrown if one is encountered while converting a `String` to a zval. pub fn str_example(input: String) -> String { format!("Hello {}", input) } +# fn main() {} ``` ## PHP example diff --git a/guide/src/types/vec.md b/guide/src/types/vec.md index 0aface47f1..62dcf3e9e9 100644 --- a/guide/src/types/vec.md +++ b/guide/src/types/vec.md @@ -18,7 +18,7 @@ fail. ## Rust example -```rust +```rust,no_run # #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; @@ -26,6 +26,7 @@ fail. pub fn test_vec(vec: Vec) -> String { vec.join(" ") } +# fn main() {} ``` ## PHP example diff --git a/tests/guide.rs b/tests/guide.rs new file mode 100644 index 0000000000..ff46c9c01c --- /dev/null +++ b/tests/guide.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs")); From 89a1f147205f799536353fcbbf8b25c0d9bce1c8 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 15:07:57 +1300 Subject: [PATCH 19/34] cargo-php: Disable stub generation, fix ext install/remove The plan is to replace the stub generation by generating them with PHP code. This is cross-platform and means we don't need to worry about ABI. We also don't need to embed information into the library. --- .github/workflows/build.yml | 14 ++-- Cargo.toml | 3 +- crates/cli/src/lib.rs | 131 ++++++++++++++++-------------------- crates/cli/src/main.rs | 1 + src/builders/class.rs | 8 +-- src/describe/stub.rs | 7 +- tests/guide.rs | 3 + windows_build.rs | 40 +++++++---- 8 files changed, 103 insertions(+), 104 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fc7931819..e96a6ca46f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,31 +27,25 @@ jobs: toolchain: nightly override: true components: rustfmt, clippy - # - name: Setup LLVM & Clang - # id: clang - # uses: KyleMayes/install-llvm-action@v1 - # with: - # version: ${{ matrix.llvm }} - # directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - name: Build env: EXT_PHP_RS_TEST: - run: cargo build --release --all-features + run: cargo build --all --release --all-features - name: Run tests uses: actions-rs/cargo@v1 with: command: test - args: --release --all-features + args: --all --release --all-features - name: Run rustfmt uses: actions-rs/cargo@v1 with: command: fmt - args: -- --check + args: --all -- --check - name: Run clippy uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings + args: --all -- -D warnings # build: # name: Build and Test # runs-on: ${{ matrix.os }} diff --git a/Cargo.toml b/Cargo.toml index 247788466e..184279b67c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,8 @@ cc = "1.0" skeptic = "0.13" [target.'cfg(windows)'.build-dependencies] -reqwest = { version = "*", features = ["blocking"] } +ureq = { version = "2.4", features = ["native-tls", "gzip"], default-features = false } +native-tls = "0.2" zip = "0.5" [features] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9f153d36d3..74e62e4ac9 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,5 +1,6 @@ #![doc = include_str!("../README.md")] +#[cfg(not(windows))] mod ext; use anyhow::{bail, Context, Result as AResult}; @@ -8,18 +9,12 @@ use clap::Parser; use dialoguer::{Confirm, Select}; use std::{ - borrow::Cow, - ffi::OsString, - fs::{File, OpenOptions}, + fs::OpenOptions, io::{BufRead, BufReader, Write}, path::PathBuf, process::{Command, Stdio}, - str::FromStr, }; -use self::ext::Ext; -use ext_php_rs::describe::ToStub; - /// Generates mock symbols required to generate stub files from a downstream /// crates CLI application. #[macro_export] @@ -86,6 +81,7 @@ enum Args { /// /// These stub files can be used in IDEs to provide typehinting for /// extension classes, functions and constants. + #[cfg(not(windows))] Stubs(Stubs), } @@ -127,6 +123,7 @@ struct Remove { manifest: Option, } +#[cfg(not(windows))] #[derive(Parser)] struct Stubs { /// Path to extension to generate stubs for. Defaults for searching the @@ -154,6 +151,7 @@ impl Args { match self { Args::Install(install) => install.handle(), Args::Remove(remove) => remove.handle(), + #[cfg(not(windows))] Args::Stubs(stubs) => stubs.handle(), } } @@ -167,8 +165,7 @@ impl Install { let (mut ext_dir, mut php_ini) = if let Some(install_dir) = self.install_dir { (install_dir, None) } else { - let php_config = PhpConfig::new(); - (php_config.get_ext_dir()?, Some(php_config.get_php_ini()?)) + (get_ext_dir()?, Some(get_php_ini()?)) }; if let Some(ini_path) = self.ini_path { @@ -200,8 +197,6 @@ impl Install { let mut file = OpenOptions::new() .read(true) .write(true) - .create(true) - .truncate(true) .open(php_ini) .with_context(|| "Failed to open `php.ini`")?; @@ -229,6 +224,55 @@ impl Install { } } +/// Returns the path to the extension directory utilised by the PHP interpreter, +/// creating it if one was returned but it does not exist. +fn get_ext_dir() -> AResult { + let cmd = Command::new("php") + .arg("-r") + .arg("echo ini_get('extension_dir');") + .output() + .context("Failed to call PHP")?; + if !cmd.status.success() { + bail!("Failed to call PHP: {:?}", cmd); + } + let stdout = String::from_utf8_lossy(&cmd.stdout); + let ext_dir = PathBuf::from(&*stdout); + if !ext_dir.is_dir() { + if ext_dir.exists() { + bail!( + "Extension directory returned from PHP is not a valid directory: {:?}", + ext_dir + ); + } else { + std::fs::create_dir(&ext_dir).with_context(|| { + format!("Failed to create extension directory at {:?}", ext_dir) + })?; + } + } + Ok(ext_dir) +} + +/// Returns the path to the `php.ini` loaded by the PHP interpreter. +fn get_php_ini() -> AResult { + let cmd = Command::new("php") + .arg("-r") + .arg("echo get_cfg_var('cfg_file_path');") + .output() + .context("Failed to call PHP")?; + if !cmd.status.success() { + bail!("Failed to call PHP: {:?}", cmd); + } + let stdout = String::from_utf8_lossy(&cmd.stdout); + let ini = PathBuf::from(&*stdout); + if !ini.is_file() { + bail!( + "php.ini does not exist or is not a file at the given path: {:?}", + ini + ); + } + Ok(ini) +} + impl Remove { pub fn handle(self) -> CrateResult { use std::env::consts; @@ -238,8 +282,7 @@ impl Remove { let (mut ext_path, mut php_ini) = if let Some(install_dir) = self.install_dir { (install_dir, None) } else { - let php_config = PhpConfig::new(); - (php_config.get_ext_dir()?, Some(php_config.get_php_ini()?)) + (get_ext_dir()?, Some(get_php_ini()?)) }; if let Some(ini_path) = self.ini_path { @@ -295,6 +338,7 @@ impl Remove { } } +#[cfg(not(windows))] impl Stubs { pub fn handle(self) -> CrateResult { let ext_path = if let Some(ext_path) = self.ext { @@ -308,7 +352,7 @@ impl Stubs { bail!("Invalid extension path given, not a file."); } - let ext = Ext::load(ext_path)?; + let ext = self::ext::Ext::load(ext_path)?; let result = ext.describe(); // Ensure extension and CLI `ext-php-rs` versions are compatible. @@ -348,65 +392,6 @@ impl Stubs { } } -struct PhpConfig { - path: OsString, -} - -impl PhpConfig { - /// Creates a new `php-config` instance. - pub fn new() -> Self { - Self { - path: if let Some(php_config) = std::env::var_os("PHP_CONFIG") { - php_config - } else { - OsString::from("php-config") - }, - } - } - - /// Calls `php-config` and retrieves the extension directory. - pub fn get_ext_dir(&self) -> AResult { - Ok(PathBuf::from( - self.exec( - |cmd| cmd.arg("--extension-dir"), - "retrieve extension directory", - )? - .trim(), - )) - } - - /// Calls `php-config` and retrieves the `php.ini` file path. - pub fn get_php_ini(&self) -> AResult { - let mut path = PathBuf::from( - self.exec(|cmd| cmd.arg("--ini-path"), "retrieve `php.ini` path")? - .trim(), - ); - path.push("php.ini"); - - if !path.exists() { - File::create(&path).with_context(|| "Failed to create `php.ini`")?; - } - - Ok(path) - } - - /// Executes the `php-config` binary. The given function `f` is used to - /// modify the given mutable [`Command`]. If successful, a [`String`] - /// representing stdout is returned. - fn exec(&self, f: F, ctx: &str) -> AResult - where - F: FnOnce(&mut Command) -> &mut Command, - { - let mut cmd = Command::new(&self.path); - f(&mut cmd); - let out = cmd - .output() - .with_context(|| format!("Failed to {} from `php-config`", ctx))?; - String::from_utf8(out.stdout) - .with_context(|| "Failed to convert `php-config` output to string") - } -} - /// Attempts to find an extension in the target directory. fn find_ext(manifest: &Option) -> AResult { // TODO(david): Look for cargo manifest option or env diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d62ac98550..a3dee63a78 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -5,6 +5,7 @@ macro_rules! bind { } } +#[cfg(not(windows))] include!("../allowed_bindings.rs"); fn main() -> cargo_php::CrateResult { diff --git a/src/builders/class.rs b/src/builders/class.rs index f09a76a60e..003361ac3b 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -70,10 +70,10 @@ impl ClassBuilder { /// /// Panics when the given class entry `interface` is not an interface. pub fn implements(mut self, interface: &'static ClassEntry) -> Self { - if !interface.is_interface() { - panic!("Given class entry was not an interface."); - } - + assert!( + interface.is_interface(), + "Given class entry was not an interface." + ); self.interfaces.push(interface); self } diff --git a/src/describe/stub.rs b/src/describe/stub.rs index 43fb338fdf..067d720602 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -366,9 +366,7 @@ fn indent(s: &str, depth: usize) -> String { #[cfg(test)] mod test { - use crate::describe::stub::NEW_LINE_SEPARATOR; - - use super::{indent, split_namespace}; + use super::split_namespace; #[test] pub fn test_split_ns() { @@ -380,6 +378,9 @@ mod test { #[test] #[cfg(not(windows))] pub fn test_indent() { + use super::indent; + use crate::describe::stub::NEW_LINE_SEPARATOR; + assert_eq!(indent("hello", 4), " hello"); assert_eq!( indent(&format!("hello{nl}world{nl}", nl = NEW_LINE_SEPARATOR), 4), diff --git a/tests/guide.rs b/tests/guide.rs index ff46c9c01c..15958889db 100644 --- a/tests/guide.rs +++ b/tests/guide.rs @@ -1 +1,4 @@ +#![allow(clippy::all)] +#![allow(warnings)] + include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs")); diff --git a/windows_build.rs b/windows_build.rs index 1987f3f172..e6e386ca35 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -1,9 +1,10 @@ use std::{ convert::TryFrom, fmt::Display, - io::{Cursor, Write}, + io::{Cursor, Read, Write}, path::{Path, PathBuf}, process::Command, + sync::Arc, }; use anyhow::{bail, Context, Result}; @@ -174,20 +175,33 @@ impl DevelPack { if archive { "/archives" } else { "" }, zip_name ); - let request = reqwest::blocking::ClientBuilder::new() - .user_agent(USER_AGENT) + // let request = reqwest::blocking::ClientBuilder::new() + // .user_agent(USER_AGENT) + // .build() + // .context("Failed to create HTTP client")? + // .get(url) + // .send() + // .context("Failed to download development pack")?; + // request + // .error_for_status_ref() + // .context("Failed to download development pack")?; + // let bytes = request + // .bytes() + // .context("Failed to read content from PHP website")?; + // let response = ureq::get(&url) + let response = ureq::AgentBuilder::new() + .tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap())) .build() - .context("Failed to create HTTP client")? - .get(url) - .send() + .get(&url) + .set("User-Agent", USER_AGENT) + .call() .context("Failed to download development pack")?; - request - .error_for_status_ref() - .context("Failed to download development pack")?; - let bytes = request - .bytes() - .context("Failed to read content from PHP website")?; - let mut content = Cursor::new(bytes); + let mut content = vec![]; + response + .into_reader() + .read_to_end(&mut content) + .context("Failed to read development pack")?; + let mut content = Cursor::new(&mut content); let mut zip_content = zip::read::ZipArchive::new(&mut content) .context("Failed to unzip development pack")?; let inner_name = zip_content From 9f80166d315d092930b09b39f407bfcabf679c75 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 15:16:39 +1300 Subject: [PATCH 20/34] cargo-php: Fix on unix OS --- crates/cli/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 74e62e4ac9..dba7f0a2e0 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -341,6 +341,9 @@ impl Remove { #[cfg(not(windows))] impl Stubs { pub fn handle(self) -> CrateResult { + use ext_php_rs::describe::ToStub; + use std::{borrow::Cow, str::FromStr}; + let ext_path = if let Some(ext_path) = self.ext { ext_path } else { From bc5143de750999b71bd9c46664ce5af62e6e040b Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 15:18:39 +1300 Subject: [PATCH 21/34] Fix clippy lint --- crates/cli/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a3dee63a78..75c700ef77 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,4 +1,5 @@ // Mock macro for the `allowed_bindings.rs` script. +#[cfg(not(windows))] macro_rules! bind { ($($s: ident),*) => { cargo_php::stub_symbols!($($s),*); From 742cba0cf2f197fd003bb35a959c08acf4527ea2 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 16:15:32 +1300 Subject: [PATCH 22/34] Updated README --- README.md | 62 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index f28a084148..8a0059ca77 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ # ext-php-rs -[](https://discord.gg/dphp) +[![Crates.io](https://img.shields.io/crates/v/ext-php-rs)](https://lib.rs/ext-php-rs) +[![docs.rs](https://img.shields.io/docsrs/ext-php-rs/latest)](https://docs.rs/ext-php-rs) +[![Guide Workflow Status](https://img.shields.io/github/workflow/status/davidcole1340/ext-php-rs/Deploy%20documentation?label=guide)](https://davidcole1340.github.io/ext-php-rs) +![CI Workflow Status](https://img.shields.io/github/workflow/status/davidcole1340/ext-php-rs/Build%20and%20Lint) +[![Discord](https://img.shields.io/discord/115233111977099271)](https://discord.gg/dphp) Bindings and abstractions for the Zend API to build PHP extensions natively in Rust. +- Documentation: +- Guide: + ## Example Export a simple function `function hello_world(string $name): string` to PHP: @@ -106,30 +113,37 @@ best resource at the moment. This can be viewed at [docs.rs]. ## Requirements -- PHP 8.0 or later - - No support is planned for lower versions. -- Linux, macOS or Windows-based operating system -- Rust - no idea which version -- Clang 3.9 or greater - -See the following links for the dependency crate requirements: - -- [`cc`](https://github.com/alexcrichton/cc-rs#compile-time-requirements) -- [`bindgen`](https://rust-lang.github.io/rust-bindgen/requirements.html) +- Linux, macOS or Windows-based operating system. +- PHP 8.0 or later. + - No support is planned for earlier versions of PHP. +- Rust. + - Currently, we maintain no guarantee of a MSRV, however lib.rs suggests Rust + 1.57 at the time of writing. +- Clang 5.0 or later. -### Windows Support - -Windows has some extra requirements: +### Windows Requirements - Extensions can only be compiled for PHP installations sourced from - [windows.php.net]. -- Only PHP installations compiled with MSVC are supported (no support for - `x86_64-pc-windows-gnu`). -- Microsoft Visual C++ must be installed. The compiler version must match or be - older than the compiler that was used to compile your PHP installation (at the - time of writing Visual Studio 2019 is supported). -- Extensions can only be compiled with nightly Rust, and the `abi_vectorcall` - feature must be enabled in your crates's Cargo Features + . Support is planned for other installations + eventually. +- Rust nightly is required for Windows. This is due to the [vectorcall] calling + convention being used by some PHP functions on Windows, which is only + available as a nightly unstable feature in Rust. +- It is suggested to use the `rust-lld` linker to link your extension. The MSVC + linker (`link.exe`) is supported however you may run into issues if the linker + version is not supported by your PHP installation. You can use the `rust-lld` + linker by creating a `.cargo\config.toml` file with the following content: + ```toml + # Replace target triple if you have a different architecture than x86_64 + [target.x86_64-pc-windows-msvc] + linker = "rust-lld" + ``` +- The `cc` crate requires `cl.exe` to be present on your system. This is usually + bundled with Microsoft Visual Studio. +- `cargo-php`'s stub generation feature does not work on Windows. Rewriting this + functionality to be cross-platform is on the roadmap. + +[vectorcall]: https://docs.microsoft.com/en-us/cpp/cpp/vectorcall?view=msvc-170 ## Cargo Features @@ -142,10 +156,6 @@ All features are disabled by default. ## Usage -This project only works for PHP >= 8.0 (for now). Due to the fact that the PHP -extension system relies heavily on C macros (which cannot be exported to Rust -easily), structs have to be hard coded in. - Check out one of the example projects: - [anonaddy-sequoia](https://gitlab.com/willbrowning/anonaddy-sequoia) - Sequoia From bd13d44a046eeda0d6d721b4ebe402a923bbb8cf Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 16:27:16 +1300 Subject: [PATCH 23/34] Re-add CI for Unix + PHP 8.0 --- .github/workflows/build.yml | 162 +++++++++++++++++------------------- 1 file changed, 76 insertions(+), 86 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e96a6ca46f..0e9ea7fbfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: php: - # - '8.0' + - '8.0' - '8.1' steps: - name: Checkout code @@ -46,88 +46,78 @@ jobs: with: command: clippy args: --all -- -D warnings - # build: - # name: Build and Test - # runs-on: ${{ matrix.os }} - # strategy: - # matrix: - # os: - # - ubuntu-latest - # - macos-latest - # rust-toolchain: - # - stable - # - nightly - # php: - # - '8.0' - # - '8.1' - # llvm: - # - '11.0' - # steps: - # - name: Checkout code - # uses: actions/checkout@v2 - # - name: Setup PHP - # uses: shivammathur/setup-php@v2 - # with: - # php-version: ${{ matrix.php }} - # - name: Setup Rust - # uses: actions-rs/toolchain@v1 - # with: - # toolchain: ${{ matrix.rust-toolchain }} - # override: true - # components: rustfmt, clippy - # - name: Setup LLVM & Clang - # id: clang - # uses: KyleMayes/install-llvm-action@v1 - # with: - # version: ${{ matrix.llvm }} - # directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - # - name: Configure Clang - # run: | - # echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV - # echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV - # - name: Configure Clang (macOS only) - # if: "contains(matrix.os, 'macos')" - # run: echo "SDKROOT=$(xcrun --show-sdk-path)" >> $GITHUB_ENV - # - name: Install mdbook - # uses: peaceiris/actions-mdbook@v1 - # with: - # mdbook-version: latest - # - name: Build - # env: - # EXT_PHP_RS_TEST: - # run: cargo build --release --all-features --all - # - name: Test guide examples - # env: - # CARGO_PKG_NAME: mdbook-tests - # CARGO_PKG_VERSION: 0.1.0 - # run: | - # mdbook test guide -L target/release/deps - # - name: Test inline examples - # uses: actions-rs/cargo@v1 - # with: - # command: test - # args: --release --all - # - name: Run rustfmt - # uses: actions-rs/cargo@v1 - # with: - # command: fmt - # args: --all -- --check - # - name: Run clippy - # uses: actions-rs/cargo@v1 - # with: - # command: clippy - # args: --all -- -D warnings - # - name: Build with docs stub - # if: "contains(matrix.os, 'ubuntu') && ${{ matrix.php }} == '8.1'" - # env: - # DOCS_RS: - # run: - # cargo clean && cargo build - # build-zts: - # name: Build with ZTS - # runs-on: ubuntu-latest - # steps: - # - name: Checkout code - # uses: actions/checkout@v2 - # - name: Build - # uses: ./.github/actions/zts + build: + name: Build and Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + rust-toolchain: + - stable + - nightly + php: + - '8.0' + - '8.1' + llvm: + - '11.0' + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust-toolchain }} + override: true + components: rustfmt, clippy + - name: Setup LLVM & Clang + id: clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ matrix.llvm }} + directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} + - name: Configure Clang + run: | + echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV + echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV + - name: Configure Clang (macOS only) + if: "contains(matrix.os, 'macos')" + run: echo "SDKROOT=$(xcrun --show-sdk-path)" >> $GITHUB_ENV + - name: Build + env: + EXT_PHP_RS_TEST: + run: cargo build --release --all-features --all + - name: Test inline examples + uses: actions-rs/cargo@v1 + with: + command: test + args: --release --all --all-features + - name: Run rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Run clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all -- -D warnings + - name: Build with docs stub + if: "contains(matrix.os, 'ubuntu') && ${{ matrix.php }} == '8.1'" + env: + DOCS_RS: + run: + cargo clean && cargo build + build-zts: + name: Build with ZTS + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Build + uses: ./.github/actions/zts From 843aaaa7bb6da580a23192939b8f5c633a324524 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 21:48:27 +1300 Subject: [PATCH 24/34] Fix building on thread-safe PHP --- allowed_bindings.rs | 1 + src/wrapper.c | 41 +++++++++++++++++------------------------ src/wrapper.h | 26 +++++++++++++++----------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 5ff76b6a47..bdada29b73 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -189,6 +189,7 @@ bind! { zend_standard_class_def, zend_class_serialize_deny, zend_class_unserialize_deny, + zend_executor_globals, zend_objects_store_del, gc_possible_root, ZEND_ACC_NOT_SERIALIZABLE, diff --git a/src/wrapper.c b/src/wrapper.c index 5dfcec5792..240b2d68cd 100644 --- a/src/wrapper.c +++ b/src/wrapper.c @@ -1,39 +1,32 @@ #include "wrapper.h" -zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, bool persistent) -{ - return zend_string_init(str, len, persistent); +zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, + bool persistent) { + return zend_string_init(str, len, persistent); } -void ext_php_rs_zend_string_release(zend_string *zs) -{ - zend_string_release(zs); +void ext_php_rs_zend_string_release(zend_string *zs) { + zend_string_release(zs); } -const char *ext_php_rs_php_build_id() -{ - return ZEND_MODULE_BUILD_ID; -} +const char *ext_php_rs_php_build_id() { return ZEND_MODULE_BUILD_ID; } -void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce) -{ - return zend_object_alloc(obj_size, ce); +void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce) { + return zend_object_alloc(obj_size, ce); } -void ext_php_rs_zend_object_release(zend_object *obj) -{ - zend_object_release(obj); +void ext_php_rs_zend_object_release(zend_object *obj) { + zend_object_release(obj); } -zend_executor_globals *ext_php_rs_executor_globals() -{ +zend_executor_globals *ext_php_rs_executor_globals() { #ifdef ZTS -# ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE - return TSRMG_FAST_BULK_STATIC(executor_globals_offset, zend_executor_globals); -# else - return TSRMG_FAST_BULK(executor_globals_offset, zend_executor_globals *); -# endif +#ifdef ZEND_ENABLE_STATIC_TSRMLS_CACHE + return TSRMG_FAST_BULK_STATIC(executor_globals_offset, zend_executor_globals); +#else + return TSRMG_FAST_BULK(executor_globals_offset, zend_executor_globals *); +#endif #else - return &executor_globals; + return &executor_globals; #endif } diff --git a/src/wrapper.h b/src/wrapper.h index 016dc4a0c5..f55f3eca14 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -1,24 +1,28 @@ -// PHP for Windows uses the `vectorcall` calling convention on some functions. This is guarded by -// the `ZEND_FASTCALL` macro, which is set to `__vectorcall` on Windows and nothing on other systems. +// PHP for Windows uses the `vectorcall` calling convention on some functions. +// This is guarded by the `ZEND_FASTCALL` macro, which is set to `__vectorcall` +// on Windows and nothing on other systems. // -// However, `ZEND_FASTCALL` is only set when compiling with MSVC and the PHP source code checks for -// the __clang__ macro and will not define `__vectorcall` if it is set (even on Windows). This is a -// problem as Bindgen uses libclang to generate bindings. To work around this, we include the header -// file containing the `ZEND_FASTCALL` macro but not before undefining `__clang__` to pretend we are -// compiling on MSVC. +// However, `ZEND_FASTCALL` is only set when compiling with MSVC and the PHP +// source code checks for the __clang__ macro and will not define `__vectorcall` +// if it is set (even on Windows). This is a problem as Bindgen uses libclang to +// generate bindings. To work around this, we include the header file containing +// the `ZEND_FASTCALL` macro but not before undefining `__clang__` to pretend we +// are compiling on MSVC. #if defined(_MSC_VER) && defined(__clang__) -# undef __clang__ -# include "zend_portability.h" -# define __clang__ +#undef __clang__ +#include "zend_portability.h" +#define __clang__ #endif #include "php.h" + #include "ext/standard/info.h" #include "zend_exceptions.h" #include "zend_inheritance.h" #include "zend_interfaces.h" -zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, bool persistent); +zend_string *ext_php_rs_zend_string_init(const char *str, size_t len, + bool persistent); void ext_php_rs_zend_string_release(zend_string *zs); const char *ext_php_rs_php_build_id(); void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce); From 915e1bd69dfa0e2658795fc59ad200cc3bb04a1a Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 21:55:37 +1300 Subject: [PATCH 25/34] Tidy up build scripts --- build.rs | 3 +-- windows_build.rs | 14 -------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/build.rs b/build.rs index dd40d81001..ec4153f6c7 100644 --- a/build.rs +++ b/build.rs @@ -4,7 +4,6 @@ mod impl_; use std::{ env, - ffi::OsStr, fs::File, io::{BufWriter, Write}, path::{Path, PathBuf}, @@ -249,7 +248,7 @@ fn main() -> Result<()> { #[cfg(not(feature = "closure"))] let test_md: Vec<_> = test_md .into_iter() - .filter(|p| p.file_stem() != Some(OsStr::new("closure"))) + .filter(|p| p.file_stem() != Some(std::ffi::OsStr::new("closure"))) .collect(); skeptic::generate_doc_tests(&test_md); diff --git a/windows_build.rs b/windows_build.rs index e6e386ca35..5be4deb536 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -175,20 +175,6 @@ impl DevelPack { if archive { "/archives" } else { "" }, zip_name ); - // let request = reqwest::blocking::ClientBuilder::new() - // .user_agent(USER_AGENT) - // .build() - // .context("Failed to create HTTP client")? - // .get(url) - // .send() - // .context("Failed to download development pack")?; - // request - // .error_for_status_ref() - // .context("Failed to download development pack")?; - // let bytes = request - // .bytes() - // .context("Failed to read content from PHP website")?; - // let response = ureq::get(&url) let response = ureq::AgentBuilder::new() .tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap())) .build() From 07a2badef79dbcb16d1d23494c10451cf8f910b2 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:00:50 +1300 Subject: [PATCH 26/34] Use dynamic lookup on Linux, test with TS Windows --- .cargo/config.toml | 2 +- .github/workflows/build.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 470934bbb2..08138800de 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,4 @@ -[target.'cfg(target_os = "macos")'] +[target.'cfg(not(target_os = "windows"))'] rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] [target.x86_64-pc-windows-msvc] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e9ea7fbfb..be679365fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,9 @@ jobs: php: - '8.0' - '8.1' + phpts: + - 'ts' + - 'nts' steps: - name: Checkout code uses: actions/checkout@v2 @@ -21,6 +24,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + env: + phpts: ${{ matrix.phpts }} - name: Setup Rust uses: actions-rs/toolchain@v1 with: From 4c6342b1372e6493c1605af543c4b5c2387e885d Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:19:19 +1300 Subject: [PATCH 27/34] Define `ZTS` when compiling PHP ZTS --- windows_build.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/windows_build.rs b/windows_build.rs index 5be4deb536..69a0c8df55 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -51,13 +51,17 @@ impl<'a> PHPProvider<'a> for Provider<'a> { } fn get_defines(&self) -> Result> { - Ok(vec![ + let mut defines = vec![ ("ZEND_WIN32", "1"), ("PHP_WIN32", "1"), ("WINDOWS", "1"), ("WIN32", "1"), ("ZEND_DEBUG", if self.info.debug()? { "1" } else { "0" }), - ]) + ]; + if self.info.thread_safety()? { + defines.push(("ZTS", "")); + } + Ok(defines) } fn write_bindings(&self, bindings: String, writer: &mut impl Write) -> Result<()> { From 733c65890d21471e9382c3b5ed08d58d489a42be Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:30:06 +1300 Subject: [PATCH 28/34] Combine Windows and Unix CI, fix linking for Win32TS --- .github/workflows/build.yml | 73 +++++++++---------------------------- windows_build.rs | 7 +++- 2 files changed, 23 insertions(+), 57 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be679365fc..a23fcde28d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,67 +6,24 @@ on: pull_request: jobs: - build-win32: - name: Build and Test (Windows) - runs-on: windows-latest - strategy: - matrix: - php: - - '8.0' - - '8.1' - phpts: - - 'ts' - - 'nts' - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - env: - phpts: ${{ matrix.phpts }} - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - override: true - components: rustfmt, clippy - - name: Build - env: - EXT_PHP_RS_TEST: - run: cargo build --all --release --all-features - - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all --release --all-features - - name: Run rustfmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all -- -D warnings build: name: Build and Test runs-on: ${{ matrix.os }} strategy: matrix: - os: - - ubuntu-latest - - macos-latest - rust-toolchain: - - stable - - nightly - php: - - '8.0' - - '8.1' - llvm: - - '11.0' + os: [ubuntu-latest, macos-latest, windows-latest] + php: ['8.0', '8.1'] + rust: [stable, nightly] + phpts: [ts, nts] + exclude: + # ext-php-rs requires nightly Rust when on Windows. + - os: windows-latest + rust: stable + # setup-php doesn't support thread safe PHP on Linux and macOS. + - os: macos-latest + phpts: ts + - os: ubuntu-latest + phpts: ts steps: - name: Checkout code uses: actions/checkout@v2 @@ -74,6 +31,8 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + env: + phpts: ${{ matrix.phpts }} - name: Setup Rust uses: actions-rs/toolchain@v1 with: @@ -81,12 +40,14 @@ jobs: override: true components: rustfmt, clippy - name: Setup LLVM & Clang + if: "!contains(matrix.os, 'windows')" id: clang uses: KyleMayes/install-llvm-action@v1 with: version: ${{ matrix.llvm }} directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} - name: Configure Clang + if: "!contains(matrix.os, 'windows')" run: | echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV diff --git a/windows_build.rs b/windows_build.rs index 69a0c8df55..cdf2da0a54 100644 --- a/windows_build.rs +++ b/windows_build.rs @@ -219,7 +219,12 @@ impl DevelPack { /// Returns the path of the PHP library containing symbols for linking. pub fn php_lib(&self) -> PathBuf { - self.0.join("lib").join("php8.lib") + let php_nts = self.0.join("lib").join("php8.lib"); + if php_nts.exists() { + php_nts + } else { + self.0.join("lib").join("php8ts.lib") + } } /// Returns a list of include paths to pass to the compiler. From 7d995f857a6b805830ba81c31c1a1bc1379d0fc3 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:31:06 +1300 Subject: [PATCH 29/34] Fix exclusions in build CI --- .github/workflows/build.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a23fcde28d..8b710bea0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,15 +15,15 @@ jobs: php: ['8.0', '8.1'] rust: [stable, nightly] phpts: [ts, nts] - exclude: - # ext-php-rs requires nightly Rust when on Windows. - - os: windows-latest - rust: stable - # setup-php doesn't support thread safe PHP on Linux and macOS. - - os: macos-latest - phpts: ts - - os: ubuntu-latest - phpts: ts + exclude: + # ext-php-rs requires nightly Rust when on Windows. + - os: windows-latest + rust: stable + # setup-php doesn't support thread safe PHP on Linux and macOS. + - os: macos-latest + phpts: ts + - os: ubuntu-latest + phpts: ts steps: - name: Checkout code uses: actions/checkout@v2 From b5070ce3fadbb885e65724b73fb163cf3112d0d3 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:31:51 +1300 Subject: [PATCH 30/34] rust-toolchain -> rust --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b710bea0b..658ff256e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: - name: Setup Rust uses: actions-rs/toolchain@v1 with: - toolchain: ${{ matrix.rust-toolchain }} + toolchain: ${{ matrix.rust }} override: true components: rustfmt, clippy - name: Setup LLVM & Clang From b65477f3b7f08a6a256cd4cd497a98eecc5cc3c0 Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 22:33:13 +1300 Subject: [PATCH 31/34] Set LLVM version --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 658ff256e5..ddcb90aa94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,12 +44,12 @@ jobs: id: clang uses: KyleMayes/install-llvm-action@v1 with: - version: ${{ matrix.llvm }} - directory: ${{ runner.temp }}/llvm-${{ matrix.llvm }} + version: '13.0' + directory: ${{ runner.temp }}/llvm - name: Configure Clang if: "!contains(matrix.os, 'windows')" run: | - echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.llvm }}/lib" >> $GITHUB_ENV + echo "LIBCLANG_PATH=${{ runner.temp }}/llvm/lib" >> $GITHUB_ENV echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV - name: Configure Clang (macOS only) if: "contains(matrix.os, 'macos')" From cd28d24a87bc2e39102ee53ee94c632a323a40ae Mon Sep 17 00:00:00 2001 From: David Cole Date: Wed, 16 Mar 2022 23:13:58 +1300 Subject: [PATCH 32/34] Only build docs.rs on Ubuntu PHP 8.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddcb90aa94..baed461434 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,7 +74,7 @@ jobs: command: clippy args: --all -- -D warnings - name: Build with docs stub - if: "contains(matrix.os, 'ubuntu') && ${{ matrix.php }} == '8.1'" + if: "contains(matrix.os, 'ubuntu') && matrix.php == '8.1'" env: DOCS_RS: run: From 4787762fb46d4def2cebb7514847ebd1af339c27 Mon Sep 17 00:00:00 2001 From: David Cole Date: Thu, 17 Mar 2022 23:38:18 +1300 Subject: [PATCH 33/34] Fix build on Linux thread-safe --- allowed_bindings.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/allowed_bindings.rs b/allowed_bindings.rs index bdada29b73..bd3a651f28 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -195,5 +195,7 @@ bind! { ZEND_ACC_NOT_SERIALIZABLE, executor_globals, php_printf, - __zend_malloc + __zend_malloc, + tsrm_get_ls_cache, + executor_globals_offset } From ce45dc6dd76608b988208852d0a091f70934dea3 Mon Sep 17 00:00:00 2001 From: David Cole Date: Fri, 18 Mar 2022 00:25:19 +1300 Subject: [PATCH 34/34] Update guide example --- .cargo/config.toml | 3 +++ guide/src/examples/hello_world.md | 45 +++++++++++++++++++------------ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 08138800de..fd663c7cd6 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,6 @@ rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] [target.x86_64-pc-windows-msvc] linker = "rust-lld" + +[target.i686-pc-windows-msvc] +linker = "rust-lld" diff --git a/guide/src/examples/hello_world.md b/guide/src/examples/hello_world.md index 7d7ae11534..b650bc4b1f 100644 --- a/guide/src/examples/hello_world.md +++ b/guide/src/examples/hello_world.md @@ -8,11 +8,11 @@ $ cargo new hello_world --lib $ cd hello_world ``` +### `Cargo.toml` + Let's set up our crate by adding `ext-php-rs` as a dependency and setting the crate type to `cdylib`. Update the `Cargo.toml` to look something like so: -### `Cargo.toml` - ```toml [package] name = "hello_world" @@ -20,24 +20,32 @@ version = "0.1.0" edition = "2018" [dependencies] -ext-php-rs = "0.2" +ext-php-rs = "*" [lib] crate-type = ["cdylib"] ``` -As the linker will not be able to find the PHP installation that we are -dynamically linking to, we need to enable dynamic linking with undefined -symbols. We do this by creating a Cargo config file in `.cargo/config.toml` with -the following contents: - ### `.cargo/config.toml` +When compiling for Linux and macOS, we do not link directly to PHP, rather PHP +will dynamically load the library. We need to tell the linker it's ok to have +undefined symbols (as they will be resolved when loaded by PHP). + +On Windows, we also need to switch to using the `rust-lld` linker. + +> Microsoft Visual C++'s `link.exe` is supported, however you may run into +> issues if your linker is not compatible with the linker used to compile PHP. + +We do this by creating a Cargo config file in `.cargo/config.toml` with the +following contents: + ```toml -[build] -rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] +{{#include ../../../.cargo/config.toml}} ``` +### `src/lib.rs` + Let's actually write the extension code now. We start by importing the `ext-php-rs` prelude, which contains most of the imports required to make a basic extension. We will then write our basic `hello_world` function, which will @@ -47,14 +55,16 @@ your module. The `#[php_module]` attribute automatically registers your new function so we don't need to do anything except return the `ModuleBuilder` that we were given. -### `src/lib.rs` +We also need to enable the `abi_vectorcall` feature when compiling for Windows. +This is a nightly-only feature so it is recommended to use the `#[cfg_attr]` +macro to not enable the feature on other operating systems. ```rust,ignore -# #![cfg_attr(windows, feature(abi_vectorcall))] +#![cfg_attr(windows, feature(abi_vectorcall))] use ext_php_rs::prelude::*; #[php_function] -pub fn hello_world(name: String) -> String { +pub fn hello_world(name: &str) -> String { format!("Hello, {}!", name) } @@ -64,10 +74,10 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { } ``` -Let's make a test script. - ### `test.php` +Let's make a test script. + ```php