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/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..fd663c7cd6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.'cfg(not(target_os = "windows"))'] +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/.github/workflows/build.yml b/.github/workflows/build.yml index 8eff9d004a..baed461434 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,17 +11,19 @@ jobs: 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 @@ -29,44 +31,38 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + env: + phpts: ${{ matrix.phpts }} - 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 + if: "!contains(matrix.os, 'windows')" 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')" 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 + args: --release --all --all-features - name: Run rustfmt uses: actions-rs/cargo@v1 with: @@ -78,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: diff --git a/Cargo.toml b/Cargo.toml index 0c80d8bb8e..184279b67c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,20 @@ 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] -bindgen = { version = "0.59" } -regex = "1" +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] +ureq = { version = "2.4", features = ["native-tls", "gzip"], default-features = false } +native-tls = "0.2" +zip = "0.5" [features] closure = [] diff --git a/README.md b/README.md index 369e2b9062..8a0059ca77 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,24 @@ # 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: ```rust +#![cfg_attr(windows, feature(abi_vectorcall))] + use ext_php_rs::prelude::*; /// Gives you a nice greeting! @@ -104,16 +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 or Darwin-based OS -- 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 Requirements + +- Extensions can only be compiled for PHP installations sourced from + . 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 @@ -126,16 +156,12 @@ 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 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/allowed_bindings.rs b/allowed_bindings.rs index ef834d90e1..bd3a651f28 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, @@ -189,10 +189,13 @@ 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, executor_globals, php_printf, - __zend_malloc + __zend_malloc, + tsrm_get_ls_cache, + executor_globals_offset } diff --git a/build.rs b/build.rs index 69f6fc0b4b..ec4153f6c7 100644 --- a/build.rs +++ b/build.rs @@ -1,111 +1,167 @@ +#[cfg_attr(windows, path = "windows_build.rs")] +#[cfg_attr(not(windows), path = "unix_build.rs")] +mod impl_; + use std::{ env, + fs::File, + io::{BufWriter, Write}, path::{Path, PathBuf}, process::Command, - str, + str::FromStr, }; -use regex::Regex; +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; -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"); +pub trait PHPProvider<'a>: Sized { + /// Create a new PHP provider. + fn new(info: &'a PHPInfo) -> Result; - let out_dir = env::var_os("OUT_DIR").expect("Failed to get OUT_DIR"); - let out_path = PathBuf::from(out_dir).join("bindings.rs"); + /// Retrieve a list of absolute include paths. + fn get_includes(&self) -> Result>; - // 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"); + /// Retrieve a list of macro definitions to pass to the compiler. + fn get_defines(&self) -> Result>; - std::fs::copy("docsrs_bindings.rs", out_path) - .expect("Unable to copy docs.rs stub bindings to output directory."); - return; + /// 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(()) } - // 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."); + /// Prints any extra link arguments. + fn print_extra_link_args(&self) -> Result<()> { + Ok(()) + } +} - 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); +/// 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()?; + if cmd.status.success() { + let stdout = String::from_utf8_lossy(&cmd.stdout); + Some(stdout.trim().into()) + } else { + None } +} - // 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."); +/// 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"); + if let Ok(env) = env { + return Ok(env.into()); + } - 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); + find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") +} + +pub 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())) } - 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`."); + // Only present on Windows. + #[cfg(windows)] + pub fn architecture(&self) -> Result { + use std::convert::TryInto; - 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); + self.get_key("Architecture") + .context("Could not find architecture of PHP")? + .try_into() } - // 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; + pub fn thread_safety(&self) -> Result { + Ok(self + .get_key("Thread Safety") + .context("Could not find thread safety of PHP")? + == "enabled") + } - if api_ver >= PHP_81_API_VER { - println!("cargo:rustc-cfg=php81"); + 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")) } - let includes = - String::from_utf8(includes_cmd.stdout).expect("unable to parse `php-config` stdout"); + 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 + } +} - // Build `wrapper.c` and link to Rust. - cc::Build::new() +/// Builds the wrapper library. +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( - str::replace(includes.as_ref(), "-I", "") - .split(' ') - .map(Path::new), - ) - .compile("wrapper"); + .includes(includes) + .try_compile("wrapper") + .context("Failed to compile ext-php-rs C interface")?; + 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") - .clang_args(includes.split(' ')) - .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + .clang_args( + includes + .iter() + .map(|inc| format!("-I{}", inc.to_string_lossy())), + ) + .clang_args( + defines + .iter() + .map(|(var, val)| format!("-D{}={}", var, val)), + ) .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()); + .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 @@ -114,51 +170,89 @@ fn main() { .allowlist_var(binding); } - bindgen + let bindings = bindgen .generate() - .expect("Unable to generate bindings for PHP") - .write_to_file(out_path) - .expect("Unable to write bindings file."); + .map_err(|_| anyhow!("Unable to generate bindings for PHP"))? + .to_string(); - let configure = Configure::get(); + Ok(bindings) +} - if configure.has_zts() { - println!("cargo:rustc-cfg=php_zts"); +/// Checks the PHP Zend API version for compatibility with ext-php-rs, setting +/// any configuration flags required. +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); } - if configure.debug() { - println!("cargo:rustc-cfg=php_debug"); + // 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 version >= PHP_81_API_VER { + println!("cargo:rustc-cfg=php81"); } + + Ok(()) } -struct Configure(String); +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"), + manifest.join("windows_build.rs"), + manifest.join("unix_build.rs"), + ] { + println!("cargo:rerun-if-changed={}", path.to_string_lossy()); + } -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."); + let php = find_php()?; + let info = PHPInfo::get(&php)?; + let provider = Provider::new(&info)?; - 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); - } + let includes = provider.get_includes()?; + let defines = provider.get_defines()?; - // 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) - } + check_php_version(&info)?; + build_wrapper(&defines, &includes)?; + let bindings = generate_bindings(&defines, &includes)?; - pub fn has_zts(&self) -> bool { - self.0.contains("--enable-zts") - } + 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)?; - pub fn debug(&self) -> bool { - self.0.contains("--enable-debug") + if info.debug()? { + println!("cargo:rustc-cfg=php_debug"); } + if info.thread_safety()? { + println!("cargo:rustc-cfg=php_zts"); + } + 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(std::ffi::OsStr::new("closure"))) + .collect(); + skeptic::generate_doc_tests(&test_md); + + Ok(()) } // Mock macro for the `allowed_bindings.rs` script. diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 9f153d36d3..dba7f0a2e0 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,8 +338,12 @@ 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 { @@ -308,7 +355,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 +395,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..75c700ef77 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,10 +1,12 @@ // Mock macro for the `allowed_bindings.rs` script. +#[cfg(not(windows))] macro_rules! bind { ($($s: ident),*) => { cargo_php::stub_symbols!($($s),*); } } +#[cfg(not(windows))] include!("../allowed_bindings.rs"); fn main() -> cargo_php::CrateResult { 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/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..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,13 +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))] use ext_php_rs::prelude::*; #[php_function] -pub fn hello_world(name: String) -> String { +pub fn hello_world(name: &str) -> String { format!("Hello, {}!", name) } @@ -63,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 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 e44246ae8e..9e6a2b011e 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -36,7 +36,8 @@ 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::*; #[php_class] @@ -50,12 +51,14 @@ 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::*; use ext_php_rs::{exception::PhpException, zend::ce}; @@ -74,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 77e1cc6318..72f83446ad 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -5,7 +5,8 @@ that implements `IntoConst`. ## Examples -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_const] @@ -13,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 9e7c549cbb..30fdb84d48 100644 --- a/guide/src/macros/function.md +++ b/guide/src/macros/function.md @@ -13,7 +13,8 @@ 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::*; #[php_function] @@ -26,13 +27,15 @@ 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::*; #[php_function(defaults(offset = 0))] @@ -40,13 +43,15 @@ 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::*; /// `age` will be deemed required and nullable rather than optional. @@ -61,13 +66,15 @@ 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::*; /// `age` will be deemed required and nullable rather than optional, @@ -86,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 95d9423f32..e342dfc676 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -95,7 +95,8 @@ 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}; # #[php_class] @@ -146,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.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..cd950171d9 100644 --- a/guide/src/macros/module_startup.md +++ b/guide/src/macros/module_startup.md @@ -16,11 +16,13 @@ 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::*; #[php_startup] pub fn startup_function() { } +# fn main() {} ``` diff --git a/guide/src/macros/zval_convert.md b/guide/src/macros/zval_convert.md index 9ed476141d..876a4b1757 100644 --- a/guide/src/macros/zval_convert.md +++ b/guide/src/macros/zval_convert.md @@ -13,7 +13,8 @@ all generics types. ### Examples -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -37,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: @@ -55,7 +58,8 @@ 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::*; @@ -70,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 @@ -92,7 +98,8 @@ 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::*; @@ -113,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 ea6d72548f..aa296b16cd 100644 --- a/guide/src/types/binary.md +++ b/guide/src/types/binary.md @@ -21,7 +21,8 @@ f32, f64). ## Rust Usage -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; use ext_php_rs::binary::Binary; @@ -36,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 11a8c797a9..1974ff305a 100644 --- a/guide/src/types/bool.md +++ b/guide/src/types/bool.md @@ -22,7 +22,8 @@ enum Zval { ## Rust example -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] @@ -33,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 309d46d92c..ed6125cf4a 100644 --- a/guide/src/types/class_object.md +++ b/guide/src/types/class_object.md @@ -12,7 +12,8 @@ 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}; @@ -35,11 +36,13 @@ 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::*; @@ -59,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 9a41a4b842..d1586caa52 100644 --- a/guide/src/types/closure.md +++ b/guide/src/types/closure.md @@ -42,7 +42,8 @@ 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::*; @@ -65,6 +66,7 @@ pub fn closure_count() -> Closure { count }) as Box i32>) } +# fn main() {} ``` ## `FnOnce` @@ -81,7 +83,8 @@ will be thrown. ### Example -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; use ext_php_rs::prelude::*; @@ -94,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 @@ -107,7 +111,8 @@ 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::*; @@ -116,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 ddc06b0147..d0003e2b2f 100644 --- a/guide/src/types/hashmap.md +++ b/guide/src/types/hashmap.md @@ -16,7 +16,8 @@ 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::*; # use std::collections::HashMap; @@ -30,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 b50636ad6c..68755a1dbf 100644 --- a/guide/src/types/numbers.md +++ b/guide/src/types/numbers.md @@ -21,7 +21,8 @@ fallible. ## Rust example -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] @@ -29,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 de7fe36a31..c953d99d36 100644 --- a/guide/src/types/object.md +++ b/guide/src/types/object.md @@ -17,7 +17,8 @@ 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}; @@ -31,11 +32,13 @@ 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}; @@ -50,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 e2391d121e..8acb48765f 100644 --- a/guide/src/types/option.md +++ b/guide/src/types/option.md @@ -18,13 +18,15 @@ 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::*; #[php_function] 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 08759291ba..26688553c5 100644 --- a/guide/src/types/str.md +++ b/guide/src/types/str.md @@ -17,7 +17,8 @@ PHP strings. ## Rust example -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] @@ -29,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 fa7fd137b7..317bcf994f 100644 --- a/guide/src/types/string.md +++ b/guide/src/types/string.md @@ -16,13 +16,15 @@ 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::*; #[php_function] 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 03b5b81173..62dcf3e9e9 100644 --- a/guide/src/types/vec.md +++ b/guide/src/types/vec.md @@ -18,13 +18,15 @@ fail. ## Rust example -```rust +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] # extern crate ext_php_rs; # use ext_php_rs::prelude::*; #[php_function] pub fn test_vec(vec: Vec) -> String { vec.join(" ") } +# fn main() {} ``` ## PHP example diff --git a/src/builders/class.rs b/src/builders/class.rs index 9fe57b5f55..003361ac3b 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. @@ -69,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 } @@ -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/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/describe/stub.rs b/src/describe/stub.rs index 6673a7cbcd..067d720602 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -366,7 +366,7 @@ fn indent(s: &str, depth: usize) -> String { #[cfg(test)] mod test { - use super::{indent, split_namespace}; + use super::split_namespace; #[test] pub fn test_split_ns() { @@ -376,8 +376,15 @@ 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("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..79d2333032 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 @@ -67,6 +74,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// # Example /// /// ``` +/// # #![cfg_attr(windows, feature(abi_vectorcall))] /// # use ext_php_rs::prelude::*; /// #[php_const] /// const TEST_CONSTANT: i32 = 100; @@ -111,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] @@ -176,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) @@ -220,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 { @@ -236,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 { @@ -262,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 { @@ -332,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)] @@ -401,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 { @@ -448,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 { @@ -466,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; @@ -503,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() { @@ -537,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> { @@ -575,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> { @@ -613,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> { @@ -650,3 +671,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.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 2793fe213e..f55f3eca14 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -1,10 +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. +// +// 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" #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); 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 { diff --git a/tests/guide.rs b/tests/guide.rs new file mode 100644 index 0000000000..15958889db --- /dev/null +++ b/tests/guide.rs @@ -0,0 +1,4 @@ +#![allow(clippy::all)] +#![allow(warnings)] + +include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs")); 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..cdf2da0a54 --- /dev/null +++ b/windows_build.rs @@ -0,0 +1,238 @@ +use std::{ + convert::TryFrom, + fmt::Display, + io::{Cursor, Read, Write}, + path::{Path, PathBuf}, + process::Command, + sync::Arc, +}; + +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()) + } +} + +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)?; + 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 }) + } + + fn get_includes(&self) -> Result> { + Ok(self.devel.include_paths()) + } + + fn get_defines(&self) -> Result> { + 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<()> { + // 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(()) + } +} + +/// 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()) +} + +/// 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; + } + } + false +} + +#[derive(Debug, PartialEq, Eq)] +pub 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", + } + ) + } +} + +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 response = ureq::AgentBuilder::new() + .tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap())) + .build() + .get(&url) + .set("User-Agent", USER_AGENT) + .call() + .context("Failed to download development pack")?; + 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 + .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 { + 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. + pub fn include_paths(&self) -> Vec { + let includes = self.includes(); + ["", "main", "Zend", "TSRM", "ext"] + .iter() + .map(|p| includes.join(p)) + .collect() + } +}