ext-php-rs 0.16.0

Bindings for the Zend API to build PHP extensions natively in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
# 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:

| Before (v0.15) | After (v0.16) |
|---|---|
| `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
let values: Vec<&Zval> = iterator.iter()?.map(|(_, v)| v).collect();
```

### After (v0.16)

```rust,ignore
let values: Vec<Zval> = iterator.iter()?.map(|(_, v)| v).collect();
```

Destructuring `for (k, v) in iterator.iter()?` keeps working; `v` is a `Zval`
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
.registration(|ce| MY_META.set_ce(ce))

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

| v0.15 | v0.16 |
|---|---|
| `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.

| Before (compiled, no effect) | After |
|---|---|
| `#[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.

| Rust method | Before | After |
|---|---|---|
| `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.

| Before (v0.15) | After (v0.16) |
|---|---|
| `#[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. |