# Migrating to `v0.16`
## `Zval::set_resource` Requires an Unsafe Block
`Zval::set_resource` accepts a raw `zend_resource` pointer and transfers one
owned Zend reference into the zval, so callers must now uphold its safety
contract explicitly.
### Before (v0.15)
```rust,ignore
zval.set_resource(resource);
```
### After (v0.16)
```rust,ignore
// SAFETY: `resource` is a valid Zend-managed resource whose owned reference is
// transferred into `zval`.
unsafe { zval.set_resource(resource) };
```
The pointer must be non-null and valid until Zend releases the transferred
reference. `set_resource` does not increment the resource refcount, so the
caller must relinquish that reference after the call. Replacing a zval with the
same resource requires a separate incoming reference.
## SAPI Header Raw Constructors Removed
`SapiHeader::from_raw` and `SapiHeaders::from_raw` have been removed. These
constructors accepted unchecked Zend pointers through safe Rust.
ext-php-rs now copies the header data into owned snapshots before invoking the
`Sapi::send_header` and `Sapi::send_headers` callbacks.
### Before (v0.15)
```rust,ignore
let header = SapiHeader::from_raw(raw_header);
let headers = SapiHeaders::from_raw(raw_headers);
```
### After (v0.16)
Read header data from the snapshots supplied to your `Sapi` implementation:
```rust,ignore
fn send_header(_ctx: &mut MyContext, header: &SapiHeader) {
if let Some((name, value)) = header.as_name_value() {
// Send the header to the client.
}
}
fn send_headers(
_ctx: &mut MyContext,
headers: &SapiHeaders,
) -> SendHeadersResult {
let response_code = headers.http_response_code();
SendHeadersResult::DoSend
}
```
Code that needs direct access to `sapi_header_struct` must use the raw FFI API
and uphold Zend's pointer validity and lifetime requirements itself.
## Void Return Type for Functions Without Explicit Return
Functions and methods without an explicit Rust return type now declare `void` as their PHP return type.
### Before (v0.15)
```rust,ignore
#[php_function]
pub fn do_something() {
println!("Hello");
}
```
Generated PHP signature: `function do_something()` (implicit `mixed` return)
### After (v0.16)
The same Rust code now generates: `function do_something(): void`
### Migration
If your function actually returns a value but didn't declare it, you must now add the return type:
```rust,ignore
// Before: worked but was incorrect
#[php_function]
pub fn get_value() {
42 // implicitly returned, but no declared type
}
// After: must declare return type
#[php_function]
pub fn get_value() -> i32 {
42
}
```
### Exceptions
The magic methods `__destruct` and `__clone` are excluded from this change, as PHP forbids return type declarations on them.
## Mutable Accessors Require `&mut`
This closes [#461](https://github.com/extphprs/ext-php-rs/issues/461).
Several safe functions used to hand out a `&mut` derived from a shared `&`. That
let ordinary safe code hold two mutable references to the same zval at once,
which is undefined behaviour under Rust's aliasing rules. The signatures below
now require a mutable borrow so the compiler rejects that.
The affected functions are `ZendHashTable::get_mut`,
`ZendHashTable::get_index_mut`, `Zval::indirect_mut`,
`ExecutorGlobals::function_table_mut`, `Zval::traversable`, `Zval::iterable`,
`ClassEntry::get_iterator`, and `StreamWrapper::get` / `StreamWrapper::get_mut`.
### Mutable Hashtable Access
`get_mut` and `get_index_mut` take `&mut self`, so the binding must be mutable.
#### Before (v0.15)
```rust,ignore
fn bump(ht: &ZendHashTable) {
if let Some(zv) = ht.get_mut("counter") {
zv.set_long(1);
}
}
```
#### After (v0.16)
```rust,ignore
fn bump(ht: &mut ZendHashTable) {
if let Some(zv) = ht.get_mut("counter") {
zv.set_long(1);
}
}
```
The following no longer compiles, which is the point of the change:
```rust,ignore
let first = ht.get_mut("test").unwrap();
let second = ht.get_mut("test").unwrap(); // error[E0499]
first.set_long(2);
```
If you need to read two entries at once, take the second as a shared reference
with `get`, or finish with the first before requesting the second.
### Executor Globals
`function_table_mut` takes `&mut self`, so acquire the write guard rather than
the read guard.
#### Before (v0.15)
```rust,ignore
let table = ExecutorGlobals::get().function_table_mut();
```
#### After (v0.16)
```rust,ignore
let mut globals = ExecutorGlobals::get_mut();
let table = globals.function_table_mut();
```
`GlobalWriteGuard` already implements `DerefMut`, so no other change is needed.
Bind the guard to a variable, as the guard must outlive the borrow.
### Iterables and Traversables
`Zval::traversable` and `Zval::iterable` take `&mut self`, and `Iterable` is now
extracted through `FromZvalMut` instead of `FromZval`. Resolving the
`Traversable` variant invokes the Zend `get_iterator` handler, which mutates the
object, so a shared borrow was never sound.
Function signatures using `Iterable` as a parameter are **unchanged** and keep
working, because the macro already extracts arguments through `FromZvalMut`:
```rust,ignore
#[php_function]
pub fn count_items(mut iterable: Iterable) -> usize {
iterable.iter().map_or(0, Iterator::count)
}
```
Only hand-written extraction needs updating.
#### Before (v0.15)
```rust,ignore
let iterable = Iterable::from_zval(zval);
let iterator = zval.traversable();
```
#### After (v0.16)
```rust,ignore
let iterable = Iterable::from_zval_mut(zval);
let iterator = zval.traversable();
```
where `zval` is now a `&mut Zval`. `ClassEntry::get_iterator` likewise takes
`&'a mut Zval` and ties the returned iterator's lifetime to it.
### Stream Wrappers
`StreamWrapper::get` and `StreamWrapper::get_mut` are now `unsafe`. They return a
reference into PHP's per-request stream wrapper table with a lifetime that is not
constrained by their arguments, and `get_mut` cannot verify that no other
reference to the same wrapper is live. This is the same class of change as
`Zval::set_resource` above.
#### Before (v0.15)
```rust,ignore
let wrapper = StreamWrapper::get_mut("http").unwrap();
```
#### After (v0.16)
```rust,ignore
// SAFETY: used within the current request, and no other reference to the
// `http` wrapper is live for the duration of this borrow.
let wrapper = unsafe { StreamWrapper::get_mut("http") }.unwrap();
```
Both functions also copy the name into a NUL-terminated C string before calling
into PHP. Previously they passed the bytes of a `&str` directly, and
`php_stream_locate_url_wrapper` scans the path until it reaches a character
outside `[A-Za-z0-9+-.]`, so it could read past the end of the string slice and
resolve the wrong wrapper.
### Threading `&mut` Through a Handler
The requirement can propagate up several frames. A realistic case: a function
that mutates entries of an array argument.
#### Before (v0.15)
```rust,ignore
#[php_function]
pub fn normalise(ht: &ZendHashTable) {
rescale(ht);
}
fn rescale(ht: &ZendHashTable) {
for key in ["width", "height"] {
if let Some(zv) = ht.get_mut(key) {
if let Some(value) = zv.long() {
zv.set_long(value * 2);
}
}
}
}
```
#### After (v0.16)
```rust,ignore
#[php_function]
pub fn normalise(ht: &mut ZendHashTable) {
rescale(ht);
}
fn rescale(ht: &mut ZendHashTable) {
for key in ["width", "height"] {
if let Some(zv) = ht.get_mut(key) {
if let Some(value) = zv.long() {
zv.set_long(value * 2);
}
}
}
}
```
`&mut ZendHashTable` is accepted as a `#[php_function]` parameter type, so the
PHP-facing signature does not change. If you cannot obtain a mutable borrow at
the outermost frame, the array is genuinely shared and mutating it was unsound
before this release.
## Engine-Input Accessors Return `Option` Instead of Panicking
Accessors that read data controlled by the engine or the request used to
`panic!`, which aborts the process inside an `extern "C"` frame. They now
return `Option` and align with `http_server_vars`, which already did.
Affected signatures:
| `ProcessGlobals::http_get_vars() -> &ZendHashTable` | `-> Option<&ZendHashTable>` |
| `ProcessGlobals::http_post_vars() -> &ZendHashTable` | `-> Option<&ZendHashTable>` |
| `ProcessGlobals::http_cookie_vars() -> &ZendHashTable` | `-> Option<&ZendHashTable>` |
| `ProcessGlobals::http_env_vars() -> &ZendHashTable` | `-> Option<&ZendHashTable>` |
| `ProcessGlobals::http_files_vars() -> &ZendHashTable` | `-> Option<&ZendHashTable>` |
| `ProcessGlobals::http_request_vars() -> Option<&ZendHashTable>` | `-> Result<Option<&ZendHashTable>>` |
| `SapiHeader::as_str() -> &str` | `-> Option<&str>` |
| `SapiHeader::name() -> &str` | `-> Option<&str>` |
| `zend::php_sapi_name() -> String` | `-> Option<String>` |
`http_request_vars` distinguishes engine failures from absence: `Err` carries
`Error::AutoGlobalLoadFailed("_REQUEST")` or `Error::ZvalConversion` where it
previously panicked, and `Ok(None)` means the global is not populated.
`SapiHeader::value` keeps its `Option` signature but returns `None` where it
previously panicked (non-UTF-8 header).
### Before (v0.15)
```rust,ignore
let get = ProcessGlobals::get().http_get_vars().to_owned();
let name = header.name();
```
### After (v0.16)
```rust,ignore
let get = ProcessGlobals::get().http_get_vars()?.to_owned();
let name = header.name()?;
```
`None` means the global has not been populated as an array (yet) or, for
headers, that the bytes are not valid UTF-8. In a regular request context the
superglobals are populated and unwrapping stays reasonable; before request
startup or in exotic SAPIs it no longer aborts the process.
## `describe` Types Moved to `ext-php-rs-introspection`
The types behind `ext_php_rs::describe` now live in the
`ext-php-rs-introspection` crate, which has no dependency on the Zend engine.
`cargo-php` depends on that crate alone, so installing it no longer needs PHP.
The paths under `ext_php_rs::describe` are unchanged re-exports. Three details
of the ABI changed with the move.
### `DataType` Zend Conversions Moved to `DataTypeExt`
`DataType` is defined in the new crate, so the conversions that need the Zend
`IS_*` constants moved to the `DataTypeExt` trait, exported from the prelude.
#### Before (v0.15)
```rust,ignore
let raw = DataType::Long.as_u32();
let ty = DataType::from(raw);
```
#### After (v0.16)
```rust,ignore
use ext_php_rs::prelude::*;
let raw = DataType::Long.as_u32();
let ty = DataType::from_u32(raw);
```
### `describe::Class::flags` Replaced by `is_interface`
`Class` carried the raw `ce_flags` word. It now carries the only bit stub
generation needs.
#### Before (v0.15)
```rust,ignore
let is_interface = ClassFlags::from_bits(class.flags)
.unwrap_or(ClassFlags::empty())
.contains(ClassFlags::Interface);
```
#### After (v0.16)
```rust,ignore
let is_interface = class.is_interface;
```
### Tuple `From` Impls for `Method` and `Constant` Removed
`describe::Method` and `describe::Constant` are now defined outside
`ext-php-rs`, and Rust forbids implementing `From<(A, B)>` for a foreign type,
so these conversions are gone: `From<(FunctionBuilder, MethodFlags)> for
Method`, `From<(String, Box<dyn IntoConst + Send>, DocComments)> for Constant`
and `From<(String, DocComments)> for Constant`. `ext-php-rs` uses private
helpers internally. Downstream code that built these values by hand can fill
the structs directly, every field is public.
#### Before (v0.15)
```rust,ignore
let constant: Constant = ("MY_CONST".to_string(), docs).into();
```
#### After (v0.16)
```rust,ignore
let constant = Constant {
name: "MY_CONST".into(),
docs: docs.into(),
value: abi::Option::None,
};
```
### `Description::version` Is the Introspection Version
`Description::version` was a `&'static str` holding the `ext-php-rs` version.
It is now the first field, an `abi::Str` holding the `ext-php-rs-introspection`
version. The entry point is renamed `ext_php_rs_describe_module_v2` and returns
`*mut Description` allocated with `Box::into_raw` instead of a `Description` by
value; the new name keeps a v0.15 extension and a v0.16 `cargo-php` (or the
reverse) from calling each other with the wrong calling convention. Only the
`#[php_module]` macro emits that symbol, so this matters solely if you
implement it by hand. `cargo php stubs` reads the version through the pointer
before touching anything else, and
`cargo php stubs` accepts an extension whose version is semver compatible with
the one the CLI was built with. `Module::enums` is always present and
`Class::closure()` is always available; the `enum` and `closure` features only
decide whether `ext-php-rs` fills them.
### `DataType::Object` Holds an `abi::Str`
`DataType::Object` carried an `Option<&'static str>`. That payload has a Rust
layout inside a `#[repr(C)]` enum, so an extension and a `cargo-php` built with
different Rust versions could read it differently. It now holds an `abi::Str`.
An empty name means any object. `DataType` no longer implements `PartialOrd`
and `Ord`.
Build the type with the two constructors and read it with `class_name()`.
The `#[php_class]`, `#[php_interface]` and `#[derive(ZvalConvert)]` macros
already use them. Only hand-written `IntoZval` and `FromZval` implementations
have to change.
#### Before (v0.15)
```rust,ignore
const TYPE: DataType = DataType::Object(Some(MyClass::CLASS_NAME));
const TYPE: DataType = DataType::Object(None);
if let DataType::Object(Some(name)) = ty {
println!("{name}");
}
```
#### After (v0.16)
```rust,ignore
const TYPE: DataType = DataType::object(MyClass::CLASS_NAME);
const TYPE: DataType = DataType::ANY_OBJECT;
if let Some(name) = ty.class_name() {
println!("{name}");
}
```
## `PackSlice::unpack_into` Requires an Unsafe Block
`PackSlice::unpack_into` builds a `&[T]` over the bytes of a `zend_string`, so
its length and alignment preconditions are now part of the signature.
`Zval::binary_slice` checks both and stays safe. `Pack::unpack_into` stays safe
as well: it now reads unaligned and never relied on alignment.
### Before (v0.15)
```rust,ignore
let values: &[u64] = u64::unpack_into(zend_str);
```
### After (v0.16)
```rust,ignore
// Preferred: the checked entry point.
let values: &[u64] = zval.binary_slice::<u64>()?;
// Or, when the checks are done by hand:
// SAFETY: `zend_str.len` is a multiple of 8 and `val` is 8-byte aligned.
let values: &[u64] = unsafe { u64::unpack_into(zend_str) };
```
`Zval::binary_slice` also returns `None` when the string bytes are not aligned
for `T`. Zend allocates every string on an 8-byte boundary, so this only fires
for types wider than 8 bytes or strings from a foreign allocator.
## `Zval::set_ptr` Requires an Unsafe Block
`Zval::set_ptr` stores a raw pointer in an `IS_PTR` zval. The engine reads
`IS_PTR` entries with a fixed type in several of its tables (property tables as
`zend_property_info *`, constant tables as `zend_class_constant *`, INI
directives as `zend_ini_entry *`), so storing an arbitrary pointer and letting
the zval reach one of those tables is undefined behaviour.
### Before (v0.15)
```rust,ignore
zval.set_ptr(ptr);
```
### After (v0.16)
```rust,ignore
// SAFETY: `ptr` outlives the zval and the zval never reaches an engine table.
unsafe { zval.set_ptr(ptr) };
```
`set_ptr` now also releases the previous value of the zval instead of
overwriting it in place.
## Iterators Yield Owned Values
`ZendIterator::get_current_data` returned `&'a Zval` with an unconstrained
`'a`, so callers could keep the reference past the next `move_forward`. Zend
releases that zval on every step (`zend_user_it_invalidate_current` runs
`zval_ptr_dtor` on it), so the reference dangled. The reference is now tied to
the `&mut self` borrow, and both `ZendIterator`'s and `Iterable`'s `Iter` yield
`(Zval, Zval)`: the value is a shallow clone, the same copy `foreach` makes in
`ZEND_FE_FETCH_R`.
### Before (v0.15)
```rust,ignore
instead of a `&Zval`, so drop the explicit `&` where you spelled the type.
## `ZBox::into_raw` Returns `*mut T`
`ZBox::into_raw` fabricated a `&'static mut T`. It now mirrors `Box::into_raw`
and returns `*mut T`, which `ZBox::from_raw` accepts back unchanged.
### Before (v0.15)
```rust,ignore
let obj: &'static mut ZendObject = zbox.into_raw();
zval.set_object(obj);
```
### After (v0.16)
```rust,ignore
let obj: *mut ZendObject = zbox.into_raw();
// SAFETY: `into_raw` yields a valid, exclusively owned object.
zval.set_object(unsafe { &mut *obj });
```
## Heap-Leaking `into_raw` Constructors Removed
`FunctionEntry::into_raw`, `IniEntryDef::into_raw`, `SapiModule::into_raw` and
the deprecated `ModuleEntry::into_raw` boxed a value and returned the pointer
with no owner. None of them was needed: `ModuleBuilder` and `ClassBuilder` hand
function tables to the engine themselves, `IniEntryDef::register` takes a
`Vec`, and `sapi_startup`/`php_module_startup` copy the `sapi_module_struct`
by value (`sapi_module = *sf`), so a local is enough.
### Before (v0.15)
```rust,ignore
let sapi = builder.build()?.into_raw();
unsafe { sapi_startup(sapi) };
// ...
unsafe { cleanup_sapi_allocations(sapi); drop(Box::from_raw(sapi)) };
```
### After (v0.16)
```rust,ignore
let mut sapi = builder.build()?;
unsafe { sapi_startup(&raw mut sapi) };
// ...
unsafe { cleanup_sapi_allocations(&raw mut sapi) };
```
Keep the `SapiModule` alive until `sapi_shutdown()` has returned: the engine
copies the struct but keeps reading the strings it points to.
## `PhpRc` Removed
The `rc` module and its `PhpRc` trait are removed. `inc_count`, `dec_count` and
`get_rc_mut` changed the reference count from safe code. A wrong count lets the
engine free an object that PHP still uses. `ZendObject` and `ZendStr` now have
a read-only `ref_count` method.
### Before (v0.15)
```rust,ignore
use ext_php_rs::rc::PhpRc;
let count = obj.get_count();
```
### After (v0.16)
```rust,ignore
let count = obj.ref_count();
```
The crate has no public method to change the reference count. Use these
wrappers instead:
- To keep one more reference to a value, use `Zval::shallow_clone`.
- To move an owned object into a zval, use the `IntoZval` impl of
`ZBox<ZendObject>` or `ZBox<ZendClassObject<T>>`. Do not call `dec_count`
and then `Zval::set_object`.
`ZendStr::ref_count` returns 1 for an interned string, like
`zend_string_refcount` in PHP.
## Observer `ErrorType` Merged Into `flags::ErrorType`
Two types had the name `ErrorType`. The observer type is removed.
`ErrorObserver`, `ErrorInfo` and `php_error` now use `flags::ErrorType`.
The prelude exports `flags::ErrorType` without the `observer` feature, and
`observer::ErrorType` is a re-export of it.
The constants use CamelCase, like every other type in `flags`.
### Before (v0.15)
```rust,ignore
fn should_observe(&self, error_type: ErrorType) -> bool {
(ErrorType::FATAL | ErrorType::USER_WARNING).contains(error_type)
}
```
### After (v0.16)
```rust,ignore
fn should_observe(&self, error_type: ErrorType) -> bool {
(ErrorType::Fatal | ErrorType::UserWarning).contains(error_type)
}
```
Other changes:
- `ErrorType::All` is `E_ALL` of the PHP version that you build against.
Before PHP 8.4, it contains `ErrorType::Strict`. The old observer `ALL`
never contained it.
- `ErrorType::Strict` and `ErrorType::UserError` are deprecated on PHP 8.4
and later. The old observer `USER_ERROR` was not deprecated.
- The type holds a `u32`, not an `i32`. `bits()` returns a `u32`, and
`from_bits` and `from_bits_truncate` take a `u32`.
## `GlobalConstantFlags::CaseSensitive` Removed
The flag was deprecated and had no effect since PHP 8.0, where constants are
always case-sensitive. Remove it from any flag set you build.
## `alloc::emalloc` Honours `Layout::align`
`emalloc` ignored the alignment of the `Layout` it was given. The Zend allocator
guarantees `ZEND_MM_ALIGNMENT` (8 bytes) and has no aligned-allocation entry
point, so `emalloc` now returns a null pointer for any `Layout` whose alignment
exceeds 8 instead of handing back under-aligned memory.
## `zend::cleanup_module_allocations` Removed
The helper freed the module's function table, argument info and strings during
MSHUTDOWN. That is unsound on every supported PHP version:
- For a `dl()`-loaded extension, `module_destructor` calls
`zend_unregister_functions(module->functions, …)` **after** the shutdown
function returns, and that walks the table reading `fname`.
- `zend_register_functions` copies the `arg_info` array only shallowly, so
`arg_info[i].name` and `.default_value` stay borrowed by the engine and are
read at runtime by `ReflectionParameter`. A function with neither parameters
nor a return type keeps the array itself.
There is no replacement, and nothing to call. ext-php-rs now reclaims class and
enum method tables as soon as the engine has interned them, and owns the tables
the engine borrows for the life of the process. Extensions must not free
anything they pass to `zend_register_*`.
### Before (v0.15)
```rust,ignore
unsafe { ext_php_rs::zend::cleanup_module_allocations(entry) };
```
### After (v0.16)
Delete the call. If you set `module_shutdown_func` through
`ModuleBuilder::shutdown_function`, it is no longer wrapped and runs exactly as
you wrote it.
## Registration Tables Are Owned Per Extension
ext-php-rs no longer keeps a crate-wide list of the tables the Zend engine
borrows for the life of the process. Each table is owned by a `static` belonging
to the extension, the way a C extension owns its `zend_module_entry`,
`zend_function_entry[]` and `zend_internal_arg_info[]`.
Three signatures changed.
### `FunctionBuilder::build`
Returns the argument info table alongside the entry, so the caller decides where
it lives. Dropping it dangles every registered function.
```rust,ignore
// v0.15
let entry = builder.build()?;
// v0.16
let (entry, arg_info) = builder.build()?;
```
`ClassBuilder` and `EnumBuilder` do this for you: `registration` now receives the
tables alongside the class entry, and `ModuleBuilder::class::<T>()` wires it to
`ClassMetadata<T>`. A class built by hand must park them:
```rust,ignore
// v0.15
// v0.16
.registration(|ce, arg_info| {
MY_META.set_ce(ce);
MY_META.set_arg_info(arg_info);
})
```
### `TryFrom<ModuleBuilder>`
Yields a third element, the allocations the `ModuleEntry` points into. The
`#[php_module]` macro stores it in the extension's `StaticModuleEntry`; only
code that calls `try_into()` by hand is affected.
```rust,ignore
// v0.15
let (entry, startup): (ModuleEntry, ModuleStartup) = builder.try_into()?;
// v0.16
let (entry, startup, owned): (ModuleEntry, ModuleStartup, ModuleAllocations) =
builder.try_into()?;
```
### `IniEntryDef::register`
Takes a `&'static [IniEntryDef]` terminated by `IniEntryDef::end()` instead of a
`Vec`, because PHP 8.5 keeps the table as `zend_ini_entry.def` and the CLI SAPI
reads `def->value` at runtime. `IniEntryDef::new` is now a `const fn` taking
`&'static CStr`, so the table is a plain `static`.
#### Before (v0.15)
```rust,ignore
let entries = vec![IniEntryDef::new(
"my_ext.enabled".to_owned(),
"1".to_owned(),
&IniEntryPermission::All,
)];
IniEntryDef::register(entries, mod_num);
```
#### After (v0.16)
```rust,ignore
static INI: IniEntryDefs<2> = IniEntryDefs::new([
IniEntryDef::new(c"my_ext.enabled", c"1", IniEntryPermission::All),
IniEntryDef::end(),
]);
IniEntryDef::register(INI.as_slice(), mod_num);
```
`IniEntryDefs<N>` exists because `IniEntryDef` holds raw pointers and an array of
them is not `Sync` on its own.
### `From<Arg> for ZendExpectedType` removed
`impl From<Arg<'_>> for _zend_expected_type` is gone. It existed only to feed
`zend_wrong_parameter_type_error`, which ext-php-rs never calls: argument
mismatches go through `ArgParser`, which raises the error itself. Call
`zend_wrong_parameter_type_error` through `ext_php_rs::ffi` if you relied on it.
### The `static` feature is now `_static`
Linking libclang statically moved from `static` to `_static`. It still pairs with
`runtime` the same way: the two are mutually exclusive, because clang-sys refuses
to build them together.
```toml
# v0.15
ext-php-rs = { version = "0.15", default-features = false, features = ["enum", "static"] }
# v0.16
ext-php-rs = { version = "0.16", default-features = false, features = ["enum", "_static"] }
```
The underscore is what keeps `cargo-semver-checks` from selecting the feature on
its own. It enables every feature it does not consider unstable, so with the old
name it turned on both halves of the pair and could never build the crate, which
in turn blocked the release tooling that runs it.
`runtime` is unchanged and stays the default.
## The Error Model
### `Error` is now `Send + Sync`
`Error::Exception(ZBox<ZendObject>)` is gone. It was the only variant holding
engine memory, and it made the crate-wide `Result` unusable with threads or
`anyhow`, which requires `Send + Sync + 'static`.
`ZendCallable::try_call` and friends keep their signatures. When the callable
throws, the exception stays in the executor globals and the call returns
`Error::ExceptionPending { class }`. Returning to PHP propagates the original
exception with its class, message and stack trace; a Rust caller that wants to
handle it takes ownership with `ExecutorGlobals::take_exception()`.
```rust,ignore
// v0.15
match callable.try_call(vec![]) {
Err(Error::Exception(obj)) => { /* the exception object */ }
_ => {}
}
// v0.16
match callable.try_call(vec![]) {
Err(Error::ExceptionPending { class }) => {
let obj = ExecutorGlobals::take_exception();
}
_ => {}
}
```
`PhpException::throw` is now a no-op when an exception is already pending, so a
Rust error can never bury the PHP exception that caused it.
### Error messages are lowercase fragments
Every `Display` implementation follows the Rust convention: lowercase, no
trailing period. `Error::InvalidScope` prints `invalid scope`, not
`Invalid scope.`. Exception messages produced from an `Error` change to match.
### Variants that carry context
| `Error::InvalidProperty` | `Error::InvalidProperty { property: String }` |
| `Error::InvalidCString` | `Error::InvalidCString { position: usize }` |
| `Error::InvalidProperty` (enum case lookup) | `Error::InvalidEnumCase { case: String }` |
| `Error::InvalidProperty` (array key conversion) | `Error::ZvalConversion(DataType::String)` |
| `Error::InvalidProperty` (`ArrayKey::ZendString` that is not UTF-8) | `Error::InvalidUtf8` |
| `Error::InvalidProperty` (object passed to an enum conversion is not an enum) | `Error::ZvalConversion(DataType::ANY_OBJECT)` |
| `Error::UnknownDatatype(0)` from `TryFrom<ZvalTypeFlags>` | `Error::InvalidTypeToDatatype(flags)` |
`Error::UnknownDatatype` is removed. It only ever appeared with a hardcoded `0`
from the conversion above, which discarded the real flag bits.
`Error::Catch(CatchError)` is new: `try_catch` failures convert into it with
`?`, and `source()` reports whether the engine bailed out or the result pointer
was null.
### `CatchError` and `EmbedError` are printable
Both now implement `Display` and `std::error::Error`. `CatchError` is an enum
with `Bailout` and `NullPanicPtr` instead of a unit struct that meant either.
`EmbedError::InitError`, which was never constructed, is gone, and
`EmbedError::CatchError` went from a unit variant to one carrying the
`CatchError` that caused it:
```rust,ignore
// v0.15
Err(EmbedError::CatchError) => { /* was it a bailout? no way to tell */ }
// v0.16
Err(EmbedError::CatchError(CatchError::Bailout)) => { /* ... */ }
```
`CatchError`, `EmbedError` and `PhpEvalError` are now `#[non_exhaustive]`, so a
downstream `match` on any of them needs a wildcard arm. `WorkerError` already
was.
### `PhpException::default` is now `from_message`
The inherent `default` shadowed `Default::default` while taking an argument.
```rust,ignore
// v0.15
PhpException::default("something went wrong".into());
// v0.16
PhpException::from_message("something went wrong".into());
```
`throw_object` now validates that the zval holds an object and returns
`Err(Error::Object)` otherwise, instead of letting the engine raise a fatal
error.
### Argument and return-type builders report a conversion error
`Arg::as_arg_info` and `FunctionBuilder`'s return-type path returned
`Error::InvalidCString` when `ZendType::empty_from_type` could not represent a
type, which had nothing to do with C strings. They now return
`Error::ZvalConversion(ty)` naming the type that could not be represented.
### `ExecutorGlobals::throw_if_exception` builds the exception directly
It used to go through `Error::Exception` and `From<Error> for PhpException`. It
now takes the exception and attaches it to the `PhpException` itself, so the
class and stack trace survive without a round trip through `Error`. The
signature, `PhpResult<()>`, is unchanged.
### `Function::function_type` returns a `Result`
`From<u8> for FunctionType` panicked on a value read straight out of
`zend_function`. It is now `TryFrom<u8>`, and `function_type()` returns
`Result<FunctionType>` with `Error::UnknownFunctionType`.
### `ZendHashTable::remove` returns the removed value
`remove` and `remove_index` returned `Option<()>`, a boolean in costume. They
now return `Option<Zval>`, matching `OccupiedEntry::remove`.
```rust,ignore
// v0.15
ht.remove("key");
// v0.16
let removed: Option<Zval> = ht.remove("key");
```
### `StreamWrapper::register` no longer panics on a NUL byte
`register` and `unregister` returned a `Result` but panicked when the wrapper
name contained a NUL byte. They now return `Error::InvalidCString`.
## `BailoutGuard` Cleanup Belongs to the `try_catch` Frame
A `try_catch` frame now drops the `BailoutGuard`s that were created inside its
closure when it catches a bailout. Guards created before the frame are not
changed. The old thread-wide cleanup ran later, in the generated handler, and
could free a guard that an outer frame still used.
### Before (v0.15)
```rust,ignore
let file = BailoutGuard::new(File::open("data.txt")?);
let result = try_catch(|| {
let _tmp = BailoutGuard::new(TempFile::create());
risky_engine_call();
});
// On Err, _tmp is still allocated here. The exported handler frees both
// _tmp and file later, and file dangles.
```
### After (v0.16)
```rust,ignore
let file = BailoutGuard::new(File::open("data.txt")?);
let result = try_catch(|| {
let _tmp = BailoutGuard::new(TempFile::create());
risky_engine_call();
});
// On Err, try_catch already dropped _tmp. file is still valid.
```
`BailoutGuard` is no longer `Send`. Code that moves a guard to another thread
does not compile anymore. Keep the guard on the thread that created it, or call
`into_inner` before you send the value.
`zend::run_bailout_cleanups` is removed. `try_catch` runs the cleanup itself.
## Panics Stay on the Rust Side of the FFI Boundary
A panic that unwinds out of an `extern "C"` function aborts the process. In
v0.15 every generated handler let a panic do exactly that, because `try_catch`
caught the panic only to resume it. v0.16 keeps every panic on the Rust side.
See [Panics](../exceptions.md#panics) for the full policy.
### `try_catch` reports a panic instead of resuming it
`try_catch` and `try_catch_first` return `Err(CatchError::Panic(message))` when
the closure panics. `CatchError::Panic` is a new variant, and `Error::Catch`
prints its message.
```rust,ignore
// v0.15: the panic continued past this line
let result = try_catch(|| risky());
// v0.16
match try_catch(|| risky()) {
Err(CatchError::Panic(message)) => std::panic::resume_unwind(Box::new(message)),
Err(CatchError::Bailout) => { /* ... */ }
Err(_) => { /* ... */ }
Ok(value) => { /* ... */ }
}
```
A test that asserts inside a `try_catch` closure and ignores the result now
passes even when the assertion fails. Assert on the result of `try_catch`.
`Embed::eval` and `Embed::run_script` return
`EmbedError::CatchError(CatchError::Panic(..))` in that case. `PhpEvalError`
gained a `Panic(String)` variant. `Embed::run` still resumes a panic, so a test
that panics inside `Embed::run` still fails.
### A panic in a handler throws a PHP `Error`
A panic in a generated function, method, constructor, `#[php_impl_interface]`
method, `Closure`, property getter or setter, or `clone` throws a PHP `Error`
with the message `Rust panic: <message>` and returns normally. A panic in
`Drop` of a Rust-backed object reports an `E_WARNING` and frees the object.
Before v0.16 each of these aborted the PHP process.
A handler that you write by hand for `FunctionBuilder::new` does not get this
behaviour on its own. Run its body through the new `zend::run_handler`.
### `PhpException::throw` returns `()`
`throw()` no longer returns a `Result`. If the exception cannot be thrown, for
example because the class is an interface or the message contains a NUL byte,
`throw()` throws a plain `Error` whose message names the cause and contains the
original message.
```rust,ignore
// v0.15
exception.throw()?;
exception.throw().expect("Failed to throw PHP exception.");
// v0.16
exception.throw();
```
`IntoZval for Result<T, E>` throws the error as before and now returns
`Err(Error::ExceptionPending { class })` instead of the conversion error.
### MINIT fails instead of panicking
`ModuleStartup::startup` returns an `Err` when a class, an interface or an enum
cannot be registered. The generated startup function logs the cause as an
`E_CORE_WARNING` and returns `FAILURE`, and PHP reports
`Unable to start <extension> module`. A panic during registration gets the same
treatment. In v0.15 a failed constant registration made the startup function
return `1`, which is not `FAILURE` (`-1`), so PHP started the module anyway.
`get_module` never panics. If the module cannot be built, the macro returns a
placeholder entry whose startup function logs the build error and fails as
above.
### New items
`zend::run_handler` runs the body of a hand-written handler with the panic and
bailout behaviour of generated code. `zend::ce::error` returns the `Error` class
entry.
### What still aborts
The observer, zend extension, error observer and exception observer hooks, the
module globals constructor and destructor, the embed `Sapi` trampolines, and
every `extern "C"` function that you give to the engine yourself still abort on
a panic. This includes the `startup` function of `#[php_module]`.
`BailoutGuard` semantics do not change.
## Macro Options That Did Nothing Are Now Compile Errors
Several attribute options parsed and then had no effect. Each one is now a
compile error that points at the option. The generated PHP API did not change
for code that compiled without them.
| `#[php_enum(name = "Suit")]`, `#[php_interface(...)]`, `#[php_extern(...)]` | Error. Write `#[php(name = "Suit")]` on the item. `#[php_extern]` takes no options. |
| `#[php_function]` + `#[php(vis = "private")]` | Error. PHP functions have no visibility. Remove `vis`. |
| `#[php_enum]` + `#[php(vis = "protected")]` | Error. PHP enums are always public. Remove `vis`. |
| `#[php(getter)]` or `#[php(setter)]` on a trait method | Error. Declare a plain method. |
| `#[php(constructor)]` on a trait method | Error. This option removed the method from the interface. Declare a plain method. |
| `#[php(prop, default = 0)]` on a non-static field | Error. `default` applies to `static` properties only. Set the value in the constructor. |
| `#[php(name = "x")]` on a field without `prop` | Error. Add `prop`, or remove the attribute. |
| `#[php(...)]` on a `#[php_impl_interface]` method, a `#[php_extern]` function or a `#[derive(ZvalConvert)]` item | Error. These macros accept no `#[php]` option. |
| `#[php(getter, setter)]` on one method | Error. Declare one method for each. |
| Two `#[php(getter)]` methods, or two `#[php(setter)]` methods, for one property name | Error. Keep one getter and one setter for each property. |
| `#[php(getter)]` and `#[php(setter)]` of one property with a different `vis` | Error. Give both methods the same `vis`. The property took the most permissive visibility before. |
| `#[php(defaults(x = 1))]` where no parameter is named `x` | Error on `x`. Each key of `defaults` must name a parameter. |
| `#[php(optional = x)]` where no parameter is named `x` | Error on `x`. The required count came from the parameter types before. |
| `#[php(defaults(self_ = ...))]` or `#[php(optional = self_)]` on a method that takes `self_: &mut ZendClassObject<T>` | Error. `self_` is the PHP object, not a PHP parameter. |
| `#[php(defaults(...))]` or `#[php(optional = ...)]` on a `getter` or `setter` method | Error. The method maps to a PHP property, which has no parameters. |
Existing diagnostics now point at the offending attribute or item instead of
the whole macro call.
## Getter and Setter Property Names Are camelCase
A property declared with `#[php(getter)]` or `#[php(setter)]` now follows the
same naming rule as a `#[php(prop)]` field. The macro removes the `get_` or
`set_` prefix from the method name and converts the rest to camelCase. Before,
the rest of the method name was used as is. The `name` and `change_case`
options on a getter or setter were ignored before. They now rename the property.
| `get_name` | `$obj->name` | `$obj->name` |
| `get_first_name` | `$obj->first_name` | `$obj->firstName` |
| `#[php(getter, name = "first_name")] fn get_first_name` | `$obj->first_name` | `$obj->first_name` |
To keep the old name, add `#[php(name = "first_name")]` or
`#[php(change_case = "snake_case")]` to the getter and to the setter.
## A Field Property and a Method Property Cannot Share a Name
Before, a `#[php(getter)]` method with the same property name as a
`#[php(prop)]` field was registered a second time, and reads used the field.
The guide said that the method took precedence. Now `ClassBuilder::register`
returns `Error::DuplicateProperty` and the extension does not load. Remove the
`#[php(prop)]` attribute from the field, or rename one of the two properties
with `#[php(name = "...")]`.
## Class Constant Visibility
Constants inside a `#[php_impl]` block accept `#[php(vis = "public" | "protected" | "private")]`.
The default stays `public`. `vis` on a `#[php_const]` or on an interface
constant is a compile error.
The manual registration API carries the visibility:
### Before (v0.15)
```rust,ignore
builder.constant("MAX", 10, &[])?;
builder.dyn_constant("MAX", &10, &[])?;
fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)] {
&[("MAX", &10, &[])]
}
```
### After (v0.16)
```rust,ignore
use ext_php_rs::flags::ConstantFlags;
builder.constant("MAX", 10, &[], ConstantFlags::Public)?;
builder.dyn_constant("MAX", &10, &[], ConstantFlags::Public)?;
fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments, ConstantFlags)] {
&[("MAX", &10, &[], ConstantFlags::Public)]
}
```
`ConstantFlags::Promoted` is removed. `ZEND_ACC_PROMOTED` is a property flag,
and on PHP 8.1 to 8.4 its bit means `final` in the constant column.
`ext_php_rs_introspection::Constant` gains a `visibility` field, and the crate
moves to `0.2.0`. Stubs print `public const` for class constants. Rebuild
`cargo-php` with `cargo install cargo-php --force`, because the `0.1` binary
refuses an extension built with `0.2`.
## Optional Nullable Arguments Report a `null` Default
An `Option<T>` argument in trailing position is optional and nullable. Before
v0.16, the extension registered it without a default value.
`ReflectionParameter::isOptional()` returned `true` and
`isDefaultValueAvailable()` returned `false`. PHP code cannot produce this
combination, and the generated stub already printed `= null`.
The argument info now carries `null` as the default value. This is what PHP
declares for `?int $x = null`. An explicit `#[php(defaults(x = ...))]` does not
change.
## Argument Metadata Comes From the Type
The macros decided nullability, pass-by-reference and variadics by comparing
type names as text. `Option` was matched by its last path segment, the
variadic slice by the exact spelling `&[&Zval]`, and `PhpRef` by its name. A type
alias or a qualified path produced a wrong PHP signature.
The metadata now comes from constants on the extraction traits.
`FromZval` and `FromZvalMut` have `NULLABLE`, `FromZvalMut` has `BY_REF`, and
both have `from_missing()`, the value used when the caller omits the argument.
Custom implementations do not have to change: the constants default to `false`
and `from_missing()` defaults to `None`. `Arg::of::<T>(name)` builds an `Arg`
from those constants. `FunctionBuilder::required_args(n)` and
`ArgParser::required_args(n)` set the required count directly; the generated
code computes it with `args::required_count`.
### Type aliases are honored
A parameter declared with `type MaybeAge = Option<i64>` or with
`std::option::Option<i64>` is now nullable and optional. Before, it was a
required `int`, and a debug build of PHP stopped with
"Arginfo / zpp mismatch" when the caller passed `null`. If you relied on the old
signature, spell the type without `Option`.
### Object arguments are passed by value
`&mut T` for a registered class, `&mut ZendClassObject<T>` and
`&mut ZendObject` no longer set `ZEND_SEND_BY_REF`. PHP objects are handles, so
the callee still mutates the caller's object. Callers can now write
`process(new Foo())`, which used to fail with "could not be passed by
reference". `ReflectionParameter::isPassedByReference()` returns `false` for
these parameters. `&mut Zval`, `PhpRef` and `&mut ZendHashTable` keep the flag.
### `Option<&mut T>` object arguments
`Option<&mut T>` for a class type does not compile any more. Use `&mut T`: the
argument is a handle, and a nullable object argument that is also
pass-by-reference has no meaning. `Option<&mut ZendHashTable>` keeps working as
a nullable pass-by-reference array.
### Variadic parameters accept any element type
The variadic tail is any `&[T]` slice in last position, for example `&[i64]`.
A variadic parameter followed by another parameter is a compile error.
### Default values are rendered from the value
The stub text of a `#[php(defaults(...))]` value was produced by matching the
Rust expression as text, so `String::from("x")` and `std::string::String::from("x")`
rendered differently. The generated code now converts the expression into the
parameter type at module load and renders it with the new `StubLiteral` trait,
without touching the engine. The parameter type must implement `StubLiteral`
when it has a default. Scalars, strings, `Option<T>`, `Vec<T>`, arrays and
`HashMap<K, V>` implement it. Strings render with single quotes, which PHP
reflection reads back as the string value.
### `Default` and `Clone` are detected through the type system
The class macro looked for the words `Default` and `Clone` inside `#[derive(...)]`
attributes. A qualified path such as `#[derive(std::default::Default)]` or a
hand-written `impl Clone` was not seen, so `default_init()` returned `None` and
`clone` failed. The generated `RegisteredClass` implementation now probes the
trait bounds at compile time. Every `Default` or `Clone` implementation counts,
whatever its spelling.
## Parameters After `optional` Must Accept a Missing Value
`#[php(optional = x)]` lets PHP callers omit `x` and every parameter after it.
Before v0.16, a parameter in that range could have a type such as `i64` with
no default. The function compiled, and PHP showed the parameter as optional.
When a caller omitted it, the call threw "Invalid value given for argument".
Each parameter from `x` to the end must now be an `Option<T>`, have a default
in `#[php(defaults(...))]`, or be the variadic `&[T]` tail. Otherwise the build
fails with an error on the type of that parameter. The macro reads this rule
from `FromZvalMut::NULLABLE`. If you implement `FromZval` or `FromZvalMut`,
`from_missing()` must return `Some` exactly when `NULLABLE` is `true`.
```rust,ignore
// Before: compiled, `describe("Ann")` threw at run time.
#[php_function]
#[php(optional = age)]
pub fn describe(name: String, age: Option<i64>, city: String) -> String
// After: `city` has a default, or is `Option<String>`.
#[php_function]
#[php(optional = age, defaults(city = "\"Paris\""))]
pub fn describe(name: String, age: Option<i64>, city: String) -> String
```
## Interface Methods Keep Their Parameters After `self_`
A `#[php_interface]` method whose first parameter is
`self_: &mut ZendClassObject<T>` lost its last parameter instead of `self_`.
PHP declared `scale(self_)` for `fn scale(self_: ..., factor: f64)`, and a PHP
class that implemented `scale(float $factor)` did not load. The interface now
declares `scale(float $factor)`, like `#[php_impl]` does for methods.
## Backed Enums Implement `From`
`#[php_enum]` generated `impl Into<i64> for E` and
`impl Into<&'static str> for E`. It now generates `impl From<E> for i64` and
`impl From<E> for &'static str`. Calls to `.into()` do not change. You can also
write `i64::from(e)`.
## Renaming Options Are Consistent
The options that set a PHP name now follow one set of rules. Code that used
them in a way the guide did not describe is now a compile error or a different
PHP name.
| `#[php(name = "x", change_case = "snake_case")]` compiled and `change_case` had no effect | Compile error. Keep one of the two options. |
| `#[php(rename_cases = "snake_case")]` on a `#[php_enum]` | Compile error. Write `#[php(change_cases_case = "snake_case")]`, like `change_method_case` and `change_constant_case`. |
| `#[php_impl_interface(change_method_case = "snake_case")]` repeated the rule of the trait | Compile error. Remove the argument. The class takes each method name from `#[php_interface]`. |
| `#[php_impl_interface]` ignored the `name` or `change_case` of a trait method, and the trait's `change_method_case`. The class registered the method under another name, and `new` failed with "Cannot instantiate abstract class" | The class registers each method under the name of the interface. |
| A trait method named `__to_string` in `#[php_interface]` was declared as `toString` | It is declared as `__toString`, like in `#[php_impl]`. The other magic method names follow the same rule. |
| Two methods, two constants or two enum cases of one `#[php_impl]`, `#[php_interface]` or `#[php_enum]` got the same PHP name. PHP then dropped every method of the class, or stopped at startup with "Cannot redefine class constant" | Compile error at the second item. Rename one of them. |
| Two methods of one class from different blocks, two module functions, or two classes got the same PHP name. PHP dropped the methods of the class, did not load the extension, or replaced the first class without a message | `ClassBuilder::register`, `EnumBuilder::register` and the module build return `Error::DuplicateMethod`, `Error::DuplicateConstant`, `Error::DuplicateFunction` or `Error::DuplicateClass`, and the extension does not start. Rename one of them. |