Merge branch 'master' into throw-exception-object

This commit is contained in:
Joe Hoyle 2022-12-20 14:24:22 +00:00
commit 33c6732a6e
46 changed files with 1082 additions and 435 deletions

View File

@ -1,4 +1,4 @@
FROM php:zts
FROM php:8.1-zts
WORKDIR /tmp

View File

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

View File

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

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
Cargo.lock
/.vscode
/.idea
/tmp
expand.rs

View File

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

View File

@ -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 <david.cole1340@gmail.com>"]
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"

View File

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

View File

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

View File

@ -43,7 +43,7 @@ pub trait PHPProvider<'a>: Sized {
}
/// Finds the location of an executable `name`.
fn find_executable(name: &str) -> Option<PathBuf> {
pub 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() {
@ -54,15 +54,25 @@ fn find_executable(name: &str) -> Option<PathBuf> {
}
}
/// Returns an environment variable's value as a PathBuf
pub fn path_from_env(key: &str) -> Option<PathBuf> {
std::env::var_os(key).map(PathBuf::from)
}
/// 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());
// 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);

View File

@ -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 <david.cole1340@gmail.com>"]
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"

View File

@ -71,6 +71,9 @@ OPTIONS:
--release
Whether to install the release version of the extension
--yes
Bypasses the confirmation prompt
$ cargo php remove --help
cargo-php-remove
@ -97,6 +100,9 @@ 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

View File

@ -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<PathBuf>,
/// 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<PathBuf>,
/// 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`")?;
}

View File

@ -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 <david.cole1340@gmail.com>"]
edition = "2018"

View File

@ -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<String>,
pub flags: Option<String>,
}
#[derive(Debug)]
@ -37,6 +38,7 @@ pub enum ParsedAttribute {
pub struct AttrArgs {
name: Option<String>,
modifier: Option<String>,
flags: Option<Expr>,
}
pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result<TokenStream> {
@ -117,6 +119,7 @@ pub fn parser(args: AttributeArgs, mut input: ItemStruct) -> Result<TokenStream>
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<TokenStream>
docs: comments,
properties,
modifier: args.modifier,
flags,
..Default::default()
};

View File

@ -17,6 +17,7 @@ pub struct AttrArgs {
optional: Option<String>,
ignore_module: bool,
defaults: HashMap<String, Lit>,
name: Option<String>,
}
#[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,

View File

@ -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<Option<ParsedAttribute>> {
"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() {

View File

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

View File

@ -38,6 +38,7 @@ pub struct Method {
pub optional: Option<String>,
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 })

View File

@ -34,7 +34,7 @@ pub fn parser(input: ItemFn) -> Result<TokenStream> {
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| {

View File

@ -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<TokenStream> {
#[derive(Default, Debug, FromMeta)]
#[darling(default)]
struct StartupArgs {
before: bool,
}
pub fn parser(args: Option<AttributeArgs>, input: ItemFn) -> Result<TokenStream> {
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<TokenStream> {
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<TokenStream> {
::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<String, Class>) -> Result<Vec<TokenStream>> {
}
});
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<String, Class>) -> Result<Vec<TokenStream>> {
#(#interfaces)*
// #(#properties)*
#parent
.object_override::<#ident>();
#flags
#object_override
;
#class_modifier
let class = builder.build()
.expect(concat!("Unable to build class `", #class_name, "`"));

View File

@ -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<unsafe extern "C" fn(zend_module: *mut zend_module_entry)>,
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<unsafe extern "C" fn(global: *mut ::std::os::raw::c_void)>,
@ -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;
}

View File

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

View File

@ -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
<?php
var_dump(hello_world("David"));
```
Now let's build our extension and run our test script. This is done through
`cargo` like any other Rust crate. It is required that the `php-config`
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`, on macOS it will be
`libhello_world.dylib` and on Windows it will be `hello_world.dll` (no `lib`
prefix).
```sh
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
$ php -dextension=./target/debug/libhello_world.dylib test.php
string(13) "Hello, David!"
```

View File

@ -1,3 +0,0 @@
# Examples
- [Hello World](./hello_world.md)

View File

@ -164,6 +164,9 @@ OPTIONS:
--release
Whether to install the release version of the extension
--yes
Bypasses the confirmation prompt
```
## Extension Removal
@ -203,6 +206,9 @@ OPTIONS:
--manifest <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

View File

@ -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
<?php
var_dump(hello_world("David"));
```
And run it:
```sh
$ php -d extension=./target/debug/libhello_world.so test.php
string(13) "Hello, David!"
```

View File

@ -0,0 +1,68 @@
# Installation
To get started using `ext-php-rs` you will need both a Rust toolchain
and a PHP development environment. We'll cover each of these below.
## Rust toolchain
First, make sure you have rust installed on your system.
If you haven't already done so you can do so by following the instructions [here](https://www.rust-lang.org/tools/install).
`ext-php-rs` runs on both the stable and nightly versions so you can choose whichever one fits you best.
## PHP development environment
In order to develop PHP extensions, you'll need the following installed on your system:
1. The PHP CLI executable itself
2. The PHP development headers
3. The `php-config` binary
While the easiest way to get started is to use the packages provided by your distribution,
we recommend building PHP from source.
**NB:** To use `ext-php-rs` you'll need at least PHP 8.0.
### Using a package manager
```sh
# Debian and derivatives
apt install php-dev
# Arch Linux
pacman -S php
# Fedora
dnf install php-devel
# Homebrew
brew install php
```
### Compiling PHP from source
Please refer to this [PHP internals book chapter](https://www.phpinternalsbook.com/php7/build_system/building_php.html)
for an in-depth guide on how to build PHP from source.
**TL;DR;** use the following commands to build a minimal development version
with debug symbols enabled.
```sh
# clone the php-src repository
git clone https://github.com/php/php-src.git
cd php-src
# by default you will be on the master branch, which is the current
# development version. You can check out a stable branch instead:
git checkout PHP-8.1
./buildconf
PREFIX="${HOME}/build/php"
./configure --prefix="${PREFIX}" \
--enable-debug \
--disable-all --disable-cgi
make -j "$(nproc)"
make install
```
The PHP CLI binary should now be located at `${PREFIX}/bin/php`
and the `php-config` binary at `${PREFIX}/bin/php-config`.
## Next steps
Now that we have our development environment in place,
let's go [build an extension](./hello_world.md) !

View File

@ -32,6 +32,34 @@ You can rename the property with options:
- `rename` - Allows you to rename the property, e.g.
`#[prop(rename = "new_name")]`
## Restrictions
### No lifetime parameters
Rust lifetimes are used by the Rust compiler to reason about a program's memory safety.
They are a compile-time only concept;
there is no way to access Rust lifetimes at runtime from a dynamic language like PHP.
As soon as Rust data is exposed to PHP,
there is no guarantee which the Rust compiler can make on how long the data will live.
PHP is a reference-counted language and those references can be held
for an arbitrarily long time, which is untraceable by the Rust compiler.
The only possible way to express this correctly is to require that any `#[php_class]`
does not borrow data for any lifetime shorter than the `'static` lifetime,
i.e. the `#[php_class]` cannot have any lifetime parameters.
When you need to share ownership of data between PHP and Rust,
instead of using borrowed references with lifetimes, consider using
reference-counted smart pointers such as [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html).
### No generic parameters
A Rust struct `Foo<T>` 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<i32> {
# }
# 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<bool> {
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() {}
```

View File

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

View File

@ -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<T> 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<Self> {
fn from_zval(zval: &'a Zval) -> Option<Self> {
zval.binary_slice().map(BinarySlice)
}
}
impl<T> TryFrom<Zval> for BinarySlice<'_, T>
where
T: PackSlice,
{
type Error = Error;
fn try_from(value: Zval) -> Result<Self> {
Self::from_zval(&value).ok_or_else(|| Error::ZvalConversion(value.get_type()))
}
}
impl<'a, T> From<BinarySlice<'a, T>> 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;

View File

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

View File

@ -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<T: Into<String>>(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.
///

View File

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

View File

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

View File

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

View File

@ -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<T, E>`]: 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<T>`]: 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<T>`]: 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

View File

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

View File

@ -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<String> {
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<T: RegisteredClass>(&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<bool> {
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> {

View File

@ -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<ZendStr>`], 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: <https://github.com/rust-lang/rust/issues/81513>
@ -46,7 +47,7 @@ static INTERNED_LOCK: Mutex<()> = const_mutex(());
// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
#[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<ZBox<Self>> {
Ok(Self::from_c_str(&CString::new(str)?, persistent))
pub fn new(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
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<ZBox<Self>> {
Ok(Self::interned_from_c_str(&CString::new(str)?, persistent))
pub fn new_interned(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
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<T> PartialEq<T> 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<ZendStr>;
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<Self> {
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<Self> {
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<Self> {
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<CString> for ZBox<ZendStr> {
}
}
impl TryFrom<&str> for ZBox<ZendStr> {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
ZendStr::new(value, false)
impl From<&str> for ZBox<ZendStr> {
fn from(value: &str) -> Self {
ZendStr::new(value.as_bytes(), false)
}
}
impl TryFrom<String> for ZBox<ZendStr> {
type Error = Error;
fn try_from(value: String) -> Result<Self> {
impl From<String> for ZBox<ZendStr> {
fn from(value: String) -> Self {
ZendStr::new(value.as_str(), false)
}
}

View File

@ -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<T: Pack>(&self) -> Option<Vec<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
}
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<T: PackSlice>(&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(())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> {
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<PathBuf> {
// 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 {