diff --git a/.cargo/config b/.cargo/config
deleted file mode 100644
index 0ba0090..0000000
--- 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 0000000..fd663c7
--- /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 8eff9d0..baed461 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 3f47d1c..c514b51 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.4", 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 369e2b9..8a0059c 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
+- 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.
-See the following links for the dependency crate requirements:
+### Windows Requirements
-- [`cc`](https://github.com/alexcrichton/cc-rs#compile-time-requirements)
-- [`bindgen`](https://rust-lang.github.io/rust-bindgen/requirements.html)
+- 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 ef834d9..bd3a651 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 69f6fc0..ec4153f 100644
--- a/build.rs
+++ b/build.rs
@@ -1,72 +1,190 @@
+#[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
+ }
+}
+
+/// 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());
}
- // 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.");
+ find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.")
+}
- 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);
+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"),
+ // Only present on Windows.
+ #[cfg(windows)]
+ pub fn architecture(&self) -> Result {
+ use std::convert::TryInto;
+
+ self.get_key("Architecture")
+ .context("Could not find architecture of PHP")?
+ .try_into()
+ }
+
+ 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
+ }
+}
+
+/// 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(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
+ .iter()
+ .map(|inc| format!("-I{}", inc.to_string_lossy())),
)
- .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`.");
+ .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")
+ .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);
- 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);
+ 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();
+
+ Ok(bindings)
+}
+
+/// 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);
}
// Infra cfg flags - use these for things that change in the Zend API that don't
@@ -80,85 +198,61 @@ fn main() {
// should get both the `php81` and `php82` flags.
const PHP_81_API_VER: u32 = 20210902;
- if api_ver >= PHP_81_API_VER {
+ if version >= 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 = Configure::get();
-
- if configure.has_zts() {
- println!("cargo:rustc-cfg=php_zts");
- }
-
- if configure.debug() {
- println!("cargo:rustc-cfg=php_debug");
- }
+ Ok(())
}
-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)
+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());
}
- pub fn has_zts(&self) -> bool {
- self.0.contains("--enable-zts")
- }
+ let php = find_php()?;
+ let info = PHPInfo::get(&php)?;
+ let provider = Provider::new(&info)?;
- pub fn debug(&self) -> bool {
- self.0.contains("--enable-debug")
+ let includes = provider.get_includes()?;
+ let defines = provider.get_defines()?;
+
+ check_php_version(&info)?;
+ build_wrapper(&defines, &includes)?;
+ let bindings = generate_bindings(&defines, &includes)?;
+
+ 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");
}
+ 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 9f153d3..dba7f0a 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 d62ac98..75c700e 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 5fa4b7b..75770b7 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 0000000..5d8e4a2
--- /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 bdd7dea..4c9520c 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 2ac9814..e744703 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 c038088..d6bbaab 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;
+ ::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 = #this #ident(#(#arg_accessors,)*);
+ 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");
+ 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 ea982ed..b650bc4 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 e44246a..9e6a2b0 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 77e1cc6..72f8344 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 9e7c549..30fdb84 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 95d9423..e342dfc 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 750134d..273fd43 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 610555e..cd95017 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 9ed4761..876a4b1 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 ea6d725..aa296b1 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 11a8c79..1974ff3 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 309d46d..ed6125c 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 9a41a4b..d1586ca 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 ddc06b0..d0003e2 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 b50636a..68755a1 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 de7fe36..c953d99 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 e2391d1..8acb487 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 0875929..2668855 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 fa7fd13..317bcf9 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 03b5b81..62dcf3e 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 9fe57b5..003361a 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 64fc7a0..1fd3bbf 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 f4106db..a8aeccd 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 9767d9d..03e0265 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 6673a7c..067d720 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 46f9e74..e750f47 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 212cc45..79d2333 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 5dfcec5..240b2d6 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 &executor_globals;
+ return TSRMG_FAST_BULK(executor_globals_offset, zend_executor_globals *);
+#endif
+#else
+ return &executor_globals;
#endif
}
diff --git a/src/wrapper.h b/src/wrapper.h
index 2793fe2..f55f3ec 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 11c7fcd..ba889d7 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 0000000..1595888
--- /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 0000000..623d356
--- /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 0000000..cdf2da0
--- /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()
+ }
+}