Windows support (#128)

* Preliminary Windows support

* Start work on cross-platform build script

* Fix compilation on macOS

* Updated README, tidied up build script

* Check linker version before starting compilation

It doesn't seem like it's possible to change the linker from within the
build script, however, we can retrieve the linker in use and give the
user a suggestion if the linker will not work.

* Switch to using Github repository for bindgen

* Split Windows and Unix implementations into two files

* Fix building on Windows

* Remove `reqwest` and `zip` as dependencies on Unix

* Fix guide tests on Windows

* Started work on Windows CI

* runs -> run

* Use preinstalled LLVM on Windows

* Debugging for Windows CI

* Switch to upstream `rust-bindgen` master branch

* Switch to `rust-lld` for Windows linking

* Don't compile `cargo-php` on Windows

* Switch to using skeptic for tests

* cargo-php: Disable stub generation, fix ext install/remove

The plan is to replace the stub generation by generating them with PHP
code. This is cross-platform and means we don't need to worry about ABI.
We also don't need to embed information into the library.

* cargo-php: Fix on unix OS

* Fix clippy lint

* Updated README

* Re-add CI for Unix + PHP 8.0

* Fix building on thread-safe PHP

* Tidy up build scripts

* Use dynamic lookup on Linux, test with TS Windows

* Define `ZTS` when compiling PHP ZTS

* Combine Windows and Unix CI, fix linking for Win32TS

* Fix exclusions in build CI

* rust-toolchain -> rust

* Set LLVM version

* Only build docs.rs on Ubuntu PHP 8.1

* Fix build on Linux thread-safe

* Update guide example
This commit is contained in:
David Cole 2022-03-18 16:36:51 +13:00 committed by GitHub
parent 7520720558
commit 664981f4fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1015 additions and 380 deletions

View File

@ -1,2 +0,0 @@
[build]
rustflags = ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]

8
.cargo/config.toml Normal file
View File

@ -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"

View File

@ -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:

View File

@ -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 = []

View File

@ -1,15 +1,24 @@
# ext-php-rs
[<img align="right" src="https://discord.com/api/guilds/115233111977099271/widget.png?style=banner2">](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: <https://docs.rs/ext-php-rs>
- Guide: <https://davidcole1340.github.io/ext-php-rs>
## 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
<https://windows.php.net>. 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

View File

@ -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
}

322
build.rs
View File

@ -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<Self>;
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<Vec<PathBuf>>;
// 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<Vec<(&'static str, &'static str)>>;
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.");
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);
/// Prints any extra link arguments.
fn print_extra_link_args(&self) -> Result<()> {
Ok(())
}
}
// 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")
/// Finds the location of an executable `name`.
fn find_executable(name: &str) -> Option<PathBuf> {
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<PathBuf> {
// If PHP path is given via env, it takes priority.
let env = std::env::var("PHP");
if let Ok(env) = env {
return Ok(env.into());
}
find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.")
}
pub struct PHPInfo(String);
impl PHPInfo {
pub fn get(php: &Path) -> Result<Self> {
let cmd = Command::new(php)
.arg("-i")
.output()
.expect("Unable to run `php -i`. Please ensure it is visible in your PATH.");
if !php_i_cmd.status.success() {
let stderr = str::from_utf8(&includes_cmd.stderr).unwrap_or("Unable to read stderr");
panic!("Error running `php -i`: {}", stderr);
.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::<u32>().ok())
.expect("Unable to retrieve PHP API version from `php -i`.");
// Only present on Windows.
#[cfg(windows)]
pub fn architecture(&self) -> Result<impl_::Arch> {
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()
}
pub fn thread_safety(&self) -> Result<bool> {
Ok(self
.get_key("Thread Safety")
.context("Could not find thread safety of PHP")?
== "enabled")
}
pub fn debug(&self) -> Result<bool> {
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<u32> {
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<String> {
let mut bindgen = bindgen::Builder::default()
.header("src/wrapper.h")
.clang_args(
includes
.iter()
.map(|inc| format!("-I{}", inc.to_string_lossy())),
)
.clang_args(
defines
.iter()
.map(|(var, val)| format!("-D{}={}", var, val)),
)
.rustfmt_bindings(true)
.no_copy("_zval_struct")
.no_copy("_zend_string")
.no_copy("_zend_array")
.no_debug("_zend_function_entry") // On Windows when the handler uses vectorcall, Debug cannot be derived so we do it in code.
.layout_tests(env::var("EXT_PHP_RS_TEST").is_ok())
.rust_target(RustTarget::Nightly);
for binding in ALLOWED_BINDINGS.iter() {
bindgen = bindgen
.allowlist_function(binding)
.allowlist_type(binding)
.allowlist_var(binding);
}
let bindings = bindgen
.generate()
.map_err(|_| anyhow!("Unable to generate bindings for PHP"))?
.to_string();
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);
Ok(())
}
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");
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());
}
if configure.debug() {
let php = find_php()?;
let info = PHPInfo::get(&php)?;
let provider = Provider::new(&info)?;
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()?;
struct Configure(String);
// 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);
impl Configure {
pub fn get() -> Self {
let cmd = Command::new("php-config")
.arg("--configure-options")
.output()
.expect("Unable to run `php-config --configure-options`. Please ensure it is visible in your PATH.");
if !cmd.status.success() {
let stderr = String::from_utf8(cmd.stderr)
.unwrap_or_else(|_| String::from("Unable to read stderr"));
panic!("Error running `php -i`: {}", stderr);
}
// check for the ZTS feature flag in configure
let stdout =
String::from_utf8(cmd.stdout).expect("Unable to read stdout from `php-config`.");
Self(stdout)
}
pub fn has_zts(&self) -> bool {
self.0.contains("--enable-zts")
}
pub fn debug(&self) -> bool {
self.0.contains("--enable-debug")
}
Ok(())
}
// Mock macro for the `allowed_bindings.rs` script.

View File

@ -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<PathBuf>,
}
#[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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<PathBuf> {
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<F>(&self, f: F, ctx: &str) -> AResult<String>
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<PathBuf>) -> AResult<cargo_metadata::Target> {
// TODO(david): Look for cargo manifest option or env

View File

@ -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 {

View File

@ -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"

View File

@ -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<TokenStream> {
if let Some(abi) = &mut input.sig.abi {
abi.name = Some(LitStr::new(ABI, Span::call_site()));
}
Ok(input.to_token_stream())
}

View File

@ -68,8 +68,9 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi
let func = quote! {
#input
::ext_php_rs::zend_fastcall! {
#[doc(hidden)]
pub extern "C" fn #internal_ident(ex: &mut ::ext_php_rs::zend::ExecuteData, retval: &mut ::ext_php_rs::types::Zval) {
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)*
@ -82,6 +83,7 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi
e.throw().expect("Failed to throw exception");
}
}
}
};
let mut state = STATE.lock();

View File

@ -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()
}

View File

@ -180,8 +180,9 @@ pub fn parser(
quote! {
#input
::ext_php_rs::zend_fastcall! {
#[doc(hidden)]
pub extern "C" fn #internal_ident(
pub extern fn #internal_ident(
ex: &mut ::ext_php_rs::zend::ExecuteData,
retval: &mut ::ext_php_rs::types::Zval
) {
@ -198,6 +199,7 @@ pub fn parser(
}
}
}
}
};
let method = Method {

View File

@ -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
<?php
@ -80,8 +91,9 @@ executable is able to be found by the `ext-php-rs` build script.
The extension is stored inside `target/debug` (if you did a debug build,
`target/release` for release builds). The file name will be based on your crate
name, so for us it will be `libhello_world`. The extension is based on your OS -
on Linux it will be `libhello_world.so` and on macOS it will be
`libhello_world.dylib`.
on Linux it will be `libhello_world.so`, on macOS it will be
`libhello_world.dylib` and on Windows it will be `hello_world.dll` (no `lib`
prefix).
```sh
$ cargo build

View File

@ -26,7 +26,8 @@ the `#[php_function]` attribute.
### Examples
```rust
```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use std::convert::TryInto;
@ -43,6 +44,7 @@ pub fn something_fallible(n: u64) -> PhpResult<u32> {
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

View File

@ -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<i32> {
# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
# module
# }
# fn main() {}
```

View File

@ -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

View File

@ -13,7 +13,8 @@ of `Option<T>`. The macro will then figure out which parameters are optional by
using the last consecutive arguments that are a variant of `Option<T>` 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<i32>) -> 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<usize>
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<T>`, the `Option<T>` 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<i32>, 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<i32>, description: Option<String>) -> Str
greeting
}
# fn main() {}
```
## Returning `Result<T, E>`

View File

@ -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:

View File

@ -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};

View File

@ -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() {}
```

View File

@ -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<T: PartialEq<i32>> {
pub fn take_object(obj: CompareVals<i32>) {
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:

View File

@ -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<u32>) -> Binary<u32> {
.into_iter()
.collect::<Binary<_>>()
}
# fn main() {}
```
## PHP Usage

View File

@ -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

View File

@ -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() {}
```

View File

@ -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<dyn FnMut(i32) -> 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<dyn FnOnce() -> 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() {}
```

View File

@ -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<String, String>) -> Vec<String> {
.map(|(_, v)| v)
.collect::<Vec<_>>()
}
# fn main() {}
```
## PHP example

View File

@ -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

View File

@ -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<ZendObject> {
# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
# module
# }
# fn main() {}
```
[class object]: ./class_object.md

View File

@ -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<String>) -> Option<String> {
input.map(|input| format!("Hello {}", input).into())
}
# fn main() {}
```
## PHP example

View File

@ -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

View File

@ -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

View File

@ -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>) -> String {
vec.join(" ")
}
# fn main() {}
```
## PHP example

View File

@ -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,7 +168,8 @@ impl ClassBuilder {
obj.into_raw().get_mut_zend_obj()
}
extern "C" fn constructor<T: RegisteredClass>(ex: &mut ExecuteData, _: &mut Zval) {
zend_fastcall! {
extern fn constructor<T: RegisteredClass>(ex: &mut ExecuteData, _: &mut Zval) {
let ConstructorMeta { constructor, .. } = match T::CONSTRUCTOR {
Some(c) => c,
None => {
@ -198,6 +200,7 @@ impl ClassBuilder {
};
this_obj.initialize(this);
}
}
debug_assert_eq!(
self.name.as_str(),

View File

@ -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)]

View File

@ -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::<ModuleEntry>() 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(),

View File

@ -12,6 +12,7 @@ use crate::{
props::Property,
types::Zval,
zend::ExecuteData,
zend_fastcall,
};
/// Class entry and handlers for Rust closures.
@ -137,6 +138,7 @@ impl Closure {
CLOSURE_META.set_ce(ce);
}
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::<Self>();
@ -145,6 +147,7 @@ impl Closure {
this.0.invoke(parser, ret)
}
}
}
impl RegisteredClass for Closure {
const CLASS_NAME: &'static str = "RustClosure";

View File

@ -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)
);
}
}

View File

@ -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"));

View File

@ -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<String>, age: Option<i32>) -> 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<T: PartialEq<i32>> {
@ -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;

View File

@ -1,32 +1,25 @@
#include "wrapper.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) {
return zend_string_init(str, len, persistent);
}
void ext_php_rs_zend_string_release(zend_string *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)
{
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)
{
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);

View File

@ -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);

View File

@ -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 {

4
tests/guide.rs Normal file
View File

@ -0,0 +1,4 @@
#![allow(clippy::all)]
#![allow(warnings)]
include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs"));

42
unix_build.rs Normal file
View File

@ -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<String> {
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<Self> {
Ok(Self {})
}
fn get_includes(&self) -> Result<Vec<PathBuf>> {
Ok(self
.php_config("--includes")?
.split(' ')
.map(|s| s.trim_start_matches("-I"))
.map(PathBuf::from)
.collect())
}
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
Ok(vec![])
}
}

238
windows_build.rs Normal file
View File

@ -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<String> {
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<Self> {
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<Vec<PathBuf>> {
Ok(self.devel.include_paths())
}
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
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<PathBuf> {
// `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<Self> {
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<DevelPack> {
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<PathBuf> {
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<PathBuf> {
let includes = self.includes();
["", "main", "Zend", "TSRM", "ext"]
.iter()
.map(|p| includes.join(p))
.collect()
}
}