diff --git a/.github/actions/zts/Dockerfile b/.github/actions/zts/Dockerfile index 75c6ffc..1a87d08 100644 --- a/.github/actions/zts/Dockerfile +++ b/.github/actions/zts/Dockerfile @@ -1,4 +1,4 @@ -FROM php:zts +FROM php:8.1-zts WORKDIR /tmp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 315d2f8..73feb9b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,8 @@ name: Build and Lint on: + schedule: + # runs every monday at midnight + - cron: "0 0 * * 1" push: branches: - master @@ -12,8 +15,9 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - php: ['8.0', '8.1'] + php: ["8.0", "8.1"] rust: [stable, nightly] + clang: ["14"] phpts: [ts, nts] exclude: # ext-php-rs requires nightly Rust when on Windows. @@ -24,6 +28,8 @@ jobs: phpts: ts - os: ubuntu-latest phpts: ts + env: + CARGO_TERM_COLOR: always steps: - name: Checkout code uses: actions/checkout@v3 @@ -34,52 +40,67 @@ jobs: env: phpts: ${{ matrix.phpts }} - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - override: true components: rustfmt, clippy - - name: Setup LLVM & Clang + - run: rustup show + - name: Cache cargo dependencies + uses: Swatinem/rust-cache@v2 + # Uncomment the following if statement if caching nightly deps + # ends up causing too much cache invalidation. + # if: matrix.rust == 'stable' + with: + # increment this manually to force cache eviction + prefix-key: "v0-rust" + # LLVM & Clang + - name: Cache LLVM and Clang + id: cache-llvm + uses: actions/cache@v3 if: "!contains(matrix.os, 'windows')" + with: + path: ${{ runner.temp }}/llvm-${{ matrix.clang }} + key: ${{ matrix.os }}-llvm-${{ matrix.clang }} + - name: Setup LLVM & Clang id: clang uses: KyleMayes/install-llvm-action@v1 + if: "!contains(matrix.os, 'windows')" with: - version: '13.0' - directory: ${{ runner.temp }}/llvm + version: ${{ matrix.clang }} + directory: ${{ runner.temp }}/llvm-${{ matrix.clang }} + cached: ${{ steps.cache-llvm.outputs.cache-hit }} - name: Configure Clang if: "!contains(matrix.os, 'windows')" run: | - echo "LIBCLANG_PATH=${{ runner.temp }}/llvm/lib" >> $GITHUB_ENV + echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/lib" >> $GITHUB_ENV echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV + echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ matrix.clang }}/bin/llvm-config" >> $GITHUB_ENV - name: Configure Clang (macOS only) if: "contains(matrix.os, 'macos')" run: echo "SDKROOT=$(xcrun --show-sdk-path)" >> $GITHUB_ENV + # Build - name: Build env: - EXT_PHP_RS_TEST: + EXT_PHP_RS_TEST: "" run: cargo build --release --all-features --all + # Test & lint - name: Test inline examples - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --all --all-features + run: cargo test --release --all --all-features - name: Run rustfmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.1' + run: cargo fmt --all -- --check - name: Run clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all -- -D warnings - if: matrix.rust == 'stable' + if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.1' + run: cargo clippy --all -- -D warnings + # Docs + - name: Run rustdoc + if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.1' + run: cargo rustdoc -- -D warnings - name: Build with docs stub - if: "contains(matrix.os, 'ubuntu') && matrix.php == '8.1'" + if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.1' env: - DOCS_RS: - run: - cargo clean && cargo build + DOCS_RS: "" + run: cargo clean && cargo build build-zts: name: Build with ZTS runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8e06f52..0719b64 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,9 @@ name: Deploy documentation on: workflow_dispatch: + # runs every monday at midnight + schedule: + - cron: "0 0 * * 1" push: branches: - master @@ -8,28 +11,38 @@ on: jobs: docs: name: Build and Deploy - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ["ubuntu-latest"] + php: ["8.0"] + clang: ["14"] + mdbook: ["latest"] steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: ${{ matrix.php }} - name: Setup Rust - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@nightly + - name: Cache LLVM and Clang + id: cache-llvm + uses: actions/cache@v3 with: - toolchain: nightly - override: true + path: ${{ runner.temp }}/llvm-${{ matrix.clang }} + key: ${{ matrix.os }}-llvm-${{ matrix.clang }} - name: Setup LLVM & Clang uses: KyleMayes/install-llvm-action@v1 with: - version: 11.0 - directory: ${{ runner.temp }}/llvm-11.0 + version: ${{ matrix.clang }} + directory: ${{ runner.temp }}/llvm-${{ matrix.clang }} + cached: ${{ steps.cache-llvm.outputs.cache-hit }} - name: Install mdbook uses: peaceiris/actions-mdbook@v1 with: - mdbook-version: latest + mdbook-version: ${{ matrix.mdbook }} - name: Build guide run: mdbook build guide - name: Publish docs diff --git a/.gitignore b/.gitignore index 4f29e17..89e3a70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ Cargo.lock /.vscode /.idea -expand.rs +/tmp +expand.rs \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 457613a..37c0a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,125 @@ # Changelog +## Version 0.9.0 + +- ci+docs: honour PHP_CONFIG & rebuild automatically when env vars change by @julius [#210] +- chore: Update generated FFI bindings with bindgen 0.63 by @ptondereau [#211] + +**BC changes** +- feat: allows ZendStr to contain null bytes by @julius [#202] + +**Migration** +See: [#202] + +[#202]: https://github.com/davidcole1340/ext-php-rs/pull/202 +[#210]: https://github.com/davidcole1340/ext-php-rs/pull/210 +[#211]: https://github.com/davidcole1340/ext-php-rs/pull/211 + + +## Version 0.8.3 + +- build: Check docs warnings in CI by @davidcole1340 in [#180] +- fix: Fixes inifinte loop in ClassEntry::instance_of() by @ju1ius in [#188] +- fix: Fix binary slice lifetimes by @davidcole1340 in [#181] +- build: Fixes CI workflow configuration by @ju1ius in [#195] +- feat: Add get_id() and hash() methods on ZendObject by @ju1ius in [#196] +- docs: Describes restrictions on generic parameters for `php_class` by @ju1ius in [#194] +- feat: Add instance_of() and get_class_entry() methods on ZendObject by @ju1ius in [#197] + +[#180]: https://github.com/davidcole1340/ext-php-rs/pull/180 +[#188]: https://github.com/davidcole1340/ext-php-rs/pull/188 +[#181]: https://github.com/davidcole1340/ext-php-rs/pull/181 +[#195]: https://github.com/davidcole1340/ext-php-rs/pull/195 +[#196]: https://github.com/davidcole1340/ext-php-rs/pull/196 +[#194]: https://github.com/davidcole1340/ext-php-rs/pull/194 +[#197]: https://github.com/davidcole1340/ext-php-rs/pull/197 + +## Version 0.8.2 + +- Update changelog for latest versions by @striezel in [#161] +- fix building docs on docs.rs by @davidcole1340 in [#165] +- Add some standard zend interfaces by @nikeee in [#164] +- Correct parameter name. by @denzyldick in [#168] +- fix describe when using `#[implements]` by @davidcole1340 in [#169] +- Add example that shows how to implement an interface by @nikeee in [#167] +- add `before` flag to `#[php_startup]` by @davidcole1340 in [#170] +- add ability to define abstract methods by @davidcole1340 in [#171] +- chore(cli): Bump Clap for CLI tool by @ptondereau in [#177] +- fix type links in docs.rs by @davidcole1340 in [#179] + +[#161]: https://github.com/davidcole1340/ext-php-rs/pull/161 +[#165]: https://github.com/davidcole1340/ext-php-rs/pull/165 +[#164]: https://github.com/davidcole1340/ext-php-rs/pull/164 +[#168]: https://github.com/davidcole1340/ext-php-rs/pull/168 +[#169]: https://github.com/davidcole1340/ext-php-rs/pull/169 +[#167]: https://github.com/davidcole1340/ext-php-rs/pull/167 +[#170]: https://github.com/davidcole1340/ext-php-rs/pull/170 +[#171]: https://github.com/davidcole1340/ext-php-rs/pull/171 +[#177]: https://github.com/davidcole1340/ext-php-rs/pull/177 +[#179]: https://github.com/davidcole1340/ext-php-rs/pull/179 + +## Version 0.8.1 + +- 404 /guide doesn't exists. by @denzyldick in [#149] +- Fixed some typos by @denzyldick in [#148] +- Fix a few typos by @striezel in [#150] +- fix causes of some clippy warnings by @striezel in [#152] +- fix more causes of clippy warnings by @striezel in [#157] +- attempt to fix errors related to clap by @striezel in [#158] +- ci: run clippy only on stable Rust channel by @striezel in [#159] +- update actions/checkout in GitHub Actions workflows to v3 by @striezel in + [#151] +- Add ability to set function name on php_function macro by @joehoyle in [#153] +- Specify classes as fully-qualified names in stubs by @joehoyle in [#156] +- Support marking classes as interfaces by @joehoyle in [#155] +- Support marking methods as abstract by @joehoyle in [#154] +- Add php-scrypt as a example project by @PineappleIOnic in [#146] +- Fix ini file duplication and truncation when using cargo-php command by + @roborourke in [#136] +- Allow passing --yes parameter to bypass prompts by @roborourke in [#135] + +[#135]: https://github.com/davidcole1340/ext-php-rs/pull/135 +[#136]: https://github.com/davidcole1340/ext-php-rs/pull/136 +[#146]: https://github.com/davidcole1340/ext-php-rs/pull/146 +[#148]: https://github.com/davidcole1340/ext-php-rs/pull/148 +[#149]: https://github.com/davidcole1340/ext-php-rs/pull/149 +[#150]: https://github.com/davidcole1340/ext-php-rs/pull/150 +[#151]: https://github.com/davidcole1340/ext-php-rs/pull/151 +[#152]: https://github.com/davidcole1340/ext-php-rs/pull/152 +[#153]: https://github.com/davidcole1340/ext-php-rs/pull/153 +[#154]: https://github.com/davidcole1340/ext-php-rs/pull/154 +[#155]: https://github.com/davidcole1340/ext-php-rs/pull/155 +[#156]: https://github.com/davidcole1340/ext-php-rs/pull/156 +[#157]: https://github.com/davidcole1340/ext-php-rs/pull/157 +[#158]: https://github.com/davidcole1340/ext-php-rs/pull/158 +[#159]: https://github.com/davidcole1340/ext-php-rs/pull/159 + +## Version 0.8.0 + +- Windows support by @davidcole1340 in [#128] +- Support for binary slice to avoid extra allocation by @TobiasBengtsson in + [#139] +- Bump dependencies by @ptondereau in [#144] + +[#128]: https://github.com/davidcole1340/ext-php-rs/pull/128 +[#139]: https://github.com/davidcole1340/ext-php-rs/pull/139 +[#144]: https://github.com/davidcole1340/ext-php-rs/pull/144 + +## Version 0.7.4 + +- Fix is_true() / is_false() in Zval by @joehoyle in [#116] +- readme: fix link to guide by @TorstenDittmann in [#120] +- Fix request_(startup|shutdown)_function in ModuleBuilder by @glyphpoch in + [#119] +- Fix CI on macOS by @davidcole1340 in [#126] +- Add ability to pass modifier function for classes by @davidcole1340 in [#127] + +[#116]: https://github.com/davidcole1340/ext-php-rs/pull/116 +[#119]: https://github.com/davidcole1340/ext-php-rs/pull/119 +[#120]: https://github.com/davidcole1340/ext-php-rs/pull/120 +[#126]: https://github.com/davidcole1340/ext-php-rs/pull/126 +[#127]: https://github.com/davidcole1340/ext-php-rs/pull/127 + ## Version 0.7.3 - Upgrade `clap` to `3.0.0-rc3`. [#113] diff --git a/Cargo.toml b/Cargo.toml index 54462d6..8744093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/davidcole1340/ext-php-rs" homepage = "https://github.com/davidcole1340/ext-php-rs" license = "MIT OR Apache-2.0" keywords = ["php", "ffi", "zend"] -version = "0.8.0" +version = "0.9.0" authors = ["David Cole "] edition = "2018" categories = ["api-bindings"] @@ -17,15 +17,14 @@ parking_lot = "0.12.1" cfg-if = "1.0" once_cell = "1.8.0" anyhow = { version = "1", optional = true } -ext-php-rs-derive = { version = "=0.8.0", path = "./crates/macros" } +ext-php-rs-derive = { version = "=0.9.0", path = "./crates/macros" } [dev-dependencies] skeptic = "0.13" [build-dependencies] anyhow = "1" -# bindgen = { version = "0.59" } -bindgen = "0.60" +bindgen = "0.63" cc = "1.0" skeptic = "0.13" diff --git a/README.md b/README.md index 3e546c9..d199760 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,8 @@ Check out one of the example projects: - [opus-php](https://github.com/davidcole1340/opus-php) - Audio encoder for the Opus codec in PHP. - [tomlrs-php](https://github.com/jphenow/tomlrs-php) - TOML data format parser. +- [php-scrypt](https://github.com/appwrite/php-scrypt) - PHP wrapper for the + scrypt password hashing algorithm. ## Contributions diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 972e151..9d0e8a9 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -4,6 +4,20 @@ // exist in the bindings file. Which ever script include!s the bindings must // define the `bind` macro. This allows us to have the list in string format // inside the build script and in macro format inside the CLI crate. +// +// NOTE TO EDITORS: +// When updating this file, you must re-generate the `docsrs_bindings.rs` +// file used by docs.rs to build documentation. To perform this: +// +// $ cargo clean +// $ cargo build +// $ cp target/debug/build/ext-php-rs-e2cb315d27898d01/out/bindings.rs +// docsrs_bindings.rs +// $ git add . && git commit -m "update docs.rs bindings" +// +// The hash after `ext-php-rs-` in the bindings path may change. There should +// be two folders beginning with `ext-php-rs-` in `target/debug/build`, so +// check both for the presense of the bindings file. bind! { HashTable, @@ -29,6 +43,8 @@ bind! { // ext_php_rs_zend_object_release, // ext_php_rs_zend_string_init, // ext_php_rs_zend_string_release, + // ext_php_rs_is_kown_valid_utf8, + // ext_php_rs_set_kown_valid_utf8, object_properties_init, php_info_print_table_end, php_info_print_table_header, @@ -49,6 +65,13 @@ bind! { zend_ce_type_error, zend_ce_unhandled_match_error, zend_ce_value_error, + zend_ce_traversable, + zend_ce_aggregate, + zend_ce_iterator, + zend_ce_arrayaccess, + zend_ce_serializable, + zend_ce_countable, + zend_ce_stringable, zend_class_entry, zend_declare_class_constant, zend_declare_property, diff --git a/build.rs b/build.rs index 518b494..c2c5b9e 100644 --- a/build.rs +++ b/build.rs @@ -43,7 +43,7 @@ pub trait PHPProvider<'a>: Sized { } /// Finds the location of an executable `name`. -fn find_executable(name: &str) -> Option { +pub 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() { @@ -54,15 +54,25 @@ fn find_executable(name: &str) -> Option { } } +/// Returns an environment variable's value as a PathBuf +pub fn path_from_env(key: &str) -> Option { + std::env::var_os(key).map(PathBuf::from) +} + /// Finds the location of the PHP executable. fn find_php() -> Result { - // If PHP path is given via env, it takes priority. - let env = std::env::var("PHP"); - if let Ok(env) = env { - return Ok(env.into()); + // If path is given via env, it takes priority. + if let Some(path) = path_from_env("PHP") { + if !path.try_exists()? { + // If path was explicitly given and it can't be found, this is a hard error + bail!("php executable not found at {:?}", path); + } + return Ok(path); } - - find_executable("php").context("Could not find PHP path. Please ensure `php` is in your PATH or the `PHP` environment variable is set.") + find_executable("php").with_context(|| { + "Could not find PHP executable. \ + Please ensure `php` is in your PATH or the `PHP` environment variable is set." + }) } pub struct PHPInfo(String); @@ -206,6 +216,8 @@ fn check_php_version(info: &PHPInfo) -> Result<()> { } fn main() -> Result<()> { + let out_dir = env::var_os("OUT_DIR").context("Failed to get OUT_DIR")?; + let out_path = PathBuf::from(out_dir).join("bindings.rs"); let manifest: PathBuf = std::env::var("CARGO_MANIFEST_DIR").unwrap().into(); for path in [ manifest.join("src").join("wrapper.h"), @@ -216,6 +228,21 @@ fn main() -> Result<()> { ] { println!("cargo:rerun-if-changed={}", path.to_string_lossy()); } + for env_var in ["PHP", "PHP_CONFIG", "PATH"] { + println!("cargo:rerun-if-env-changed={}", env_var); + } + + println!("cargo:rerun-if-changed=build.rs"); + + // docs.rs runners only have PHP 7.4 - use pre-generated bindings + if env::var("DOCS_RS").is_ok() { + println!("cargo:warning=docs.rs detected - using stub bindings"); + println!("cargo:rustc-cfg=php_debug"); + println!("cargo:rustc-cfg=php81"); + std::fs::copy("docsrs_bindings.rs", out_path) + .expect("failed to copy docs.rs stub bindings to out directory"); + return Ok(()); + } let php = find_php()?; let info = PHPInfo::get(&php)?; @@ -228,8 +255,6 @@ fn main() -> Result<()> { 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); diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index ccfd546..e2f15b0 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ repository = "https://github.com/davidcole1340/ext-php-rs" homepage = "https://github.com/davidcole1340/ext-php-rs" license = "MIT OR Apache-2.0" keywords = ["php", "ffi", "zend"] -version = "0.1.5" +version = "0.1.7" authors = ["David Cole "] edition = "2018" categories = ["api-bindings", "command-line-interface"] @@ -13,9 +13,9 @@ categories = ["api-bindings", "command-line-interface"] [dependencies] ext-php-rs = { version = ">=0.7.1", path = "../../" } -clap = { version = ">=3.2.5", features = ["derive"] } +clap = { version = "4.0", features = ["derive"] } anyhow = "1" dialoguer = "0.10" libloading = "0.7" -cargo_metadata = "0.14" +cargo_metadata = "0.15" semver = "1.0" diff --git a/crates/cli/README.md b/crates/cli/README.md index 4fd2a04..8d71caf 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -40,7 +40,7 @@ SUBCOMMANDS: Generates stub PHP files for the extension $ cargo php install --help -cargo-php-install +cargo-php-install Installs the extension in the current PHP installation. @@ -71,8 +71,11 @@ OPTIONS: --release Whether to install the release version of the extension + --yes + Bypasses the confirmation prompt + $ cargo php remove --help -cargo-php-remove +cargo-php-remove Removes the extension in the current PHP installation. @@ -97,8 +100,11 @@ OPTIONS: Path to the Cargo manifest of the extension. Defaults to the manifest in the directory the command is called + --yes + Bypasses the confirmation prompt + $ cargo php stubs --help -cargo-php-stubs +cargo-php-stubs Generates stub PHP files for the extension. @@ -120,7 +126,7 @@ OPTIONS: --manifest Path to the Cargo manifest of the extension. Defaults to the manifest in the directory the command is called. - + This cannot be provided alongside the `ext` option, as that option provides a direct path to the extension shared library. diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 639a7d9..c145bbd 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -10,7 +10,7 @@ use dialoguer::{Confirm, Select}; use std::{ fs::OpenOptions, - io::{BufRead, BufReader, Write}, + io::{BufRead, BufReader, Seek, SeekFrom, Write}, path::PathBuf, process::{Command, Stdio}, }; @@ -105,6 +105,9 @@ struct Install { /// the directory the command is called. #[arg(long)] manifest: Option, + /// Whether to bypass the install prompt. + #[clap(long)] + yes: bool, } #[derive(Parser)] @@ -121,6 +124,9 @@ struct Remove { /// the directory the command is called. #[arg(long)] manifest: Option, + /// Whether to bypass the remove prompt. + #[clap(long)] + yes: bool, } #[cfg(not(windows))] @@ -172,12 +178,13 @@ impl Install { php_ini = Some(ini_path); } - if !Confirm::new() - .with_prompt(format!( - "Are you sure you want to install the extension `{}`?", - artifact.name - )) - .interact()? + if !self.yes + && !Confirm::new() + .with_prompt(format!( + "Are you sure you want to install the extension `{}`?", + artifact.name + )) + .interact()? { bail!("Installation cancelled."); } @@ -207,6 +214,8 @@ impl Install { let line = line.with_context(|| "Failed to read line from `php.ini`")?; if !line.contains(&ext_line) { new_lines.push(line); + } else { + bail!("Extension already enabled."); } } @@ -216,6 +225,8 @@ impl Install { } new_lines.push(ext_line); + file.seek(SeekFrom::Start(0))?; + file.set_len(0)?; file.write(new_lines.join("\n").as_bytes()) .with_context(|| "Failed to update `php.ini`")?; } @@ -301,12 +312,13 @@ impl Remove { bail!("Unable to find extension installed."); } - if !Confirm::new() - .with_prompt(format!( - "Are you sure you want to remove the extension `{}`?", - artifact.name - )) - .interact()? + if !self.yes + && !Confirm::new() + .with_prompt(format!( + "Are you sure you want to remove the extension `{}`?", + artifact.name + )) + .interact()? { bail!("Installation cancelled."); } @@ -318,7 +330,6 @@ impl Remove { .read(true) .write(true) .create(true) - .truncate(true) .open(php_ini) .with_context(|| "Failed to open `php.ini`")?; @@ -330,6 +341,8 @@ impl Remove { } } + file.seek(SeekFrom::Start(0))?; + file.set_len(0)?; file.write(new_lines.join("\n").as_bytes()) .with_context(|| "Failed to update `php.ini`")?; } diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 780b1f5..dadcb83 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -4,7 +4,7 @@ description = "Derive macros for ext-php-rs." repository = "https://github.com/davidcole1340/ext-php-rs" homepage = "https://github.com/davidcole1340/ext-php-rs" license = "MIT OR Apache-2.0" -version = "0.8.0" +version = "0.9.0" authors = ["David Cole "] edition = "2018" diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 4e5e81e..c10a015 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -22,6 +22,7 @@ pub struct Class { /// A function name called when creating the class entry. Given an instance /// of `ClassBuilder` and must return it. pub modifier: Option, + pub flags: Option, } #[derive(Debug)] @@ -37,6 +38,7 @@ pub enum ParsedAttribute { pub struct AttrArgs { name: Option, modifier: Option, + flags: Option, } pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result { @@ -117,6 +119,7 @@ pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result let ItemStruct { ident, .. } = &input; let class_name = args.name.unwrap_or_else(|| ident.to_string()); let struct_path = ident.to_string(); + let flags = args.flags.map(|flags| flags.to_token_stream().to_string()); let class = Class { class_name, struct_path, @@ -125,6 +128,7 @@ pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result docs: comments, properties, modifier: args.modifier, + flags, ..Default::default() }; diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 4c9520c..9efa5b1 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -17,6 +17,7 @@ pub struct AttrArgs { optional: Option, ignore_module: bool, defaults: HashMap, + name: Option, } #[derive(Debug, Clone)] @@ -93,7 +94,7 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi } let function = Function { - name: ident.to_string(), + name: attr_args.name.unwrap_or_else(|| ident.to_string()), docs: get_docs(&input.attrs), ident: internal_ident.to_string(), args, diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 2fe45df..ca7851b 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -85,6 +85,7 @@ pub enum ParsedAttribute { }, Constructor, This, + Abstract, } #[derive(Default, Debug, FromMeta)] @@ -212,6 +213,7 @@ pub fn parse_attribute(attr: &Attribute) -> Result> { "public" => ParsedAttribute::Visibility(Visibility::Public), "protected" => ParsedAttribute::Visibility(Visibility::Protected), "private" => ParsedAttribute::Visibility(Visibility::Private), + "abstract_method" => ParsedAttribute::Abstract, "rename" => { let ident = if let Meta::List(list) = meta { if let Some(NestedMeta::Lit(lit)) = list.nested.first() { diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index e744703..2e6deaf 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -87,10 +87,11 @@ pub fn php_module(_: TokenStream, input: TokenStream) -> TokenStream { } #[proc_macro_attribute] -pub fn php_startup(_: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_startup(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as AttributeArgs); let input = parse_macro_input!(input as ItemFn); - match startup_function::parser(input) { + match startup_function::parser(Some(args), input) { Ok(parsed) => parsed, Err(e) => syn::Error::new(Span::call_site(), e).to_compile_error(), } diff --git a/crates/macros/src/method.rs b/crates/macros/src/method.rs index d6bbaab..87e12c7 100644 --- a/crates/macros/src/method.rs +++ b/crates/macros/src/method.rs @@ -38,6 +38,7 @@ pub struct Method { pub optional: Option, pub output: Option<(String, bool)>, pub _static: bool, + pub _abstract: bool, pub visibility: Visibility, } @@ -81,6 +82,7 @@ pub fn parser( let mut visibility = Visibility::Public; let mut as_prop = None; let mut identifier = None; + let mut is_abstract = false; let mut is_constructor = false; let docs = get_docs(&input.attrs); @@ -90,6 +92,7 @@ pub fn parser( ParsedAttribute::Default(list) => defaults = list, ParsedAttribute::Optional(name) => optional = Some(name), ParsedAttribute::Visibility(vis) => visibility = vis, + ParsedAttribute::Abstract => is_abstract = true, ParsedAttribute::Rename(ident) => identifier = Some(ident), ParsedAttribute::Property { prop_name, ty } => { if as_prop.is_some() { @@ -211,6 +214,7 @@ pub fn parser( optional, output: get_return_type(struct_ty, &input.sig.output)?, _static: matches!(method_type, MethodType::Static), + _abstract: is_abstract, visibility, }; @@ -447,6 +451,10 @@ impl Method { flags.push(quote! { Static }); } + if self._abstract { + flags.push(quote! { Abstract }); + } + flags .iter() .map(|flag| quote! { ::ext_php_rs::flags::MethodFlags::#flag }) diff --git a/crates/macros/src/module.rs b/crates/macros/src/module.rs index 44fe17a..2c118c0 100644 --- a/crates/macros/src/module.rs +++ b/crates/macros/src/module.rs @@ -34,7 +34,7 @@ pub fn parser(input: ItemFn) -> Result { fn php_module_startup() {} }) .map_err(|_| anyhow!("Unable to generate PHP module startup function."))?; - let startup = startup_function::parser(parsed)?; + let startup = startup_function::parser(None, parsed)?; state = STATE.lock(); Some(startup) @@ -227,10 +227,7 @@ impl Describe for Class { } else { quote! { None } }; - let interfaces = self - .interfaces - .iter() - .map(|iface| quote! { #iface.into(), }); + let interfaces = self.interfaces.iter().map(|iface| quote! { #iface.into() }); let properties = self.properties.iter().map(|d| d.describe()); let mut methods: Vec<_> = self.methods.iter().map(Describe::describe).collect(); let docs = self.docs.iter().map(|c| { diff --git a/crates/macros/src/startup_function.rs b/crates/macros/src/startup_function.rs index c6f078c..76b9877 100644 --- a/crates/macros/src/startup_function.rs +++ b/crates/macros/src/startup_function.rs @@ -1,13 +1,27 @@ use std::collections::HashMap; use anyhow::{anyhow, Result}; +use darling::FromMeta; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use syn::{Expr, ItemFn, Signature}; +use syn::{AttributeArgs, Expr, ItemFn, Signature}; use crate::{class::Class, constant::Constant, STATE}; -pub fn parser(input: ItemFn) -> Result { +#[derive(Default, Debug, FromMeta)] +#[darling(default)] +struct StartupArgs { + before: bool, +} + +pub fn parser(args: Option, input: ItemFn) -> Result { + let args = if let Some(args) = args { + StartupArgs::from_list(&args) + .map_err(|e| anyhow!("Unable to parse attribute arguments: {:?}", e))? + } else { + StartupArgs::default() + }; + let ItemFn { sig, block, .. } = input; let Signature { ident, .. } = sig; let stmts = &block.stmts; @@ -17,6 +31,11 @@ pub fn parser(input: ItemFn) -> Result { let classes = build_classes(&state.classes)?; let constants = build_constants(&state.constants); + let (before, after) = if args.before { + (Some(quote! { internal(); }), None) + } else { + (None, Some(quote! { internal(); })) + }; let func = quote! { #[doc(hidden)] @@ -30,11 +49,10 @@ pub fn parser(input: ItemFn) -> Result { ::ext_php_rs::internal::ext_php_rs_startup(); + #before #(#classes)* #(#constants)* - - // TODO return result? - internal(); + #after 0 } @@ -121,6 +139,31 @@ fn build_classes(classes: &HashMap) -> Result> { } }); + let flags = { + if let Some(flags) = &class.flags { + let mut name = "::ext_php_rs::flags::ClassFlags::".to_owned(); + name.push_str(flags); + let expr: Expr = syn::parse_str(&name).map_err(|_| { + anyhow!("Invalid expression given for `{}` flags", class_name) + })?; + Some(quote! { .flags(#expr) }) + } else { + None + } + }; + + let object_override = { + if let Some(flags) = &class.flags { + if flags == "Interface" { + None + } else { + Some(quote! { .object_override::<#ident>() }) + } + } else { + Some(quote! { .object_override::<#ident>() }) + } + }; + Ok(quote! {{ let builder = ::ext_php_rs::builders::ClassBuilder::new(#class_name) #(#methods)* @@ -128,7 +171,9 @@ fn build_classes(classes: &HashMap) -> Result> { #(#interfaces)* // #(#properties)* #parent - .object_override::<#ident>(); + #flags + #object_override + ; #class_modifier let class = builder.build() .expect(concat!("Unable to build class `", #class_name, "`")); diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index a6d7795..ff121e3 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -1,7 +1,6 @@ -/* automatically generated by rust-bindgen 0.59.1 */ +/* automatically generated by rust-bindgen 0.63.0 */ pub const ZEND_DEBUG: u32 = 1; -pub const ZEND_MM_ALIGNMENT: u32 = 8; pub const _ZEND_TYPE_NAME_BIT: u32 = 16777216; pub const _ZEND_TYPE_NULLABLE_BIT: u32 = 2; pub const HT_MIN_SIZE: u32 = 8; @@ -32,7 +31,6 @@ pub const IS_OBJECT_EX: u32 = 776; pub const IS_RESOURCE_EX: u32 = 265; pub const IS_REFERENCE_EX: u32 = 266; pub const IS_CONSTANT_AST_EX: u32 = 267; -pub const ZEND_MM_ALIGNMENT_MASK: i32 = -8; pub const ZEND_PROPERTY_ISSET: u32 = 0; pub const ZEND_PROPERTY_EXISTS: u32 = 2; pub const ZEND_ACC_PUBLIC: u32 = 1; @@ -90,8 +88,11 @@ pub const CONST_CS: u32 = 0; pub const CONST_PERSISTENT: u32 = 1; pub const CONST_NO_FILE_CACHE: u32 = 2; pub const CONST_DEPRECATED: u32 = 4; -pub type __darwin_size_t = ::std::os::raw::c_ulong; -pub type size_t = __darwin_size_t; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct __sigset_t { + pub __val: [::std::os::raw::c_ulong; 16usize], +} pub type zend_long = i64; pub type zend_ulong = u64; pub type zend_uchar = ::std::os::raw::c_uchar; @@ -202,7 +203,7 @@ pub struct _zend_refcounted { pub struct _zend_string { pub gc: zend_refcounted_h, pub h: zend_ulong, - pub len: size_t, + pub len: usize, pub val: [::std::os::raw::c_char; 1usize], } #[repr(C)] @@ -284,7 +285,7 @@ pub struct _zend_ast_ref { } extern "C" { pub fn _emalloc( - size: size_t, + size: usize, __zend_filename: *const ::std::os::raw::c_char, __zend_lineno: u32, __zend_orig_filename: *const ::std::os::raw::c_char, @@ -301,12 +302,12 @@ extern "C" { ); } extern "C" { - pub fn __zend_malloc(len: size_t) -> *mut ::std::os::raw::c_void; + pub fn __zend_malloc(len: usize) -> *mut ::std::os::raw::c_void; } pub type zend_string_init_interned_func_t = ::std::option::Option< unsafe extern "C" fn( str_: *const ::std::os::raw::c_char, - size: size_t, + size: usize, permanent: bool, ) -> *mut zend_string, >; @@ -320,7 +321,7 @@ extern "C" { pub fn zend_hash_str_update( ht: *mut HashTable, key: *const ::std::os::raw::c_char, - len: size_t, + len: usize, pData: *mut zval, ) -> *mut zval; } @@ -335,7 +336,7 @@ extern "C" { pub fn zend_hash_str_del( ht: *mut HashTable, key: *const ::std::os::raw::c_char, - len: size_t, + len: usize, ) -> zend_result; } extern "C" { @@ -345,7 +346,7 @@ extern "C" { pub fn zend_hash_str_find( ht: *const HashTable, key: *const ::std::os::raw::c_char, - len: size_t, + len: usize, ) -> *mut zval; } extern "C" { @@ -548,7 +549,7 @@ pub struct _zend_class_entry { unsafe extern "C" fn( object: *mut zval, buffer: *mut *mut ::std::os::raw::c_uchar, - buf_len: *mut size_t, + buf_len: *mut usize, data: *mut zend_serialize_data, ) -> ::std::os::raw::c_int, >, @@ -557,7 +558,7 @@ pub struct _zend_class_entry { object: *mut zval, ce: *mut zend_class_entry, buf: *const ::std::os::raw::c_uchar, - buf_len: size_t, + buf_len: usize, data: *mut zend_unserialize_data, ) -> ::std::os::raw::c_int, >, @@ -982,7 +983,15 @@ pub struct _zend_execute_data { pub run_time_cache: *mut *mut ::std::os::raw::c_void, pub extra_named_params: *mut zend_array, } -pub type sigjmp_buf = [::std::os::raw::c_int; 49usize]; +pub type __jmp_buf = [::std::os::raw::c_long; 8usize]; +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct __jmp_buf_tag { + pub __jmpbuf: __jmp_buf, + pub __mask_was_saved: ::std::os::raw::c_int, + pub __saved_mask: __sigset_t, +} +pub type jmp_buf = [__jmp_buf_tag; 1usize]; pub type zend_executor_globals = _zend_executor_globals; extern "C" { pub static mut executor_globals: zend_executor_globals; @@ -1043,7 +1052,7 @@ pub struct _zend_executor_globals { pub symtable_cache_ptr: *mut *mut zend_array, pub symbol_table: zend_array, pub included_files: HashTable, - pub bailout: *mut sigjmp_buf, + pub bailout: *mut jmp_buf, pub error_reporting: ::std::os::raw::c_int, pub exit_status: ::std::os::raw::c_int, pub function_table: *mut HashTable, @@ -1052,7 +1061,7 @@ pub struct _zend_executor_globals { pub vm_stack_top: *mut zval, pub vm_stack_end: *mut zval, pub vm_stack: zend_vm_stack, - pub vm_stack_page_size: size_t, + pub vm_stack_page_size: usize, pub current_execute_data: *mut _zend_execute_data, pub fake_scope: *mut zend_class_entry, pub jit_trace_num: u32, @@ -1149,7 +1158,7 @@ pub struct _zend_module_entry { >, pub info_func: ::std::option::Option, pub version: *const ::std::os::raw::c_char, - pub globals_size: size_t, + pub globals_size: usize, pub globals_ptr: *mut ::std::os::raw::c_void, pub globals_ctor: ::std::option::Option, @@ -1185,7 +1194,7 @@ pub struct _zend_vm_stack { pub prev: zend_vm_stack, } #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Copy, Clone)] pub struct _zend_function_entry { pub fname: *const ::std::os::raw::c_char, pub handler: zif_handler, @@ -1211,7 +1220,7 @@ extern "C" { pub fn zend_declare_property( ce: *mut zend_class_entry, name: *const ::std::os::raw::c_char, - name_length: size_t, + name_length: usize, property: *mut zval, access_type: ::std::os::raw::c_int, ); @@ -1220,7 +1229,7 @@ extern "C" { pub fn zend_declare_class_constant( ce: *mut zend_class_entry, name: *const ::std::os::raw::c_char, - name_length: size_t, + name_length: usize, value: *mut zval, ); } @@ -1286,7 +1295,7 @@ extern "C" { pub fn zend_wrong_parameters_count_error(min_num_args: u32, max_num_args: u32); } extern "C" { - pub fn php_printf(format: *const ::std::os::raw::c_char, ...) -> size_t; + pub fn php_printf(format: *const ::std::os::raw::c_char, ...) -> usize; } #[repr(C)] #[derive(Debug, Copy, Clone)] @@ -1318,7 +1327,7 @@ pub struct _zend_ini_entry { extern "C" { pub fn zend_register_bool_constant( name: *const ::std::os::raw::c_char, - name_len: size_t, + name_len: usize, bval: bool, flags: ::std::os::raw::c_int, module_number: ::std::os::raw::c_int, @@ -1327,7 +1336,7 @@ extern "C" { extern "C" { pub fn zend_register_long_constant( name: *const ::std::os::raw::c_char, - name_len: size_t, + name_len: usize, lval: zend_long, flags: ::std::os::raw::c_int, module_number: ::std::os::raw::c_int, @@ -1336,7 +1345,7 @@ extern "C" { extern "C" { pub fn zend_register_double_constant( name: *const ::std::os::raw::c_char, - name_len: size_t, + name_len: usize, dval: f64, flags: ::std::os::raw::c_int, module_number: ::std::os::raw::c_int, @@ -1345,7 +1354,7 @@ extern "C" { extern "C" { pub fn zend_register_string_constant( name: *const ::std::os::raw::c_char, - name_len: size_t, + name_len: usize, strval: *const ::std::os::raw::c_char, flags: ::std::os::raw::c_int, module_number: ::std::os::raw::c_int, @@ -1408,27 +1417,23 @@ extern "C" { pub fn zend_do_implement_interface(ce: *mut zend_class_entry, iface: *mut zend_class_entry); } extern "C" { - pub fn ext_php_rs_zend_string_init( - str_: *const ::std::os::raw::c_char, - len: size_t, - persistent: bool, - ) -> *mut zend_string; + pub static mut zend_ce_traversable: *mut zend_class_entry; } extern "C" { - pub fn ext_php_rs_zend_string_release(zs: *mut zend_string); + pub static mut zend_ce_aggregate: *mut zend_class_entry; } extern "C" { - pub fn ext_php_rs_php_build_id() -> *const ::std::os::raw::c_char; + pub static mut zend_ce_iterator: *mut zend_class_entry; } extern "C" { - pub fn ext_php_rs_zend_object_alloc( - obj_size: size_t, - ce: *mut zend_class_entry, - ) -> *mut ::std::os::raw::c_void; + pub static mut zend_ce_arrayaccess: *mut zend_class_entry; } extern "C" { - pub fn ext_php_rs_zend_object_release(obj: *mut zend_object); + pub static mut zend_ce_serializable: *mut zend_class_entry; } extern "C" { - pub fn ext_php_rs_executor_globals() -> *mut zend_executor_globals; + pub static mut zend_ce_countable: *mut zend_class_entry; +} +extern "C" { + pub static mut zend_ce_stringable: *mut zend_class_entry; } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index c050b60..f725964 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -1,9 +1,15 @@ # Summary -- [Introduction](./introduction.md) -- [`cargo php`](./cargo-php.md) -- [Examples](./examples/index.md) - - [Hello World](./examples/hello_world.md) +[Introduction](./introduction.md) + +# Getting Started + +- [Installation](./getting-started/installation.md) +- [Hello World](./getting-started/hello_world.md) +- [`cargo php`](./getting-started/cargo-php.md) + +# Reference Guide + - [Types](./types/index.md) - [Primitive Numbers](./types/numbers.md) - [`String`](./types/string.md) diff --git a/guide/src/examples/hello_world.md b/guide/src/examples/hello_world.md deleted file mode 100644 index b650bc4..0000000 --- a/guide/src/examples/hello_world.md +++ /dev/null @@ -1,103 +0,0 @@ -# Hello World - -Let's create a basic PHP extension. We will start by creating a new Rust library -crate: - -```sh -$ 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: - -```toml -[package] -name = "hello_world" -version = "0.1.0" -edition = "2018" - -[dependencies] -ext-php-rs = "*" - -[lib] -crate-type = ["cdylib"] -``` - -### `.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 -{{#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 -take a string argument for the callers name, and we will return another string. -Finally, we write a `get_module` function which is used by PHP to find out about -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. - -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: &str) -> String { - format!("Hello, {}!", name) -} - -#[php_module] -pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - module -} -``` - -### `test.php` - -Let's make a test script. - -```php - Path to the Cargo manifest of the extension. Defaults to the manifest in the directory the command is called. - + This cannot be provided alongside the `ext` option, as that option provides a direct path to the extension shared library. @@ -130,7 +130,7 @@ so you are able to restore if you run into any issues. ```text $ cargo php install --help -cargo-php-install +cargo-php-install Installs the extension in the current PHP installation. @@ -164,6 +164,9 @@ OPTIONS: --release Whether to install the release version of the extension + + --yes + Bypasses the confirmation prompt ``` ## Extension Removal @@ -175,7 +178,7 @@ from your `php.ini` if present. ```text $ cargo php remove --help -cargo-php-remove +cargo-php-remove Removes the extension in the current PHP installation. @@ -203,6 +206,9 @@ OPTIONS: --manifest Path to the Cargo manifest of the extension. Defaults to the manifest in the directory the command is called + + --yes + Bypasses the confirmation prompt ``` [`cargo-php`]: https://crates.io/crates/cargo-php diff --git a/guide/src/getting-started/hello_world.md b/guide/src/getting-started/hello_world.md new file mode 100644 index 0000000..05918bd --- /dev/null +++ b/guide/src/getting-started/hello_world.md @@ -0,0 +1,162 @@ +# Hello World + +## Project Setup + +We will start by creating a new Rust library crate: + +```sh +$ 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: + +```toml +[package] +name = "hello_world" +version = "0.1.0" +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ext-php-rs = "*" + +[profile.release] +strip = "debuginfo" +``` + +### `.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 +{{#include ../../../.cargo/config.toml}} +``` + +## Writing our extension + +### `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 +take a string argument for the callers name, and we will return another string. +Finally, we write a `get_module` function which is used by PHP to find out about +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. + +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: &str) -> String { + format!("Hello, {}!", name) +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module +} +``` + +## Building the extension + +Now let's build our extension. +This is done through `cargo` like any other Rust crate. + +If you installed php using a package manager in the previous chapter +(or if the `php` and `php-config` binaries are already in your `$PATH`), +then you can just run + +```sh +cargo build +``` + +If you have multiple PHP versions in your PATH, or your installation +resides in a custom location, you can use the following environment variables: + +```sh +# explicitly specifies the path to the PHP executable: +export PHP=/path/to/php +# explicitly specifies the path to the php-config executable: +export PHP_CONFIG=/path/to/php-config +``` + +As an alternative, if you compiled PHP from source and installed it under +it's own prefix (`configure --prefix=/my/prefix`), you can just put +this prefix in front of your PATH: + +```sh +export PATH="/my/prefix:${PATH}" +``` + +Once you've setup these variables, you can just run + +```sh +cargo build +``` + +Cargo will track changes to these environment variables and rebuild the library accordingly. + +## Testing our extension + +The extension we just built is stored inside the cargo target directory: +`target/debug` if you did a debug build, `target/release` for release builds. + +The extension file name is OS-dependent. The naming works as follows: + +- let `S` be the empty string +- append to `S` the value of [std::env::consts::DLL_PREFIX](https://doc.rust-lang.org/std/env/consts/constant.DLL_PREFIX.html) + (empty on windows, `lib` on unixes) +- append to `S` the lower-snake-case version of your crate name +- append to `S` the value of [std::env::consts::DLL_SUFFIX](https://doc.rust-lang.org/std/env/consts/constant.DLL_SUFFIX.html) + (`.dll` on windows, `.dylib` on macOS, `.so` on other unixes). +- set the filename to the value of `S` + +Which in our case would give us: + +- linux: `libhello_world.so` +- macOS: `libhello_world.dylib` +- windows: `hello_world.dll` + +Now we need a way to tell the PHP CLI binary to load our extension. +There are [several ways to do that](https://www.phpinternalsbook.com/php7/build_system/building_extensions.html#loading-shared-extensions). +For now we'll simply pass the `-d extension=/path/to/extension` option to the PHP CLI binary. + +Let's make a test script: + +### `test.php` + +```php +` with a generic parameter `T` generates new compiled implementations +each time it is used with a different concrete type for `T`. +These new implementations are generated by the compiler at each usage site. +This is incompatible with wrapping `Foo` in PHP, +where there needs to be a single compiled implementation of `Foo` which is integrated with the PHP interpreter. + ## Example This example creates a PHP class `Human`, adding a PHP property `address`. @@ -79,3 +107,56 @@ pub fn throw_exception() -> PhpResult { # } # fn main() {} ``` + +## Implementing an Interface + +To implement an interface, use `#[implements(ce)]` where `ce` is an expression returning a `ClassEntry`. +The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::{exception::PhpResult, types::Zval, zend::ce}; + +#[php_class] +#[implements(ce::arrayaccess())] +#[derive(Default)] +pub struct EvenNumbersArray; + +/// Returns `true` if the array offset is an even number. +/// Usage: +/// ```php +/// $arr = new EvenNumbersArray(); +/// var_dump($arr[0]); // true +/// var_dump($arr[1]); // false +/// var_dump($arr[2]); // true +/// var_dump($arr[3]); // false +/// var_dump($arr[4]); // true +/// var_dump($arr[5] = true); // Fatal error: Uncaught Exception: Setting values is not supported +/// ``` +#[php_impl] +impl EvenNumbersArray { + pub fn __construct() -> EvenNumbersArray { + EvenNumbersArray {} + } + // We need to use `Zval` because ArrayAccess needs $offset to be a `mixed` + pub fn offset_exists(&self, offset: &'_ Zval) -> bool { + offset.is_long() + } + pub fn offset_get(&self, offset: &'_ Zval) -> PhpResult { + let integer_offset = offset.long().ok_or("Expected integer offset")?; + Ok(integer_offset % 2 == 0) + } + pub fn offset_set(&mut self, _offset: &'_ Zval, _value: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } + pub fn offset_unset(&mut self, _offset: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } +} +# #[php_module] +# pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +# module +# } +# fn main() {} +``` diff --git a/src/args.rs b/src/args.rs index 4bb5be0..ef32334 100644 --- a/src/args.rs +++ b/src/args.rs @@ -118,7 +118,7 @@ impl<'a> Arg<'a> { /// return value of the function, or an error. /// /// You should not call this function directly, rather through the - /// [`call_user_func`] macro. + /// [`call_user_func`](crate::call_user_func) macro. /// /// # Parameters /// diff --git a/src/binary_slice.rs b/src/binary_slice.rs index 058d3a7..223f480 100644 --- a/src/binary_slice.rs +++ b/src/binary_slice.rs @@ -6,16 +6,11 @@ use crate::ffi::zend_string; -use std::{convert::TryFrom, ops::Deref, slice::from_raw_parts}; +use std::{ops::Deref, slice::from_raw_parts}; -use crate::{ - convert::FromZval, - error::{Error, Result}, - flags::DataType, - types::Zval, -}; +use crate::{convert::FromZval, flags::DataType, types::Zval}; -/// Acts as a wrapper around [`&[T]`] where `T` implements [`PackSlice`]. +/// Acts as a wrapper around `&[T]` where `T` implements [`PackSlice`]. /// Primarily used for passing read-only binary data into Rust functions. #[derive(Debug)] pub struct BinarySlice<'a, T>(&'a [T]) @@ -47,28 +42,17 @@ where } } -impl FromZval<'_> for BinarySlice<'_, T> +impl<'a, T> FromZval<'a> for BinarySlice<'a, T> where T: PackSlice, { const TYPE: DataType = DataType::String; - fn from_zval(zval: &Zval) -> Option { + fn from_zval(zval: &'a Zval) -> Option { zval.binary_slice().map(BinarySlice) } } -impl TryFrom for BinarySlice<'_, T> -where - T: PackSlice, -{ - type Error = Error; - - fn try_from(value: Zval) -> Result { - Self::from_zval(&value).ok_or_else(|| Error::ZvalConversion(value.get_type())) - } -} - impl<'a, T> From> for &'a [T] where T: PackSlice, @@ -117,7 +101,7 @@ pub unsafe trait PackSlice: Clone { /// * `s` - The Zend string containing the binary data. /// /// [`pack`]: https://www.php.net/manual/en/function.pack.php - fn unpack_into<'a>(s: &zend_string) -> &'a [Self]; + fn unpack_into(s: &zend_string) -> &[Self]; } /// Implements the [`PackSlice`] trait for a given type. @@ -128,7 +112,7 @@ macro_rules! pack_slice_impl { ($t: ty, $d: expr) => { unsafe impl PackSlice for $t { - fn unpack_into<'a>(s: &zend_string) -> &'a [Self] { + fn unpack_into(s: &zend_string) -> &[Self] { let bytes = ($d / 8) as usize; let len = (s.len as usize) / bytes; let ptr = s.val.as_ptr() as *const $t; diff --git a/src/builders/class.rs b/src/builders/class.rs index bc03847..7609445 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -85,7 +85,7 @@ impl ClassBuilder { /// * `func` - The function entry to add to the class. /// * `flags` - Flags relating to the function. See [`MethodFlags`]. pub fn method(mut self, mut func: FunctionEntry, flags: MethodFlags) -> Self { - func.flags = flags.bits(); + func.flags |= flags.bits(); self.methods.push(func); self } @@ -226,7 +226,7 @@ impl ClassBuilder { /// /// Returns an [`Error`] variant if the class could not be registered. pub fn build(mut self) -> Result<&'static mut ClassEntry> { - self.ce.name = ZendStr::new_interned(&self.name, true)?.into_raw(); + self.ce.name = ZendStr::new_interned(&self.name, true).into_raw(); self.methods.push(FunctionEntry::end()); let func = Box::into_raw(self.methods.into_boxed_slice()) as *const FunctionEntry; @@ -284,7 +284,7 @@ impl ClassBuilder { zend_declare_class_constant( class, CString::new(name.as_str())?.as_ptr(), - name.len() as u64, + name.len(), value, ) }; diff --git a/src/builders/function.rs b/src/builders/function.rs index 363aad2..e75a90d 100644 --- a/src/builders/function.rs +++ b/src/builders/function.rs @@ -1,7 +1,7 @@ use crate::{ args::{Arg, ArgInfo}, error::{Error, Result}, - flags::DataType, + flags::{DataType, MethodFlags}, types::Zval, zend::{ExecuteData, FunctionEntry, ZendType}, }; @@ -64,6 +64,30 @@ impl<'a> FunctionBuilder<'a> { } } + /// Create a new function builder for an abstract function that can be used + /// on an abstract class or an interface. + /// + /// # Parameters + /// + /// * `name` - The name of the function. + pub fn new_abstract>(name: T) -> Self { + Self { + name: name.into(), + function: FunctionEntry { + fname: ptr::null(), + handler: None, + arg_info: ptr::null(), + num_args: 0, + flags: MethodFlags::Abstract.bits(), + }, + args: vec![], + n_req: None, + retval: None, + ret_as_ref: false, + ret_as_null: false, + } + } + /// Creates a constructor builder, used to build the constructor /// for classes. /// diff --git a/src/describe/stub.rs b/src/describe/stub.rs index ab1660c..4c501f6 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -153,6 +153,7 @@ impl ToStub for Parameter { impl ToStub for DataType { fn fmt_stub(&self, buf: &mut String) -> FmtResult { + let mut fqdn = "\\".to_owned(); write!( buf, "{}", @@ -162,7 +163,10 @@ impl ToStub for DataType { DataType::Double => "float", DataType::String => "string", DataType::Array => "array", - DataType::Object(Some(ty)) => ty, + DataType::Object(Some(ty)) => { + fqdn.push_str(ty); + fqdn.as_str() + } DataType::Object(None) => "object", DataType::Resource => "resource", DataType::Reference => "reference", diff --git a/src/error.rs b/src/error.rs index eb24096..eaa0773 100644 --- a/src/error.rs +++ b/src/error.rs @@ -48,6 +48,8 @@ pub enum Error { /// The string could not be converted into a C-string due to the presence of /// a NUL character. InvalidCString, + /// The string could not be converted into a valid Utf8 string + InvalidUtf8, /// Could not call the given function. Callable, /// An invalid exception type was thrown. @@ -82,6 +84,7 @@ impl Display for Error { f, "String given contains NUL-bytes which cannot be present in a C string." ), + Error::InvalidUtf8 => write!(f, "Invalid Utf8 byte sequence."), Error::Callable => write!(f, "Could not call given function."), Error::InvalidException(flags) => { write!(f, "Invalid exception type was thrown: {:?}", flags) diff --git a/src/ffi.rs b/src/ffi.rs index e750f47..92614c4 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -19,6 +19,9 @@ extern "C" { persistent: bool, ) -> *mut zend_string; pub fn ext_php_rs_zend_string_release(zs: *mut zend_string); + pub fn ext_php_rs_is_known_valid_utf8(zs: *const zend_string) -> bool; + pub fn ext_php_rs_set_known_valid_utf8(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); diff --git a/src/lib.rs b/src/lib.rs index f056133..720a23d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -139,8 +139,8 @@ pub use ext_php_rs_derive::php_const; /// ``` /// /// [`strpos`]: https://www.php.net/manual/en/function.strpos.php -/// [`IntoZval`]: ext_php_rs::php::types::zval::IntoZval -/// [`Zval`]: ext_php_rs::php::types::zval::Zval +/// [`IntoZval`]: crate::convert::IntoZval +/// [`Zval`]: crate::types::Zval pub use ext_php_rs_derive::php_extern; /// Attribute used to annotate a function as a PHP function. @@ -244,7 +244,7 @@ pub use ext_php_rs_derive::php_extern; /// ``` /// /// Parameters can also be deemed optional by passing the parameter name in the -/// attribute options. This function takes one required parameter (`hello`) and +/// attribute options. This function takes one required parameter (`name`) and /// two optional parameters (`description` and `age`). /// /// ``` @@ -289,12 +289,12 @@ pub use ext_php_rs_derive::php_extern; /// /// [`Result`]: std::result::Result /// [`FunctionBuilder`]: crate::php::function::FunctionBuilder -/// [`FromZval`]: crate::php::types::zval::FromZval -/// [`IntoZval`]: crate::php::types::zval::IntoZval -/// [`Zval`]: crate::php::types::zval::Zval -/// [`Binary`]: crate::php::types::binary::Binary -/// [`ZendCallable`]: crate::php::types::callable::ZendCallable -/// [`PhpException`]: crate::php::exceptions::PhpException +/// [`FromZval`]: crate::convert::FromZval +/// [`IntoZval`]: crate::convert::IntoZval +/// [`Zval`]: crate::types::Zval. +/// [`Binary`]: crate::binary::Binary +/// [`ZendCallable`]: crate::types::ZendCallable +/// [`PhpException`]: crate::exception::PhpException pub use ext_php_rs_derive::php_function; /// Annotates a structs `impl` block, declaring that all methods and constants @@ -518,6 +518,11 @@ pub use ext_php_rs_derive::php_class; /// this macro if you have registered any classes or constants when using the /// [`macro@php_module`] macro. /// +/// The attribute accepts one optional flag -- `#[php_startup(before)]` -- +/// which forces the annotated function to be called _before_ the other classes +/// and constants are registered. By default the annotated function is called +/// after these classes and constants are registered. +/// /// # Example /// /// ``` @@ -665,12 +670,12 @@ pub use ext_php_rs_derive::php_startup; /// var_dump(give_union()); // int(5) /// ``` /// -/// [`FromZval`]: crate::php::types::zval::FromZval -/// [`IntoZval`]: crate::php::types::zval::IntoZval -/// [`FromZendObject`]: crate::php::types::object::FromZendObject -/// [`IntoZendObject`]: crate::php::types::object::IntoZendObject -/// [`Zval`]: crate::php::types::zval::Zval -/// [`Zval::string`]: crate::php::types::zval::Zval::string +/// [`FromZval`]: crate::convert::FromZval +/// [`IntoZval`]: crate::convert::IntoZval +/// [`FromZendObject`]: crate::convert::FromZendObject +/// [`IntoZendObject`]: crate::convert::IntoZendObject +/// [`Zval`]: crate::types::Zval. +/// [`Zval::string`]: crate::types::Zval.::string pub use ext_php_rs_derive::ZvalConvert; /// Defines an `extern` function with the Zend fastcall convention based on diff --git a/src/types/array.rs b/src/types/array.rs index d39105e..cfd99cd 100644 --- a/src/types/array.rs +++ b/src/types/array.rs @@ -312,14 +312,7 @@ impl ZendHashTable { V: IntoZval, { let mut val = val.into_zval(false)?; - unsafe { - zend_hash_str_update( - self, - CString::new(key)?.as_ptr(), - key.len() as u64, - &mut val, - ) - }; + unsafe { zend_hash_str_update(self, CString::new(key)?.as_ptr(), key.len(), &mut val) }; val.release(); Ok(()) } diff --git a/src/types/object.rs b/src/types/object.rs index 6b73ffe..606c483 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -80,6 +80,17 @@ impl ZendObject { unsafe { ZBox::from_raw(this.get_mut_zend_obj()) } } + /// Returns the [`ClassEntry`] associated with this object. + /// + /// # Panics + /// + /// Panics if the class entry is invalid. + pub fn get_class_entry(&self) -> &'static ClassEntry { + // SAFETY: it is OK to panic here since PHP would segfault anyway + // when encountering an object with no class entry. + unsafe { self.ce.as_ref() }.expect("Could not retrieve class entry.") + } + /// Attempts to retrieve the class name of the object. pub fn get_class_name(&self) -> Result { unsafe { @@ -91,8 +102,21 @@ impl ZendObject { } } + /// Returns whether this object is an instance of the given [`ClassEntry`]. + /// + /// This method checks the class and interface inheritance chain. + /// + /// # Panics + /// + /// Panics if the class entry is invalid. + pub fn instance_of(&self, ce: &ClassEntry) -> bool { + self.get_class_entry().instance_of(ce) + } + /// Checks if the given object is an instance of a registered class with /// Rust type `T`. + /// + /// This method doesn't check the class and interface inheritance chain. pub fn is_instance(&self) -> bool { (self.ce as *const ClassEntry).eq(&(T::get_metadata().ce() as *const _)) } @@ -113,7 +137,7 @@ impl ZendObject { return Err(Error::InvalidProperty); } - let mut name = ZendStr::new(name, false)?; + let mut name = ZendStr::new(name, false); let mut rv = Zval::new(); let zv = unsafe { @@ -138,7 +162,7 @@ impl ZendObject { /// * `name` - The name of the property. /// * `value` - The value to set the property to. pub fn set_property(&mut self, name: &str, value: impl IntoZval) -> Result<()> { - let mut name = ZendStr::new(name, false)?; + let mut name = ZendStr::new(name, false); let mut value = value.into_zval(false)?; unsafe { @@ -163,7 +187,7 @@ impl ZendObject { /// * `name` - The name of the property. /// * `query` - The 'query' to classify if a property exists. pub fn has_property(&self, name: &str, query: PropertyQuery) -> Result { - let mut name = ZendStr::new(name, false)?; + let mut name = ZendStr::new(name, false); Ok(unsafe { self.handlers()?.has_property.ok_or(Error::InvalidScope)?( @@ -196,6 +220,29 @@ impl ZendObject { T::from_zend_object(self) } + /// Returns an unique identifier for the object. + /// + /// The id is guaranteed to be unique for the lifetime of the object. + /// Once the object is destroyed, it may be reused for other objects. + /// This is equivalent to calling the [`spl_object_id`] PHP function. + /// + /// [`spl_object_id`]: https://www.php.net/manual/function.spl-object-id + #[inline] + pub fn get_id(&self) -> u32 { + self.handle + } + + /// Computes an unique hash for the object. + /// + /// The hash is guaranteed to be unique for the lifetime of the object. + /// Once the object is destroyed, it may be reused for other objects. + /// This is equivalent to calling the [`spl_object_hash`] PHP function. + /// + /// [`spl_object_hash`]: https://www.php.net/manual/function.spl-object-hash.php + pub fn hash(&self) -> String { + format!("{:016x}0000000000000000", self.handle) + } + /// Attempts to retrieve a reference to the object handlers. #[inline] unsafe fn handlers(&self) -> Result<&ZendObjectHandlers> { diff --git a/src/types/string.rs b/src/types/string.rs index dd9624a..6256ee2 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -16,6 +16,7 @@ use crate::{ convert::{FromZval, IntoZval}, error::{Error, Result}, ffi::{ + ext_php_rs_is_known_valid_utf8, ext_php_rs_set_known_valid_utf8, ext_php_rs_zend_string_init, ext_php_rs_zend_string_release, zend_string, zend_string_init_interned, }, @@ -30,7 +31,7 @@ use crate::{ /// cannot represent unsized types, an array of size 1 is used at the end of the /// type to represent the contents of the string, therefore this type is /// actually unsized. All constructors return [`ZBox`], the owned -/// varaint. +/// variant. /// /// Once the `ptr_metadata` feature lands in stable rust, this type can /// potentially be changed to a DST using slices and metadata. See the tracking issue here: @@ -46,7 +47,7 @@ static INTERNED_LOCK: Mutex<()> = const_mutex(()); // on the alias `ZendStr` :( #[allow(clippy::len_without_is_empty)] impl ZendStr { - /// Creates a new Zend string from a [`str`]. + /// Creates a new Zend string from a slice of bytes. /// /// # Parameters /// @@ -54,12 +55,6 @@ impl ZendStr { /// * `persistent` - Whether the string should persist through the request /// boundary. /// - /// # Returns - /// - /// Returns a result containing the Zend string if successful. Returns an - /// error if the given string contains NUL bytes, which cannot be - /// contained inside a C string. - /// /// # Panics /// /// Panics if the function was unable to allocate memory for the Zend @@ -78,10 +73,19 @@ impl ZendStr { /// ```no_run /// use ext_php_rs::types::ZendStr; /// - /// let s = ZendStr::new("Hello, world!", false).unwrap(); + /// let s = ZendStr::new("Hello, world!", false); + /// let php = ZendStr::new([80, 72, 80], false); /// ``` - pub fn new(str: &str, persistent: bool) -> Result> { - Ok(Self::from_c_str(&CString::new(str)?, persistent)) + pub fn new(str: impl AsRef<[u8]>, persistent: bool) -> ZBox { + let s = str.as_ref(); + // TODO: we should handle the special cases when length is either 0 or 1 + // see `zend_string_init_fast()` in `zend_string.h` + unsafe { + let ptr = ext_php_rs_zend_string_init(s.as_ptr().cast(), s.len(), persistent) + .as_mut() + .expect("Failed to allocate memory for new Zend string"); + ZBox::from_raw(ptr) + } } /// Creates a new Zend string from a [`CStr`]. @@ -126,7 +130,7 @@ impl ZendStr { } } - /// Creates a new interned Zend string from a [`str`]. + /// Creates a new interned Zend string from a slice of bytes. /// /// An interned string is only ever stored once and is immutable. PHP stores /// the string in an internal hashtable which stores the interned @@ -145,16 +149,12 @@ impl ZendStr { /// * `persistent` - Whether the string should persist through the request /// boundary. /// - /// # Returns - /// - /// Returns a result containing the Zend string if successful. Returns an - /// error if the given string contains NUL bytes, which cannot be - /// contained inside a C string. - /// /// # Panics /// - /// Panics if the function was unable to allocate memory for the Zend - /// string. + /// Panics under the following circumstances: + /// + /// * The function used to create interned strings has not been set. + /// * The function could not allocate enough memory for the Zend string. /// /// # Safety /// @@ -171,8 +171,16 @@ impl ZendStr { /// /// let s = ZendStr::new_interned("PHP", true); /// ``` - pub fn new_interned(str: &str, persistent: bool) -> Result> { - Ok(Self::interned_from_c_str(&CString::new(str)?, persistent)) + pub fn new_interned(str: impl AsRef<[u8]>, persistent: bool) -> ZBox { + let _lock = INTERNED_LOCK.lock(); + let s = str.as_ref(); + unsafe { + let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready"); + let ptr = init(s.as_ptr().cast(), s.len() as _, persistent) + .as_mut() + .expect("Failed to allocate memory for new Zend string"); + ZBox::from_raw(ptr) + } } /// Creates a new interned Zend string from a [`CStr`]. @@ -222,11 +230,8 @@ impl ZendStr { let _lock = INTERNED_LOCK.lock(); unsafe { - let ptr = zend_string_init_interned.expect("`zend_string_init_interned` not ready")( - str.as_ptr(), - str.to_bytes().len() as _, - persistent, - ); + let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready"); + let ptr = init(str.as_ptr(), str.to_bytes().len() as _, persistent); ZBox::from_raw( ptr.as_mut() @@ -242,7 +247,7 @@ impl ZendStr { /// ```no_run /// use ext_php_rs::types::ZendStr; /// - /// let s = ZendStr::new("hello, world!", false).unwrap(); + /// let s = ZendStr::new("hello, world!", false); /// assert_eq!(s.len(), 13); /// ``` pub fn len(&self) -> usize { @@ -256,39 +261,61 @@ impl ZendStr { /// ```no_run /// use ext_php_rs::types::ZendStr; /// - /// let s = ZendStr::new("hello, world!", false).unwrap(); + /// let s = ZendStr::new("hello, world!", false); /// assert_eq!(s.is_empty(), false); /// ``` pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Returns a reference to the underlying [`CStr`] inside the Zend string. - pub fn as_c_str(&self) -> &CStr { - // SAFETY: Zend strings store their readable length in a fat pointer. - unsafe { - let slice = slice::from_raw_parts(self.val.as_ptr() as *const u8, self.len() + 1); - CStr::from_bytes_with_nul_unchecked(slice) - } + /// Attempts to return a reference to the underlying bytes inside the Zend + /// string as a [`CStr`]. + /// + /// Returns an [Error::InvalidCString] variant if the string contains null + /// bytes. + pub fn as_c_str(&self) -> Result<&CStr> { + let bytes_with_null = + unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len() + 1) }; + CStr::from_bytes_with_nul(bytes_with_null).map_err(|_| Error::InvalidCString) } - /// Attempts to return a reference to the underlying [`str`] inside the Zend + /// Attempts to return a reference to the underlying bytes inside the Zend /// string. /// - /// Returns the [`None`] variant if the [`CStr`] contains non-UTF-8 - /// characters. + /// Returns an [Error::InvalidUtf8] variant if the [`str`] contains + /// non-UTF-8 characters. /// /// # Example /// /// ```no_run /// use ext_php_rs::types::ZendStr; /// - /// let s = ZendStr::new("hello, world!", false).unwrap(); - /// let as_str = s.as_str(); - /// assert_eq!(as_str, Some("hello, world!")); + /// let s = ZendStr::new("hello, world!", false); + /// assert!(s.as_str().is_ok()); /// ``` - pub fn as_str(&self) -> Option<&str> { - self.as_c_str().to_str().ok() + pub fn as_str(&self) -> Result<&str> { + if unsafe { ext_php_rs_is_known_valid_utf8(self.as_ptr()) } { + let str = unsafe { std::str::from_utf8_unchecked(self.as_bytes()) }; + return Ok(str); + } + let str = std::str::from_utf8(self.as_bytes()).map_err(|_| Error::InvalidUtf8)?; + unsafe { ext_php_rs_set_known_valid_utf8(self.as_ptr() as *mut _) }; + Ok(str) + } + + /// Returns a reference to the underlying bytes inside the Zend string. + pub fn as_bytes(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len()) } + } + + /// Returns a raw pointer to this object + pub fn as_ptr(&self) -> *const ZendStr { + self as *const _ + } + + /// Returns a mutable pointer to this object + pub fn as_mut_ptr(&mut self) -> *mut ZendStr { + self as *mut _ } } @@ -300,7 +327,22 @@ unsafe impl ZBoxable for ZendStr { impl Debug for ZendStr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.as_c_str().fmt(f) + self.as_str().fmt(f) + } +} + +impl AsRef<[u8]> for ZendStr { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl PartialEq for ZendStr +where + T: AsRef<[u8]>, +{ + fn eq(&self, other: &T) -> bool { + self.as_ref() == other.as_ref() } } @@ -308,19 +350,14 @@ impl ToOwned for ZendStr { type Owned = ZBox; fn to_owned(&self) -> Self::Owned { - Self::from_c_str(self.as_c_str(), false) + Self::new(self.as_bytes(), false) } } -impl PartialEq for ZendStr { - #[inline] - fn eq(&self, other: &Self) -> bool { - self.as_c_str().eq(other.as_c_str()) - } -} +impl<'a> TryFrom<&'a ZendStr> for &'a CStr { + type Error = Error; -impl<'a> From<&'a ZendStr> for &'a CStr { - fn from(value: &'a ZendStr) -> Self { + fn try_from(value: &'a ZendStr) -> Result { value.as_c_str() } } @@ -329,7 +366,7 @@ impl<'a> TryFrom<&'a ZendStr> for &'a str { type Error = Error; fn try_from(value: &'a ZendStr) -> Result { - value.as_str().ok_or(Error::InvalidCString) + value.as_str() } } @@ -337,10 +374,7 @@ impl TryFrom<&ZendStr> for String { type Error = Error; fn try_from(value: &ZendStr) -> Result { - value - .as_str() - .map(|s| s.to_string()) - .ok_or(Error::InvalidCString) + value.as_str().map(ToString::to_string) } } @@ -362,18 +396,14 @@ impl From for ZBox { } } -impl TryFrom<&str> for ZBox { - type Error = Error; - - fn try_from(value: &str) -> Result { - ZendStr::new(value, false) +impl From<&str> for ZBox { + fn from(value: &str) -> Self { + ZendStr::new(value.as_bytes(), false) } } -impl TryFrom for ZBox { - type Error = Error; - - fn try_from(value: String) -> Result { +impl From for ZBox { + fn from(value: String) -> Self { ZendStr::new(value.as_str(), false) } } diff --git a/src/types/zval.rs b/src/types/zval.rs index f6c13c5..34c8c81 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -113,12 +113,12 @@ impl Zval { /// convert other types into a [`String`], as it could not pass back a /// [`&str`] in those cases. pub fn str(&self) -> Option<&str> { - self.zend_str().and_then(|zs| zs.as_str()) + self.zend_str().and_then(|zs| zs.as_str().ok()) } /// Returns the value of the zval if it is a string and can be unpacked into - /// a vector of a given type. Similar to the [`unpack`](https://www.php.net/manual/en/function.unpack.php) - /// in PHP, except you can only unpack one type. + /// a vector of a given type. Similar to the [`unpack`] function in PHP, + /// except you can only unpack one type. /// /// # Safety /// @@ -129,22 +129,31 @@ impl Zval { /// documentation for more details. /// /// [`pack`]: https://www.php.net/manual/en/function.pack.php + /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php pub fn binary(&self) -> Option> { - if self.is_string() { - // SAFETY: Type is string therefore we are able to take a reference. - Some(T::unpack_into(unsafe { self.value.str_.as_ref() }?)) - } else { - None - } + self.zend_str().map(T::unpack_into) } - pub fn binary_slice<'a, T: PackSlice>(&self) -> Option<&'a [T]> { - if self.is_string() { - // SAFETY: Type is string therefore we are able to take a reference. - Some(T::unpack_into(unsafe { self.value.str_.as_ref() }?)) - } else { - None - } + /// Returns the value of the zval if it is a string and can be unpacked into + /// a slice of a given type. Similar to the [`unpack`] function in PHP, + /// except you can only unpack one type. + /// + /// This function is similar to [`Zval::binary`] except that a slice is + /// returned instead of a vector, meaning the contents of the string is + /// not copied. + /// + /// # Safety + /// + /// There is no way to tell if the data stored in the string is actually of + /// the given type. The results of this function can also differ from + /// platform-to-platform due to the different representation of some + /// types on different platforms. Consult the [`pack`] function + /// documentation for more details. + /// + /// [`pack`]: https://www.php.net/manual/en/function.pack.php + /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php + pub fn binary_slice(&self) -> Option<&[T]> { + self.zend_str().map(T::unpack_into) } /// Returns the value of the zval if it is a resource. @@ -331,7 +340,7 @@ impl Zval { /// * `val` - The value to set the zval as. /// * `persistent` - Whether the string should persist between requests. pub fn set_string(&mut self, val: &str, persistent: bool) -> Result<()> { - self.set_zend_string(ZendStr::new(val, persistent)?); + self.set_zend_string(ZendStr::new(val, persistent)); Ok(()) } @@ -365,7 +374,7 @@ impl Zval { /// * `val` - The value to set the zval as. /// * `persistent` - Whether the string should persist between requests. pub fn set_interned_string(&mut self, val: &str, persistent: bool) -> Result<()> { - self.set_zend_string(ZendStr::new_interned(val, persistent)?); + self.set_zend_string(ZendStr::new_interned(val, persistent)); Ok(()) } diff --git a/src/wrapper.c b/src/wrapper.c index 240b2d6..faf585e 100644 --- a/src/wrapper.c +++ b/src/wrapper.c @@ -1,7 +1,6 @@ #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); } @@ -9,6 +8,16 @@ void ext_php_rs_zend_string_release(zend_string *zs) { zend_string_release(zs); } +bool ext_php_rs_is_known_valid_utf8(const zend_string *zs) { + return GC_FLAGS(zs) & IS_STR_VALID_UTF8; +} + +void ext_php_rs_set_known_valid_utf8(zend_string *zs) { + if (!ZSTR_IS_INTERNED(zs)) { + GC_ADD_FLAGS(zs, IS_STR_VALID_UTF8); + } +} + 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) { diff --git a/src/wrapper.h b/src/wrapper.h index f55f3ec..2813263 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -21,9 +21,11 @@ #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); +bool ext_php_rs_is_known_valid_utf8(const zend_string *zs); +void ext_php_rs_set_known_valid_utf8(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); void ext_php_rs_zend_object_release(zend_object *obj); diff --git a/src/zend/ce.rs b/src/zend/ce.rs index b2e9d43..57b1877 100644 --- a/src/zend/ce.rs +++ b/src/zend/ce.rs @@ -3,70 +3,107 @@ #![allow(clippy::unwrap_used)] use crate::ffi::{ - zend_ce_argument_count_error, zend_ce_arithmetic_error, zend_ce_compile_error, - zend_ce_division_by_zero_error, zend_ce_error_exception, zend_ce_exception, - zend_ce_parse_error, zend_ce_throwable, zend_ce_type_error, zend_ce_unhandled_match_error, - zend_ce_value_error, zend_standard_class_def, + zend_ce_aggregate, zend_ce_argument_count_error, zend_ce_arithmetic_error, zend_ce_arrayaccess, + zend_ce_compile_error, zend_ce_countable, zend_ce_division_by_zero_error, + zend_ce_error_exception, zend_ce_exception, zend_ce_iterator, zend_ce_parse_error, + zend_ce_serializable, zend_ce_stringable, zend_ce_throwable, zend_ce_traversable, + zend_ce_type_error, zend_ce_unhandled_match_error, zend_ce_value_error, + zend_standard_class_def, }; use super::ClassEntry; -/// Returns the base `stdClass` class. +/// Returns the base [`stdClass`](https://www.php.net/manual/en/class.stdclass.php) class. pub fn stdclass() -> &'static ClassEntry { unsafe { zend_standard_class_def.as_ref() }.unwrap() } -/// Returns the base `Throwable` class. +/// Returns the base [`Throwable`](https://www.php.net/manual/en/class.throwable.php) class. pub fn throwable() -> &'static ClassEntry { unsafe { zend_ce_throwable.as_ref() }.unwrap() } -/// Returns the base `Exception` class. +/// Returns the base [`Exception`](https://www.php.net/manual/en/class.exception.php) class. pub fn exception() -> &'static ClassEntry { unsafe { zend_ce_exception.as_ref() }.unwrap() } -/// Returns the base `ErrorException` class. +/// Returns the base [`ErrorException`](https://www.php.net/manual/en/class.errorexception.php) class. pub fn error_exception() -> &'static ClassEntry { unsafe { zend_ce_error_exception.as_ref() }.unwrap() } -/// Returns the base `CompileError` class. +/// Returns the base [`CompileError`](https://www.php.net/manual/en/class.compileerror.php) class. pub fn compile_error() -> &'static ClassEntry { unsafe { zend_ce_compile_error.as_ref() }.unwrap() } -/// Returns the base `ParseError` class. +/// Returns the base [`ParseError`](https://www.php.net/manual/en/class.parseerror.php) class. pub fn parse_error() -> &'static ClassEntry { unsafe { zend_ce_parse_error.as_ref() }.unwrap() } -/// Returns the base `TypeError` class. +/// Returns the base [`TypeError`](https://www.php.net/manual/en/class.typeerror.php) class. pub fn type_error() -> &'static ClassEntry { unsafe { zend_ce_type_error.as_ref() }.unwrap() } -/// Returns the base `ArgumentCountError` class. +/// Returns the base [`ArgumentCountError`](https://www.php.net/manual/en/class.argumentcounterror.php) class. pub fn argument_count_error() -> &'static ClassEntry { unsafe { zend_ce_argument_count_error.as_ref() }.unwrap() } -/// Returns the base `ValueError` class. +/// Returns the base [`ValueError`](https://www.php.net/manual/en/class.valueerror.php) class. pub fn value_error() -> &'static ClassEntry { unsafe { zend_ce_value_error.as_ref() }.unwrap() } -/// Returns the base `ArithmeticError` class. +/// Returns the base [`ArithmeticError`](https://www.php.net/manual/en/class.arithmeticerror.php) class. pub fn arithmetic_error() -> &'static ClassEntry { unsafe { zend_ce_arithmetic_error.as_ref() }.unwrap() } -/// Returns the base `DivisionByZeroError` class. +/// Returns the base [`DivisionByZeroError`](https://www.php.net/manual/en/class.divisionbyzeroerror.php) class. pub fn division_by_zero_error() -> &'static ClassEntry { unsafe { zend_ce_division_by_zero_error.as_ref() }.unwrap() } -/// Returns the base `UnhandledMatchError` class. +/// Returns the base [`UnhandledMatchError`](https://www.php.net/manual/en/class.unhandledmatcherror.php) class. pub fn unhandled_match_error() -> &'static ClassEntry { unsafe { zend_ce_unhandled_match_error.as_ref() }.unwrap() } + +/// Returns the [`Traversable`](https://www.php.net/manual/en/class.traversable.php) interface. +pub fn traversable() -> &'static ClassEntry { + unsafe { zend_ce_traversable.as_ref() }.unwrap() +} + +/// Returns the [`IteratorAggregate`](https://www.php.net/manual/en/class.iteratoraggregate.php) interface. +pub fn aggregate() -> &'static ClassEntry { + unsafe { zend_ce_aggregate.as_ref() }.unwrap() +} + +/// Returns the [`Iterator`](https://www.php.net/manual/en/class.iterator.php) interface. +pub fn iterator() -> &'static ClassEntry { + unsafe { zend_ce_iterator.as_ref() }.unwrap() +} + +/// Returns the [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php) interface. +pub fn arrayaccess() -> &'static ClassEntry { + unsafe { zend_ce_arrayaccess.as_ref() }.unwrap() +} + +/// Returns the [`Serializable`](https://www.php.net/manual/en/class.serializable.php) interface. +pub fn serializable() -> &'static ClassEntry { + unsafe { zend_ce_serializable.as_ref() }.unwrap() +} + +/// Returns the [`Countable`](https://www.php.net/manual/en/class.countable.php) interface. +pub fn countable() -> &'static ClassEntry { + unsafe { zend_ce_countable.as_ref() }.unwrap() +} + +/// Returns the [`Stringable`](https://www.php.net/manual/en/class.stringable.php) interface. +pub fn stringable() -> &'static ClassEntry { + unsafe { zend_ce_stringable.as_ref() }.unwrap() +} diff --git a/src/zend/class.rs b/src/zend/class.rs index 7e61c87..e3f6cee 100644 --- a/src/zend/class.rs +++ b/src/zend/class.rs @@ -15,7 +15,7 @@ impl ClassEntry { /// could not be found or the class table has not been initialized. pub fn try_find(name: &str) -> Option<&'static Self> { ExecutorGlobals::get().class_table()?; - let mut name = ZendStr::new(name, false).ok()?; + let mut name = ZendStr::new(name, false); unsafe { crate::ffi::zend_lookup_class_ex(name.deref_mut(), std::ptr::null_mut(), 0).as_ref() @@ -37,37 +37,19 @@ impl ClassEntry { /// /// # Parameters /// - /// * `ce` - The inherited class entry to check. - pub fn instance_of(&self, ce: &ClassEntry) -> bool { - if self == ce { + /// * `other` - The inherited class entry to check. + pub fn instance_of(&self, other: &ClassEntry) -> bool { + if self == other { return true; } - if ce.flags().contains(ClassFlags::Interface) { - let interfaces = match self.interfaces() { - Some(interfaces) => interfaces, - None => return false, - }; - - for i in interfaces { - if ce == i { - return true; - } - } - } else { - loop { - let parent = match self.parent() { - Some(parent) => parent, - None => return false, - }; - - if parent == ce { - return true; - } - } + if other.is_interface() { + return self + .interfaces() + .map_or(false, |mut it| it.any(|ce| ce == other)); } - false + std::iter::successors(self.parent(), |p| p.parent()).any(|ce| ce == other) } /// Returns an iterator of all the interfaces that the class implements. @@ -95,7 +77,7 @@ impl ClassEntry { unsafe { self.__bindgen_anon_1.parent.as_ref() } } else { let name = unsafe { self.__bindgen_anon_1.parent_name.as_ref()? }; - Self::try_find(name.as_str()?) + Self::try_find(name.as_str().ok()?) } } } diff --git a/src/zend/handlers.rs b/src/zend/handlers.rs index 3d1371a..4ec4ef9 100644 --- a/src/zend/handlers.rs +++ b/src/zend/handlers.rs @@ -87,11 +87,7 @@ impl ZendObjectHandlers { .ok_or("Invalid property name pointer given")?; let self_ = &mut **obj; let props = T::get_metadata().get_properties(); - let prop = props.get( - prop_name - .as_str() - .ok_or("Invalid property name was given")?, - ); + let prop = props.get(prop_name.as_str()?); // retval needs to be treated as initialized, so we set the type to null let rv_mut = rv.as_mut().ok_or("Invalid return zval given")?; @@ -138,7 +134,7 @@ impl ZendObjectHandlers { .ok_or("Invalid property name pointer given")?; let self_ = &mut **obj; let props = T::get_metadata().get_properties(); - let prop = props.get(prop_name.as_str().ok_or("Invalid property name given")?); + let prop = props.get(prop_name.as_str()?); let value_mut = value.as_mut().ok_or("Invalid return zval given")?; Ok(match prop { @@ -220,7 +216,7 @@ impl ZendObjectHandlers { .as_ref() .ok_or("Invalid property name pointer given")?; let props = T::get_metadata().get_properties(); - let prop = props.get(prop_name.as_str().ok_or("Invalid property name given")?); + let prop = props.get(prop_name.as_str()?); let self_ = &mut **obj; match has_set_exists { diff --git a/unix_build.rs b/unix_build.rs index 623d356..9bdd446 100644 --- a/unix_build.rs +++ b/unix_build.rs @@ -2,14 +2,14 @@ use std::{path::PathBuf, process::Command}; use anyhow::{bail, Context, Result}; -use crate::{PHPInfo, PHPProvider}; +use crate::{find_executable, path_from_env, 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") + let cmd = Command::new(self.find_bin()?) .arg(arg) .output() .context("Failed to run `php-config`")?; @@ -20,6 +20,22 @@ impl Provider { } Ok(stdout.to_string()) } + + fn find_bin(&self) -> Result { + // If path is given via env, it takes priority. + if let Some(path) = path_from_env("PHP_CONFIG") { + if !path.try_exists()? { + // If path was explicitly given and it can't be found, this is a hard error + bail!("php-config executable not found at {:?}", path); + } + return Ok(path); + } + find_executable("php-config").with_context(|| { + "Could not find `php-config` executable. \ + Please ensure `php-config` is in your PATH or the \ + `PHP_CONFIG` environment variable is set." + }) + } } impl<'a> PHPProvider<'a> for Provider {