Skip to content

feat: Implement extension methods (RFC prototype, phase 1)#22635

Draft
hollyschilling wants to merge 5 commits into
php:masterfrom
hollyschilling:extension-methods-phase1
Draft

feat: Implement extension methods (RFC prototype, phase 1)#22635
hollyschilling wants to merge 5 commits into
php:masterfrom
hollyschilling:extension-methods-phase1

Conversation

@hollyschilling

Copy link
Copy Markdown

Adds Swift-style extension blocks that declare additional methods for an existing class or interface, resolved at call time only when the receiver does not define the method itself:

extension Target {
    public function helper(): string { /* $this is the receiver */ }
}
  • extension is a contextual keyword (T_EXTENSION), lexed only when followed by another identifier, mirroring the enum technique; a extension extends/implements carve-out preserves classes named "extension", and T_EXTENSION is semi-reserved.
  • Extension blocks compile to a synthetic final anonymous class; a new ZEND_BIND_EXTENSION opcode (212) registers its methods, keyed by the lowercased resolved target name, when the declaration executes (load-gated, like function declarations).
  • zend_std_get_method gains one fallback on its miss path: real method, then __call trampoline, then extension registry lookup (most-derived target first: inheritance chain, then interfaces). Registered methods are flagged ZEND_ACC_NEVER_CACHE so the polymorphic inline cache and JIT are bypassed in this prototype.
  • Methods only: properties, constants, and cases are compile errors.
  • Indirect invocation (callables, reflection, method_exists) deliberately does not see extensions.

PROTOTYPE LIMITATIONS: the registry is a process global, so user-class method pointers dangle after request 1 in multi-request SAPIs (CLI only until it moves into zend_executor_globals); duplicate registrations are silently ignored (first wins).

Adds Swift-style extension blocks that declare additional methods for an
existing class or interface, resolved at call time only when the receiver
does not define the method itself:

    extension Target {
        public function helper(): string { /* $this is the receiver */ }
    }

- `extension` is a contextual keyword (T_EXTENSION), lexed only when
  followed by another identifier, mirroring the enum technique; a
  `extension extends/implements` carve-out preserves classes named
  "extension", and T_EXTENSION is semi-reserved.
- Extension blocks compile to a synthetic final anonymous class; a new
  ZEND_BIND_EXTENSION opcode (212) registers its methods, keyed by the
  lowercased resolved target name, when the declaration executes
  (load-gated, like function declarations).
- zend_std_get_method gains one fallback on its miss path: real method,
  then __call trampoline, then extension registry lookup (most-derived
  target first: inheritance chain, then interfaces). Registered methods
  are flagged ZEND_ACC_NEVER_CACHE so the polymorphic inline cache and
  JIT are bypassed in this prototype.
- Methods only: properties, constants, and cases are compile errors.
- Indirect invocation (callables, reflection, method_exists) deliberately
  does not see extensions.

PROTOTYPE LIMITATIONS: the registry is a process global, so user-class
method pointers dangle after request 1 in multi-request SAPIs (CLI only
until it moves into zend_executor_globals); duplicate registrations are
silently ignored (first wins).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hollyschilling and others added 2 commits July 8, 2026 08:26
zend_extension_methods_register() set ZEND_ACC_NEVER_CACHE on the
synthetic class's methods at bind time. Under opcache the class image
lives in shared memory and is immutable at runtime; with
opcache.protect_memory=1 (as CI runs) the write faults (SIGBUS on
macOS, Termsig=10 in run-tests output).

Apply the flag at compile time instead, before the class is persisted:
zend_compile_class_decl() now returns the compiled zend_class_entry
(all existing callers ignore the return value), and
zend_compile_extension_decl() flags the methods on the returned CE.
Registration no longer writes to the functions at all.

Verified with opcache.enable_cli=1 + opcache.protect_memory=1 and with
the tracing JIT; full Zend suite green under both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trace recorder handles ZEND_ACC_NEVER_CACHE callees by recording
the INIT with an unknown (NULL) function, but until now every such
function was trampoline-like, so the recorder could never follow into
its body. Extension methods are never-cached functions with ordinary,
traceable bodies: the recorder logged a NULL-func call frame, then
recorded ENTER into the body, and zend_jit_trace_build_tssa asserted
(&call->func->op_array == op_array) on the contradiction.

Stop the trace when entering a never-cached function's body, matching
the existing trampoline/property-hook/fake-closure treatment. Calls to
extension methods stay interpreted, as intended for this prototype.

Reproduces with run-tests' JIT settings (opcache.jit_hot_func=1 et al).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zend_extension_methods.c was added to configure.ac but not to the
Windows source list in win32/build/config.w32, so the Windows build
failed at link with unresolved externals.

Also regenerate ext/tokenizer data (tokenizer_data_gen.php + gen_stub)
so the T_EXTENSION constant and token_name() know the new token.

Verified locally on a ZTS debug build (--enable-zts) with opcache
protect_memory and the tracing JIT; Zend + tokenizer suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Magic methods dispatch through dedicated class-entry slots and object
handlers (ce->constructor, ce->__call, the cast handler, ...), never
through the zend_std_get_method miss path where extension methods
resolve. An extension-declared magic method could therefore never work
as a magic method - at best dead code, at worst reachable only through
a direct $obj->__construct() style call. Reject the reserved __ prefix
wholesale at compile time, mirroring the enum restriction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants